JS逆向实战:Hook技术对抗与绕过无限Debugger的防御策略

张开发
2026/4/21 5:03:04 15 分钟阅读

分享文章

JS逆向实战:Hook技术对抗与绕过无限Debugger的防御策略
1. 无限Debugger的常见类型与原理剖析第一次遇到无限Debugger时我正试图抓取某电商网站的价格数据。刚打开开发者工具页面就像卡死的音乐盒一样不断弹出调试窗口鼠标根本来不及点继续执行。这种防御机制看似无解其实背后都是可破解的代码逻辑。根据实战经验我把它们分为三大类定时器类是最常见的实现方式。比如这段典型代码setInterval(function(){ debugger; }, 100);原理很简单每100毫秒执行一次debugger语句。在Chrome开发者工具中这会导致持续的中断就像有人按住F8键不放。我做过测试当间隔时间小于200ms时人工操作几乎无法正常调试。递归类则更隐蔽些。去年分析某金融网站时遇到过这种变体function antiDebug(){ debugger; return antiDebug(); // 尾递归调用 }这种实现会快速耗尽调用栈在Chrome中大约3秒就会触发Maximum call stack size exceeded错误。有趣的是在Firefox中反而能持续更久这说明不同浏览器的调试器实现存在差异。代码混淆类是进阶玩法。有次逆向某票务系统时遇到了这样的魔鬼代码[][filter][constructor](debugger)[call](action)这其实是利用数组对象的filter方法获取Function构造函数再动态执行debugger指令。更变态的版本还会对字符串做ASCII码拆分比如Function(String.fromCharCode(100,101,98,117,103,103,101,114))()这种防御需要先还原代码逻辑才能破解属于逆向工程师的日常噩梦。2. 基础绕过手段与适用场景刚开始接触反调试时我最爱用Chrome的一律不在此处暂停。右键点击debugger行选择这个选项就能轻松绕过。但很快发现这招对动态生成的debugger无效——比如通过eval执行的调试语句。条件断点是更稳妥的方案。在debugger行右键设置条件为false相当于给这个断点加了失效开关。实测中我发现个小技巧可以先在setInterval的callback开始处设普通断点等暂停后再给内部的debugger设条件断点这样能避免手速跟不上定时器的问题。函数置空需要把握时机。有次爬取视频网站时我直接在控制台输入setInterval function(){}结果页面功能全挂了。后来才明白这个站点用setInterval处理播放器状态更新。正确做法应该是const _originSetInterval setInterval; setInterval function(fn, delay){ if(!fn.toString().includes(debugger)){ return _originSetInterval(fn, delay); } console.log(Blocked debugger!); }这样既绕过调试陷阱又不影响正常业务逻辑。建议操作前先用toString()检查函数内容就像拆弹前要看清电线颜色。3. Hook技术的实战应用真正让我突破瓶颈的是Hook技术。有次逆向某社交平台的加密参数遇到层层嵌套的debugger防御。最终用构造函数Hook一网打尽// 保存原始构造函数 const _constructor Function.prototype.constructor; // 重写构造函数 Function.prototype.constructor function() { // 检测到debugger语句时返回空函数 if(arguments[0] arguments[0].includes(debugger)){ return function(){}; } return _constructor.apply(this, arguments); }这个方案的精妙之处在于它拦截了所有通过new Function()或Function()创建的动态代码。后来我又做了增强版可以同时处理以下几种情况// 处理Function构造函数的调用 Function.prototype.constructor function() { const args Array.from(arguments); if(args.some(arg typeof arg string arg.toLowerCase().includes(debugger))){ console.warn(Debugger injection detected); return function(){}; } return _constructor.apply(this, args); } // 处理eval调用 const _eval eval; window.eval function(code) { if(typeof code string code.includes(debugger)){ return undefined; } return _eval(code); }最近的项目中还遇到更狡猾的变种debugger被拆分成多个字符串拼接比如debugger。针对这种情况我改进了检测逻辑Function.prototype.constructor function() { const rawCode Array.from(arguments).join(); if(/d[^\w]?e[^\w]?b[^\w]?u[^\w]?g[^\w]?g[^\w]?e[^\w]?r/.test(rawCode)){ return function(){}; } return _constructor.apply(this, arguments); }4. 高级对抗与反检测策略有些网站会检测Hook行为本身。比如某次我发现自己的Hook代码执行后页面立即跳转到验证界面。后来用代理工具抓包发现对方在检测关键API是否被修改if(Function.prototype.constructor.toString().indexOf(native) -1){ location.href /anti-cheat; }对付这种防御需要更隐蔽的Hook方式。我现在常用的是原型链污染属性描述符的方案// 保存原始实现 const _nativeConstructor Function.prototype.constructor; // 定义伪装对象 const fakeConstructor function() { const args Array.from(arguments); if(args.some(arg typeof arg string isDebuggerCode(arg))){ return function(){}; } return _nativeConstructor.apply(this, args); } // 复制原始属性描述符 const descriptor Object.getOwnPropertyDescriptor( Function.prototype, constructor); Object.defineProperty(Function.prototype, constructor, { ...descriptor, value: fakeConstructor });这样检查toString()时仍然会返回function Function() { [native code] }因为我们是通过合法途径修改的。还有个更绝的方案是直接修改V8引擎的调试器实现不过这就涉及到浏览器插件开发了。最近半年还遇到个棘手案例网站会定时检查关键函数的内存地址。我的解决方案是使用ES6 Proxy来创建透明代理const _setInterval setInterval; window.setInterval new Proxy(_setInterval, { apply(target, thisArg, args) { const [fn, interval] args; if(typeof fn function fn.toString().includes(debugger)){ console.log(Blocked debugger interval); return -1; // 返回无效的timerId } return target.apply(thisArg, args); } });这种方案的优势在于setInterval instanceof Function仍然返回true甚至setInterval.toString()也保持原样但实际行为已经被我们控制。

更多文章