现在,我们都知道像 Facebook 或 Google 这样的主要科技巨头了解我们生活中的所有事情,包括我们多久去一次洗手间(因此所有前列腺药物广告不断弹出,即使在信誉良好的新闻网站上)。毕竟,我们已经允许他们这样做,通过阅读他们条款与条件页面上的无数法律条款(我们都读过,不是吗?)并点击“接受”按钮。
但是,如果未经您的明确同意,网站可以对您或您的设备做什么?当您访问一个略微“不当”的网站,或者您访问的“正常”网站包含一些未经彻底检查的第三方脚本时会发生什么?
您是否曾经遇到过浏览器被劫持,无数弹出窗口出现,并且您似乎无法关闭它们,除非完全退出浏览器或在“后退”按钮上点击 25 次?发生这种情况时,您确实会感到危险,不是吗?
在 CSS-Tricks 的 Chris 的建议下,我决定寻找一个执行此操作的脚本,并查看幕后发生了什么。这看起来像是一项相当艰巨的任务,但我从中了解了许多东西,最终玩得很开心。我希望我能与您分享一些乐趣。
寻找脚本
我们的想法是寻找,引用 Chris 的话,“执行令人惊讶的可怕操作的 JavaScript 代码片段”。
我首先在我的主要 Ubuntu 开发 PC 上使用Virtual Box设置了一个虚拟机。这样,如果我访问的网站及其包含的脚本试图对我的计算机执行一些可怕的操作,我只需要擦除虚拟机,而不会危及我的宝贵笔记本电脑。我在虚拟机上安装了最新版本的 Ubuntu,打开浏览器并开始搜索。
我正在寻找的一件事是臭名昭著的Evercookie(又名“不可删除的 Cookie”)的变体的用法,这将是可疑跟踪技术的明确标志。
在哪里寻找这样的脚本?我试图在合法网站上找到上述侵入性广告之一,但没有找到任何。我假设,提供广告的公司在通过自动化审查流程来发现可疑脚本方面变得更好。
我尝试了一些信誉良好的新闻网站,看看是否有任何有趣的东西,但我只发现了很多标准跟踪脚本(以及控制台日志中的 JavaScript 错误)。在这些情况下,脚本执行的大部分操作是将数据发送到服务器,并且由于您几乎无法知道服务器实际上对数据做了什么,因此很难对其进行剖析。
然后我认为,寻找“可怕”内容的最佳地点是那些如果对用户执行“可怕”操作,其所有者不会冒法律诉讼风险的网站。这意味着,基本上,用户试图执行一些接近非法操作的网站。
我查看了一些海盗湾代理,但没有结果。然后我决定转向提供体育赛事非法流媒体链接的网站。我浏览了几个网站,仔细查看了它们使用 Chromium 的 DevTools 包含的脚本。
在一个提供包括乒乓球比赛非法流媒体在内的网站上,我注意到(在 DevTools 网络选项卡的 JavaScript 列表中),在第三方库、标准 UI 脚本和 Google Analytics 库(ouch!)的过于频繁的重复包含之间,一个奇怪命名的脚本没有 .js 扩展名,并且 URL 只是一个数字。

我查看了构成脚本代码大部分内容的看似无限的几行混淆代码,并找到了诸如chromePDFPopunderNew
、adblockPopup
、flashFileUrl
、转义的<script>
标记,甚至包含内联 PDF 的字符串。这看起来很有趣。搜索结束了!我将脚本下载到我的电脑上,并开始尝试理解它。
我不会明确披露参与此操作的域名,因为我们对这里的罪恶感兴趣,而不是罪人。但是,我故意留下了确定脚本将用户发送到的主要 URL 的方法。如果您设法解开了谜团,请向我发送私信,我会告诉您是否猜对了!
脚本:去混淆和确定配置参数
脚本的外观
该脚本被混淆,既是为了安全目的,也是为了确保更快的下载速度。它由一个大型 IIFE(立即调用函数表达式)组成,这是一种用于将 JavaScript 代码片段与其周围环境隔离的技术。上下文不会与其他脚本混合,并且不同脚本中函数或变量名称之间不存在命名空间冲突的风险。
以下是脚本的开头。请注意最后一行上 base64 编码的 PDF 的开头

这是脚本的结尾

显然,在全局上下文中执行的唯一操作是将全局变量zfgloadedpopup
设置为 true,大概是告诉属于同一“家族”的其他脚本此脚本已加载。此变量仅使用一次,因此脚本本身不会检查它是否已加载。因此,如果您访问的网站错误地包含了两次,您将以相同的价格获得双倍的弹出窗口。幸运!
大型 IFEE 期望两个参数,分别称为options
和lary
。我实际上检查了第二个参数的名称以了解其含义,我找到的唯一含义是英国俚语中的“侵略性,反社会”。“所以,我们在这里很强势,”我想。“有趣。”
options
参数显然是一个带有键和值的物件,即使它们完全难以理解。lary
参数是某种字符串。为了理解这一点,唯一的选择是去混淆整个脚本。继续阅读,一切都会解释清楚。
去混淆脚本
我首先尝试使用现有的工具,但所有可用的在线工具似乎都没有按照我的预期执行。它们执行的大部分操作是对代码进行格式化,而我的 IDE 可以轻松地自行完成。我阅读了有关JSDetox的信息,它实际上是计算机软件,应该对调试此类脚本非常有帮助。但是,我尝试将其安装到两个不同版本的 Ubuntu 中,并且在这两种情况下最终都陷入了 Ruby GEM 依赖项地狱。JSDetox 已经相当旧了,我想它现在实际上已经成为废弃软件了。剩下的唯一选择是主要手动或通过手动或半自动正则表达式替换来处理事情。我必须经过几个步骤才能完全破译脚本。
这是一个动画 GIF,显示了在破译的不同阶段的相同代码部分

第一步非常简单:它需要重新格式化脚本的代码,以添加空格和换行符。我得到了正确缩进的代码,但它仍然充满了非常难以理解的内容,如下所示
var w6D0 = window;
for (var Z0 in w6D0) {
if (Z0.length === ((129.70E1, 0x1D2) < 1.237E3 ? (47, 9) : (0x1CE, 1.025E3) < (3.570E2, 122.) ? (12.9E1, true) : (5E0, 99.) > 0x247 ? true : (120.7E1, 0x190)) && Z0.charCodeAt((0x19D > (0x199, 1.5E1) ? (88., 6) : (57., 0x1D9))) === (121.30E1 > (1.23E2, 42) ? (45.2E1, 116) : (129., 85) > (87., 5.7E2) ? (45.1E1, 0x4) : (103., 0x146) >= (0x17D, 6.19E2) ? (1.244E3, 80) : (1.295E3, 149.)) && Z0.charCodeAt(((1.217E3, 90.10E1) <= (0xC2, 128.) ? (66, 'sw') : (0x25, 0xAB) > 1.26E2 ? (134, 8) : (2.59E2, 0x12) > 0xA9 ? 'sw' : (0x202, 0x20F))) === ((95, 15) <= 63 ? (0x10B, 114) : (0xBB, 8.72E2) <= (62, 51.) ? 'r' : (25, 70.) >= (110.4E1, 0x8D) ? (121, 72) : (42, 11)) && Z0.charCodeAt(((96.80E1, 4.7E1) >= 62. ? (25.70E1, 46) : 0x13D < (1.73E2, 133.1E1) ? (0x1A4, 4) : (28, 0x1EE) <= 36.30E1 ? 37 : (14.61E2, 0x152))) === (81. > (0x1FA, 34) ? (146, 103) : (0x8A, 61)) && Z0.charCodeAt(((92.60E1, 137.6E1) > (0x8, 0x3F) ? (123., 0) : (1.41E2, 12.11E2))) === ((0xA, 0x80) > (19, 2.17E2) ? '' : (52, 0x140) > (80., 0x8E) ? (42, 110) : 83.2E1 <= (0x69, 0x166) ? (41., 'G') : (6.57E2, 1.093E3))) break
}
;
这段代码在做什么?唯一的解决方案是尝试在控制台中执行代码并查看发生了什么。事实证明,这段代码循环遍历所有window
的属性,并在非常复杂的条件匹配时退出循环。最终结果有点滑稽,因为上面所有的代码都做了以下事情
var Z0 = 'navigator'
…也就是说,将window
的navigator
属性保存到名为Z0
的变量中。这确实是为了分配一个变量而付出了很多努力!有几个变量像这样被混淆了,在控制台中执行了几轮后,我设法获得了以下全局变量
var Z0 = 'navigator';
var Q0 = 'history';
var h0 = 'window'; // see comment below
/* Window has already been declared as w6D0. This is used to call the Window object of a variable containing a reference to a different window, other than the current one */
同样的方法可以应用于脚本开头声明的其他几个全局变量。整个操作对我来说有点愚蠢,因为脚本中许多其他变量在几行之后更公开地声明,如下所示
var m7W = {'K2': 'documentElement',
'W0': 'navigator',
'A2': 'userAgent',
'o2': 'document'};
但没关系。经过此过程后,我得到了一系列对脚本全局且在整个脚本中使用的变量。
是时候进行一些批量替换了。我将w6D0
变量替换为window
,然后继续处理其他全局变量。
还记得上面提到的h0
变量吗?它无处不在,用于以下语句中
if (typeof w6D0[h0][H8] == M3) {
…替换后,变成了
if (typeof window['window'][H8] == M3) {
这比之前没有清晰多少,但仍然比我开始时的状态前进了一小步。同样,以下行
var p = w6D0[X0][H](d3);
…变成了这样
var p = window["document"][H](d3);
在此脚本中使用的混淆技术中,函数内部局部变量的名称通常会被替换为单个字母的名称,例如这样
function D9(O, i, p, h, j) {
var Q = 'newWin.opener = null;', Z = 'window.parent = null;', u = ' = newWin;', N = 'window.parent.',
w = '' + atob('Ig==') + ');', g = '' + atob('Ig==') + ', ' + atob('Ig==') + '',
f = 'var newWin = window.open(' + atob('Ig==') + '', d = 'window.frameElement = null;',
k = 'window.top = null;', r = 'text', l = 'newWin_', F = 'contentWindow', O9 = 'new_popup_window_',
I = 'disableSafeOpen', i9 = e['indexOf']('MSIE') !== -'1';
// more function code here
}
然而,大多数全局变量名称已被替换为包含多个字母的名称,并且所有这些名称都是唯一的。这意味着我可以在整个脚本中全局替换它们。
还有一大堆全局变量
var W8 = 'plugins', f7 = 'startTimeout', z1 = 'attachEvent', b7 = 'mousemove', M1 = 'noScrollPlease',
w7 = 'isOnclickDisabledInKnownWebView', a1 = 'notificationsUrl', g7 = 'notificationEnable', m8 = 'sliderUrl',
T8 = 'interstitialUrl', v7 = '__interstitialInited', C8 = '%22%3E%3C%2Fscript%3E',
O8 = '%3Cscript%20defer%20async%20src%3D%22', i8 = 'loading', p8 = 'readyState', y7 = '__pushupInited',
o8 = 'pushupUrl', G7 = 'mahClicks', x7 = 'onClickTrigger', J7 = 'p', r7 = 'ppu_overlay', d7 = 'PPFLSH',
I1 = 'function', H7 = 'clicksSinceLastPpu', k7 = 'clicksSinceSessionStart', s7 = 'lastPpu', l7 = 'ppuCount',
t7 = 'seriesStart', e7 = 2592000000, z7 = 'call', Y1 = '__test', M7 = 'hostname', F1 = 'host',
a7 = '__PPU_SESSION_ON_DOMAIN', I7 = 'pathname', Y7 = '__PPU_SESSION', F7 = 'pomc', V7 = 'ActiveXObject',
q7 = 'ActiveXObject', c7 = 'iOSClickFix',
m7 = 10802, D8 = 'screen',
// ... and many more
我也使用自动脚本替换了所有这些变量,并且许多函数变得更容易理解。有些甚至无需进一步操作即可完全理解。例如,一个函数从以下代码
function a3() {
var W = E;
if (typeof window['window'][H8] == M3) {
W = window['window'][H8];
} else {
if (window["document"][m7W.K2] && window["document"][m7W.K2][q5]) {
W = window["document"][m7W.K2][q5];
} else {
if (window["document"][z] && window["document"][z][q5]) {
W = window["document"][z][q5];
}
}
}
return W;
}
…变成了以下代码
function a3() {
var W = 0;
if (typeof window['window']['innerWidth'] == 'number') {
W = window['window']['innerWidth'];
} else {
if (window["document"]['documentElement'] && window["document"]['documentElement']['clientWidth']) {
W = window["document"]['documentElement']['clientWidth'];
} else {
if (window["document"]['body'] && window["document"]['body']['clientWidth']) {
W = window["document"]['body']['clientWidth'];
}
}
}
return W;
}
如您所见,此函数尝试使用所有可用的跨浏览器选项来确定客户端窗口的宽度。这似乎有点过头了,因为从 IE9 开始,所有浏览器都支持window.innerWidth
。
然而,window.document.documentElement.clientWidth
即使在 IE6 中也能正常工作;这表明我们的脚本试图尽可能地兼容各种浏览器。我们稍后将详细了解这一点。
请注意,为了加密所有使用的属性和函数名称,此脚本大量使用了括号表示法,例如
window["document"]['documentElement']['clientWidth']
…而不是
window.document.documentElement.clientWidth
这使得脚本可以将对象方法和属性的名称替换为随机字符串,然后在脚本开头定义一次——使用正确的名称。这使得代码非常难以阅读,因为您必须反转所有替换。然而,它显然不仅仅是一种混淆技术,因为如果长属性名称经常出现,则将它们替换为一个或两个字母可以节省脚本总文件大小的相当一部分字节,从而使其下载速度更快。
我执行的最后一系列替换的结果使代码更加清晰,但我仍然得到一个非常长的脚本,其中包含许多名称难以理解的函数,例如以下函数
function k9(W, O) {
var i = 0, p = [], h;
while (i < W.length) {
h = O(W[i], i, W);
if (h !== undefined) {
p['push'](h);
}
i += '1';
}
return p;
}
所有这些函数在每个函数的开头都有变量声明,这很可能是对原始代码使用的混淆/压缩技术的的结果。也有可能此代码的编写者非常谨慎,并在每个函数的开头声明了所有变量,但我对此表示怀疑。
上面的k9
函数在脚本中被广泛使用,因此它是我首先要处理的函数之一。它期望两个参数,W
和O
,并准备一个初始化为空数组的返回值(p
)以及一个临时变量(h
)。
然后它使用while
循环遍历W
while (i < W.length) {
这告诉我们W
参数将是一个数组,或者至少是可遍历的对象或字符串。然后,它将循环中的当前元素、循环的当前索引以及整个W
参数作为参数传递给初始的O
参数,这告诉我们后者将是某种函数。它将函数执行的结果存储在临时变量h
中
h = O(W[i], i, W);
如果此函数的结果不是undefined
,则将其附加到结果数组p
中
if (h !== undefined) {
p['push'](h);
}
返回值是p
。
这是什么样的结构?它显然是一个映射/过滤函数,但不仅仅是映射初始对象W
,因为它不会返回所有值,而是选择其中一些。它也不仅仅是过滤它们,因为它没有简单地检查true
或false
并返回原始元素。它有点像两者的混合体。
我必须重命名此函数,就像我对大多数其他函数所做的那样,给它一个易于理解并解释函数目的的名称。
由于此函数通常在脚本中用于以某种方式转换原始对象W
,因此我决定将其重命名为mapByFunction
。以下是它未经混淆的版本
function mapByFunction(myObject, mappingFunction) {
var i = 0, result = [], h;
while (i < myObject.length) {
h = mappingFunction(myObject[i], i, myObject);
if (h !== undefined) {
result['push'](h);
}
i += 1;
}
return result;
}
必须对脚本中的所有函数应用类似的过程,逐个猜测它们试图实现的目标、传递给它们的参数以及它们返回的内容。在许多情况下,这涉及在解密一个函数时来回查看代码,因为该函数使用了尚未解密的另一个函数。
其他一些函数嵌套在其他函数内部,因为它们仅在封闭函数的上下文中使用,或者因为它们是某些粘贴到脚本中的第三方代码的一部分。
在所有这些繁琐的工作完成后,我得到了一个包含大量相当易于理解的函数的大型脚本,所有这些函数都具有很好的描述性(尽管非常长)名称。
以下是一些名称,来自我的IDE的“结构”面板

现在函数有了名称,您可以开始猜测此脚本正在执行的一些操作。现在您是否想尝试在某人的浏览器中injectPDFAndDoStuffDependingOnChromeVersion
?
脚本结构
一旦脚本的各个函数被解密,我就尝试理解整个脚本。
脚本开头包含许多辅助函数,这些函数通常会调用其他函数,有时还会在全局作用域中设置变量(糟糕!)。然后,脚本的主要逻辑开始,大约在我未经混淆的版本的第 1680 行。
脚本的行为会根据传递给它的配置而有很大差异:许多函数会检查主options
参数中的一个或多个参数,例如
if (options['disableSafeOpen'] || notMSIE) {
// code here
}
或者像这样
if (!options['disableChromePDFPopunderEventPropagation']) {
p['target']['click']();
}
但是,如果您还记得,options
参数是加密的。因此,接下来要做的就是对其进行解密。
解密配置参数
在脚本主代码的开头,有以下调用
// decode options;
if (typeof options === 'string') {
options = decodeOptions(options, lary);
}
decodeOptions
是我赋予执行此任务的函数的名称。最初,它的名称是简单的g4
。
最后,我们还使用了神秘的lary
参数,其值为
"abcdefghijklmnopqrstuvwxyz0123456789y90x4wa5kq72rftj3iepv61lgdmhbn8ouczs"
字符串的前半部分显然是小写字母表,后跟数字 0 到 9。后半部分由随机字符组成。这看起来像密码吗?如果您的答案是肯定的,那么您绝对正确。事实上,它是一个带有一点变化的简单替换密码。
整个decodeOptions
函数如下所示
function decodeOptions(Options, lary) {
var p = ')',
h = '(',
halfLaryLength = lary.length / 2,
firstHalfOfLary = lary['substr'](0, halfLaryLength),
secondHalfOfLary = lary['substr'](halfLaryLength),
w,
// decrypts the option string before JSON parsing it
g = mapByFunction(Options, function (W) {
w = secondHalfOfLary['indexOf'](W);
return w !== -1 ? firstHalfOfLary[w] : W;
})['join']('');
if (window['JSON'] && window['JSON']['parse']) {
try {
return window['JSON']['parse'](g);
} catch (W) {
return eval(h + g + p);
}
}
return eval(h + g + p);
}
它首先设置几个包含左右括号的变量,这些变量将在稍后使用
var p = ')',
h = '(',
然后它将我们的lary
参数分成两半
halfLaryLength = lary.length / 2,
firstHalfOfLary = lary['substr'](0, halfLaryLength),
secondHalfOfLary = lary['substr'](halfLaryLength),
接下来,它使用以下函数逐个字母地映射Options
字符串
function (W) {
w = secondHalfOfLary['indexOf'](W);
return w !== -1 ? firstHalfOfLary[w] : W;
}
如果当前字母出现在lary
参数的后半部分,则返回相同参数前半部分的小写字母表中的对应字母。否则,它将返回当前字母,保持不变。这意味着options参数只是一半被加密了,可以这么说。
映射完成后,生成的解密字母数组g
(请记住,mapByFunction
始终返回一个数组)将再次转换为字符串
g['join']('')
配置最初是一个 JSON 对象,因此脚本尝试使用浏览器的原生 JSON.parse 函数将其转换为对象字面量。如果 JSON 对象不可用(IE7 或更低版本,Firefox 和 Safari 3 或更低版本),则会将其放在括号之间并进行求值
if (window['JSON'] && window['JSON']['parse']) {
try {
return window['JSON']['parse'](g);
} catch (W) {
return eval(h + g + p);
}
}
return eval(h + g + p);
这是脚本极度兼容各种浏览器的另一个例子,甚至支持超过 10 年历史的浏览器。我稍后会尝试解释原因。
因此,现在options
变量已解密。以下是它所有解密后的辉煌之处,尽管省略了原始 URL
let options = {
SS: true,
adblockPopup: true,
adblockPopupLink: null,
adblockPopupTimeout: null,
addOverlay: false,
addOverlayOnMedia: true,
aggressive: false,
backClickAd: false,
backClickNoHistoryOnly: false,
backClickZone: null,
chromePDFPopunder: false,
chromePDFPopunderNew: false,
clickAnywhere: true,
desktopChromeFixPopunder: false,
desktopPopunderEverywhere: false,
desktopPopunderEverywhereLinks: false,
disableChromePDFPopunderEventPropagation: false,
disableOnMedia: false,
disableOpenViaMobilePopunderAndFollowLinks: false,
disableOpenViaMobilePopunderAndPropagateEvents: false,
disablePerforamnceCompletely: false,
dontFollowLink: false,
excludes: [],
excludesOpenInPopunder: false,
excludesOpenInPopunderCapping: null,
expiresBackClick: null,
getOutFromIframe: false,
iOSChromeSwapPopunder: false,
iOSClickFix: true,
iframeTimeout: 30000,
imageToTrackPerformanceOn: "", /* URL OMITTED */
includes: [],
interstitialUrl: "", /* URL OMITTED */
isOnclickDisabledInKnownWebView: false,
limLo: false,
mahClicks: true,
mobilePopUpTargetBlankLinks: false,
mobilePopunderTargetBlankLinks: false,
notificationEnable: false,
openPopsWhenInIframe: false,
openViaDesktopPopunder: false,
openViaMobilePopunderAndPropagateFormSubmit: false,
partner: "pa",
performanceUrl: "", /* URL OMITTED */
pomc: false,
popupThroughAboutBlankForAdBlock: false,
popupWithoutPropagationAnywhere: false,
ppuClicks: 0,
ppuQnty: 3,
ppuTimeout: 25,
prefetch: "",
resetCounters: false,
retargetingFrameUrl: "",
scripts: [],
sessionClicks: 0,
sessionTimeout: 1440,
smartOverlay: true,
smartOverlayMinHeight: 100,
smartOverlayMinWidth: 450,
startClicks: 0,
startTimeout: 0,
url: "", /* URL OMITTED */
waitForIframe: true,
zIndex: 2000,
zoneId: 1628975
}
我发现存在aggressive
选项这一事实非常有趣,即使此选项不幸未在代码中使用。鉴于此脚本对您的浏览器执行的所有操作,我很好奇如果它更“激进”,它会做什么。
并非传递给脚本的所有选项实际上都在脚本中使用;并且并非脚本检查的所有选项都存在于此版本中传递的options
参数中。我认为脚本配置中不存在的一些选项在部署到其他网站的版本中使用,尤其是在此脚本在多个域中使用的情况下。某些选项也可能是出于遗留原因而存在,并且不再使用。脚本中还保留了一些空函数,这些函数可能使用了某些缺少的选项。
脚本到底做了什么?
仅通过阅读上面选项的名称,您就可以猜测出此脚本执行的大量操作:它将打开一个smartOverlay
,甚至使用特殊的adblockPopup
。如果您clickAnywhere
,它将打开一个url
。在我们特定版本的脚本中,它不会openPopsWhenInIframe
,也不会getOutFromIframe
,即使它会应用iOSClickFix
。它将计算弹出窗口的数量并将值保存在ppuCount
中,甚至使用imageToTrackPerformanceOn
跟踪性能(即使我省略了 URL,我也可以告诉您,它托管在 CDN 上)。它将跟踪ppuClicks
(我猜是弹出窗口点击),并谨慎地将其限制在ppuQnty
(可能是弹出窗口数量)。
通过阅读代码,我显然可以发现更多内容。让我们看看脚本做了什么并遵循其逻辑。我将尝试描述它可以执行的所有有趣操作,包括那些未由我能够解密的选项集触发的操作。
此脚本的主要目的是将用户重定向到其配置中存储为options['url']
的 URL。我发现的配置中的 URL 将我重定向到一个垃圾网站,为了清楚起见,从现在起我将此 URL 称为“垃圾网站”。
1. 我想退出这个 iframe!
这段脚本首先尝试获取顶级窗口的引用(如果脚本本身是在 iframe 中运行的),并且如果当前配置需要,则将顶级窗口设置为操作的主窗口,并将所有对文档元素和用户代理的引用都设置为顶级窗口的引用。
if (options['getOutFromIframe'] && iframeStatus === 'InIframeCanExit') {
while (myWindow !== myWindow.top) {
myWindow = myWindow.top;
}
myDocument = myWindow['document'];
myDocumentElement = myWindow['document']['documentElement'];
myUserAgent = myWindow['navigator']['userAgent'];
}
2. 你最喜欢的浏览器是什么?
其次,它会通过解析用户代理字符串来非常细微地检测当前浏览器、浏览器版本和操作系统。它会检测用户是否使用 Chrome 及其特定版本、Firefox、Firefox for Android、UC 浏览器、Opera Mini、Yandex,或者用户是否正在使用 Facebook 应用。一些检查非常具体。
isYandexBrowser = /YaBrowser/['test'](myUserAgent),
isChromeNotYandex = chromeVersion && !isYandexBrowser,
我们稍后会看到原因。
3. 你的所有浏览器都属于我们。

脚本做的第一个令人不安的事情是检查 `history.pushState()` 函数是否存在,如果存在,则脚本会使用当前 URL 的标题注入一个伪造的历史记录条目。这使得它能够拦截后退按钮点击事件(使用 `popstate` 事件),并将用户重定向到垃圾网站,而不是用户实际访问的上一个页面。如果它没有先添加一个伪造的历史记录条目,那么这种技术将无法工作。
function addBackClickAd(options) {
if (options['backClickAd'] && options['backClickZone'] && typeof window['history']['pushState'] === 'function') {
if (options['backClickNoHistoryOnly'] && window['history'].length > 1) {
return false;
}
// pushes a fake history state with the current doc title
window['history']['pushState']({exp: Math['random']()}, document['title'], null);
var createdAnchor = document['createElement']('a');
createdAnchor['href'] = options['url'];
var newURL = 'http://' + createdAnchor['host'] + '/afu.php?zoneid=' + options['backClickZone'] + '&var=' + options['zoneId'];
setTimeout(function () {
window['addEventListener']('popstate', function (W) {
window['location']['replace'](newURL);
});
}, 0);
}
}
此技术仅在 iframe 上下文之外使用,并且不在 Chrome iOS 和 UC 浏览器上使用。
4. 这个浏览器需要更多脚本。
如果一个恶意脚本还不够,那么脚本会根据配置尝试注入更多脚本。所有脚本都附加到文档的 `
` 中,并且可能包含一些被称为插屏广告、滑块或弹窗的内容,我假设这些都是几种形式的侵入式广告,会显示在浏览器中。我无法确定,因为在我们的脚本案例中,配置中没有任何这些内容,除了一个我检查时是无效 URL 的内容。5. 点击拦截器的攻击
接下来,脚本将“点击拦截器”函数附加到文档上所有类型的点击事件,包括移动设备上的触摸事件。此函数会拦截用户在文档上的所有点击或点击操作,并继续使用不同的技术(取决于设备)打开不同类型的弹出窗口。
在某些情况下,它会尝试打开一个“弹出式窗口”。这意味着它会拦截链接上的任何点击,读取原始链接目标,在当前窗口中打开该链接,并同时在新窗口中打开垃圾网站。在大多数情况下,它会继续将焦点恢复到原始窗口,而不是它创建的新窗口。我认为这是为了规避一些浏览器安全措施,这些措施会检查是否有东西正在更改用户实际点击的 URL。然后,用户会发现自己打开了正确的链接,但同时还有一个包含垃圾网站的标签页,用户在切换标签页时迟早会看到它。
在其他情况下,脚本会执行相反的操作,在新窗口中打开用户点击的链接,但将当前窗口的 URL 更改为垃圾网站的 URL。
为了做到这一切,脚本针对不同的浏览器使用了不同的函数,每个函数可能都是为了规避每个浏览器的安全措施而编写的,包括 AdBlock(如果存在)。这里有一些执行此操作的代码,让您了解一下。
if (options['openPopsWhenInIframe'] && iframeStatus === 'InIframeCanNotExit') {
if (isIphoneIpadIpod && (V || p9)) {
return openPopunder(W);
}
return interceptEventAndOpenPopup(W);
}
if (options['adblockPopup'] && currentScriptIsApuAfuPHP) {
return createLinkAndTriggerClick(options['adblockPopupLink'], options['adblockPopupTimeout']);
}
if (options['popupThroughAboutBlankForAdBlock'] && currentScriptIsApuAfuPHP) {
return openPopup();
}
if (!isIphoneIpadIpodOrAndroid && (options['openViaDesktopPopunder'] || t)) {
if (isChromeNotYandex && chromeVersion > 40) {
return injectPDFAndDoStuffDependingOnChromeVersion(W);
}
if (isSafari) {
return openPopupAndBlank(W);
}
if (isYandexBrowser) {
return startMobilePopunder(W, I);
}
}
/* THERE ARE SEVERAL MORE LINES OF THIS KIND OF CODE */
举一个浏览器特定行为的例子,脚本在 Mac 版 Safari 上打开一个包含垃圾网站的新窗口,立即打开一个空白窗口,将焦点赋予该窗口,然后立即关闭它。
function openPopupAndBlank(W) {
var O = 'about:blank';
W['preventDefault']();
// opens popup with options URL
safeOpen(
options['url'],
'ppu' + new Date()['getTime'](),
['scrollbars=1', 'location=1', 'statusbar=1', 'menubar=0', 'resizable=1', 'top=0', 'left=0', 'width=' + window['screen']['availWidth'], 'height=' + window['screen']['availHeight']]['join'](','),
document,
function () {
return window['open'](options['url']);
}
);
// opens blank window, gives it focuses and closes it (??)
var i = window['window']['open'](O);
i['focus']();
i['close']();
}
设置点击拦截后,它会创建一系列“智能覆盖”。这些层使用透明 GIF 作为背景图像,位于原始文档中存在的每个 `
if (options['smartOverlay']) {
var f = [];
(function d() {
var Z = 750,
affectedTags = 'object, iframe, embed, video, audio';
mapByFunction(f, function (W) {
if (W['parentNode']) {
W['parentNode']['removeChild'](W);
}
});
f = mapByFunction(safeQuerySelectorAll(affectedTags), function (W) {
var O = 'px'
if (!checkClickedElementTag(W, true)) {
return;
}
if (flashPopupId && W['className'] === flashPopupId) {
return;
}
if (options['smartOverlayMinWidth'] <= W['offsetWidth'] && options['smartOverlayMinHeight'] <= W['offsetHeight']) {
var Q = getElementTopAndLeftPosition(W);
return createNewDivWithGifBackgroundAndCloneStylesFromInput({
left: Q['left'] + O,
top: Q.top + O,
height: W['offsetHeight'] + O,
width: W['offsetWidth'] + O,
position: 'absolute'
});
}
});
popupTimeOut2 = setTimeout(d, Z);
})();
}
这样,脚本甚至能够拦截媒体对象上的点击,而这些点击在 JavaScript 中可能不会触发标准的“点击”行为。
脚本试图做另外几件奇怪的事情。例如,在移动设备上,它会尝试扫描指向空白窗口的链接,并尝试使用自定义函数拦截它们。该函数甚至会在打开新窗口之前临时修改链接的 `rel` 属性,并将其设置为 `'noopener noreferer'` 的值。这是一个奇怪的操作,因为这据说是针对某些旧版浏览器的安全措施。想法可能是为了避免如果垃圾网站消耗了过多的资源并阻塞了原始页面(Jake Archibald在这里解释),则避免对主页面造成性能影响。但是,这种技术仅在此函数中使用,其他地方未使用,这对我来说有点神秘。
脚本做的另一件奇怪的事情是尝试创建一个新窗口并添加一个以 PDF 字符串作为源的 iframe。如果页面焦点或可见性发生变化,则此新窗口会立即放置在屏幕外,并且 PDF iframe 会被移除。在某些情况下,只有在 PDF 被移除后,脚本才会重定向到垃圾网站。此功能似乎仅针对 Chrome,我无法确定 PDF 是否具有恶意性。
6. 告诉我更多关于你的信息
最后,脚本会收集大量有关浏览器的信息,这些信息将附加到垃圾网站的 URL 中。它会检查以下内容:
- 是否安装了 Flash。
- 屏幕、当前窗口的宽度和高度,以及窗口相对于屏幕的位置。
- 顶级窗口中 iframe 的数量。
- 页面的当前 URL。
- 浏览器是否安装了插件。
- 浏览器是否是 PhantomJs 或 Selenium WebDriver(可能是为了检查站点当前是否被某种自动化浏览器访问,并且可能执行一些不太可怕的操作,因为自动化浏览器很可能被生产反病毒软件的公司或执法机构使用)。
- 浏览器是否支持 `Navigator` 对象的 `sendBeacon` 方法。
- 浏览器是否支持地理位置。
- 脚本当前是否在 iframe 中运行。
然后,它会将这些值添加到垃圾网站的 URL 中,每个值都使用其自己的变量进行编码。垃圾网站显然会利用这些信息根据浏览器窗口的大小调整其内容大小,并且可能还会根据浏览器是否高度脆弱(例如,安装了 Flash)或可能是反垃圾邮件机器人(如果检测到它是自动化浏览器)来调整内容的恶意程度。
此后,脚本就完成了。它做了很多有趣的事情,不是吗?
技术和跨浏览器兼容性
让我们看一下脚本通常使用的一些技术以及为什么需要它们。
浏览器检测
在编写 Web 代码时,避免浏览器检测通常被认为是一种最佳实践,因为它是一种容易出错的技术:用户代理字符串非常复杂,难以解析,并且随着时间的推移,随着新浏览器的发布,它们可能会发生变化。我个人在我的项目中像避瘟神一样避免浏览器检测。
但是,在这种情况下,正确的浏览器检测可能意味着在用户计算机上成功或失败地打开垃圾网站。这就是脚本尽可能仔细地尝试检测浏览器和操作系统的原因。
浏览器兼容性
出于同样的原因,脚本使用了大量的跨浏览器技术来最大程度地提高兼容性。这可能是由于一个非常旧的脚本经过多年多次更新的结果,同时保留了所有旧代码。但也可能是为了使脚本与尽可能多的浏览器兼容。
毕竟,对于那些可能试图在毫无戒心的用户上安装恶意软件的人来说,一个使用非常过时的浏览器或甚至是更新的浏览器但带有过时插件的用户更容易受到攻击,当然也是一个绝佳的目标!
一个例子是脚本在所有其他函数中用于打开新窗口的功能,我已将其重命名为safeOpen
// SAFE OPEN FOR MSIE
function safeOpen(URLtoOpen, popupname, windowOptions, myDocument, windowOpenerFunction) {
var notMSIE = myUserAgent['indexOf']('MSIE') !== -1;
if (options['disableSafeOpen'] || notMSIE) {
var W9 = windowOpenerFunction();
if (W9) {
try {
W9['opener']['focus']();
} catch (W) {
}
W9['opener'] = null;
}
return W9;
} else {
var t, c, V;
if (popupname === '' || popupname == null) {
popupname = 'new_popup_window_' + new Date()['getTime']();
}
t = myDocument['createElement']('iframe');
t['style']['display'] = 'none';
myDocument['body']['appendChild'](t);
c = t['contentWindow']['document'];
var p9 = 'newWin_' + new Date()['getTime']();
V = c['createElement']('script');
V['type'] = 'text/javascript';
V['text'] = [
'window.top = null;',
'window.frameElement = null;',
'var newWin = window.open(' + atob('Ig==') + '' + URLtoOpen + '' + atob('Ig==') + ', ' + atob('Ig==') + '' + popupname + '' + atob('Ig==') + ', ' + atob('Ig==') + '' + windowOptions + '' + atob('Ig==') + ');',
'window.parent.' + p9 + ' = newWin;',
'window.parent = null;',
'newWin.opener = null;'
]['join']('');
c['body']['appendChild'](V);
myDocument['body']['removeChild'](t);
return window[p9];
}
}
每次调用此函数时,都会传递另一个用于打开新窗口的函数(它是传递给上面称为windowOpenerFunction
的函数的最后一个参数)。此函数在每次调用时根据当前用例的特定需求进行自定义。但是,如果脚本检测到它在 Internet Explorer 上运行,并且disableSafeOpen
选项未设置为 true,则它会采用一种相当复杂的方法来使用其他参数(URLtoOpen
、popupname
、windowOptions
、myDocument)
打开窗口,而不是使用windowOpenerFunction
函数来打开新窗口。它创建一个 iFrame,将其插入到当前文档中,然后向该 iFrame 添加一个 JavaScript 脚本节点,该节点打开新窗口。最后,它删除刚刚创建的 iFrame。
捕获所有异常
此脚本始终保持安全性的另一种方法是捕获异常,以防它们导致可能阻止 JavaScript 执行的错误。每次它调用在所有浏览器上都不完全安全的函数或方法时,它都会通过一个捕获异常的函数来传递它(如果传递了处理程序,则会处理它们,尽管我还没有发现实际传递异常处理程序的用例)。我已将原始函数重命名为tryFunctionCatchException
,但它很容易被称为safeExecute
function tryFunctionCatchException(mainFunction, exceptionHandler) {
try {
return mainFunction();
} catch (exception) {
if (exceptionHandler) {
return exceptionHandler(exception);
}
}
}
这个脚本通向哪里?
如您所见,该脚本可配置为将用户重定向到特定 URL(垃圾邮件网站),该 URL 必须在每个部署的此脚本的各个版本中以半加密选项进行编译。这意味着每个脚本实例的垃圾邮件网站可能不同。在我们的案例中,目标网站是某种广告服务器,提供不同的页面,大概基于拍卖(URL 包含名为auction_id
的参数)。
当我第一次点击链接时,它将我重定向到一个确实非常垃圾的网站:它在宣传基于在线交易的快速致富计划,并配有坐在暗示是他通过该计划致富后购买的新兰博基尼中的一个人的图片。目标网站甚至使用 Evercookie Cookie 来追踪用户。
我最近几次重新运行了 URL,它将我重定向到
- 一个著名的在线博彩公司的登录页面(至少是欧洲冠军联赛决赛的一家官方赞助商),配有通常的“免费投注积分”
- 几个虚假新闻网站,意大利语和法语
- 宣传“轻松”减肥计划的网站
- 宣传在线加密货币交易的网站
结论
从某种程度上来说,这是一个奇怪的脚本。它似乎是为了完全控制用户的浏览器并将其重定向到特定目标页面而创建的。理论上,如果此脚本选择这样做,它可以任意注入其他恶意脚本,例如键盘记录器、挖矿程序等。这种激进的行为(控制所有链接、拦截所有视频和其他交互元素上的点击、注入 PDF 等)似乎更像是恶意脚本,该脚本已在未经网站所有者同意的情况下添加到网站中。
但是,在我第一次发现它的一个多月后,该脚本(略微不同的版本)仍然存在于原始网站上。它限制自己拦截其他所有点击,至少部分地保持原始网站可用。鉴于该脚本已经存在这么长时间,网站的原始所有者不太可能没有注意到它的存在。
另一件奇怪的事情是,此脚本指向的是一个广告竞价服务,尽管它为非常垃圾的客户提供服务。至少有一个主要的例外:前面提到的著名博彩公司。这个脚本是一个恶意脚本,它已经演变成某种半合法的广告投放系统,尽管它非常侵入性?互联网可能是一个非常复杂的地方,很多时候事情并非完全合法或完全非法——在黑白之间总是有许多灰色地带。
在分析完此脚本后,我唯一能给你的建议是:下次你感到无法抗拒地想在线观看一场乒乓球比赛时,请前往合法的流媒体服务并付费观看。这将为你节省很多麻烦。
哇,我喜欢这篇文章。我喜欢你深入分析脚本内部工作原理的方式。
你在开头附近提到它甚至没有声明为 .js 文件,它是如何加载到页面上的?
谢谢 Jasper。如果你在
script
标签中包含该文件,则浏览器会尝试将其解释为 Javascript,类似于网页即使很少再具有 .html 扩展名也会被解释为 HTML 的事实。谢谢。Jasper。简而言之,不是。如果它是加密的,那么浏览器将无法解密它,除非它们拥有某种解密密钥。为了让任何浏览器上的加密代码运行,代码创建者和所有浏览器之间应该有一些秘密协议来提供某种解密方法。这是不可行的。
Javascript 代码可以被严重混淆、编译等,但据我所知不能加密。
哇!这就像观看程序员版的福尔摩斯。令人激动……更不用说具有教育意义了。
世界需要一个专门介绍这种内容的博客——也许 Mozilla 或其他机构可以资助你成为网络侦探?
谢谢 Casey,很高兴你喜欢。嘿 @Mozilla,你们在听吗?
我希望我能使我的代码像这个一样具有弹性!非常棒的东西。谢谢!
谢谢 Zomars。这个脚本的代码可能非常有弹性,但它需要适应每个浏览器,因为它需要利用每个浏览器的弱点来完成其工作。我认为,坚持标准,尤其是在你不需要支持旧浏览器的情况下,总是会导致更少的代码和更少的工作……
很棒的兔子洞,非常有趣的阅读
很高兴你喜欢 Todd。
一项强有力的分析(不幸的是,你任务的难度恰恰表明了模糊处理者拥有多少优势)。你关于使用括号表示法来启用对象方法和属性替换的见解很有启发性。感谢你与社区分享你的工作。
谢谢 Jeremy。括号表示法非常有用,尤其是在你需要根据应用程序配置等动态调用对象属性和方法时。例如,它在某些 OOP 设计模式中非常有用。但是,在这种情况下,我怀疑这一切都是由负责混淆和压缩的软件执行的,而不是由原始程序员执行的。
有趣的阅读,与众不同。非常感谢。
不客气!
感谢这篇有趣的阅读!PDF 的问题可能与旧版 Chrome 浏览器的错误有关,该错误可被利用来创建弹出式窗口。我在 YouTube 上找到了另一个此类脚本的不错的反汇编,从略微不同的角度出发,并解释了该漏洞:https://www.youtube.com/watch?v=8UqHCrGdxOM
感谢你解释 PDF 的问题,Tim!
我看了视频:非常有趣!我分析的脚本使用的技术非常相似。但是,我分析的那个没有许可证检查,并且可能是市场上所有可用弹出窗口技术的组合。它可能是几个预先存在的脚本的拼凑而成。
该死!我喜欢你如何深入研究这个。巨大的赞美!
非常感谢Jeff!
感谢你的帖子,我很喜欢。
这有一些高级的JS内容。我可以就基于您在此处发现的JS编码技术主题提出一个建议吗?
这是“行业如何做”类型的工作。更多人可以从中学习编写JS的良好方法和技巧。老实说,这比学习一个新的框架要好得多。
您对JS的了解足以解读如此复杂的东西。阅读您更多教育性的文章将会很有趣。
谢谢。对于开发人员来说,掌握编程语言确实更为重要,因为框架会不断变化,而编程语言的寿命则长得多,通常长达几十年。
我正在撰写新的文章。在此期间,如果您想阅读一些关于高级JavaScript的非常有趣的内容,我可以向您推荐这篇关于函数式编程的文章。
哇,编写这样的代码非常棒,但解读它并将其分解成有意义的解释性代码片段则更加伟大……我向您致敬,先生
谢谢。我相信编写原始代码(或从在线资源复制/粘贴)比我解读它要困难得多,因为我不必在所有浏览器中测试它以检查它是否有效!!
真是精彩的阅读!我想知道您在脚本攻击您时使用的是哪个版本的浏览器。(不确定您是否在 Ubuntu 虚拟机上获得了最新版本。)我原本以为正确配置的最新浏览器应该能够阻止任何此类攻击,例如您描述的弹出窗口的打开或从 iframe 中退出。
我使用的是带有 Ubuntu 18.04 的 Chromium,但坦白地说,我不记得浏览器是否受到攻击。我可能只是在网络选项卡中查看了脚本。
不要期望最新的浏览器能够阻止所有攻击,一旦某个漏洞不再有效,黑客就会寻找新的漏洞,直到找到为止。
有趣的文章。感谢分享。恶意脚本作者可以加密代码并在浏览器中仍然可执行吗?我认为加密代码会进一步混淆代码。