js
(function() {
'use strict';
console.log('[Anti-Debug Bypass] 初始化反调试绕过...');
// ==================== 方法1: 拦截 Function 构造函数 ====================
const originalFunction = window.Function;
const FunctionConstructorBackup = originalFunction.bind(null);
window.Function = new Proxy(originalFunction, {
apply(target, thisArg, args) {
const body = args[args.length - 1];
// 检测是否包含 debugger
if (typeof body === 'string') {
// 移除 debugger 语句
if (body.includes('debugger')) {
console.log('[Anti-Debug Bypass] 阻止了 Function 构造函数中的 debugger');
// 替换为空语句或无害代码
const cleanedBody = body
.replace(/debugger/g, '//debugger removed')
.replace(/;\s*;/g, ';');
// 构建新的参数
const newArgs = [...args];
newArgs[newArgs.length - 1] = cleanedBody;
return target.apply(thisArg, newArgs);
}
}
return target.apply(thisArg, args);
},
construct(target, args, newTarget) {
const body = args[args.length - 1];
if (typeof body === 'string' && body.includes('debugger')) {
console.log('[Anti-Debug Bypass] 阻止了 Function 构造中的 debugger');
const cleanedBody = body.replace(/debugger/g, '//debugger removed');
const newArgs = [...args];
newArgs[newArgs.length - 1] = cleanedBody;
return target.construct(args, newTarget);
}
return target.construct(args, newTarget);
}
});
// ==================== 方法2: 拦截 setInterval ====================
const originalSetInterval = window.setInterval;
window.setInterval = new Proxy(originalSetInterval, {
apply(target, thisArg, args) {
const fn = args[0];
try {
const fnStr = typeof fn === 'string' ? fn : (fn ? fn.toString() : '');
if (fnStr.includes('debugger')) {
console.log('[Anti-Debug Bypass] 阻止了 setInterval 中的 debugger');
return NaN; // 返回无效的 timer ID
}
} catch (e) {}
return target.apply(thisArg, args);
}
});
// ==================== 方法3: 拦截 setTimeout ====================
const originalSetTimeout = window.setTimeout;
window.setTimeout = new Proxy(originalSetTimeout, {
apply(target, thisArg, args) {
const fn = args[0];
try {
const fnStr = typeof fn === 'string' ? fn : (fn ? fn.toString() : '');
if (fnStr.includes('debugger')) {
console.log('[Anti-Debug Bypass] 阻止了 setTimeout 中的 debugger');
return NaN;
}
} catch (e) {}
return target.apply(thisArg, args);
}
});
// ==================== 方法4: 拦截 requestAnimationFrame ====================
const originalRAF = window.requestAnimationFrame;
window.requestAnimationFrame = new Proxy(originalRAF, {
apply(target, thisArg, args) {
try {
if (args[0] && args[0].toString && args[0].toString().includes('debugger')) {
console.log('[Anti-Debug Bypass] 阻止了 rAF 中的 debugger');
return NaN;
}
} catch (e) {}
return target.apply(thisArg, args);
}
});
// ==================== 方法5: 欺骗 DevTools 检测 ====================
// 伪造窗口尺寸,使 outerWidth === innerWidth
Object.defineProperty(window, 'outerWidth', {
get: function() {
return window.innerWidth;
},
configurable: true
});
Object.defineProperty(window, 'outerHeight', {
get: function() {
return window.innerHeight;
},
configurable: true
});
// ==================== 方法6: 禁用 eval 中的 debugger ====================
const originalEval = window.eval;
window.eval = new Proxy(originalEval, {
apply(target, thisArg, args) {
const code = args[0];
if (typeof code === 'string' && code.includes('debugger')) {
console.log('[Anti-Debug Bypass] 阻止了 eval 中的 debugger');
// 移除 debugger 后执行
const cleanedCode = code.replace(/debugger/g, '');
if (cleanedCode.trim()) {
return target.apply(thisArg, [cleanedCode]);
}
return undefined;
}
return target.apply(thisArg, args);
}
});
// ==================== 方法7: 禁止脚本清空我们的保护 ====================
// 防止页面脚本删除我们设置的 hooks
const originalDeleteProperty = Object.prototype.__lookupSetter__ ? Object.prototype.__lookupSetter__('delete') : null;
// 保护关键的 hook 函数
Object.defineProperty(window, 'Function', { configurable: false, writable: false });
Object.defineProperty(window, 'setInterval', { configurable: false, writable: false });
Object.defineProperty(window, 'setTimeout', { configurable: false, writable: false });
Object.defineProperty(window, 'eval', { configurable: false, writable: false });
// ==================== 方法8: 禁用 console 访问检测 ====================
let consoleAccessCount = 0;
const originalConsole = Object.assign({}, console);
// 创建假的 getter 来检测 console 访问
Object.defineProperty(window, 'console', {
get: function() {
consoleAccessCount++;
// 如果被频繁访问,可能是反调试检测
if (consoleAccessCount > 100) {
// 返回一个空的 console 对象
return {
log: function() {},
warn: function() {},
error: function() {},
info: function() {},
table: function() {},
trace: function() {},
debug: function() {}
};
}
return console;
},
set: function(val) {},
configurable: false
});
console.log('[Anti-Debug Bypass] 反调试绕过已启用!');
console.log('[Anti-Debug Bypass] 保护的方法: Function, setInterval, setTimeout, eval, requestAnimationFrame');
})();
// ==================== Playwright/Puppeteer 使用示例 ====================
/*
// Playwright:
await page.addInitScript(() => {
// 将上面的绕过脚本放在这里
});
// Puppeteer:
await page.evaluateOnNewDocument(() => {
// 将上面的绕过脚本放在这里
});
*/
// ==================== Selenium 使用示例 ====================
/*
// 使用 Chrome DevTools Protocol
driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
'source': '''
// 将上面的绕过脚本放在这里
'''
});
*/
这段代码是一个用于绕过网站反调试机制的JavaScript脚本。它通过拦截和修改关键的JavaScript内置函数和属性,阻止网站利用 debugger 语句进行无限断点调试,同时试图欺骗开发者工具的检测。下面我会详细解析代码的各个部分及其工作原理。
1. 反调试机制概述
根据注释,网站的反调试机制主要有三个特点:
- 使用
Function('debugger')()动态生成函数并执行,造成无限debugger中断。 - 检测开发者工具是否打开(例如通过窗口尺寸差异、
console访问频率等)。 - 使用递归函数持续检测,防止被简单绕过。
2. 绕过脚本的总体设计
脚本采用立即执行函数,在页面加载时注入,对多个可能导致 debugger 的全局函数进行代理(Proxy)拦截,并修改一些属性来欺骗检测。同时,它还试图保护自己的修改不被后续脚本覆盖。
3. 各绕过方法的详细解析
方法1:拦截 Function 构造函数
javascript
const originalFunction = window.Function;
window.Function = new Proxy(originalFunction, { apply, construct });
原理 :Function 构造函数通常用于动态创建函数,例如 new Function('debugger') 会创建一个包含 debugger 语句的函数。脚本通过 Proxy 拦截 Function 的调用(apply)和构造(construct),检查最后一个参数(函数体)是否包含字符串 'debugger'。如果包含,则将其替换为 '//debugger removed' 或空语句,从而移除 debugger。
优点 :直接消除了最经典的 debugger 注入方式。
局限 :如果网站使用 Function.prototype.constructor 而非直接调用 Function,或者函数体不直接包含 'debugger' 字符串(例如通过变量拼接),则可能无法拦截。
方法2~4:拦截 setInterval、setTimeout、requestAnimationFrame
javascript
window.setInterval = new Proxy(originalSetInterval, { apply });
// 类似处理 setTimeout 和 requestAnimationFrame
原理 :这些定时器函数常被用来反复执行 debugger 函数。脚本通过检查传入的第一个参数(函数或字符串)的字符串表示是否包含 'debugger',如果包含则返回 NaN(无效的定时器ID),从而阻止 debugger 的执行。
优点 :可以阻止通过定时器持续触发的 debugger。
局限 :如果传入的函数不是字符串,而是通过其他方式间接执行 debugger(例如调用另一个函数),则无法通过简单的字符串包含检测识别。
方法5:欺骗开发者工具窗口尺寸检测
javascript
Object.defineProperty(window, 'outerWidth', {
get: function() { return window.innerWidth; }
});
原理 :许多网站通过比较 window.outerWidth 和 window.innerWidth 的差值来判断开发者工具是否打开(因为开发者工具会占用窗口部分区域)。脚本将 outerWidth 和 outerHeight 的 getter 改为返回 innerWidth/Height,使得两者始终相等,从而欺骗这种检测。
优点 :简单有效,能绕过基于窗口尺寸的检测。
局限 :如果网站采用更复杂的检测(如检查 outerWidth - innerWidth 的精确值、监听 resize 事件或使用 devtools-detect 库的其他特征),则可能仍会被识别。
方法6:拦截 eval
javascript
window.eval = new Proxy(originalEval, { apply });
原理 :类似于 Function 拦截,检查传入的代码字符串是否包含 'debugger',若有则移除后执行,若移除后为空则返回 undefined。
优点 :防止通过 eval('debugger') 执行。
局限 :同样只适用于字符串直接包含 'debugger' 的情况。
方法7:保护修改后的函数不被重新定义或删除
javascript
Object.defineProperty(window, 'Function', { configurable: false, writable: false });
// 类似处理 setInterval, setTimeout, eval
原理 :将修改后的全局函数设置为不可配置(configurable: false)和不可写(writable: false),防止后续脚本通过重新赋值或 delete 操作恢复原函数。
优点 :增强了绕过的稳定性。
局限:如果脚本在页面加载的极早期执行,后续代码可能会尝试重新定义这些属性,但由于设置为不可写,会静默失败(非严格模式下)或抛出错误(严格模式下)。这可能导致某些依赖这些函数的正常功能失效,但通常影响不大。
方法8:禁用 console 访问检测
javascript
Object.defineProperty(window, 'console', {
get: function() {
consoleAccessCount++;
if (consoleAccessCount > 100) {
return { log: function() {}, ... }; // 返回空console
}
return console;
}
});
原理 :一些网站会通过频繁访问 console 对象来检测开发者工具是否打开(例如在控制台输出内容)。脚本监控 console 的访问次数,一旦超过阈值(100次),就返回一个空的 console 对象,使得后续的 console 调用失效,从而隐藏调试痕迹。
优点 :能抑制部分基于 console 的检测。
局限 :阈值是硬编码的,可能不准确;且返回的空 console 会导致正常的调试输出丢失;另外,如果检测逻辑不是简单计数,而是每次调用都检查(例如 console.log 被重写),则此方法可能无效。
4. 整体绕过策略评价
- 综合性 :脚本覆盖了多种常见的
debugger注入方式(Function、定时器、eval)以及典型的开发者工具检测手段(窗口尺寸、console访问),形成了一个相对全面的绕过方案。 - 适用场景:特别适合在自动化工具(如 Playwright、Puppeteer、Selenium)中作为初始化脚本注入,使爬虫或自动化脚本能够顺利运行而不被反调试机制干扰。
- 局限性 :
- 字符串检测过于简单:如果反调试代码将
debugger语句拆分或动态生成(例如'\x64\x65\x62\x75\x67\x67\x65\x72'),则无法被识别。 - 未处理
Web Workers或iframe中的反调试。 - 部分方法可能影响正常功能(例如
console劫持可能导致页面本身的调试信息丢失)。 - 如果网站采用更先进的检测(如检测堆栈深度、执行时间差、DOM 元素变化等),该脚本可能无法绕过。
- 字符串检测过于简单:如果反调试代码将
5. 使用示例
脚本末尾提供了在 Playwright、Puppeteer 和 Selenium 中注入的示例代码,方便开发者集成到自动化工具中。
总结
这段代码是一个针对同程旅行网站反调试机制的绕过脚本,通过拦截和修改关键 API、欺骗属性检测来阻止 debugger 执行并隐藏开发者工具的存在。它的设计思路清晰,覆盖了常见绕过点,但受限于简单的字符串匹配,仍可能被更隐蔽的反调试技术突破。在实际使用中,建议结合其他手段(如禁用 debugger 的浏览器启动参数、使用 undetected-chromedriver 等)以提高成功率。