反无限 Debugger三层防护方案文档信息属性内容适用场景网页无限debugger断点、动态反调试脚本导致内存暴涨依赖工具Chrome 浏览器、Tampermonkey 扩展劫持脚本同级目录resetDebugger.js一、问题背景1.1 问题现象打开某些网站后F12 开发者工具进入无限debugger断点无法正常调试Sources 面板中出现大量无真实文件路径的脚本Chrome 常显示为VM123、VM456等编号仅为调试器里的虚拟脚本名不是站点上的VM.js文件浏览器内存占用持续增长从几百 MB 涨到几 GB最终页面卡死或崩溃1.2 根本原因网站通过eval、new Function、内联script等动态执行代码不断生成含debugger的脚本在 Sources 里可能显示为VMxxx只是展示名例如setInterval(function(){newFunction(debugger)();},10);为什么单靠黑盒不够黑盒Ignore List只让调试器「别在这些脚本里停断点」并不能阻止页面继续生成和执行它们内存仍会累积——能调试了但内存仍会爆。二、三层防护架构层级方法解决问题第一层Chrome Ignore List黑盒调试器不卡在动态脚本的debugger上提升调试体验第二层Tampermonkey resetDebugger.js从运行时阻断反调试逻辑减少动态脚本持续生成根治内存问题第三层浏览器性能优化进一步降低内存和 CPU 占用核心原理第二层通过 Tampermonkey 在document-start最早注入借助grant unsafeWindow在页面上下文中劫持Function、setInterval、eval等原生 API从运行时阻断反调试逻辑。2.1 三层分别管什么配合关系三层同时启用、互相补充不是「先做完第一层再做第二层」的流水线层级何时生效作用对象一句话第一层你按 F12 打开开发者工具时Chrome DevTools按 Ignore List 规则跳过指定脚本不在其debugger处停住第二层页面开始加载时油猴脚本页面里的 JavaScript别让new Function(debugger)等代码真正跑起来第三层浏览器全局设置Chrome 进程降低标签页长期占用的内存和 CPU第二层治本拦截动态函数、高频定时器减少反调试脚本持续生成解决内存暴涨。第一层治标调试侧即便仍有少量残留脚本F12 也不会被无限断点卡住。第三层辅助在治本基础上再压低浏览器自身的资源占用。三、操作步骤3.1 第一层Chrome Ignore List黑盒目的让开发者工具在匹配到的脚本里自动跳过断点不解决内存问题只改善 F12 体验。说明Ignore List 匹配的是 Sources 面板里显示的脚本名称/URL不是磁盘上的某个固定文件。VM123这类名字是 Chrome 给eval/Function等动态代码起的虚拟编号并不存在一个叫VM.js的实体文件。步骤操作1打开目标网页按F12→Sources2观察左侧脚本树里反复出现的名称常见为VM 数字也可能是eval、匿名片段等3点击右上角⚙️ 设置→Ignore List旧版名 Blackboxing4点击Add pattern按你在 Sources 里看到的名称添加规则以下为常用示例可按需增删5勾选Ignore content scripts6关闭设置并刷新页面示例 pattern非必须照抄示例规则适用情况VM\d脚本名类似VM123、VM456Chrome 动态脚本常见展示名^VM名称以VM开头你在 Sources 里看到的其它名称例如某站显示为chunk-xxx.js或固定前缀应按实际名称自行添加 pattern文档中的VM\d等仅作示例。若站点不用VM前缀请根据 Sources 面板真实名称配置不要误以为必须存在VM.js或只需配置这三条。预期打开 F12 后匹配到的脚本不再反复卡在debugger未匹配的脚本仍可能触发断点需结合第二层。3.2 第二层Tampermonkey resetDebugger.js3.2.1 安装 TampermonkeyChromeChrome 网上应用店EdgeEdge 加载项安装后确认扩展图标为彩色、已启用。3.2.2 导入劫持脚本步骤操作1点击 Tampermonkey 图标 →管理面板2点击新建脚本3删除编辑器默认内容4打开同级文件resetDebugger.js完整复制并粘贴5CtrlSMacCommandS保存6确认脚本状态为已启用蓝色开关3.2.2.1match在哪里配置不在本文档里也不在Chrome / 油猴「设置」页面。规则写在脚本文件最上方的// UserScript…// /UserScript元数据块中。在油猴管理面板里打开本脚本或编辑仓库里的resetDebugger.js可见类似// UserScript// name 终极反无限Debugger - ...// match *://*/* ← 所有网站都生效无需再写具体域名// exclude-match *://*/*frame* ← 排除 URL 含 frame 的页面// noframes ← 不在 iframe 里运行// /UserScript只让个别站点生效时删掉或注释match *://*/*只保留目标域名例如// 先删掉或注释// match *://*/*// match *://*.example.com/*改完后在油猴编辑器里CtrlS保存再刷新目标网页。3.2.3 脚本拦截点resetDebugger.js使用inject-into page在页面上下文直接安装 Hookgrant none不用unsafeWindow/GM_addElement避免油猴addElement: failed to find created element等桥接错误。控制台日志前缀[终极方案]。拦截逻辑序号拦截点行为1FunctionProxy函数体含debugger时返回空函数保留原生Function.prototype2setInterval/setTimeout仅当回调明确含debugger时返回03requestAnimationFrame回调含debugger时返回04DOM 清理每 10 秒移除短小的含debugger内联script5window.gc可选浏览器暴露gc时每 60 秒回收一次元数据要点inject-into page、grant none、noframes、exclude-match *frame*默认match *://*/*已移除eval劫持。3.3 第三层浏览器性能优化目的降低长期打开问题页时的资源占用。优化项操作路径建议禁用不必要扩展chrome://extensions/调试时临时关闭无关扩展内存节省模式chrome://settings/performance开启「内存节省程序」等选项清理缓存CtrlShiftDelete时间范围「所有时间」勾选缓存后清除四、验证方法4.1 验证步骤步骤操作预期结果1打开问题网页页面正常加载无明显卡顿2按F12不进入无限debugger断点3查看Console出现[终极方案] 脚本已加载、所有反调试拦截器已部署完成触发拦截时会有对应阻止日志4ShiftEsc打开任务管理器观察该标签页内存5保持页面 10–30 分钟内存稳定在约 200–500 MB不持续飙涨4.2 效果对比状态调试体验内存趋势无任何措施❌ 无限断点无法操作 涨至数 GB 后崩溃仅第一层黑盒✅ 可调试 仍持续增长仅第二层油猴✅ 可调试 稳定可控三层全开推荐✅ 流畅 稳定可控五、故障排查问题可能原因解决方案脚本未生效Tampermonkey 未启用确认扩展已安装并启用硬刷新页面页面功能异常match过宽误伤改为具体域名match仍有少量内存增长站点使用非常规反调试在 Sources 中确认脚本显示名后向 Ignore List追加对应 pattern非固定VM规则报bind/apply/intrinsic错误用普通函数整体替换了Function破坏原型链使用仓库最新resetDebugger.jsProxy 包装硬刷新报addListener仅油猴content.jsiframe 中扩展 API 不可用确认脚本含noframes关闭油猴「在所有框架中注入」报addElement: failed to find created elementGM_addElement误把parentNode写在 attributes 里使用最新脚本inject-into page不再用GM_addElement控制台其它报错与其他扩展冲突临时禁用其他扩展逐个排查更新脚本后不生效缓存清缓存后重新加载六、常见问题FAQQ1脚本会影响所有网站吗默认match *://*/*匹配全部站点。若只需特定站改为例如match *://*.example.com/*。Q2会被网站检测到吗脚本在document-start注入并 Hook 原生方法站点自身的检测逻辑通常也依赖这些方法实践中难以单独识别本脚本。Q3为什么第一层不能根治内存Ignore List 只影响「调试器要不要停断点」不阻止页面执行动态反调试代码高频setIntervalnew Function(debugger)仍会消耗 CPU 与内存需依赖第二层拦截。Q4window.gc是什么Chrome 可选暴露的强制垃圾回收 API。需用启动参数开启后脚本才会进入第 7 项逻辑每 60 秒调用一次chrome.exe --js-flags--expose-gc未开启时脚本前 6 项拦截与 DOM 清理仍正常工作只是不会周期性调用gc。Q5脚本能加载但 pageSpy 等库报Cannot read properties of undefined (reading bind)说明脚本曾用普通函数整体替换Function导致Function.prototype.bind失效。当前脚本已改为Proxy 包装原生Function仅在代码含debugger时拦截请用仓库最新resetDebugger.js覆盖油猴中的脚本后硬刷新。Q6content.js报Cannot read properties of undefined (reading addListener)这是油猴在iframe如__migu_web_refactor_frame里注入的桥接脚本该环境下chrome.runtime.onMessage常为undefined。若脚本使用unsafeWindow在document-start改页面 API会加重 iframe 桥接异常。当前脚本使用inject-into pagegrant none在页面上下文直接 Hook并配合noframes。若仍报错请在油猴设置 → 常规关闭「在所有框架中注入」保存后硬刷新。Q7addElement: failed to find created element或indexOf called on null常见错误写法GM_addElement(script, { parentNode: document, textContent: ... })——parentNode不能写在 attributes 里。正确写法为GM_addElement(parent, script, { textContent })或省略 parent 由油猴挂到head。当前脚本已改为inject-into page不再使用GM_addElement。七、配合使用脚本 resetDebugger.js// UserScript// name 终极反无限Debugger - 内存优化源头阻断// namespace http://tampermonkey.net/// description 彻底解决动态脚本无限debugger导致的内存暴涨问题// author Ultimate Solution//// --- 生效范围在油猴编辑器里改这里不是在 Chrome 设置里---// match *://*/*// 在哪些网址运行*://*/* 表示所有 http/https 页面。// 若只想对部分站点生效注释掉本行改为例如 match https://***.com/*// exclude-match *://*/*frame*// exclude-match *://*/*Frame*// exclude-match *://*/*iframe*// 排除 URL 路径中含 frame/iframe 的页面减少在嵌入页里触发油猴桥接报错。// noframes// 不在 iframe 子框架中运行本脚本反调试一般在顶层页可减轻 content.js 类报错。//// --- 注入时机与方式 ---// inject-into page// 在「页面上下文」执行才能直接 Hook 页面的 Function / 定时器等// 不用 unsafeWindow / GM_addElement避免 addElement、addListener 等桥接错误。// run-at document-start// 尽早注入抢在站点反调试脚本之前安装 Hook。// grant none// 不申请 GM_* 权限与 inject-into page 配合脚本即页面代码。// /UserScript(function(){use strict;if(window!window.top){return;}if(window.__RESET_DEBUGGER_HOOKED__){return;}window.__RESET_DEBUGGER_HOOKED__true;console.log([终极方案] 脚本已加载开始拦截所有反调试行为);functionhasDebugger(code){if(typeofcode!string)returnfalse;consttcode.trim();returntdebugger||tdebugger;||/\bdebugger\s*;/.test(code);}functionnoopFn(){}constOriginalFunctionFunction;constPatchedFunctionnewProxy(OriginalFunction,{apply(target,thisArg,argArray){constbodyargArray[argArray.length-1]||;if(hasDebugger(body)){console.log([终极方案] 已阻止debugger函数生成);returnnoopFn;}returnReflect.apply(target,thisArg,argArray);},construct(target,argArray,newTarget){constbodyargArray[argArray.length-1]||;if(hasDebugger(body)){console.log([终极方案] 已阻止debugger函数生成);returnnoopFn;}returnReflect.construct(target,argArray,newTarget);},});window.FunctionPatchedFunction;constoriginalSetIntervalwindow.setInterval;window.setIntervalfunction(handler,timeout,...args){if(typeofhandlerfunctionhasDebugger(Function.prototype.toString.call(handler))){console.log([终极方案] 已阻止反调试 setInterval);return0;}if(typeofhandlerstringhasDebugger(handler)){console.log([终极方案] 已阻止反调试 setInterval(字符串));return0;}returnoriginalSetInterval.call(this,handler,timeout,...args);};constoriginalSetTimeoutwindow.setTimeout;window.setTimeoutfunction(handler,timeout,...args){if(typeofhandlerfunctionhasDebugger(Function.prototype.toString.call(handler))){console.log([终极方案] 已阻止反调试 setTimeout);return0;}if(typeofhandlerstringhasDebugger(handler)){console.log([终极方案] 已阻止反调试 setTimeout(字符串));return0;}returnoriginalSetTimeout.call(this,handler,timeout,...args);};constoriginalRAFwindow.requestAnimationFrame;if(typeoforiginalRAFfunction){window.requestAnimationFramefunction(callback){if(typeofcallbackfunctionhasDebugger(Function.prototype.toString.call(callback))){console.log([终极方案] 已阻止动画帧中的debugger);return0;}returnoriginalRAF.call(this,callback);};}constoriginalSetIntervalForCleanuporiginalSetInterval;constscriptCleanupIntervaloriginalSetIntervalForCleanup.call(window,(){constscriptsdocument.querySelectorAll(script);letcleaned0;scripts.forEach((script){constcontentscript.textContent||script.innerText;if(contenthasDebugger(content)content.length500){script.remove();cleaned;}});if(cleaned0){console.log([终极方案] 已清理,cleaned,个残留的debugger脚本);}},10000,);window.addEventListener(beforeunload,(){window.clearInterval(scriptCleanupInterval);});if(window.gc){originalSetIntervalForCleanup.call(window,(){window.gc();},60000,);}console.log([终极方案] 所有反调试拦截器已部署完成);})();