背景
我曾经遇到过一个有横幅广告的网站,当一个adblock或者类似的程序删除了横幅广告元素,这个网站福尔斯向alerts
发送垃圾邮件,说它无法加载广告,这使得浏览体验比广告更糟糕。
我喜欢修修补补,试图打破像这些网站上的“保护”,然而,在这个时候我已经没有主意了。
问题中的网站似乎采用了广泛的保护措施,防止篡改一般,所以,虽然这个问题是“简单地”做一些与对象冻结在头部,有多个限制,以如何做到这一点。
问题是
首先,我不想通过对横幅做一些特殊的处理来解决这个问题,比如让它们可见而不是删除它们,我想攻击alert
。
问题是,当网站加载它的index.html
时,它会冻结<head>
部分中的window.alert
对象,如下所示:
<script>
Object.defineProperty(window, 'alert', {
configurable: false,
writable: false
})
</script>
之后就不可能了,例如:window.alert = null;
考虑到我们无法触及警报本身,您可能会想到尝试查找并删除对警报的调用...
...不幸的是,这似乎是不可能的,因为该网站将无法工作没有JavaScript和网站加载其脚本的其余部分与CSRF-token.所有加载的脚本也使用CSRF-token在每个请求.
网站最初在头部加载一个“加载器脚本”,它受源策略保护,因此无法从其他任何地方提供此脚本。此加载器脚本本身使用CSRF令牌通过XMLHttpRequest加载其余脚本。
还有content-security-policy-头文件。
这基本上意味着不可能,例如,让一个网络服务器充当代理,它将加载网页,剥离头部的冻结脚本,并传递修改后的网页。至少就我所知,这正是CSRF令牌所防止的。
□ □ □我在哪里
我正在使用一个名为Resource Override的chrome扩展,例如,它可以让我:
- 为网站设置基于正则表达式的规则和操作
- 进一步设置基于正则表达式的URL规则和操作,当网站请求一些特定的URL
- ·“响应”网站做出的任何请求,并以例如本地服务的文件进行响应,其限制是,该本地服务的文件将明显地以需要CSRF令牌的一切失败
- 在网站上的任意位置注入JavaScript,只要网站已加载
- 修改请求和响应标头
在头部注射js
对我来说,显而易见的第一步是简单地将一个window.alert = null
注入到网站的<head>
中,但这并不起作用,因为当页面和页面的html加载时,对象冻结已经存在于头部。
此外,这种方法永远不起作用,因为只有当存在要注入脚本的页面时,才能将脚本注入页面,但是在这种情况下,当这样的页面存在时,对象冻结代码也已经存在。
解冻窗口.alert对象
有很多关于在JavaScript中解冻/解冻冻结对象的Stackoverflow问题和答案。简而言之,这是不可能的。至少在这里没有帮助。
原型污染
老实说,我并不太熟悉原型污染攻击的应用,但我确实研究了一下,因为我觉得摆弄__proto__
可以让我在某种程度上使window.alert
可配置或可写。
我用我所掌握的知识尝试过的一件事是简单地扩展Object.prototype
,它是所有对象的“根原型”,带有一个名为alert的属性,因此:
Object.prototype.alert = 999;
它的作用是当你做这样的事情时:
const arr = [1, 2, 3];
const str = "abc";
for(prop in arr) {
console.log(prop) // will log alert at some point
}
console.log(str.alert) // will log 999
- 有趣的是,正确获取对象的所有可枚举属性名(包括原型链中的属性名)的唯一方法(警报),似乎是使用一个
for...in
-循环,如图所示。所有其他方法,如方便命名的Object.getOwnPropertyNames
,将只直接返回对象上的属性,但不是它的原型链。可以在here中找到相关文档。*
不管怎样,这个想法是:
**A)**幸运的话,也可以覆盖window.alert,因为window也是一个对象,因此应该通过Object.prototype.x进行扩展
**B)**运气好一点,希望网站以某种荒谬的方式调用window.alert
,这样在所有内容上都使用alert-property就会把事情搞砸
不出所料,这两件事都没有发生。
代理对象
我还尝试了一下Proxies,最初的理解是它可以让我“挂钩”到函数调用中,这样我就可以拦截对alert-function本身的调用,但这不是代理的工作方式,所以吸取了教训。
不过这确实提出了一个问题,是否有可能以其他方式拦截/侦听JavaScript中的函数调用。显然,我可以用自己的函数 Package 任何函数,但这样一来,所有函数都需要调用 Package 函数,而不是原来的函数,这在这里没有帮助。
反映API
还有一个JavaScript equivalent of a "Reflection API",但从外观和我做的一些测试来看,它也没有提供任何关于挂钩/侦听/拦截/重定向函数调用的功能,尽管文档somewhy指出:
Reflect是一个内置对象,为可拦截的JavaScript操作提供方法。
使用Chrome进行本地覆盖
由Chrome本地覆盖修改的脚本无法通过某种完整性检查,因此将被阻止。
其他想法
像往常一样,这些事情,我摆弄着各种不同的杂项想法,如:
- 试图替换整个窗口对象,希望“解锁”窗口。alert
- 天真地尝试将window.alert设置为可配置和可写
- 尝试了
Object.freeze = null
,但在可以将其设置为null或进行修改之前,它可能已经被调用
现在怎么办
在我看来,任何事情都是可能的,尽管在这种情况下,解决方案可能会很复杂。我想我已经用尽了大多数快速和简单的方法,但我希望我错过了一些东西。
对我来说,一个仍在进行中的想法是首先尝试拦截和剥离内容安全策略头,以更好地允许拦截和修改获取脚本的请求。我也不确定这最终是否有效,因为似乎还有某种完整性验证正在进行。
有趣的是,似乎没有针对内联脚本的政策,所以内联脚本确实有效。正因为如此,我对基于JavaScript的解决方案最感兴趣,但我会接受任何东西。
我也会非常感兴趣想知道是否有一种方法可以运行JavaScript或者在加载网站之前设置一个“修改过的JavaScript上下文”?我想人们可以修补V8并删除其核心的整个警报功能,但我正在寻找比这少一点的东西。
总结
- 网站正在发送垃圾邮件
window.alert
- 网站冻结警报对象,使其不可修改
- 网站需要JavaScript才能运行
- 网站广泛使用CSRF令牌,甚至在加载脚本时也是如此,因此简单地交换文件是不可能的
- Website使用XMLHttpRequest加载所有重要脚本,并从受CSRF令牌保护的脚本中执行此操作,该脚本本身受源策略保护
- 网站使用内容安全策略标头
- 脚本文件具有适当的完整性检查,因此无法使用例如chrome本地覆盖修改
*目标是“简单地”禁止此类网站发出警报
最后的话
我不会感到惊讶,因为它的广泛性,只是得到一个核弹,但至少我有乐趣写它。我发现这真的很有趣,如何看似良好的网站,以及它的具体功能,如警报功能,可以防止篡改这些天-调查这一直是一个大开眼界给我。
即便如此,这是相当有点可笑多少不同的措施需要使用;来源检查,完整性检查,散列内联脚本,内容安全策略,CSRF令牌,脚本加载在非常特定的方式和非常特定的地方,...所有这些都是为了防止我设置一个小警报函数为空在某种意义上!
如果有一个外卖从所有这一切,这将是 * 巨大的问题 *,为什么在永恒的f-是不可能简单地阻止网站发送警报?有权限控制的各种功能,那么为什么警报和提示不是其中之一?
编辑
正如我在回答中所说的,事实证明该网站的防篡改措施比我预期的还要广泛,因此我在这里包括了我在事后挖掘出来的内容。即使有人设法使alert对象可写,它仍然不能被轻易替换,因为这将触发额外的显式防篡改检查。
在我的回答中,我将跳过所有这些检查,但下面是我们要处理的问题:
tamperCheck()
{
for (const check of [
{ob: Function.prototype, fun: 'toString'},
{ob: window, fun: 'alert'},
{ob: window, fun: 'confirm'},
{ob: Object, fun: 'freeze'},
{ob: Object, fun: 'seal'},
{ob: Object, fun: 'getPrototypeOf'},
{ob: Object, fun: 'getOwnPropertyDescriptors'},
]) {
const regex = new RegExp('^function ' + check.fun + '\\(\\)\\s*{\\s*\\[native code]\\s*}$');
let d;
try {
d = delete check.ob[check.fun]['toString'];
} catch {
d = false;
}
if (
!d
|| typeof Object.getPrototypeOf(check.ob[check.fun]) !== 'function'
|| typeof check.ob[check.fun] !== 'function'
|| check.ob[check.fun].name !== check.fun
|| check.ob[check.fun].toString().replaceAll('\n', '').match(regex) === null
) {
return true;
}
Object.defineProperty(check.ob, check.fun, {value: check.ob[check.fun], configurable: false, writable: false});
}
return false;
}
1条答案
按热度按时间r7knjye21#
溶液
嗯,我无法入睡,直到我打败了这个东西,所以我做到了。这个解决方案被证明是相当复杂的,希望有更容易和更简单的解决方案在那里,但这是我的。
概述
这个解决方案需要创建一个chrome扩展。Manifest V2 extension是具体的。不幸的是,Manifest V2 extension将在2023年变得不受支持,正如我的扩展中的错误所示:
清单版本2已弃用,并且将在2023年移除支持。有关详细信息,请参阅https://developer.chrome.com/blog/mv2-transition/。
但是现在这个方法是可行的,我对chrome扩展不是很熟悉,所以有可能用Manifest V3扩展也能做到。
该扩展可以做很多事情:
run_at: "document_start"
内容脚本选项来尽早执行脚本的某些部分(感谢@evolutionxbox)<script>
节点之前拦截这些节点window.alert
的页面window.alert
,使其无法进行防篡改检查绕过内容安全策略
当我切换到Manifest V2时,这个非常简单。我无法让它与Manifest V3扩展一起工作。我们使用declarativeNetRequest API让扩展修改content-security-policy头。
manifest.json需要以下属性(在常用属性之上):
rules.json看起来像这样:
这样的manifest.json与rules.json结合在一起,使得扩展能够修改content-security-policy头,从而可以将内联脚本注入到页面中。The documentation here解释了为什么我们显然必须使用V2 manifest版本。
运行时间:“文档开始”
要在document_start运行扩展,manifest.json需要包含以下选项块:
这将使扩展在document_start处执行inject.js中的任何内容,即使文档还没有加载。
拦截静态
<script>
节点创建既然我们的脚本可以足够早地执行,那么在文档还没有加载之前,我们就可以设置一个MutationObserver来拦截添加到Web页面的静态服务
<script>
节点。这是inject.js的开头:
我创建了一个ScriptInterceptor类,它在内部创建了一个MutationObserver示例,以监视整个文档中元素子列表或子树的任何变化。
当MutationObserver注意到一个或一组DOM突变时,我们会检查每个突变和受影响的DOM节点,同时过滤掉所有不是
<script>
节点的内容。当我们遇到一个
<script>
节点时,我们调用一个handler函数,并将找到的节点传递给它。在本例中,我们的handler函数如下所示:其中
scriptTrace
是冻结window.alert
对象的特定<script>
元素的代码内容:这样就消除了对象的冻结,但是我们仍然没有对
window.alert
本身做任何事情来禁用它。关于MutationObserver和静态提供的
<script>
块的旁注所以我在这里使用了很多术语static,这是有原因的。MutationObserver方法不适用于动态添加到页面的脚本,例如这样的脚本:
虽然MutationObserver会捕捉到它们被添加到页面,但它只会在脚本已经执行之后才这么做。
我的情况并不需要像这样处理拦截脚本,但我确实出于兴趣找到了一个解决方案,如果有人会发现自己在这种情况下:
这只是一个样板文件,但这里的想法是,我们实际上可以覆盖所有可能用于将
<script>
标记插入到页面中的方法,并由它们自己锁定它们。关键是不要让这些方法为空,而是在那里实现某种逻辑,以捕获并取消感兴趣的动态脚本的插入,并使其他一切正常工作。不过知道了也不错。修补
window.alert
并使其不可检测js脚本的其余部分如下所示:
我们在这里做的事情本质上很简单--我们将
window.alert
转换为一个什么都不做的函数() => { }
。然而,当我说到这里时,网站开始变砖了,因为我的
window.alert
补丁被网页中的一些反篡改检查检测到了。深入挖掘后,我发现了下面的篡改检查函数(我也将把它编辑到原始问题中):它看起来有点混乱,但我们可以让它更简单,如果我们说:
check.ob = window
和check.fun = alert
首先,我们可以将RegExp-line中的
check.fun
替换为“alert”:型
这个正则表达式在这里还没有用到,但是我会回到它上面。接下来我们有try... catch块:
这实际上是检查是否可以成功执行
delete window["alert"]["toString"]
。如果我们看一下我的代码:型
我自己正在冻结alert对象,因此此
delete
操作将失败,因为您无法从冻结的对象中删除toString
之类的属性或方法。这是代理对象第一次发挥作用。正如你从我的代码中所读到的,我们实际上并没有设置
window.alert = alert
,而是做了window.alert = alertProxy
。这个alertProxy基本上是我们前面定义的const alert = () => { }
,但它有一个额外的好处,我们可以为它指定特定的处理程序:为了绕过这个删除检查,我们“钩住”了
delete window["alert"]["toString"]
执行时调用的删除函数。我们覆盖了默认行为,不删除任何东西,而是简单地说“好的,它被删除了!"我们不能删除那个toString
,因为我们已经覆盖了它,我们以后需要它。接下来是
typeof Object.getPrototypeOf(check.ob[check.fun]) !== 'function'
检查,我们可以写为:这将检查
window.alert
的原型是否为函数().这不一定是个问题,因为我们的“警报参与者”应该是一个函数,但我记得根据经验,在某些情况下,使用代理可能会触发这样的检查。如果我们想要或必须使用对象而不是函数作为警报参与者,这也是这个检查失败的时候。如果需要的话,这可以用getPrototypeOf
处理程序绕过,我们在antiTamperCheckFaker
中有这个处理程序。()虽然它清楚地表明了!== 'function',但如果任何一个检查为真,则if子句作为一个整体返回true,因此,如果
window.alert
不是函数,则检查的该部分将计算为true,从而使检查返回true,这是错误的。然后是
check.ob[check.fun] !== 'function'
检查,它检查window.alert
本身是否是一个函数,同样,我们用来替换window.alert
的警告只需要是一个函数。window.alert
,我们可以更容易地绕过这些检查,但是这个检查本质上是一个要求,即无论我们用什么替换window.alert
,它都必须是某种函数。*然后我们进行
check.ob[check.fun].name !== check.fun
检查。这一次检查window
中的当前alert
函数的名称是否实际上不是alert。如果我们有以下情况,这将失败:const myPatchedAlert = () => { }
而不是:
const alert = () => { }
在这种情况下,
check.ob[check.fun].name
将评估为“myPatchedAlert”。然后我们有一张更大的支票
现在这个很有趣,你可以看到如果你在你的开发控制台输入
window.alert.toString()
会发生什么,但是我会破坏它,告诉你它会输出'function alert() { [native code] }'
,我们之前看到的难看的正则表达式,所以这个:基本上就是做一个比较,比如:
window.alert.toString() === 'function alert() { [native code] }'
当我们的警报
const alert = () => { }
被.toString()
调用时,它将输出() => { }
,这将使检查失败。幸运的是,我们覆盖了警报的toString()
方法,以返回类似的[native code]:虽然创建我们自己的
toString()
方法并让它返回我们想要的任何东西是很容易的,但请记住,在运行此检查之前,篡改检查实际上试图从警报中删除toString()
方法。我们通过冻结自己的警报来防止
toString()
方法被删除,但是我们还必须拦截删除操作,以便检查认为它成功地删除了不可删除的toString()
。顺便说一句,我们不能使用
Function.prototype.toString = ...
,因为Function.prototype.toString = ...
会保护它不被删除,因为它会是一个原型函数而不是alert的成员,因为检查也会检查Function.prototype
的toString
是否被篡改。事实证明并非如此,Bergi给出了一个聪明的基于
Function.prototype
的解决方案:无论如何
我得道歉。这整件事有点失控。所以这里可能不是我经历过的自我实现冒险的最佳地点。
我的辩护是,我确实提出了很多不错的解决方案,我想,这些方案涉及到扩展、脚本注入、防篡改方法和浏览器安全性等不寻常的问题。希望有人会发现自己在这里寻找答案。
完整文件
清单. json
规则. json
进样. js