初版的问题
常见的 setInterval debugger 拦截写法有几个缺陷:
(function(){
window._setInterval = window.setInterval; // 污染全局
window.setInterval = function (fn, delay, ...args) {
let code = typeof fn === "function" ? fn.toString() : fn;
if (code.includes("debugger")) {
return null; // ❌ 返回 null 会让部分站点报错
}
return _setInterval(fn, delay, ...args); // ❌ _setInterval 在闭包外查找可能丢失
};
})()
问题汇总:
fn.toString()可被重写 getter 绕过- 返回
null时,站点调用clearInterval(null)或存 timer ID 会报错 _setInterval挂到window上,污染全局命名空间- 没有 hook
clearInterval,假 ID 泄漏
改进版:Proxy + 假 timer ID
(() => {
const rawSetInterval = window.setInterval;
const rawClearInterval = window.clearInterval;
const fakeTimers = new Set();
let fakeId = 1_000_000;
function shouldBlock(fn) {
try {
let code = "";
if (typeof fn === "function") {
code = Function.prototype.toString.call(fn); // 绕过 toString 重写
} else if (typeof fn === "string") {
code = fn;
}
if (!code) return false;
if (/\bdebugger\b/.test(code)) {
console.warn("拦截到 debugger interval:", code);
return true;
}
if (/while\s*\(\s*true\s*\)/.test(code)) {
console.warn("拦截到死循环 interval:", code);
return true;
}
} catch (e) {
console.error("检测 interval 出错:", e);
}
return false;
}
window.setInterval = new Proxy(rawSetInterval, {
apply(target, thisArg, args) {
const [fn] = args;
if (shouldBlock(fn)) {
const id = fakeId++;
fakeTimers.add(id);
return id; // 返回真实格式的假 ID,不返回 null
}
return Reflect.apply(target, thisArg, args);
}
});
window.clearInterval = new Proxy(rawClearInterval, {
apply(target, thisArg, args) {
const [id] = args;
if (fakeTimers.has(id)) {
fakeTimers.delete(id); // 吞掉对假 ID 的 clearInterval 调用
return;
}
return Reflect.apply(target, thisArg, args);
}
});
console.log("setInterval hook 已启动");
})();
关键设计点
为什么用 Function.prototype.toString.call(fn) 而不是 fn.toString()
站点可以重写 fn.toString 或者在函数对象上挂 getter:
const fn = () => {};
fn.toString = () => "() => {}"; // 伪造源码
而 Function.prototype.toString 直接从函数底层获取源码,无法被实例属性覆盖。
为什么返回假 timer ID 而不是 null
很多站点会存 timer ID 用于后续 clearInterval:
const tid = setInterval(check, 100);
// 之后
clearInterval(tid);
如果 tid === null,clearInterval(null) 在部分引擎下会抛异常,或者日志里产生噪声。返回 1_000_000 以上的数字既像真实 ID,又不会和浏览器分配的 ID 冲突。
为什么同步 hook clearInterval
假 ID 进入 fakeTimers Set 之后,站点持有这个 ID 并在后续调用 clearInterval(fakeId)。如果不 hook,这个调用会传给原生 clearInterval,可能误清真实定时器(原生 ID 碰巧相同)或产生错误。
为什么用 Proxy 而不是直接赋值
Proxy 保留了 length、name 等函数元信息,instanceof 检测也更接近原生。部分站点会用 typeof setInterval === "function" 或特征检测来判断是否被 hook,Proxy 比直接赋值的闭包函数更难被识别。
可扩展检测项
如果需要更宽泛的反调试拦截,可以在 shouldBlock 里补充:
// 检测 setTimeout debugger(同理 hook setTimeout)
// 检测 constructor/eval 执行 debugger 字符串
if (/\bFunction\s*\(/.test(code)) return true;
if (/\beval\s*\(/.test(code)) return true;
现代站点已经较少用 setInterval(debugger) 这种低级方式,更多见的是混淆后的字节码或 WebAssembly 版本的反调试,这类情况 setInterval hook 就无法覆盖了。
