ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

无限debugger反调试绕过:从Hook到文件替换的实战指南

无限debugger反调试绕过:从Hook到文件替换的实战指南 打开DevTools想看一下页面的核心逻辑结果刚按下F12脚本就直接卡在一个debugger上点继续执行又卡在下一个debugger循环往复根本没法正常调试。这种体验我太熟悉了早期做前端分析的时候没少被这种“无限debugger”折磨。后来摸清了它的套路才发现绕过它其实有好几层思路最核心的就两个方向Hook和文件替换。这篇内容我打算把实战中真正能用的方法拆开来讲不搞花架子。适合刚接触逆向调试的前端开发者、爬虫工程师以及纯粹想搞明白“为什么会被卡住”的技术爱好者。文章里不会涉及复杂的破解理论全程用Chrome DevTools就能操作所有技巧我都实测过照着做基本都能跑通。1. 无限debugger的原理与常见触发方式1.1 为什么一行debugger就能卡死调试器要绕过无限debugger首先得明白它为什么能卡住你。debugger是JavaScript里一个合法的关键字作用是向调试器发出一个断点信号。当浏览器开着DevTools的Sources面板时代码执行到debugger语句就会强制暂停把控制权交给你。这个行为是解释器级别的除非你主动去改代码否则它就会一直生效。你可能会想那我直接点“继续”不就行了问题就在于代码里通常不是只写一个debugger而是用循环或者定时器把它包起来让断点无限触发。经典的写法大概长这样setInterval(function() { debugger; }, 100);这段代码每100毫秒就会触发一次debugger。你上一次断点还没看完下一次断点又来了整个线程被堵死Console和Network面板都像瘫痪了一样。更狠的写法是把debugger放在一个递归函数里function blockDebugger() { debugger; blockDebugger(); } blockDebugger();这种写法不依赖定时器只要函数在调用栈里断点就会持续命中。你就算用“恢复脚本执行”快捷键F8它也只会跳到下一个debugger整个过程就像一个死循环让你根本没法在Sources里慢慢看代码。1.2 常见的几种触发场景无限debugger最常见的应用场景是JS反调试。很多网页为了阻止别人分析前端逻辑会在关键入口埋下这种代码。有的写在公共JS文件里有的隐藏在混淆后的代码块中还有的直接内联在HTML的script标签里。触发方式也不止循环和递归这两种我见过的高频套路还有这样几种基于异常触发主动抛出一个错误在catch块里执行debugger每次进入catch都会卡一下。基于计时器间接触发用setTimeout间隔调用含debugger的函数间隔设得很短。基于DOM事件触发监听scroll、mousemove等高频事件在事件处理函数里放debugger你一滚动页面就被卡住。我曾经遇到过一种情况代码里把debugger写在一个被频繁调用的公共函数中普通情况下不触发但当你打开DevTools定位到Sources面板时立刻就被卡住。后来才发现它其实是检测了console.log被代理或者页面尺寸变化间接判断有人打开了调试器。这种情况最阴的地方在于你根本没找到断点在哪就已经被卡住了。2. 第一层绕过不写代码的DevTools小技巧2.1 条件断点与“Never pause here”的组合用法先用最简单的思路破局。遇到无限debugger的时候不要急着去想Hook先试试Sources面板右侧的“Deactivate breakpoints”按钮也就是断点禁用功能快捷键是CtrlF8。它会把所有断点暂时停用包括代码里的debugger语句。这个操作不会改文件内容只是调试状态的变化对很多定时器型的无限debugger都有效。如果是右键单击代码行号菜单里会有一个“Never pause here”的选项。选上之后Chrome会在那个位置自动生成一个条件断点并且条件永远为false这样debugger在那一行就不会触发了。原理上它其实是用条件断点覆盖了原有的debugger行为等于给了你一个“跳过某个文件的某一行”的精准控制。但这里有个关键限制“Never pause here”只对单一位置生效。如果无限debugger是动态生成的一串代码每帧都重新生成右键一次只能挡一行治标不治本。所以这一招适合那些debugger写死在某个函数里的场景遇到动态生成的代码就还得靠下面两种方案。2.2 Add script to ignore list把烦人的文件直接拉黑如果你已经能定位到究竟是哪个JS文件在触发debugger还有一个更省事的办法在Sources面板左侧的文件列表里右键点击那个文件名选择“Add script to ignore list”。意思是告诉ChromeDebugger不需要进入这个文件后续执行到该文件的任何位置都自动跳过包括里面的debugger语句。这个方案的优点是完全不用改代码逻辑也不影响文件本身的运行缺点是需要你能准确判断出是哪个文件在作妖。有些场景下文件是动态加载的或者文件名是混淆后的乱码你根本分不清该拉黑哪个。而且如果多个文件都设了debugger你得一个个去加效率很低。不过“ignore list”依然是我日常最常用的第一反应因为它处理常规反调试最快。特别是当你只看了页面逻辑不打算深入做分析的时候右键拉黑一个文件几秒钟就能恢复调试状态。3. 第二层绕过Hook方案釜底抽薪3.1 Hook的核心思路在debugger执行前拦截它如果小技巧失效了说明无限debugger不是“写死在某个位置”这么简单大概率是动态生成或循环触发型的。这时候就要上Hook。Hook的本质是在目标函数执行之前先注入一段你自己的逻辑把原有的行为替换掉。放在无限debugger的场景里最经典的做法就是改写Function的构造函数。JavaScript里函数的创建最终都绕不开Function这个构造函数。而debugger这个关键字最常见的情况就是被当成函数体里的代码来解析执行。所以如果我们能在代码执行new Function(debugger)或者(function(){debugger;})之前先把Function构造函数替换掉就能让debugger这句执行逻辑消失。我在Chrome控制台里实测过一段代码效果非常直接// 保存原始构造函数 const originalFunction Function.prototype.constructor; // 重写构造函数 Function.prototype.constructor function(...args) { if (args[0] typeof args[0] string args[0].includes(debugger)) { // 当检测到函数体里含有debugger关键字时返回一个空函数 return function() {}; } return originalFunction.apply(this, args); };这段代码的逻辑不复杂先保存原有的Function构造函数再覆盖它。新的构造函数在接受参数时先检查传入的函数体字符串里有没有debugger这个关键字有就返回一个空函数让后续的执行逻辑“哑火”。没有就照常调用原构造函数。3.2 Hook的实际操作完整流程以console注入为例在Chrome DevTools里执行Hook我建议用一个独立的Snippets来管理而不是直接粘在Console里。原因是Console里执行代码时如果页面自身也监听了控制台输入有可能会被干扰。Snippets是一个独立于页面执行环境的脚本片段你可以随时运行、随时改而且它还在Sources面板里有存档比较适合反复调试。具体操作步骤是打开Sources面板左侧找到“Snippets”标签页新建一个文件比如叫bypass-debugger.js把上面的Hook代码放进去右键选择“Run”。运行完之后再回到页面触发一次debugger你会发现它已经不会卡住页面了。要注意的是Hook只对运行在Hook之后的代码生效。如果你在运行Hook脚本之前无限debugger的定时器已经启动了页面里的原函数可能已经执行过并占用了调用栈。最稳妥的顺序是在页面加载完成后尽快执行Hook然后再刷新页面或者触发后续逻辑。有些实操场景里我会先在地址栏打开页面但先不忙操作等Snippets跑完再执行交互这样覆盖范围最大。验证Hook是否生效的方法也很简单在Console里手动执行一段含debugger的动态代码比如new Function(debugger)()。如果控制台没有进入暂停状态说明Hook成功了如果还是被卡住那就得检查是不是Function已经被别的地方重写过或者页面自己覆盖了构造函数。3.3 Hook的局限性与进阶处理不过Hook方案也不是万能的。遇到下面两种场景它就会失灵页面代码里不是通过Function构造器创建的debugger而是直接写在原始JS文件里的字面量debugger。这种代码在解析阶段就已经确定了不会经过Function构造器所以Hook不到。代码里检测了Function.prototype.constructor是否被修改一旦发现异常就直接走另一套逻辑或者触发更狠的反调试策略。遇到第一种情况就得靠文件替换或者更底层的断点处理。遇到第二种情况需要做得更隐晦一些比如用Object.defineProperty去覆盖构造函数的同时保持它的toString输出和原始函数一致让检测逻辑判断不出异常。这部分属于比较进阶的操作平时用到的频率不高但知道原理总是有用的。Hook方案还有一个额外的应用场景值得提一下有些网页禁用了右键菜单或者禁用了复制这类限制本质上也是通过重写DOM方法实现的。用类似的思路在Snippets里覆盖document.addEventListener或者window.open可以绕过很多前端层面的限制。我遇到过不少做页面自动化的人把Hook手法用在这里比单纯按F12调试要灵活得多。4. 第三层绕过文件替换让网页按你的版本来4.1 从复制源码到本地覆盖的完整步骤如果Hook解决不了或者你分析到后面觉得某些关键逻辑已经影响了调试体验想直接把代码改掉那就用DevTools的文件替换功能。这个功能的原理不复杂Chrome允许你把远程的JS文件映射到本地文件页面加载时优先使用本地文件而不是服务器上的原文件。以前我总以为这个功能很复杂后来发现DevTools早就把它整合成了一套可视化的操作流程。具体步骤如下打开Sources面板在左侧找到目标JS文件右键点击文件名。选择“Save for overrides”或者是“Overrides”相关的选项Chrome会弹窗让你选择本地保存目录。选好目录后Chrome会把原文件保存到本地。之后你用任意编辑器打开这个文件把里面的debugger删掉或者把关键函数改成你想要的样子。保存后回到页面刷新。这次加载时Chrome会优先读取本地文件等于你直接修改了网页的运行代码。这里有个提一下的细节Overrides面板需要你在DevTools设置里预先配置好本地目录。配置路径是Settings - Experiments或者Settings - Workspace不同版本的Chrome入口略有区别。如果找不到直接在Settings页面搜索Overrides关键字就能看到相关选项。配置完成后后续在Sources里保存的文件都会自动同步到该项目录下方便管理。4.2 处理压缩代码与混淆代码的几个关键操作大多数线上JS文件都是压缩过的几百行代码可能被压缩成几行里面还有一堆看不懂的变量名。你直接用本地文件替换时想要改代码会非常吃力。我的经验是先用DevTools右下角的“Pretty-print”功能把压缩代码格式化再进行修改。具体操作是打开文件后点击Sources面板左下角的{}图标这会让代码变成可读的多行格式。但要注意Pretty-print之后DevTools显示的是格式化副本如果你直接保存文件会跟原文件字符数不一致可能会影响Source Map的对应关系。所以更稳的做法是先复制格式化后的代码在外部编辑器里改再保存到本地做替换。混淆代码更折腾一点。早期的混淆大多只是变量名压缩不影响结构。后来有人用各种混淆工具把函数名、字符串全部编码甚至在代码里加入自执行函数和大量无意义分支。遇到这种代码直接改不现实。我一般的处理思路是先不急着改代码而是在本地文件里加入日志输出比如在关键函数前面插入console.log(key function called)通过日志来理解整个执行流程确认逻辑之后再精准修改。文件替换还有一个妙用某些调试过程中你需要在代码里加临时断点。直接在线上文件里打断点刷新就没了。但用了文件替换后你可以在本地文件里插入debugger;语句甚至用console.trace()打印调用栈刷新后依然存在。这个技巧在做复杂的调用链分析时比手动打断点高效得多因为它能看到完整的调用来源而不仅是某一次的调用栈。4.3 文件替换的适用场景和边界文件替换不是万能的它有一个比较重要的限制脚本必须是从外部文件加载的。如果代码是直接写在HTML内的script标签里或者由某个构建工具动态拼接后输出那么Sources里虽然能看到代码但不一定能保存成独立的本地文件替换流程可能断在半路。另一个容易踩坑的地方是某些页面启用了Service Worker或HTTP缓存即使你本地文件已经覆盖了刷新后仍然加载了缓存的旧代码。遇到这种情况可以尝试勾选DevTools Network面板上的“Disable cache”或者在Application面板里清掉Service Worker的缓存再重新加载页面。我个人的习惯是文件替换主要用于“改了代码就不想再改回去”的场景比如去掉了某个影响调试的拦截逻辑或者给关键函数加上了日志。真正需要频繁修改、反复调试的时候我更倾向于在Snippets里用Hook因为Hook不需要等页面重新加载改完立刻能生效迭代速度快很多。5. 常见问题与排查技巧实录5.1 表格速查症状、原因与解决方案在实操过程中我遇到过不少看上去“搞定不了”的情况后来发现大多数是操作细节没到位。下面这张表是我根据自己踩坑经历整理出来的可以当作排查手册来用。症状常见原因解决方案按CtrlF8没效果页面还是卡住断点禁用只停用了断点debugger关键字在DevTools关闭时也会生效尝试Hook方案或文件替换右键没有“Never pause here”选项版本较旧或文件被标记为动态生成升级Chrome或使用ignore listHook执行后仍然被卡住页面在Hook之前已经执行了定时器/递归先执行Hook再刷新页面触发逻辑文件替换后刷新没变化被Service Worker或缓存拦截清理缓存禁用Service Worker强制刷新格式化后保存导致行为异常格式化改变了代码结构执行逻辑不同在外部编辑器修改后再保存避免直接保存格式化副本Hook后页面功能报错空函数返回导致原调用链缺少返回值动态分析原函数的返回值并做兼容处理5.2 我踩过的三个坑希望你避开第一个坑是Hook代码里如果直接返回function(){}有些场景下原函数的返回值会影响后续逻辑。比如某个函数返回一个对象后面还有代码依赖这个对象的属性直接把函数返回为空就会导致后面报错。后来我的处理方式是先分析原函数的调用方式如果返回值不确定就让空函数返回undefined并尽量在Hook层面对调用结果做容错处理而不是一刀切。第二个坑是文件替换时本地的文件路径必须和线上路径对应。有一次我为了图方便把文件随便保存在桌面然后桌面路径和线上路径不一致DevTools死活匹配不上。后来我学乖了严格按照DevTools引导的目录结构来保存文件名也保持完全一致问题就消失了。第三个坑相对隐蔽Function.prototype.constructor被覆盖后Function(return this)()这种常见的全局对象获取方式会受到影响进而导致一些依赖它的库崩溃。所以在做Hook的时候尽量不要完全替代原构造函数而是先判断参数再决定走哪条分支。我这个习惯到现在都在用可以大幅减少Hook带来的副作用。5.3 调试过程中的一些通用技巧沉淀聊了这么多绕过方法最后沉淀几个通用调试技巧对以后做任何前端分析都有帮助。第一遇到可疑脚本时不要一上来就找断点先看Network面板里加载了哪些JS文件把文件列表截图存证再按顺序分析这样不容易迷失。第二学会在Console里用debugger语句配合console.trace()打印错误发生时的完整调用栈能快速定位问题根源。第三养成用Snippets写常用调试代码的习惯比如一打开控制台就自动执行Hook脚本能省下不少重复操作的时间。基于个人经验的补充如果页面里存在多个递归调用或定时器建议先用“Disable breakpoints”观察整体加载是否正常确认文件替换和Hook切换的边界在哪里再决定用哪种方案。实际操作中先把所有方案的关键步骤在纸上列一遍再动手往往比反复试错更有帮助。用了这些方法之后我对“反调试”的看法也变了。以前总觉得无限debugger是特意挡路的东西现在再看它更像是网站给自己加的一道门禁研究绕过它的过程本身就能学到很多关于JavaScript运行机制的知识。之后如果再遇到更复杂的反调试基本的排查思路也是相通的先看触发方式再决定用Hook还是文件替换两者结合几乎能覆盖绝大多数场景。我个人的建议是别死磕某一种技巧把它俩放在一起用效果才是最好的。
返回列表