JavaScript逆向与爬虫实战——基础篇(调试拦截(如无限debugger)与内存爆破原理及绕过)

JavaScript逆向与爬虫实战——基础篇(调试拦截(如无限debugger)与内存爆破原理及绕过)

调试拦截(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()); // 禁右键菜单(可选)

代码解释:addEventListener 方法:

document.addEventListener('keydown', function(e) { ... })

addEventListener 是 DOM(文档对象模型)中用于 给元素绑定事件监听器 的方法。上面这行的意思是:给整个网页文档(document)注册一个 键盘按下事件监听器,当用户按下任意键时,就会执行后面的回调函数。语法通用形式:

element.addEventListener(eventType, callback, useCapture)
// ① element: 要绑定事件的元素(如 document、window、button 等)
// ② eventType: 事件类型,如 'click'、'keydown'、'keyup'、'mousemove'
// ③ callback: 当事件触发时要执行的函数
// ④ useCapture(可选): 是否使用捕获阶段,默认为 false(冒泡阶段)

e 是什么?在回调函数里:

function(e) { ... }

这个 e 是事件对象(Event Object),浏览器在触发事件时会自动传入一个包含当前事件所有信息的对象。你可以理解成: "这次按键事件的详细记录"——包括按下了哪个键、是否同时按了 Ctrl/Shift、事件目标是谁等。举例打印看看:

document.addEventListener('keydown', e => console.log(e))

按任意键后,你会看到浏览器控制台打印一个对象:

其中每个对象中包含:

//我只列举了一部分: 
// ① key: 当前按下的键的名字(如 "F12"、"u"、"I")
// ② keyCode: 对应键的数值编码(如 123 对应 F12)
// ③ ctrlKey: 是否同时按下了 Ctrl 键(true / false)
// ④ shiftKey: 是否同时按下了 Shift 键
// ⑤ altKey: 是否按下了 Alt 键
// ⑥ metaKey: 是否按下了 Mac 的 ***mand 键
// ⑦ type: 事件类型(keydown / keyup)

// 当你按下 F12: 
e.key      // "F12" 
e.keyCode  // 123

// 当你按下 Ctrl + U: 
e.ctrlKey  // true
e.key      // "u"
e.keyCode  // 85

e.preventDefault()e.stopPropagation()

e.preventDefault(): 阻止浏览器默认行为
 // ①阻止 Ctrl+U 打开"查看源码" ②阻止右键菜单 ③阻止链接跳转 ④阻止表单自动提交
意思是: 这次按键事件,不要执行浏览器原本绑定的默认动作

e.stopPropagation(): 阻止事件冒泡
事件在 DOM 结构中默认会从最内层元素往外层一层层传播(称为 "冒泡")。调用这个方法会阻止事件继续向上传递,防止触发其他监听器
意思是: 事件只在当前这一层处理,不要再传给上层元素

优点:阻止一般用户误触与懒散的调试尝试。缺点:完全可绕过;对无障碍、正常用户体验有影响(右键禁用会影响拷贝、辅助功能),需慎用并给出替代交互(例如自定义菜单)。

二、无限debugger

2.1 概念

在 JavaScript 里,debugger 是一条专门用于 断点调试 的语句。当浏览器执行到这行代码时,如果开发者工具(DevTools)已经打开,那么执行会 立刻暂停,并跳转到调试界面。简单理解:

console.log("A");
debugger;  // 如果控制台打开,这里会停住
console.log("B");

如下图:

当浏览器的 开发者工具(DevTools)处于打开状态时,执行到 debugger 语句会触发断点,脚本暂停并等待调试操作。当开发者工具未打开时,debugger 语句 不会生效,浏览器会 直接跳过并继续执行后续代码。所以,新手常见疑问:

"为什么我打开控制台就卡住了,不打开就没事?"
答案就是: debugger 只在调试模式下生效

为什么 debugger"干扰" 调试? 在正常开发中,debugger 语句用于手动设置断点以方便调试;但在 反爬或反调试场景 下,它却常被用作一种 "陷阱"。当浏览器的开发者工具(DevTools)被打开时,脚本执行到 debugger 会自动触发断点并暂停代码运行,每次都需人工点击 "恢复执行(Resume)"。若脚本中大量插入或循环触发 debugger,就形成了所谓的 “无限 debugger(debugger trap)”:页面会在 DevTools 打开状态下一次次地进入断点暂停,导致调试流程被不断打断、几乎无法顺利分析。利用这种机制,脚本能有效干扰逆向者的调试体验,大幅提升分析难度与时间成本。

2.2 实现

在实现 "无限debugger"之前,先理解浏览器中两个最常用的定时函数很重要:setTimeoutsetInterval

// ①: 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: 定期重复执行

在浏览器中,最简单的无限 debugger 实现方式是:

setInterval(() => { debugger; }, 100);

然而,这种实现方式的反调试效果非常有限。分析者只需将鼠标定位在断点处,右击选择 "Never pause here",然后点击 "Resume script execution",即可绕过该无限 debugger,页面脚本将恢复正常执行。也就是说,简单的循环触发无法对熟悉 DevTools 的调试者造成实质阻碍。

变种示例:

function debug(){
  Function("debugger")()
  eval("debugger")
}
setInterval(debug, 500)

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);

通过给每次 evalFunction 生成独立的 sourceURL 并持续触发 debugger,每次断点在 DevTools 中都对应不同的来源标识。这使得分析者无法通过 Never pause here 一次性屏蔽所有断点,每次触发都必须单独处理,从而增加了调试绕过的成本和复杂度。拓展: DevTools 在内部管理断点时,主要依据三类信息:

  1. 脚本来源(Script Source)
    • 如果是静态 JS 文件,就是 URL 或文件名。
    • 如果是 <script> 标签或动态生成的代码(eval / Function),DevTools 会自动生成一个内部标识(内部 VM script ID)。
    • 对于动态生成的代码,浏览器会给它分配一个内部标识(内部 VM 脚本 ID),或者你可以通过 //# sourceURL=xxx.js(不一定要是这种格式) 给它起一个 "伪名字",在 DevTools Sources 面板中显示。
  2. 行号 / 列号(Line/Column)
    • 断点记录在特定脚本的某一行列位置。
  3. 调用帧(Call Frame)
    • debugger 在特定的执行栈位置触发断点时,会记录当前调用帧信息,用于暂停调试。调用帧就是当前 debugger 被哪一条函数调用链触发的上下文信息

为什么简单动态 debugger 仍然能被 Never pause here 绕过:

  1. 没有指定 sourceURL 的情况下,每次 eval / Function 生成的动态脚本,DevTools 可能复用同一个内部 VM 脚本 ID,尤其是在连续 eval / Function 执行的同一作用域下。

  2. 这意味着:虽然行号和调用帧可能不同,但 脚本来源(VM script ID)被复用DevTools 内部会把之前设置的 "Never pause here" 记录在这个内部 ID 上。

  3. 因此,你选了 "Never pause here" 后,所有用同一内部 ID 执行的 debugger 语句都会被忽略,即便行号或调用帧不同。

换句话说,动态生成的代码如果没有独立 sourceURLDevTools 会把它当成 “同一脚本”,就能被一次性屏蔽。对于 Functioneval 来说,每次传入的代码参数如果不同(上面说的理解为内部 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);

除了直接在代码中写 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, { ***pact: false, retainLines: true }).code;
console.log(out);

效果:

需要注意的是:既然 debugger 可以通过 Function 构造或 eval 动态生成,攻击者/分析者可以通过重写全局 Functioneval 或覆盖某些全局接口来尝试绕过这类防护。为提高抗扰性,可以通过 沿着原型链获取构造函数(例如 (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.基于这三元素,可以生成成百上千种变体来混淆静态/动态分析器

三、控制台开启状态检测

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);

设置阈值(一般 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);

在代码执行前后分别记录时间戳,若期间 JS 执行被中断(例如打了断点或单步调试),浏览器主线程被挂起,但 Date.now() 依旧在走,所以,结束时间与开始时间的差值会 异常地大。这类检测并 不依赖 debugger; 语句,任何关键代码(例如加密计算、核心逻辑判断、数据生成等)都可以包裹进检测逻辑中,只要执行时间超过阈值,就说明执行被中断或调试器介入,此时就可以进入伪造分支逻辑,比如输出假数据或终止脚本运行。举例说明,分析者在调试过程中打了断点:

for (let i = 0; i < 1e6; i++) { Math.pow(i, 2); } // 停在这里
// 此时脚本暂停执行,Date.now() 仍在走; 
// 恢复执行后检测时间差,就会触发“检测到调试”分支。例如: 返回错误数据、输出伪造结果、或执行混淆的逻辑,让分析陷入误导

3.3 性能分析 (需要做大量测试)

性能分析类的检测思路是:通过 高精度时间测量(如 performance.now())对极轻量级、理应耗时很短的代码片段进行重复采样;当这些片段在短时间内出现异常延迟(远大于正常抖动范围)时,很可能说明脚本执行被人工暂停或被 DevTools/性能分析器介入。关键要点是:

  1. 微小工作 + 高精度计时:选取开销极小但可重复的工作,使用 performance.now() 做亚毫秒级测量;
  2. 大量采样与统计处理:多次采样并取中位数/去极值平均以降低误报;
  3. 阈值与环境校准:在目标机器/浏览器上做大量测试以确定合理阈值(不同设备差异很大);
  4. 多信号融合:单一时间检测容易被噪声影响,应与窗口尺寸、console 行为、断点探针等联合判断。

小结:这是一种 概率性 检测,需要大量采样与迭代调参,适合用作触发上报/降级的判断依据,而非唯一判定标准。为什么用 console.table / console.clear 作为实战信号?

在实际工程里,一个简单而有效的放大信号是 向控制台输出"重量级"内容 并测量耗时:当 DevTools 打开时,浏览器会为控制台输出做渲染与格式化,这比单纯的计算更耗时、更稳定地产生可测延迟。console.tableconsole.clear 常被用作这类检测的原因是:

  1. console.table(...):把数组/对象渲染为表格,DevTools 会进行额外解析和渲染,开销较大;在控制台打开时多次调用更容易产生明显延迟。
  2. 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);

四、内存爆破

内存爆破 指攻击者通过持续分配/填充内存资源,试图耗尽目标进程或环境的可用内存,导致性能严重下降、页面/进程崩溃或拒绝服务。实现思路通常依赖两个要素:容器:用于累积数据(例如数组、对象、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)
}

注意: 在明确识别到第三方爬虫正在抓取我们站点并对外发起流量时,若我们以内存耗尽等主动破坏性手段对其进行反制,这种做法本质上构成对对方的攻击(即我们成为攻击者),并可能带来法律与道德风险。

有些逆向/分析流程的常见步骤是:把浏览器里抓到的 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));

通过使用一些特殊字符进行混淆,可以防止代码被格式化或直接复制,从而增加调试和分析的难度。如下图:

如何生成的:

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"})
}()

再看:

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"})
}()

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;')

执行结果:

复制到本地:

五、绕过调试拦截

参考文章:https://blog.csdn.***/xw1680/article/details/138547184?spm=1011.2415.3001.5331

转载请说明出处内容投诉
CSS教程网 » JavaScript逆向与爬虫实战——基础篇(调试拦截(如无限debugger)与内存爆破原理及绕过)

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买