目录
一、禁用 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.youkuaiyun.com/xw1680/article/details/138547184?spm=1011.2415.3001.5331
————————————————
版权声明:本文为优快云博主「棒棒编程修炼场」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/xw1680/article/details/153742917
最新发布