恶意脚本剖析:网站如何接管你的浏览器

Avatar of Paolo Mioni
Paolo Mioni 发表

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费积分!

现在,我们都知道像 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 只是一个数字。

我们的嫌疑人,在网络选项卡中发现。

我查看了构成脚本代码大部分内容的看似无限的几行混淆代码,并找到了诸如chromePDFPopunderNewadblockPopupflashFileUrl、转义的<script>标记,甚至包含内联 PDF 的字符串。这看起来很有趣。搜索结束了!我将脚本下载到我的电脑上,并开始尝试理解它。

我不会明确披露参与此操作的域名,因为我们对这里的罪恶感兴趣,而不是罪人。但是,我故意留下了确定脚本将用户发送到的主要 URL 的方法。如果您设法解开了谜团,请向我发送私信,我会告诉您是否猜对了!

脚本:去混淆和确定配置参数

脚本的外观

该脚本被混淆,既是为了安全目的,也是为了确保更快的下载速度。它由一个大型 IIFE(立即调用函数表达式)组成,这是一种用于将 JavaScript 代码片段与其周围环境隔离的技术。上下文不会与其他脚本混合,并且不同脚本中函数或变量名称之间不存在命名空间冲突的风险。

以下是脚本的开头。请注意最后一行上 base64 编码的 PDF 的开头

这是脚本的结尾

显然,在全局上下文中执行的唯一操作是将全局变量zfgloadedpopup设置为 true,大概是告诉属于同一“家族”的其他脚本此脚本已加载。此变量仅使用一次,因此脚本本身不会检查它是否已加载。因此,如果您访问的网站错误地包含了两次,您将以相同的价格获得双倍的弹出窗口。幸运!

大型 IFEE 期望两个参数,分别称为optionslary。我实际上检查了第二个参数的名称以了解其含义,我找到的唯一含义是英国俚语中的“侵略性,反社会”。“所以,我们在这里很强势,”我想。“有趣。”

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'

…也就是说,将windownavigator属性保存到名为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函数在脚本中被广泛使用,因此它是我首先要处理的函数之一。它期望两个参数,WO,并准备一个初始化为空数组的返回值(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,因为它不会返回所有值,而是选择其中一些。它也不仅仅是过滤它们,因为它没有简单地检查truefalse并返回原始元素。它有点像两者的混合体。

我必须重命名此函数,就像我对大多数其他函数所做的那样,给它一个易于理解并解释函数目的的名称。

由于此函数通常在脚本中用于以某种方式转换原始对象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 作为背景图像,位于原始文档中存在的每个 ``、`