
交叉质询:揭露JavaScript注入以掩蔽指纹的企图
2021年 10月 21日 | 浏览器指纹
任何在互联网上寻找解决方案以掩盖自己真实“浏览器指纹”的人,都可能遇到过JavaScript注入这一方案——这是一种相对容易且便宜的方法。然而,这是否意味着它不那么安全呢?我们进行了研究,上述问题的答案为“是”;稍后您也会发现,其背后的道理是显而易见的。现在就加入我们,一起实验吧!我们将解释“JavaScript注入”的弱点,并展示我们的检测方法,包括检测代码链接,以便您自己进行检验。
什么是“JavaScript注入”式的指纹掩蔽?
目前有三种主要方法来掩蔽浏览器指纹,它们分别是:
JavaScript注入法
原生法
上述两种方法的混合
补充:我们所说的“指纹”是什么意思?
有这样一个系统,它收集并构建关于“我们是谁”的信息,形成一个独特的指纹,它极尽可能地定义我们身份的独特性,以检测我们的身份是否如我们所展示出来的那样。检测系统可以使用一些浏览器功能,即所谓的API,来提出问题。例如,它们可以问询您所用的浏览器、您的操作系统、它支持哪些语言等信息,然后结合这些信息来建立您的浏览器指纹。如果有不一致或缺失的信息,“红旗警告”就会出现。
简单的说,原生法使用基于像Chromium这样的普通浏览器(即尚未进行定制的浏览器)的可执行文件。这一方法将您的指纹的某些值替换为其他的值,以达到指纹掩蔽的目的。顾名思义,它更加地道,难以被检测出来,但需要更多的时间和资源来进行开发。
至于JavaScript(JS)注入,它指的是在页面中插入代码,以覆盖某些属性的值。让我们打个比方来解释它——我们将JavaScript注入比作警察审讯中的律师。想象一下电视剧《风骚律师》(Better Call Saul)中的“混蛋律师”Saul,他被安插在被告人和警察中间,以嫌疑人的名义回答警察的问题——目的是隐藏嫌疑人的身份。回答折射了他们模仿的其他人,而非指向谁是嫌疑人。兜兜转转,他就已经掩盖了某些特征。
以同样的方式,JS注入可以掩盖浏览器的属性,使其看起来与实际情况不同。
这也许是最常用的方法,因为它既便宜又简单。但是,正如我们将证明的那样,它的代价是容易被平台检测出来。
JS注入是通过什么方式进行部署的?
指纹掩蔽解决方案通常采用多种JS注入方式中的一种。一些人通过Selenium和Puppeteer等浏览器自动化框架来进行JS注入。部分人仍然使用浏览器扩展程序——这也许是最方便的方式。我们的研究还显示,也可能使用mitmproxy,一个开源的HTTPS代理;然而,在非特定实验场景下,我们还不知道这种方法是否可行。

通过浏览器扩展进行的JS注入是如何运作的?
正如我们在之前文章中更深入的解释那样,浏览器扩展程序是拓宽我们浏览器功能的重要方式。正如您在下图中看到的,扩展程序由一些元素组成,包括:
manifest.json:描述扩展程序的文件
内容脚本(content scripts)文件
由后台脚本(background scripts)组成的核心部分

在自然场景中测试JS注入
介绍我们的扩展程序
有趣的部分现在来了!让我们介绍一下我们自己的浏览器扩展,我们将用它作为我们的测试小白鼠。这是一个具有艺术性的指纹掩蔽解决方案,我们就称它为“风骚JS注入”吧。
考虑到关心代码的读者,我们首先展示扩展程序的核心,即manifest.json文件。它定义了我们的扩展程序的各种信息。
1Wp Block Code{2 "manifest_version": 2,3 "name": "Better JS Injection Call - Saul - A artistic solution for fingerprint masking",4 "version": "1.0.0",5 "content_scripts": [6 {7 "matches": [""],8 "js": ["bettercall.js"],9 "run_at": "document_end",10 "all_frames": true,11 "match_about_blank": true12 }13 ]14}15
接下来是扩展的内容脚本,bettercall.js。这是我们唯一需要注入我们的JavaScript代码来掩盖浏览器指纹的部分。在上述manifest.json文件的content_script中(第五行起),您可以看到一些看起来像鬼扯一样的指令。从本质上讲,它们意味着我们的内容脚本将被添加到所有的网页和框架中。换句话说,一些HTML元素可以加载我们正在访问的网页中的其他网页或HTML页面。
现在是我们扩展程序的业务逻辑部分。让我来向您介绍一下我们的bettercall.js:
1Wp Block Code2const maskLanguage = (language) => {3 Object.defineProperty(navigator, 'language', {4 get: () => language,5 });6};78const doMask = (method, ...args) => {9 const stringifiedMethod = method instanceof Function10 ? method.toString()11 : `() => { ${method} }`;1213 const stringifiedArgs = JSON.stringify(args);1415 const scriptContent = `16 (${stringifiedMethod})(...${stringifiedArgs});17 document.currentScript.parentElement18 .removeChild(document.currentScript);19 `;2021 const scriptElement = document.createElement('script');22 scriptElement.textContent = scriptContent;23 document.documentElement.append(scriptElement);2425 const scriptElement2 = document.createElement('script');26 scriptElement2.textContent = "document.body.innerHTML += 'Injected';";27 document.documentElement.append(scriptElement2);2829};3031doMask(maskLanguage, 'eo-Multilogin');32
这意味着,我们的扩展程序覆盖了navigator.language属性,把它从我们设定的语言改为“eo-Multilogin”。EO是世界语(Esperanto)的语言标记;世界语是一种人为建构的语言,旨在使全球交流对每个人来说都很容易。
这段业务逻辑代码还为所有被注入的上下文主体添加了一个 “Injected”字符串,我们将用它作为一个标志来帮助我们了解扩展程序的全面性。
将其付诸实践
让我们通过chrome://extension启用这一扩展程序,看看navigator.language发生了什么。

让我们回到此前提到的警察审讯的比方中,想象这样的场景:我们访问的主页面加载到警察局的审讯室中。理想情况下,我们期望我们的律师在整个调查过程中与我们在一起,就像我们期望全面注入……对,就是那个JS注入一样。
1Wp Block Code<html>2<head>3</head>4<body>5<br>http://www.iframetest.test/iframe.html<br>6<iframe width="100" height="55" id="normalFrame" src="http://www.iframetest.test/iframe.html"></iframe><br>7<br>about:blank<br>8<iframe width="100" height="55" id="aboutBlankFrame" src="about:blank"></iframe><br>9<br>data://<br>10<iframe width="100" height="55" id="dataFrame" src="data:text/html,<html><body></body></html>"></iframe><br>11<br>javascript:<br>12<iframe width="100" height="55" id="jsFrame" src='javascript:const a=0;'></iframe><br>13<br>sandbox<br>14<iframe width="100" height="55" id="sandboxFrame" sandbox src="http://www.iframetest.test/iframe.html"></iframe><br>15<br>srcdoc<br>16<iframe width="100" height="55" id="srcdocFrame" srcdoc="<html><head></head><body></body></html>"></iframe><br>17</body>18</html>
下方是我们看到的结果。

一键式交叉质询
我们都很熟悉(至少在电视上)警察的交叉质询技术:以不同的顺序对案件中的相同人员或要素反复提出相同问题。许多嫌犯无法忍受这样的盘问,最终出卖了自己。
我们可怜的浏览器也是如此,它发现自己并没有“律师”。下面的JavaScript代码开始了它的交叉质询,向我们使用了iframe元素的页面的网页和子页面提出同样的问题。
这是一个包含javascript:// iframe的页面:
1Wp Block Code2<html/>3<iframe id="jsFrame" src="javascript:console.log('hello');"/></iframe/>4</body/>
我们可以在交叉质询后证明,被审问的人在撒谎。我们的内容脚本,尽管被设计为注入了JavaScript以进行指纹屏蔽,但却无法在data://和javascript://框架内注入自己。反复被提出的问题的答案应该是一样的,但实际上却不是。

在比较了顶部窗口的navigator.language和javascript://框架给出的答案后,我们可以发现,测试结果的差别是一个明显的异常情况,它应被报告为一次JavaScript注入尝试。我们绕了一圈又回到了起点——我们此前强调,当指纹不一致或出现异常情况时,“红旗”警告就会立马出现。
轮到您了:您自己的实验
还有什么原因可能导致上述情况?
您想自己尝试测试吗?
这是我们全新的代码,它可用于交叉质询,以打击所有那些试图通过JS注入掩蔽浏览器指纹的行为。
请您尝试:点击查看HTML PoC文件
我们的代码收集了一些信息,如用户代理字符串、Canvas和音频指纹、WebRTC公共IP、屏幕分辨率、分别来自顶部窗口和iframe的时区值,并从收集的数据中生成一个哈希值。
这个哈希值被用作顶部窗口和javascript:// iframe的访客ID。然后,我们的代码会比较分别从顶部窗口和iframe获得的访客ID。如果这两个ID不同,那么它就会报告一个异常情况。
请注意,我们使用javascript:// iframe,而不是data:// iframe,因为我们的实验表明,一些浏览器自动化框架能被成功注入data:// iframe,但不能被注入javascript:// iframe。
热门扩展程序中的JavaScript注入
除了采用JS注入进行指纹掩蔽这一商业化的解决方案外,谷歌Chrome网络商店有数百个扩展程序,它们“承诺”通过JS注入隐藏或模仿一些浏览器属性,如Canvas哈希值、用户代理字符串等。
我们分析了一些下载量大的解决方案,通过我们在上文中使用的方法,它们都能被检测出来。一些扩展程序在正常的框架内甚至都不注入JavaScript代码,这无疑是更糟糕、更笨拙的方法!
我们通过查看其manifest.json文件中的content_script部分进行分析,并得出以下结论:

谷歌提供的User-Agent Switcher for Chrome(djflhoibgkdhkhhcedjiklpkjnoahfmg)有200万次下载量,然而,尽管它声称可以改变User-Agent字符串,却并不能覆盖navigator.userAgent值。考虑到这些细节,我们必须明白,这是值得重视的经验教训。该扩展只拦截HTTP请求并修改User-Agent HTTP头字段。
结论:JavaScript注入具有显著的风险
我们从实验中得出一个结论:对任何依赖指纹掩蔽解决方案以管理多个账号或浏览器配置文件的企业来说,通过JavaScript注入来掩盖浏览器指纹,存在重大风险。
这种方法操作容易而廉价,所以它是一个广泛被使用的解决方案;但正如我们的实验所揭示的那样,它根本不可靠,很容易被发现。即使是混合解决方案也不会降低风险——虽然解决方案的一部分可能得到较好的保护,但通过JS注入进行指纹掩蔽的这一部分,和非混合解决方案一样,很容易被检测出来。
这就是为什么Multilogin使用自己原生的解决方案——基于Chromium的Mimic浏览器和基于Mozilla Firefox代码库的StealthFox浏览器。这两种浏览器解决方案避开了JavaScript注入法,绕过重重危险,因此规避了风险,是万无一失的解决方案。有了这样的工具,您便无需再承担JavaScript注入带来的不必要的风险。
如果您喜欢这篇文章,请进行分享吧!并请您订阅我们的博客和微信公众号Multilogin,获取行业最新的研究。