今天在网页抓包的时候碰到了反调试debugger代码,打开浏览器的开发者工具就会直接触发debugger反调试断点,网页就会卡在这里,无法请求接口数据,页面也无法正常加载。
针对这个问题,我们来详细说一下如何进行绕过处理,方便以后你们遇到这种问题时知道怎么解决。
本文目录
- 问题现象:打开F12就卡死
- 定位问题:查看调用堆栈
- 分析混淆代码:找到debugger入口
- 解决方案一:条件断点替换(推荐)
- 解决方案二:油猴脚本Hook(一劳永逸)
- 解决方案三:抓包工具(最暴力)
- 方案对比与总结
1. 问题现象:打开F12就卡死
今天在网页抓包的时候碰到了反调试debugger代码,打开浏览器的开发者工具就会直接触发debugger反调试断点,网页就会卡在这里,无法请求接口数据,页面也无法正常加载。

2. 定位问题:查看调用堆栈
我在网页上打开了开发者工具,可以看到已经触发了 debugger 反调试断点。同时这个断点停在一个 VM114792 的脚本中,这代表这段代码是动态生成并执行的,并非来自原始 JS 文件。
我们需要查看调用堆栈,看是在哪里执行的。可以看到 Call Stack 面板中的调用顺序是:
(匿名)→(匿名)→ _0x429e6f → _XPMRQ
其中 _XPMRQ 是最外层的调用函数。

3. 分析混淆代码:找到debugger入口
通过堆栈调用函数的名称,明显可以看出来这些代码是经过混淆的。这样的混淆代码可读性很差,需要一个一个去控制台还原才能知道大概它在做什么。
前面讲过的《小红薯X-S签名逆向解析(上篇):突破自定义Base64与算法轮廓》就是采用的这种混淆方式。
_XPMRQ 函数代码如下:
'ZVEcM': function(_0x42853b, _0x275a55) { return _0x42853b(_0x275a55); },

这个函数的作用很简单:接收一个函数 _0x42853b 和一个参数 _0x275a55,然后执行 _0x42853b(_0x275a55)。而 _0x42853b 很可能就是 Function('debugger') 或类似的 debugger 生成函数。
4. 解决方案一:条件断点替换(推荐)
如果你动手能力强且有耐心,就直接向上跟栈,直到把 debugger 的调用入口函数跟出来,下一个条件断点,把这个函数重新赋值,返回一个空函数,就能把 debugger 反调试过掉。
例如我这个网站是在 _XPMRQ 里面调用的,我在 return _0x42853b(_0x275a55) 这一行插入如下条件断点:
(_0x42853b = function(){ return function(){} }, false)
操作步骤:
- 找到
return _0x42853b(_0x275a55)这一行 - 右键 → 添加条件断点(Add conditional breakpoint)
- 粘贴
(_0x42853b = function(){ return function(){} }, false) - 回车,然后继续运行(F8)

效果: 把 _0x42853b 替换成返回空函数的函数,原来的 debugger 不会执行,网页可以正常抓包。
注意事项:时间戳导致断点失效
这种方法的有个前提:代码所在的文件必须是静态 JS 文件,而不是每次请求都动态生成的新文件。
如果你的条件断点下在了一个带时间戳参数的 JS 文件里(例如 xxx.js?_t=123456789),那么刷新页面后,浏览器会重新请求这个文件,新的请求会生成一个新的时间戳,相当于加载了一个全新的文件。你在旧文件上下过的断点,自然就管不到新文件了。
另外,如果反调试代码是在 VM 虚拟机中动态生成的,刷新后 VM 脚本也会重新生成,之前下的断点同样会失效。
解决办法: 遇到这种情况,建议改用油猴脚本方案(见第五节),或者直接用抓包工具绕过(见第六节)。
5. 解决方案二:油猴脚本Hook(一劳永逸)
如果你不会跟栈,或者觉得上面那种方法麻烦,我们还可以使用浏览器的油猴插件,写一段 Hook 代码直接把 debugger 给 Hook 掉。
Hook 掉以后,代码就会进入我们指定的逻辑执行,直接返回空函数,debugger 就不会触发了。
油猴脚本完整代码
// ==UserScript==
// @name 彻底禁用 debugger
// @namespace http://tampermonkey.net/
// @version 1.0
// @match *://*/*
// @run-at document-start
// @grant none
// ==/UserScript==
(function() {
'use strict';
console.log('%c[油猴] 反调试脚本已加载', 'color: #00ff00; font-size: 14px; font-weight: bold');
// 1. 劫持 Function 构造器
const OriginalFunction = Function;
window.Function = function() {
const args = [...arguments];
const body = args[args.length - 1] || '';
if (body === 'de' || body.includes('debugger')) {
return function() {};
}
return new OriginalFunction(...args);
};
window.Function.prototype = OriginalFunction.prototype;
// 2. 劫持 eval
const OriginalEval = eval;
window.eval = function(code) {
if (typeof code === 'string' && (code === 'de' || code.includes('debugger'))) {
return undefined;
}
return OriginalEval.call(this, code);
};
// 3. 覆盖全局 debugger(静默,无输出)
window.debugger = function() {};
// 4. 劫持构造函数
const OriginalConstructor = Function.prototype.constructor;
Function.prototype.constructor = function() {
const args = [...arguments];
const body = args[args.length - 1] || '';
if (body.includes('debugger')) {
return function() {};
}
return new OriginalConstructor(...args);
};
// 5. 劫持 setTimeout
const OriginalSetTimeout = window.setTimeout;
window.setTimeout = function(fn, delay, ...args) {
if (typeof fn === 'string' && fn.includes('debugger')) {
return 0;
}
return OriginalSetTimeout.call(this, fn, delay, ...args);
};
// 6. 劫持 setInterval
const OriginalSetInterval = window.setInterval;
window.setInterval = function(fn, delay, ...args) {
if (typeof fn === 'string' && fn.includes('debugger')) {
return 0;
}
return OriginalSetInterval.call(this, fn, delay, ...args);
};
// 7. 劫持 addEventListener
const OriginalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
if (typeof listener === 'function' && listener.toString().includes('debugger')) {
return;
}
return OriginalAddEventListener.call(this, type, listener, options);
};
console.log('%c╔════════════════════════════════════════╗', 'color: #00ff00');
console.log('%c║ ✅ 反调试脚本已完全生效 ✅ ║', 'color: #00ff00');
console.log('%c╠════════════════════════════════════════╣', 'color: #00ff00');
console.log('%c║ ✔ Function 构造器已劫持 ║', 'color: #00ccff');
console.log('%c║ ✔ eval 已劫持 ║', 'color: #00ccff');
console.log('%c║ ✔ debugger 变量已覆盖 ║', 'color: #00ccff');
console.log('%c║ ✔ setTimeout/setInterval 已劫持 ║', 'color: #00ccff');
console.log('%c║ ✔ addEventListener 已劫持 ║', 'color: #00ccff');
console.log('%c╚════════════════════════════════════════╝', 'color: #00ff00');
console.log('%c[油猴] 🚀 现在可以放心打开开发者工具了', 'color: #00ff00; font-size: 14px; font-weight: bold');
})();
使用步骤
- 安装油猴插件(Tampermonkey)
- 点击插件图标 → 添加新脚本
- 复制上面的代码,粘贴进去
- Ctrl+S 保存
- 刷新目标页面

注意事项
- 部分网站会检测油猴脚本的注入行为,如果发现被拦截,可能会采取其他反制措施。遇到这种情况,可以尝试用 Chrome 的 Overrides 功能本地替换 JS 文件。
- 油猴脚本的
@match规则建议写成具体的目标网站域名,不要用*://*/*,避免影响其他网站。
6. 解决方案三:抓包工具(最暴力)
还有一种方法,如果你只是为了单纯的抓包,不需要网页调试,我们可以使用第三方抓包工具,像 Fiddler、Charles 这些。
你反调试代码能管得了浏览器,难道还能管得了我这个中间人?
Fiddler 方案
- 打开 Fiddler,勾选 Decrypt HTTPS 解密 HTTPS 流量
- 设置 Filters,只抓目标域名的包
- 刷新页面,直接查看请求和响应
- 完全不会被 debugger 卡住

Charles 方案
- 打开 Charles,安装 SSL 证书
- 设置 SSL Proxying,添加目标域名
- 刷新页面,直接抓包
7. 方案对比与总结
三种方案的优缺点对比如下:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 条件断点 | 快速,不需要装插件 | 文件带时间戳会失效 | 临时调试 |
| 油猴脚本 | 一劳永逸,刷新自动生效 | 需要装油猴插件,可能被检测 | 长期调试 |
| 抓包工具 | 完全不受影响,最稳定 | 只能抓包,不能调试JS | 纯抓包需求 |
遇到网页反调试 debugger,不用慌,按以下顺序尝试:
- 先用条件断点:在
return _0x42853b(_0x275a55)处下条件断点(_0x42853b = function(){ return function(){} }, false) - 如果文件带时间戳或 VM 动态生成:用油猴脚本 Hook Function 构造器
- 如果只想抓包,不需要调试:直接上 Fiddler 或 Charles
三种方法总有一种能解决你的问题。
温馨提示:本文技术仅用于安全研究与技术学习,请严格遵守相关法律法规,不得用于任何非法用途。
下篇预告: 下一篇《JS逆向实战:某点评平台_signature参数逆向与请求模拟》中,我们将分析大众点评的请求加密体系,探讨Web端反爬策略的演变趋势,并提供完整的逆向方案。

