跳到主要内容

堆栈异常抛出的利用技巧

其实这些方法已经在不同的篇章和问答之中提到很多次了

所以决定单独开一篇整理一下目前的利用方法

底层API劫持利用堆栈回溯检查函数信息

很多网页会为了防止脚本对基础API进行劫持,会对调用进行一层封装

从而导致即使劫持了,因为上层传入的函数没有任何特征,导致无从下手

例如

function runTimer(callback, time=1000) {
setTimeout(() => {
callback();
}, time);
}
function exampleA() {
runTimer(() => {
console.log("A");
});
}
function exampleB() {
runTimer(() => {
console.log("B");
});
}

exampleA()
exampleB()

因为对setTimeout劫持得到的一定是一个箭头函数,根本无法无从下手

这个时候我们就可以尝试制造一个Error错误,然后利用错误对堆栈进行检查

从而找到一个有特征的上层函数,来实现通过基础AP进行劫持和过滤操作

我们可以尝试一下

const originSetTimeout = window.setTimeout;
window.setTimeout = function (callback, time) {
const err = new Error("大赦天下");
console.log(err);
return originSetTimeout.call(this, callback, time);
};

打印的输出内容有

Error: 大赦天下
at window.setTimeout (1.js:3:16)
at runTimer (1.js:8:3)
at exampleA (1.js:13:3)
at 1.js:23:1
Error: 大赦天下
at window.setTimeout (1.js:3:16)
at runTimer (1.js:8:3)
at exampleB (1.js:18:3)
at 1.js:24:1

可以发现可以通过exampleA和exampleB的字样来选择过滤函数,例如我们想要过滤掉exampleA

const originSetTimeout = window.setTimeout;
window.setTimeout = function (callback, time) {
const err = new Error("大赦天下");
if (err.stack.indexOf("exampleA") !== -1) {
return -1;
}
return originSetTimeout.call(this, callback, time);
};

现在开始只有exampleB可以正常调用runTimer的setTimeout来等待到时间回调输出内容了

利用异常抛出终止JS文件执行

当整个文件我们都想阻断执行,或者找不到注入点的时候

可以利用该方法来阻断文件的执行

其原理是因为现代前端会使用打包工具进行打包,或框架/库存在初始化代码

所以通常一个文件的开头会调用一些基础API,我们对基础API进行劫持

当发现堆栈来自于某个我们想要拦截的文件,就抛出一个异常

由于这个异常的出现,会导致JS文件的终止,从而杀死后续的代码执行

例如现在存在两个JS文件,代码分别为

// A.js
(window.initArr ?? (window.initArr = [])).push("AFile");
console.log("A")
// B.js
(window.initArr ?? (window.initArr = [])).push("BFile");
console.log("B")

我们将window.initArr视为文件的初始化流程的模拟

这时候可以针对push劫持,并且制造一个Error检查是否来自A.js

window.initArr = window.initArr ?? [];
const originPush = window.initArr.push;
window.initArr.push = function (...args) {
const err = new Error("大赦天下");
if(err.stack.indexOf("A.js")!=-1){
throw Error("Kill A.js")
}
return originPush.call(this, ...args);
};

可以发现A.js执行到第一条由于异常直接被强制终止了,而B.js可以正确执行,打印输出如下

inject.js:6 Uncaught Error: Kill A.js
at window.initArr.push (inject.js:6:11)
at A.js:1:43
B.js:2 B

这个时候无论是想要彻底不再执行这个文件,还是在阻断后在gm_xhr请求这个文件再去执行都没问题了

当然抛出一个错误不好看,我们可以再通过onError捕获抛出的错误,如果是我们自己的就pass掉

这样就很干净了!

window.onerror = function(message, source, lineno, colno, error) {
if(error.message=="Kill A.js"){
return true
}
};