接到这个标题我就笑了,因为“无限 debugger”这个名字起得相当贴切。你在开发者工具里刚按了一下 F12,控制台立刻开始无休止地停在 debugger 语句上,就像有个无形的胶水把断点粘在你面前,你点一次“继续运行”,它马上再停一次,哪怕你选中了“Deactivate breakpoints”,有的页面照样能把你卡得动弹不得。
这篇东西我不想写成科普文档,更想用实战踩坑的记录来讲。很多前端页面,尤其是数据敏感度高的平台,都会在关键脚本里埋反调试逻辑,无限 debugger 就是最常见也最恶心的一种。它的原理并不复杂,核心就是用 debugger 语句加定时器,形成一种“你打开控制台就疯狂暂停”的效果,等于用调试器本身来对抗调试。你会用到这套办法的场景,一般是做前端性能分析、数据接口联调、或者给自己负责的站点做安全检测,偶尔也会在研究别人页面的时候撞上它。这篇文章就是把这套“只要打开工具就被人按暂停键”的问题,用一个相对极端的手段解决掉,让你能安安静静地看代码,而不是跟弹窗互怼。
整体思路我先放在前面:别去跟它的执行流缠斗,也不要试图一行行去删代码。最彻底的邪修方案,是在代码运行之前就改写掉它的触发机制,无论它是用 setInterval 循环、用构造函数做递归、还是把 debugger 塞进 getter 里面,都可以在脚本真正运行前拦截住。下面把我实际跑通的几条路线全部拆开讲,从傻瓜式到手工定制版都有,你可以直接照抄。
1. 先搞清楚碰上的是哪种“无限 debugger”
1.1 两种常见的插桩方式:正计时与轮询
我遇到过的无限 debugger,绝大多数可以归成两类。第一种最原始,也最好认,就是在脚本里写了一个 setInterval 定时器,每 100 毫秒或者 500 毫秒执行一次 debugger 语句。这种方式就像每隔一会儿往你的调试器里丢一块绊脚石,你刚把断点按掉,下一次定时器又触发了。它不需要太高深的技术,一行 setInterval 加一个 debugger 就能写得出来,但效果很稳定,几乎对所有刚打开 DevTools 的人都有效。
第二种稍微隐蔽一点,它会通过构造一个匿名函数、或者通过 try/catch 包住的逻辑触发 debugger,比如:
javascript复制(function() {
function check() {
debugger;
setTimeout(check, 100);
}
check();
})();
这段代码不管你怎么点“跳过下一个断点”,它都会在无限递归里反复暂停。这里的核心点在于,它不是靠浏览器外部的监听来判断你有没有打开控制台,而是直接在页面主线程里建立一个永不停歇的循环。只要你打开了开发者工具并停在脚本面板,它就会不断中断执行。很多人会把无限 debugger 等同于“反爬虫”,但其实它的真正目标是“反调试”,也就是说,它是想把那些试图通过动态分析页面代码的人劝退。
1.2 debugger 触发的完整链路到底长什么样
要真正解决它,我建议先搞清楚这段逻辑是怎么被加载的。一个典型的反调试脚本会藏得比较深,比如它是通过某个 JS 文件动态注入的,甚至是通过字符串拼接构造出来再 eval 执行的。你直接在 Sources 面板里全局搜 debugger,往往搜不出来,因为源码里可能写的是 "debu" + "gger",又或者被混淆工具重命名成了别的函数名。
常规页面会走这样的链路:
- HTML 加载一个入口脚本,通常是压缩过的 bundle 文件。
- bundle 里通过自执行函数生成了 debugger 逻辑,并注册了定时器。
- 定时器回调触发调试器中断,中断点落在代码位置 A。
- 当你在当前位置右键选择“Never pause here”,它会跳到下一个被混淆过的 debugger 位置 B。
所以你会发现,手动一个个去禁用断点根本不可能,因为代码可能有好几处 debugger,还套在多层循环里面。真正有效的做法不是“在断点处反制”,而是“在源头让这条链路断掉”。这个思路就像对付一个每隔几秒就给你发骚扰消息的机器人,你拉黑它当前账号没用,因为它还能换新的号继续发,你得直接关掉它的发送接口。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 开发者工具里的“正面硬刚”方法
2.1 掌握 DevTools 的断点管理面板
如果你是临时调试、不想马上写脚本改动页面,那 DevTools 里自带的几个断点控制功能可以先救急。打开 Source 面板,右侧的 Call Stack 区域下面有一个 Breakpoints 列表。所有目前因为 debugger 语句而触发的断点都会显示在里面。比较基础的操作是,在断点那一行上右键,可以选择“Edit breakpoint”,把它改成一个永远不会成立的条件,比如 1 === 2。这样,虽然源码里还有 debugger,但它实际不会中断。
这个方法很简单,但我必须提醒你,它只对“当前断点”生效。如果对方写的是多段式子、多文件交叉递归那种,你每遇到一个新的 debugger 就要手动去改一次。页面一旦刷新,所有断点全部重置,你得重新来一轮。所以这个方案适合那种只是为了快速看一眼变量值、不想大动干戈的场合,真用它对付严防死守的站点,你会按到怀疑人生。
我自己的操作习惯是,如果只想临时跳过,优先用 Conditional breakpoint。在断点行右击,选 Add conditional breakpoint,输入一个 false 值,这个断点直接失效。如果不希望 DevTools 进入某个第三方或者混淆代码内部,还可以把整个脚本目录加入 Blackboxing,这样调试时它会直接跳过这些文件,不再被 debugger 干扰。Blackboxing 的位置在 DevTools 设置里的 “Ignore List” 或 “Blackboxing” 区域,添加对应的脚本 URL 或路径即可。
2.2 用页面自己注入的断点拖住它的定时器
这一节这个方法有点“以毒攻毒”的意思。当你发现某个脚本在疯狂触发 debugger 时,可以先在代码里主动加一个不会自动恢复的断点,比如在定时器回调的上方打断点。等程序停在你的断点位置,你再看右侧作用域里有没有定时器 ID,或者直接在控制台里执行 clearInterval / clearTimeout。
操作顺序是这样的:
- 打开 Sources 面板,找到报出 debugger 的那个文件,看是不是有
setInterval在循环触发。 - 在它的定时器回调函数第一行加一个普通断点。
- 等断点命中后,在 Console 里输入
clearInterval(定时器变量)。 - 再放行断点,看是否还继续触发 debugger。
如果代码里的定时器 ID 没有暴露成全局变量,你可以换种思路,直接在控制台执行 for (var i = 1; i < 99999; i++) clearInterval(i);,把当前页面所有定时器全部清掉。这个方法粗暴但常有奇效,缺点是会把页面上其他正常定时器也清掉,可能导致轮播图停了、倒计时没了,或者某些前端交互失效。所以它只适合临时应付,不适合长期调试。
正攻法适合你不怕麻烦、手头只是碰上一个塞了一两个 debugger 的页面。但如果页面里的反调试逻辑做得比较完整,只要检测到你打开了开发者工具就会疯狂暂停,那你最需要的是下面这种“直接在脚本执行前把它换掉”的邪修思路。
3. 从源头消灭定时器的“究极邪修”路径
3.1 最暴力但也最有效的 Override 方案
现代浏览器开发者工具都支持 Overrides(本地替换)功能。它允许你把网络请求到的 JS 文件保存到本地,然后修改这份本地副本,浏览器加载页面时会用你这份替换掉远程的同名文件。用 Overrides 来做的话,等于把代码里的 setInterval、debugger 关键字统统删干净再重新加载页面,效果是真正意义上的“根治”。
开启方式很简单:
- 打开 Sources 面板,切到 Overrides 子标签页。
- 点击 “Select folder for overrides”,选一个本地空文件夹。
- 允许 DevTools 修改文件权限。
- 切回 Network 面板,找到那个带反调试逻辑的 JS 文件,右键选择 “Save for overrides”。
- 这时再打开 Sources,在 Overrides 目录下编辑文件,把
debugger语句删掉或者把setInterval参数改成不执行的状态。 - 刷新页面,替换生效,所有调试器断点都没了。
我实际用下来,Overrides 对大多数“魔鬼细节”藏在压缩 JS 里的页面都是可行的。但你也会遇到坑,比如文件特别大,压缩成一行,几十万字符,手动找 debugger 太费劲。那就直接在编辑器里用格式化功能,然后全局搜索 debugger。如果 Source 面板里格式化后没能自动换行,你可以在文件内容里点一下左下角的 {} 按钮。
这套方案最大的好处是“一劳永逸”,只要本地替换文件还在,每次打开页面都会加载你改过的副本。缺点是它依赖 DevTools 的 Overrides 机制,如果对方做了 Service Worker 强缓存或者响应校验,可能加载的还是原文件。另外,Chrome 的 Overrides 支持范围有限,对跨域资源、部分异步加载的资源未必能每次命中。这时候就需要下面的运行时 Hook 方案登场了。
3.2 用注入脚本提前接管 setInterval 和 Function 构造器
第三种也是我个人最推荐的邪修手段:在页面任何脚本执行之前,往页面里注入一段“消毒代码”。这段代码的核心逻辑,是把 window.setInterval、window.setTimeout 以及 Function 构造器全部换成我们自己的壳函数,把所有跟 debugger 相关的行为拦在门外。
这里放一个精简但完整的注入模板,是我实际跑过很多页面的版本:
javascript复制(function() {
// 保存原始方法
var origSetInterval = window.setInterval;
var origSetTimeout = window.setTimeout;
// 判断参数是否含 debugger 相关特征
function containsDebuggerCode(fn) {
if (typeof fn === 'function') {
// 转成字符串检测,压缩混淆代码里可能会出现 debugger 字样
return /debugger/.test(fn.toString());
}
if (typeof fn === 'string') {
return /debugger/.test(fn);
}
return false;
}
var safeSetInterval = function(code, delay) {
if (containsDebuggerCode(code)) {
return 0; // 不让它创建任务,返回一个假 ID
}
return origSetInterval(code, delay);
};
var safeSetTimeout = function(code, delay) {
if (containsDebuggerCode(code)) {
return 0;
}
return origSetTimeout(code, delay);
};
// 覆盖全局方法
window.setInterval = safeSetInterval;
window.setTimeout = safeSetTimeout;
// 某些代码会用 Function 构造器生成函数后执行
var OrigFunction = window.Function;
var SafeFunction = function() {
var args = Array.prototype.slice.call(arguments);
var body = args.pop() || '';
if (/debugger/.test(body)) {
return function() {};
}
return OrigFunction.apply(this, [args.join(','), body]);
};
SafeFunction.prototype = OrigFunction.prototype;
window.Function = SafeFunction;
})();
这段代码要放在页面最前面执行。如果是你用 Playwright、Puppeteer 这类自动化工具来做调试,可以用 page.addInitScript() 注入。如果是人工拿 DevTools 临时调试,可以在 Sources 面板里新建一个 Snippet,然后右键运行。关键是时机必须足够早,最好在 HTML 文档解析阶段就执行。如果等页面业务脚本已经跑起来再去注入,可能定时器早就注册完了。
不过这里有个细节,很多人会漏掉:有的反调试代码不是通过 setInterval 注册,而是直接写了一个死循环,每次循环内部都调用 debugger。这种情况下,上面的定时器拦截是无效的。你需要配合第二个方案,在脚本级做细粒度处理。
4. 定位并“爆破”反调试函数
4.1 快速找到被混淆的 debugger 调用位置
当你打开一个站点,发现它卡住的地方看起来像是某个函数内部,但你根本不知道这个函数叫什么名字,也不知道它是在哪个文件里定义的。这时候,你先不要急着关掉页面,可以先用 DevTools 的 Call Stack 看当前中断位置是被哪个函数调用起来的。虽然断点不断触发,但调用栈信息在工作台暂停的间歇里是可以看到的。
一个更实用的操作是,在 Console 里执行一段代码,把 Function.prototype.toString 篡改掉,让任何函数在被转成字符串时都返回一个普通函数字符串,让对方的检测逻辑失效。不过这招未必对真正的 debugger 循环有效,因为它是关键字级的中断,不是函数检测。最稳妥的还是先定位到 debugger 所在的那一行。
我遇到的情况里,80% 的 debugger 都来自自执行函数,类似:
javascript复制!function(e) {
function t() {
try {
!function() {
debugger
}();
} catch (e) {}
}
setInterval(t, 100)
}(window);
这种代码的特点是短、压缩过、变量名不可读。你可以把它整个替换成一个空函数,或者直接把 setInterval 改成只执行一次。用 3.2 节的 Hook 方法,检测到 t 函数字符串里有 debugger,就不会再去注册定时器,问题直接消失。
但有的代码会写得更加“鸡贼”,它把 debugger 写在异步回调里,或者藏在某个事件监听器里,比如:
javascript复制window.addEventListener('resize', function() {
(function() {
debugger;
})();
});
只要你不触发 resize 事件,它就不会中断。但如果你手动缩放窗口,或者页面里的组件触发了重排,就可能频繁中断。面对这种事件型触发,思路也是一样的,Hook 事件监听器或者直接修改源码。你可以把解包后的代码全部读下来,找出哪个事件绑定的处理函数里有 debugger,然后通过 Overrides 删掉那一句。
4.2 在压缩代码中直接改写可疑函数体
当你确定目标代码就是某个函数时,第 3.1 节的 Overrides 能派上用场。格式化压缩代码后,搜索 debugger,把整段可疑函数体改成一个空函数,然后刷新页面。这里要提醒的是,压缩 JS 里同一个变量名可能会被多处复用,如果你直接把整段函数体替换掉,可能导致后续依赖某个局部变量的代码直接报错。所以不要简单地把整行删除,更好的做法是只把 debugger 这个关键字替换成 void 0,让它的调用不再有中断效果,函数内部逻辑仍然保留。
比如原始代码是:
javascript复制function checkDebug() {
debugger;
return typeof window.callPhantom === 'object';
}
你改成:
javascript复制function checkDebug() {
void 0;
return typeof window.callPhantom === 'object';
}
这样不会影响 checkDebug 返回的结果,却能让断点不再被反复触发。凡是遇到 debugger; 缩成一行的情况,也可以用编辑器里的全局替换,把 debugger; 换成 void 0;,或者把 debugger 关键字改成其他无意义表达式。
如果你的页面不是本地文件而是需要通过 HTTPS 实时加载,并且无法用 Overrides 时,我可以再提供一个从 DevTools 控制台手动执行的偏方。当程序停在 debugger 处时,在 Console 里执行:
javascript复制location.reload()
同时把鼠标按在 F8 上反复跳?不,这个方案不靠谱。真实有效的偏方是使用“Deactivate breakpoints”按钮(一个带禁用符号的断点图标),同时把 “Pause on exceptions” 也关掉。但假如它是通过计时器循环触发,Deactivate breakpoints 依旧挡不住,因为 debugger 语句不属于断点类型,它直接受 JS 引擎语义控制,不是 DevTools 手动能全局屏蔽的。这也是为什么大家一定要从代码源头上做拦截,而不是研究怎么忽略断点。
另一个偏方是,当断点卡住时,在 Console 中使用 location.href='about:blank' 把当前页面跳走,避免反复弹调试器,但这只能让你脱离战场,并不能让你分析页面逻辑。
5. 一套自动化的“过无限 debugger”脚本模板
5.1 使用浏览器自动化工具注入的实践
在我处理多个类似场景后,沉淀下来的最佳形态是写一个独立注入脚本,然后配合 Playwright 使用。因为很多时候你不是在人工调试页面,而是要通过自动化工具去加载某个页面,再检查页面上某些数据是否正常展示。这时候页面一旦触发 debugger,整个自动化流程就卡死了,除非你给脚本加上超时控制,否则它会一直挂在那里。
下面这个脚本,是我在自动化测试环境里使用的一份模板,核心思路就是让 debugger 逻辑无法生效:
javascript复制// 在浏览器上下文初始化时注入
await context.addInitScript(() => {
// 保存原始方法
const originalDefineProperty = Object.defineProperty;
// 先把 debugger 转成空
const debuggerPatched = 'debugger';
// hook 字符串构造器,防止代码动态拼出 debugger
const originalToString = Function.prototype.toString;
Function.prototype.toString = function() {
if (this.__isPatched) {
return 'function () { [native code] }';
}
return originalToString.call(this);
};
// 关闭 setTimeout / setInterval 里的 debugger
const makeSafe = (fn) => {
return function(code, ...args) {
if (typeof code === 'function') {
const fnStr = code.toString();
if (fnStr.includes(debuggerPatched)) {
return 0;
}
}
if (typeof code === 'string' && code.includes(debuggerPatched)) {
return 0;
}
return fn.call(this, code, ...args);
};
};
window.setTimeout = makeSafe(window.setTimeout);
window.setInterval = makeSafe(window.setInterval);
// 处理 eval 和 new Function
const originalEval = window.eval;
window.eval = function(code) {
if (typeof code === 'string' && code.includes(debuggerPatched)) {
return undefined;
}
return originalEval(code);
};
const OriginalFunction = window.Function;
window.Function = function(...args) {
const body = args.pop() || '';
if (body.includes(debuggerPatched)) {
return function() {};
}
return new OriginalFunction(...args, body);
};
window.Function.prototype = OriginalFunction.prototype;
// 用 Object.defineProperty 保护对 setTimeout 的二次替换
try {
Object.defineProperty(window, 'setInterval', {
writable: false,
configurable: false
});
Object.defineProperty(window, 'setTimeout', {
writable: false,
configurable: false
});
} catch (e) {}
});
实际使用中你会发现,有些页面的反调试代码不是写死在 setInterval 里,而是直接在 Promise 的微任务里不断调用。微任务循环一旦开启,即使没有定时器,也会无限触发 debugger。面对这种写法,除了把页面里所有和 debugger 相关的函数逻辑找到并替换之外,还要小心它可能自带“检测你是否 hook 了定时器”的对抗逻辑。比如它检查 setInterval.toString() 是否还是原生方法,如果不是,就改用异常抛出或者 Console 检测等方式。
5.2 如何应对“多次检测、递归调用”的重度混淆
有一类页面会同时做好几层防御,第一层是定时器 debugger,第二层是通过检测 window.Function 的 toString 是否被改过,第三层是用 MutationObserver 持续观察 body 元素变化并重新注入 debugger。你的 Hook 如果只做了一层,很容易被它后面的逻辑重新拉回断点。
遇到反复横跳的情况,我通常不会只靠 hook,而是直接把相关脚本文件下载下来,在本地用正则把 debugger 全部替换成 void(0),再用本地资源替换或者 mock 方式加载。也可以直接在 Network 面板里,看这个脚本的响应体,然后保存,再用自建静态服务器对请求做改写。
用正则处理压缩文件时有一点务必记住,不要直接全局替换字符串 debugger,因为代码里可能有某个变量名也包含 debugger,全替换会把无关变量破坏。最安全的匹配是 /\bdebugger\b/ 这种词边界模式,并且只替换成 void 0,不要顺手替换 debug 之类的字符串。比如 debuggerEnabled 这个变量名里如果被误替换,就会变成 void(0)Enabled,代码直接报错。
这里我放一个简单的 Node 脚本思路,示例性代码,方便你用本地文件去处理:
javascript复制const fs = require('fs');
const path = process.argv[2];
let code = fs.readFileSync(path, 'utf8');
// 仅替换 debugger; 语句,保留变量名
code = code.replace(/\bdebugger\s*;/g, 'void 0;');
fs.writeFileSync(path, code);
如果你处理的文件不是用分号结尾,而是压缩后的没有分号风格,也可以改成 /\bdebugger\b/g,但你要自己在替换人工审查结果,避免误伤。
6. 避坑清单与安全边界
6.1 哪些场景该用、哪些场景绝对不该碰
先讲原则:这套思路适用于你自己参与的前端项目、你有授权做安全测试的站点,或者是本地离线 HTML 文件、纯学习向的代码分析。比如你公司某个后台页面加了这个防护,你需要看接口数据格式,结果被无限 debugger 卡住,影响开发效率,这时候用 Overrides 或者注入脚本把 debugger 去掉,完全合理。
反过来的场景就要小心了。如果目的是抓取别人平台的数据、绕过付费限制、批量获取未授权内容,那这些操作本身就处在灰色地带。技术本身是中性工具,但你的使用目的决定了它是否合规。我的建议是,凡是会影响到别人服务稳定性、涉及未授权数据获取的,一律不要套用这些方法。无限 debugger 是反爬体系里的一个环节,你费劲绕过它,不代表服务端的风控也会放你过去。很多平台的真实风控在接口层、在设备指纹层、在行为特征层,调试器只是第一道门槛。
另外,做安全研究时要保持“最小接触原则”,能通过静态分析理解逻辑就够了,不要非把别人线上环境破坏掉。比如你只是想知道它怎么实现检测,看混淆代码即可,不必真去对线上域名注入高强度脚本。
6.2 实际操作中容易翻车的几个细节
这里把我踩过的几个坑集中说一下。
第一,清理定时器时会把页面正常业务逻辑一起杀掉。上面的 clearInterval(i) 无差别清理会导致轮播、请求轮询、倒计时全部停摆,如果你是在写自动化脚本,页面可能出现数据不加载的情况,然后你会以为是自己的脚本写错了。所以有条件的话,最好根据调用栈定位到具体是哪个定时器。
第二,Hook Function.prototype.toString 后,很多前端框架内部依赖 toString 去做依赖注入或者错误堆栈处理,你把所有函数都改成 [native code],可能导致页面直接白屏。更稳妥的做法是只对特定特征函数返回伪装字符串,没必要全量 hook。
第三,window.Function 的覆写要小心。很多现代前端工具(包括 webpack 模块)内部都会用 new Function 做动态代码生成,如果你把包含 debugger 关键词的 body 一律返回空函数,有可能会导致模块加载失败。所以注入脚本里要对这类情况做一个可配置的白名单,最好只拦截确实来自反调试模块的特征,而不是一刀切。
第四,关于 Overrides 的坑。当文件很大时,DevTools 格式化后再保存,可能会改掉原始文件的编码格式或者行尾符号。虽然 JS 文件大多以 UTF-8 为主,但有时会遇到带 BOM 头的情况,保存后接口请求头 MIME 类型变化,导致文件加载被拒。解决办法是,尽量保持原文件编码;编辑完先在本地 Node 环境里转一次码,确认无 BOM 再放回去。
第五,不要忽略 Service Worker 的缓存。很多站点会用 Service Worker 把核心 JS 缓存进 Cache Storage。即使你改了网络请求返回的 JS 文件,刷新后用的还是 Service Worker 里的旧缓存。这时你需要先在 Application 面板里清空 Service Worker,或者通过 Chrome 的 “Update on reload” 选项处理。这个地方卡住的话,你往往会误以为自己操作不对,实际上只是没清干净。
6.3 当你同时遇到 debugger 和压缩混淆时
不少页面的代码不止有无限 debugger,还是用混淆工具做过字符串加密、控制流扁平化处理的。这类代码打开一看就是几千行没有意义的 switch-case 拼出来的逻辑,手动定位 debugger 异常困难。我的习惯是先把代码美化,再在所有 debugger 关键字位置加上日志输出,比如改成 console.log('[debugger position]', Error().stack),这样再次运行时,控制台会把触发点所在的调用栈打出来,你可以根据文件名和行号反查代码逻辑。
替换 debugger 为日志输出,有一个额外好处,就是它不会阻止原始函数继续执行,页面整体环境更接近真实运行状态。如果用 void 0 直接替换,可能会导致某些依赖 debugger 行为的异常分支走不到,虽然很少见,但确实有页面会把 debugger 放在 try/catch 中,利用它被中断的属性,这种玩法属于进阶反调试,我在另一篇文章里详细展开过。这里你只需记住一个判断逻辑:如果 debugger 被 try/catch 包裹,那它可能不只是中断,而是在利用异常信息做环境检测。这种场景下别急着头疼,先从被 catch 的异常类型开始排查。
7. 最后分享一点个人实操体会
前前后后处理了很多次无限 debugger 问题之后,我最大的感受是,这条路没有银弹。越是看起来粗暴的“全量 hook 定时器”就越适合快速临时用,越是想长期稳定地分析页面,就越要回到最基础的“定位代码 + 修改资源 + 重新加载”这套笨办法上。所谓“究极邪修”,其实也只是把几个家常手段组合到一起,关键是你要有一条清晰的处理顺序:先识别触发方式,再用 DevTools 临时跳过,然后尝试 Overrides 或注入 Hook,最后才到本地改写配合自动化。
我个人在实际操作中最常用的是 3.2 节的那个注入模板,因为我在做页面异常监控和前端性能分析时会遇到大量带这种防护的演示环境。只要你把注入时机控制在 document 开始阶段,大多数基于定时器的无限 debugger 都能一次搞定。不过你也要有心理准备,毕竟真实世界的代码不会按文档出牌,上一页还好好的脚本,换个页面版本就可能加了新的检测特征。
如果这篇文章介绍的思路能帮你少走两步弯路,那我的目的就达到了。真到你上了手还碰到某些诡异的变种,记住一个总原则:任何反调试都要通过“执行代码”来起作用,而“执行代码”就一定有可以被预拦截的入口。你只需要比页面脚本更早一步,把入口接住,大部分问题就都不是问题了。
