JS逆向实战24—— 补环境过某房地产瑞数4.0

前言

瑞数就不过多介绍了,算是国内 2 线产品中的天花板了。4 代其实难度不高,但要弄出来 确实挺费时间和耐心的。今天就简单来讲讲如何用补环境轻松的过瑞数。
本文首发链接为: 逆向实战之补环境过某房地产瑞数4.0

前言

本文章中所有内容仅供学习交流,抓包内容、敏感网址、数据接口均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关,若有侵权,请联系我立即删除!此外出于某种原因。本章大部分调试会省略,仅简单说个大概

目标网站

aHR0cDovL3d3dy5mYW5nZGkuY29tLmNuL25ld19ob3VzZS9uZXdfaG91c2VfZGV0YWlsLmh0bWw=

官网: https://www.riversecurity.com
其余的自己看看官网介绍吧。

网站分析

第一次 发起的请求 如下图
可见首先这是个 202 请求,其次他自返回了一个cookie: FSSBBIl1UgzbN7N80S


并且的话还生成了一个 js c.FxJzG50F.dfe1675.js。

第二次 new_house_detail.html

第二次需要携带第一次的请求 已经一个新的 cookie 才能返回正常的页面。如下图

逆向流程分析

其实 我觉得补环境和正常分析区别很大。

我们不需要了解瑞数中的真假 cookie。

只需要定义出一个函数,去接受和返回 docuement.cookie即可。

在正式分析以前。我们需要固定一份静态代码。因为瑞数的网站返回的内容每次都是动态的,不方便我们调试。后续我们把这份静态代码放到 node 里就能慢慢的去补了。

这里补环境的话需要四部分内容,如上图 红圈中圈中的三个。以及生成的虚拟机代码。这四部分代码可以生成FSSBBIl1UgzbN7N80T

这里简单说明一下他们的作用

  • meta 标签中的内容:(即 content 中的内容)会在后续的 eval 中用得到
  • 外链的 js :c.FxJzG50F.dfe1675.js这部分代码。下面的那个自执行函数会解密这个 js 从而生成虚拟机代码
  • 自执行函数:解密外链 js。生成虚拟机代码。并且添加属性
  • 生成的虚拟机代码。生成新的 cookie

我们需要把这三段代码,放到一个文件中

(生成的虚拟机文件不需要,他会自己生成的。)

补环境

在此之前。我们看看正常页面返回给我们的 cookie 是啥样的。并且观察下长度是如何
我们在我们的代码头部增加两行代码,

document.getElementsByTagName = ...

完整代码在微信公众号 逆向实战之补环境过某房地产瑞数4.0
直接放入浏览器中执行
发现图 9 成功生成了 cookie。但是好像长度怎么多了 22 位。这是怎么回事呢?

这里我猜测可能和网站的某些环境参数对应不上。因为我是用自定义页面打开且调试的。

自定义调试代码如下图所示
补环境的代码结构大概是这样的.如下图所示。
我们直接右击运行这个JS
发现报了个 window is not defined 补上

window = gloabl;

然后继续运行

如下图所示 发现又报错了,好像是缺少了 location
那我们补上 location = {}(最好 copy 网站上的 location 全一些)

然后继续运行

然后发现还是如图 13 一样 报了没有location 这是怎么回事呢?

不要慌,此时我们挂上 proxy 代理来看看究竟是哪个对象上面没有location这个属性。
挂上代理对象。发现一切都好像明亮了起来。我们把这些对象都补上环境。顺便把 document 也挂上代理。

window = global;
window.top = window;
document = {};

继续运行
继续伪造
别问怎么去伪造,直接去原网站对比就行了。

document.createElement = function (val){if(val==='div'){return div} if(val==='form'){return {}} if(val==='a'){return {}}}

然后就很简单了。缺啥补啥。这里因为篇幅问题这里就不一一详解了。然后补全到这个地方 还会有个报错。报一个东西未定义。如下图
这里可以看到 报错内容是 _$x[$46[441]] is not a function

具体原因是因为addEventListener和attachEvent没有被定义。

所以我们简单补全一下。这里重点关注一个值 addEventListener、这个也是要补在 window 环境中的。这个方法的作用就是添加事件监听器。重写 very very 重要。但是在本章中不需要。只需要补全伪装成普通的函数即可

> 完整代码在微信公众号 逆向实战之补环境过某房地产瑞数4.0

然后运行
然后发现 就成功了。并且长度也是 217。

对了,网站上还有个定时器,导致程序不会退出,且会报错。我们还需要把 setInterva这个方法给置空。

setInterval= function(){};

结果

我们放到 python 代码中运行。记得要替换掉动态代码。然后去运行。结果如下

结语

说实话。还是挺麻烦的。得多谢 流星Studio指点了一下。其实一旦学习了补环境框架,总体来说还是挺简单的。后续可能会出rs5代的补环境教程。

目录 一、禁用 F12 / Ctrl+Shift+I / 打开控制台的按键绑定 二、无限debugger 2.1 概念 2.2 实现 三、控制台开启状态检测 3.1 基于窗口尺寸差异 3.2 基于执行时间差 3.3 性能分析 (需要做大量测试) 四、内存爆破 五、绕过调试拦截 调试拦截(Debugging Interception) 是一类前端防护手段,目的在于检测并干预通过浏览器常规调试工具(如 DevTools、Console、断点、脚本注入等)对页面 JavaScript 进行观测、分析或修改的行为。当检测到调试器被打开或调试行为发生时,页面会触发一系列响应:显示误导信息、降级功能、限制据输出、或引导到验证码/挑战页,从而增加逆向与抓取的成本。 注意: 目的不是 "彻底阻止调试",而是 增加分析成本和时间消耗。 一、禁用 F12 / Ctrl+Shift+I / 打开控制台的按键绑定 注意:前端按键拦截很容易被用户关闭或绕过(例如禁用 JS)。适合作为体验层的缓冲,不要作为唯一手段。 // ps: 这些操作同样只能防君子不防小人 // 开发者可以在控制台直接移除这些事件监听器,所以不能作为真正安全的手段 document.addEventListener('keydown', function(e) { // F12 if (e.key === 'F12' || e.keyCode === 123) { e.preventDefault(); e.stopPropagation(); } // Ctrl+Shift+I / Ctrl+Shift+J / Ctrl+Shift+C if (e.ctrlKey && e.shiftKey && (e.key === 'I' || e.key === 'J' || e.key === 'C')) { e.preventDefault(); e.stopPropagation(); } // Ctrl+U (查看源码) 这个好像不行 // if (e.ctrlKey && e.key === 'U') { e.preventDefault(); } }); window.addEventListener('contextmenu', e => e.preventDefault()); // 禁右键菜单(可选) 一键获取完整项目代码 javascript 1 2 3 4 5 6 7 8 9 10 11 12 13 代码解释:addEventListener 方法: document.addEventListener('keydown', function(e) { ... }) 一键获取完整项目代码 javascript 1 addEventListener 是 DOM(文档对象模型)中用于 给元素绑定事件监听器 的方法。上面这行的意思是:给整个网页文档(document)注册一个 键盘按下事件监听器,当用户按下任意键时,就会执行后面的回调函。语法通用形式: element.addEventListener(eventType, callback, useCapture) // ① element: 要绑定事件的元素(如 document、window、button 等) // ② eventType: 事件类型,如 'click'、'keydown'、'keyup'、'mousemove' // ③ callback: 当事件触发时要执行的函 // ④ useCapture(可选): 是否使用捕获阶段,默认为 false(冒泡阶段) 一键获取完整项目代码 javascript 1 2 3 4 5 e 是什么?在回调函里: function(e) { ... } 一键获取完整项目代码 1 这个 e 是事件对象(Event Object),浏览器在触发事件时会自动传入一个包含当前事件所有信息的对象。你可以理解成: "这次按键事件的详细记录"——包括按下了哪个键、是否同时按了 Ctrl/Shift、事件目标是谁等。举例打印看看: document.addEventListener('keydown', e => console.log(e)) 一键获取完整项目代码 javascript 1 按任意键后,你会看到浏览器控制台打印一个对象: 其中每个对象中包含: //我只列举了一部分: // ① key: 当前按下的键的名字(如 "F12"、"u"、"I") // ② keyCode: 对应键的值编码(如 123 对应 F12) // ③ ctrlKey: 是否同时按下了 Ctrl 键(true / false) // ④ shiftKey: 是否同时按下了 Shift 键 // ⑤ altKey: 是否按下了 Alt 键 // ⑥ metaKey: 是否按下了 Mac 的 command 键 // ⑦ type: 事件类型(keydown / keyup) // 当你按下 F12: e.key // "F12" e.keyCode // 123 // 当你按下 Ctrl + U: e.ctrlKey // true e.key // "u" e.keyCode // 85 一键获取完整项目代码 javascript 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 e.preventDefault() 与 e.stopPropagation(): e.preventDefault(): 阻止浏览器默认行为 // ①阻止 Ctrl+U 打开"查看源码" ②阻止右键菜单 ③阻止链接跳转 ④阻止表单自动提交 意思是: 这次按键事件,不要执行浏览器原本绑定的默认动作 e.stopPropagation(): 阻止事件冒泡 事件在 DOM 结构中默认会从最内层元素往外层一层层传播(称为 "冒泡")。调用这个方法会阻止事件继续向上传递,防止触发其他监听器 意思是: 事件只在当前这一层处理,不要再传给上层元素 一键获取完整项目代码 javascript 1 2 3 4 5 6 7 优点:阻止一般用户误触与懒散的调试尝试。缺点:完全可绕过;对无障碍、正常用户体验有影响(右键禁用会影响拷贝、辅助功能),需慎用并给出替代交互(例如自定义菜单)。 二、无限debugger 2.1 概念 在 JavaScript 里,debugger 是一条专门用于 断点调试 的语句。当浏览器执行到这行代码时,如果开发者工具(DevTools)已经打开,那么执行会 立刻暂停,并跳转到调试界面。简单理解: console.log("A"); debugger; // 如果控制台打开,这里会停住 console.log("B"); 一键获取完整项目代码 javascript 1 2 3 如下图: 当浏览器的 开发者工具(DevTools)处于打开状态时,执行到 debugger 语句会触发断点,脚本暂停并等待调试操作。当开发者工具未打开时,debugger 语句 不会生效,浏览器会 直接跳过并继续执行后续代码。所以,新手常见疑问: "为什么我打开控制台就卡住了,不打开就没事?" 答案就是: debugger 只在调试模式下生效 一键获取完整项目代码 javascript 1 2 为什么 debugger 能 "干扰" 调试? 在正常开发中,debugger 语句用于手动设置断点以方便调试;但在 反爬或反调试场景 下,它却常被用作一种 "陷阱"。当浏览器的开发者工具(DevTools)被打开时,脚本执行到 debugger 会自动触发断点并暂停代码运行,每次都需人工点击 "恢复执行(Resume)"。若脚本中大量插入或循环触发 debugger,就形成了所谓的 “无限 debugger(debugger trap)”:页面会在 DevTools 打开状态下一次次地进入断点暂停,导致调试流程被不断打断、几乎无法顺利分析。利用这种机制,脚本能有效干扰逆向者的调试体验,大幅提升分析难度与时间成本。 2.2 实现 在实现 "无限debugger"之前,先理解浏览器中两个最常用的定时函很重要:setTimeout 和 setInterval。 // ①: setTimeout(fn, delay, ...args) // 在至少 delay 毫秒后执行一次 fn(只执行一次)。返回一个定时器 id(可传给 clearTimeout 取消) const tid = setTimeout(() => console.log('one-shot'), 1000); // clearTimeout(tid); // 取消 // ② setInterval(fn, delay, ...args) // 每隔大约 delay 毫秒执行一次 fn,直到调用 clearInterval 停止。返回一个定时器 id。 const iid = setInterval(() => console.log('repeat'), 1000); clearInterval(iid); // 停止 // 主要区别(一句话) // setTimeout: 一次性延迟执行 // setInterval: 定期重复执行 一键获取完整项目代码 javascript 1 2 3 4 5 6 7 8 9 10 11 12 13 在浏览器中,最简单的无限 debugger 实现方式是: setInterval(() => { debugger; }, 100); 一键获取完整项目代码 javascript 1 然而,这种实现方式的反调试效果非常有限。分析者只需将鼠标定位在断点处,右击选择 "Never pause here",然后点击 "Resume script execution",即可绕过该无限 debugger,页面脚本将恢复正常执行。也就是说,简单的循环触发无法对熟悉 DevTools 的调试者造成实质阻碍。 变种示例: function debug(){ Function("debugger")() eval("debugger") } setInterval(debug, 500) 一键获取完整项目代码 java 1 2 3 4 5 Function("debugger")():动态构造并立即执行一个新的函,其函体是字符串 "debugger"。等价于动态创建 function(){ debugger }(),执行时如果 DevTools 打开会触发断点。eval("debugger"):通过 eval 执行字符串形式的 debugger,其效果同样是在运行时触发断点(前提是 DevTools 已打开)。 这两者都属于运行时动态生成并执行断点代码,放在 debug() 里并由 setInterval 周期性调用,从而达到 "周期性触发断点" 的效果。同理在 DevTools 的断点处右键 → Never pause here(针对具体断点),点击调试器的 Resume script execution(恢复执行)一样可以绕过该无限 "debugger"。用递归 setInterval + 随机间隔(避免规律性容易绕过)----- 抗 Never: const debugStr = "debugger"; // 生成唯一标识,用于 sourceURL(让 DevTools 为每次 eval 显示不同来源) function uniqueTag() { return 'dbg_' + Math.random().toString(36).slice(2) + '_' + Date.now(); } // 在执行 debugger 的同时附带独立 sourceURL(提高静态/来源混淆) function triggerDynamicDebugger() { const tag = uniqueTag(); // 注意: //# sourceURL=... 会在 DevTools 的 Sources 里显示为该名字 const src = `${debugStr};//# sourceURL=${tag}.js`; try { // new Function 与 indirect eval 同时尝试(增加触发面) (new Function(src))(); (0, eval)(src); // indirect eval,避免某些优化 } catch (e) { // 如果 eval/Function 被 CSP / 环境禁止,会抛异常 // 我们在上层会捕获并选择降级策略 } } setInterval(triggerDynamicDebugger, 500); 一键获取完整项目代码 javascript 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 通过给每次 eval 或 Function 生成独立的 sourceURL 并持续触发 debugger,每次断点在 DevTools 中都对应不同的来源标识。这使得分析者无法通过 Never pause here 一次性屏蔽所有断点,每次触发都必须单独处理,从而增加了调试绕过的成本和复杂度。拓展: DevTools 在内部管理断点时,主要依据三类信息: 脚本来源(Script Source) 如果是静态 JS 文件,就是 URL 或文件名。 如果是 <script> 标签或动态生成的代码(eval / Function),DevTools 会自动生成一个内部标识(内部 VM script ID)。 对于动态生成的代码,浏览器会给它分配一个内部标识(内部 VM 脚本 ID),或者你可以通过 //# sourceURL=xxx.js(不一定要是这种格式) 给它起一个 "伪名字",在 DevTools Sources 面板中显示。 行号 / 列号(Line/Column) 断点记录在特定脚本的某一行列位置。 调用帧(Call Frame) debugger 在特定的执行栈位置触发断点时,会记录当前调用帧信息,用于暂停调试。调用帧就是当前 debugger 被哪一条函调用链触发的上下文信息。 为什么简单动态 debugger 仍然能被 Never pause here 绕过: 没有指定 sourceURL 的情况下,每次 eval / Function 生成的动态脚本,DevTools 可能复用同一个内部 VM 脚本 ID,尤其是在连续 eval / Function 执行的同一作用域下。 这意味着:虽然行号和调用帧可能不同,但 脚本来源(VM script ID)被复用,DevTools 内部会把之前设置的 "Never pause here" 记录在这个内部 ID 上。 因此,你选了 "Never pause here" 后,所有用同一内部 ID 执行的 debugger 语句都会被忽略,即便行号或调用帧不同。 换句话说,动态生成的代码如果没有独立 sourceURL,DevTools 会把它当成 “同一脚本”,就能被一次性屏蔽。对于 Function 或 eval 来说,每次传入的代码参如果不同(上面说的理解为内部 VM script ID 不同),浏览器会把它当作 新的脚本来源(VM script) 来处理。因此,即便行号或调用帧相同,DevTools 也会认为这是不同的脚本,所以之前设置的 "Never pause here" 无法一次性屏蔽每次触发的 debugger。 在反调试或反爬场景中,debugger 不仅可以直接写在脚本中触发,还可以通过 DOM 节点动态注入 来触发。这种方式的特点是:每次触发都生成新的 <script> 元素,浏览器会认为这是一个全新的脚本来源,从而让 debugger 的触发更加难以被 DevTools 的 "Never pause here" 绕过。下面是一个示例,通过动态创建 <script> 标签并注入 debugger,每 500ms 触发一次: const debugStr = "debugger"; // 生成唯一标识,用于 sourceURL(让 DevTools 为每次 eval 显示不同来源) function uniqueTag() { return 'dbg_' + Math.random().toString(36).slice(2) + '_' + Date.now(); } function debug() { const tag = uniqueTag(); let script = document.createElement('script'); script.innerHTML = `${debugStr};//# sourceURL=${tag}.js`; document.head.appendChild(script); document.head.removeChild(script); } setInterval(debug, 500); 一键获取完整项目代码 javascript 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 除了直接在代码中写 debugger 或通过 DOM/eval 动态触发之外,还可以利用 AST(抽象语法树) 对源代码进行分析和操作:在程序的各类代码块(如函体、循环体、条件分支等)中随机插入 DebuggerStatement。这种方式能够在源代码层面大规模生成 debugger 语句,从而对调试造成 "污染",显著增加人工分析和逆向的难度。AST 示例: const parser = require('@babel/parser'); const traverse = require('@babel/traverse').default; const generator = require('@babel/generator').default; const t = require('@babel/types'); const code = ` let a = 100; let b, c; let obj = { name: "amo", age: 18, add: function(a, b) { return a + b + 1000; }, mul: function mul(a, b) { return a * b + 1000; } }; obj.add(10, 20); obj.mul(5, 6); `; const insertCount = 20; const maxPerFunction = 5; // 解析 AST const ast = parser.parse(code, { sourceType: 'unambiguous' }); // 收集可插入语句体 const insertPoints = []; // 顶层和非函体 const funcBodies = []; // 函组 traverse(ast, { Program(path) { insertPoints.push(path.node.body); }, BlockStatement(path) { // 如果父节点是函,不加入顶层 insertPoints if (path.parent.type !== 'FunctionDeclaration' && path.parent.type !== 'FunctionExpression' && path.parent.type !== 'ObjectMethod') { insertPoints.push(path.node.body); } }, ObjectMethod(path) { if (path.node.body && Array.isArray(path.node.body.body)) { funcBodies.push({ arr: path.node.body.body, count: 0 }); } }, ObjectProperty(path) { if (path.node.value && path.node.value.type === 'FunctionExpression' && path.node.value.body && Array.isArray(path.node.value.body.body)) { funcBodies.push({ arr: path.node.value.body.body, count: 0 }); } } }); // 先尝试在函体内插入 debugger(限 maxPerFunction) let remaining = insertCount; funcBodies.forEach(f => { const toInsert = Math.min(maxPerFunction, remaining); for (let i = 0; i < toInsert; i++) { const arr = f.arr; const validPos = []; for (let j = 0; j <= arr.length; j++) { if (j === 0 || arr[j - 1].type !== 'ReturnStatement') validPos.push(j); } if (validPos.length === 0) continue; const pos = validPos[Math.floor(Math.random() * validPos.length)]; arr.splice(pos, 0, t.debuggerStatement()); f.count++; remaining--; } }); // 剩下的 debugger 插入顶层或其他非函体 while (remaining > 0 && insertPoints.length > 0) { const idx = Math.floor(Math.random() * insertPoints.length); const arr = insertPoints[idx]; if (!Array.isArray(arr)) continue; const pos = arr.length === 0 ? 0 : Math.floor(Math.random() * (arr.length + 1)); arr.splice(pos, 0, t.debuggerStatement()); remaining--; } // 生成代码 const out = generator(ast, { compact: false, retainLines: true }).code; console.log(out); 一键获取完整项目代码 javascript 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 效果: 需要注意的是:既然 debugger 可以通过 Function 构造或 eval 动态生成,攻击者/分析者可以通过重写全局 Function、eval 或覆盖某些全局接口来尝试绕过这类防护。为提高抗扰性,可以通过 沿着原型链获取构造函(例如 (function(){}).__proto__.constructor)或通过多条路径获取 Function,这样即便全局 Function 被污染,仍可能能得到原始构造器并触发断点。换言之,只要能够取得 Function 构造器(无论通过何种路径)并执行带有 debugger 的载荷,动态生成断点就可实现;正因为途径繁多,攻击者的调试与逆向成本也随之增加。小结: // ① 明文触发(最直接) 直接写出 debugger;,在 DevTools 打开时立即产生断点。 // 示例: debugger; // ② 轻度混淆(拼串 / eval) 把 debugger 作为字符串拼接或通过 eval 执行,躲避简单的静态扫描,但运行时效果等同于明文。 // 示例: eval('debug' + 'ger;'); // ③ 重度混淆(构造 + 调用链)——三要素玩法 // 将三项要素——(A)如何得到 Function 构造器 / eval(来源)、(B)debugger 字符串(载荷)、(C)如何执行/触发(call/apply/bind/直接调用)——组合起来,形成大量变体以增加分析难度。常见变体包括: // 通过 Function 构造并直接调用 Function('debugger')(); // 通过 call/apply/bind/constructor 动态触发 Function('debugger').call(); Function('debugger').apply(); Function('debugger').bind()(); Function.constructor('debugger').call('action'); funObj.constructor('debugger').call('action'); // 更复杂的取 constructor 链与 eval 嵌套 (function(){return !![];}['constructor']('debugger')['call']('action')); eval('(function (){}["constructor"]("debugger")["call"]("action"));'); //抽象出的 "三元素模型" // 把上面归纳成一个简单模型便于理解与讨论: // 1.来源(Source): 如何取得可构造/执行代码的入口,比如全局 Function / eval / obj.constructor / 原型链 // 2.载荷(Payload):实际要执行的字符串(通常包含 debugger) // 3.触发方式(Invoke):如何执行载荷:直接调用、.call()、.apply()、.bind() 等 // 4.基于这三元素,可以生成成百上千种变体来混淆静态/动态分析器 一键获取完整项目代码 javascript 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 三、控制台开启状态检测 3.1 基于窗口尺寸差异 浏览器的 开发者工具(DevTools)打开时,会占用一部分屏幕空间,通常会改变浏览器 可视窗口(window.innerWidth / window.innerHeight)与浏览器总窗口(window.outerWidth / window.outerHeight)的差值。 window.outerWidth / outerHeight:浏览器整个窗口的宽高,包括边框、滚动条和 DevTools 面板。 window.innerWidth / innerHeight:页面内容可视区域的宽高,不包括浏览器边框和 DevTools 面板。 示例代码: // 方法 A:基于窗口尺寸差异检测 DevTools 开启状态 function detectDevToolsSizeChange() { const threshold = 160; // 一般控制台面板的最小高度或宽度 const widthDiff = window.outerWidth - window.innerWidth; const heightDiff = window.outerHeight - window.innerHeight; if (widthDiff > threshold || heightDiff > threshold) { console.log("调试工具已打开"); return true; } else { console.log("调试工具未打开"); return false; } } // 每 500ms 检测一次 setInterval(detectDevToolsSizeChange, 500); 一键获取完整项目代码 javascript 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 设置阈值(一般 160px 左右,对应常见 DevTools 面板大小)。当差值超过阈值时,说明有额外空间被占用,很可能是 开发者工具已打开。优点:无需依赖特定浏览器 API,简单、轻量、兼容性好。缺点:对某些小屏幕或高分屏可能误报,用户调整窗口大小也可能触发,无法判断 DevTools 是否隐藏或在外部窗口打开(对 Docked(dock)的 DevTools 比较准确,但对 undocked/外部窗口或特制浏览器不可靠)。 3.2 基于执行时间差 这种检测方式通过 测量代码执行前后的时间差 来判断是否处于调试状态。核心思想是:如果某段代码在执行时耗时异常地长,就可能是被断点暂停或正在被调试。 function detectDevTools() { const start = Date.now(); // 模拟一段正常执行的关键业务逻辑 for (let i = 0; i < 1e6; i++) { Math.pow(i, 2); } const end = Date.now(); const duration = end - start; // 如果执行时间异常增长,说明可能被断点暂停或调试中 if (duration > 100) { console.warn("检测到调试工具可能处于打开状态"); triggerFakeBranch(); } else { console.log("正常执行"); } } // 被检测触发时执行的伪逻辑 function triggerFakeBranch() { console.log("进入伪逻辑分支: 返回错误据,干扰分析者..."); // 可以设计为干扰逻辑,例如: // window.location.reload(); // 或者返回假的据结果 } setInterval(detectDevTools, 1000); 一键获取完整项目代码 javascript 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 在代码执行前后分别记录时间戳,若期间 JS 执行被中断(例如打了断点或单步调试),浏览器主线程被挂起,但 Date.now() 依旧在走,所以,结束时间与开始时间的差值会 异常地大。这类检测并 不依赖 debugger; 语句,任何关键代码(例如加密计算、核心逻辑判断、据生成等)都可以包裹进检测逻辑中,只要执行时间超过阈值,就说明执行被中断或调试器介入,此时就可以进入伪造分支逻辑,比如输出假据或终止脚本运行。举例说明,分析者在调试过程中打了断点: for (let i = 0; i < 1e6; i++) { Math.pow(i, 2); } // 停在这里 // 此时脚本暂停执行,Date.now() 仍在走; // 恢复执行后检测时间差,就会触发“检测到调试”分支。例如: 返回错误据、输出伪造结果、或执行混淆的逻辑,让分析陷入误导 一键获取完整项目代码 javascript 1 2 3 3.3 性能分析 (需要做大量测试) 性能分析类的检测思路是:通过 高精度时间测量(如 performance.now())对极轻量级、理应耗时很短的代码片段进行重复采样;当这些片段在短时间内出现异常延迟(远大于正常抖动范围)时,很可能说明脚本执行被人工暂停或被 DevTools/性能分析器介入。关键要点是: 微小工作 + 高精度计时:选取开销极小但可重复的工作,使用 performance.now() 做亚毫秒级测量; 大量采样与统计处理:多次采样并取中位/去极值平均以降低误报; 阈值与环境校准:在目标机器/浏览器上做大量测试以确定合理阈值(不同设备差异很大); 多信号融合:单一时间检测容易被噪声影响,应与窗口尺寸、console 行为、断点探针等联合判断。 小结:这是一种 概率性 检测,需要大量采样与迭代调参,适合用作触发上报/降级的判断依据,而非唯一判定标准。为什么用 console.table / console.clear 作为实战信号? 在实际工程里,一个简单而有效的放大信号是 向控制台输出"重量级"内容 并测量耗时:当 DevTools 打开时,浏览器会为控制台输出做渲染与格式化,这比单纯的计算更耗时、更稳定地产生可测延迟。console.table 和 console.clear 常被用作这类检测的原因是: console.table(...):把组/对象渲染为表格,DevTools 会进行额外解析和渲染,开销较大;在控制台打开时多次调用更容易产生明显延迟。 console.clear():在打开的控制台上会触发清理/重绘操作,配合大量输出可以放大延迟效果,并避免产生过多可见日志噪音。 因此,在性能分析检测流程里,常把高频/重负载的 console.table 输出 + clear 作为一个 易于放大差异的观测手段,并对其耗时做多次采样与统计判断来辅助判断 DevTools 是否打开。DevTools 性能检测常用的 console 方法对比: 方法 特征 / 原理 是否可用于检测 console.table() 输出表格据,会触发 DevTools 对对象结构的解析与渲染;性能开销大。 极常用,延迟最明显。 console.clear() 清空控制台输出,DevTools 打开时会重绘 UI;未打开时几乎无影响。 常与 console.table 连用。 console.log() 输出字符串或对象,轻量级;若内容为复杂对象时 DevTools 需深度遍历。 需配合大对象输出才能体现差异。 console.dir() 打印对象的可枚举属性,DevTools 会构建可展开树结构。 对复杂对象延迟明显。 console.count() 计输出,内部操作简单;性能差异不大。 基本不适合检测。 console.group() / console.groupEnd() 控制分组显示;当控制台打开时会涉及层级渲染。 有轻微差异,可作为辅助信号。 console.profile() / console.profileEnd() 启动/停止性能分析;DevTools 未打开时通常无效。 可直接检测性能面板是否启用。 console.time() / console.timeEnd() 测量时间,偏轻量;可与其他方法组合。 本身检测力弱,但可辅助测量耗时。 console.trace() 打印调用栈;DevTools 打开时解析、格式化堆栈信息。 可用于检测堆栈解析延迟。 console.warn() / console.error() 输出警告/错误;DevTools 打开时可能触发高亮、堆栈展示。 差异轻微,但可混合作为噪声信号。 示例代码: function detectConsoleDelay() { const start = performance.now(); for (let i = 0; i < 1000; i++) { console['table'](10, 10, 10, 10, 10, 10, 10); } console['clear'](); const duration = performance.now() - start; alert(duration.toFixed(2)); // 显示耗时(毫秒,保留两位小) if (duration > 30) { // 阈值保持 30ms alert("调试工具已打开"); } else { alert("调试工具未打开"); } } setInterval(detectConsoleDelay, 1000); 一键获取完整项目代码 javascript 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 四、内存爆破 内存爆破 指攻击者通过持续分配/填充内存资源,试图耗尽目标进程或环境的可用内存,导致性能严重下降、页面/进程崩溃或拒绝服务。实现思路通常依赖两个要素:容器:用于累积据(例如组、对象、DOM 节点、缓存等);持续性:重复或持续地向容器写入据,直至资源耗尽。 // 错误的爆破形式。chrome 会对于某些内容存在着一个限制 let a = [] while(1){ a.push(1) } //如何进行隐蔽的爆破 let a = [] for (let j = 0; j <= 500000; j++) { a[j] = [] function memory_blast() { a[j].push(1) } setInterval(memory_blast, 100) } 一键获取完整项目代码 javascript 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 注意: 在明确识别到第三方爬虫正在抓取我们站点并对外发起流量时,若我们以内存耗尽等主动破坏性手段对其进行反制,这种做法本质上构成对对方的攻击(即我们成为攻击者),并可能带来法律与道德风险。 有些逆向/分析流程的常见步骤是:把浏览器里抓到的 JS 拷到本地,使用 IDE 对代码进行格式化和断点调试。格式化会引入明显的语法层面差异(额外换行、缩进和注释位置变化等),这些差异可以作为 "被本地调试/格式化" 的 线索。实现思路:在关键函处读取其源码(如 fn.toString()),统计换行、缩进或某些 minified 特征的丢失;当检测到源码与线上显著偏离时,可在严格限制和审慎控制下尝试 内存爆破(受控的内存压力探测),以增加本地调试难度并收集证据;务必保证该操作为受限采样且非破坏性,避免耗尽资源或影响正常服务。 // 模拟:未格式化的单行函(像从页面抓下来的压缩代码) const toDetect_min = function(a,b){return a*b+1}; // 模拟:本地格式化后的版本(多行) function toDetect_pretty(a, b) { // 这是格式化后会看到的多行/缩进形式 const prod = a * b; return prod + 1; } // 判断函是否被格式化(简单启发式) function isFormatted(fn) { const src = fn.toString(); const newlineCount = (src.match(/\n/g) || []).length; const hasIndent = /(^|\n)[ \t]{2,}\S/m.test(src); // 是否有缩进 return { name: fn.name || '(anonymous)', newlineCount, hasIndent, srcPreview: src.length > 200 ? src.slice(0, 200) + '...': src }; } // 在浏览器中测试 console.log('浏览器环境:'); console.log('未格式化 ->', isFormatted(toDetect_min)); console.log('格式化 ->', isFormatted(toDetect_pretty)); // 另外演示:如果你从页面复制一个已格式化的函文本并 eval 回来,仍然能检测到换行 const copiedPretty = `(function(x,y){ // 开发者在本地格式化后会像这样 const s = x + y; return s * 2; })`; const fnFromCopied = eval(copiedPretty); console.log('eval 后的格式化函 ->', isFormatted(fnFromCopied)); 一键获取完整项目代码 javascript 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 通过使用一些特殊字符进行混淆,可以防止代码被格式化或直接复制,从而增加调试和分析的难度。如下图: 如何生成的: let fs = require("fs") !function () { let var_local_use = [] for (let i = 67000; i <= 67800; i++) { try { eval(`var a = "${String.fromCharCode(i)}";`) var_local_use.push(i) } catch (e) { } } let code = "" for (let i of var_local_use) { code += `var a = "${String.fromCharCode(i)}"; /* ${i} */` } fs.writeFileSync("result.js", code, {encoding: "utf-8"}) }() 一键获取完整项目代码 javascript 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 再看: let fs = require("fs") !function () { let var_local_use = [] for (let i = 67000; i <= 67800; i++) { try { eval(`var ${String.fromCharCode(i)} = ${i};`) var_local_use.push(i) } catch (e) { } } ; let code = "" for (let i of var_local_use) { code += `var ${String.fromCharCode(Number(i))} = ${Number(i)};` + "\n" } fs.writeFileSync("result.js", code, {encoding: "utf-8"}) }() 一键获取完整项目代码 javascript 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Node.js 中执行结果: 将执行结果复制到浏览器控制台中进行调试,并插入 debugger,就像执行普通的 JS 代码一样。然而,你会发现浏览器中看到的源码格式会变成如下样式。我怀疑这是复制过程导致的差异,但不打算深入研究,就保持这样吧。如果在运行时把这段代码包装为函并调用其 toString(),两者字符串表示通常不一致——这一差异可作为检测复制/粘贴或调试行为的可靠线索。基于此,可以选择在不匹配时触发错误分支、引入 "内容爆破"(memory blow-up)或其他误导性逻辑,从而显著增加逆向分析与调试难度。 举例: //浏览器中生成: // U+0623 是 ARABIC LETTER ALEF WITH HAMZA ABOVE('أ') const name = String.fromCodePoint(0x0623); console.log('var ' + name + ' = 67107;') 一键获取完整项目代码 javascript 1 2 3 4 执行结果: 复制到本地: 五、绕过调试拦截 参考文章:https://blog.csdn.net/xw1680/article/details/138547184?spm=1011.2415.3001.5331 ———————————————— 版权声明:本文为优快云博主「棒棒编程修炼场」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/xw1680/article/details/153742917
最新发布
11-07
评论 13
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值