各位观众老爷,大家好!我是今天的主讲人,咱们今天聊点有意思的——JS反调试技术。听说有很多同学深受调试之苦,被各种花式反调试搞得焦头烂额,今天咱们就来扒一扒这些反调试的底裤,看看它们到底是怎么工作的,又该如何应对。
咱们今天主要聊三个方面:
debugger
语句:最简单也最常见的反调试手段。console.log
重写:让你看不到想看的信息,干扰调试过程。- 时间检测:通过检测调试器带来的时间差异来判断是否被调试。
准备好了吗?咱们这就开始!
一、 debugger
语句:简单粗暴的反调试
debugger
语句,顾名思义,就是用来启动调试器的。如果你在代码中插入了 debugger
语句,当浏览器执行到这一行代码时,如果调试器是打开的,那么浏览器就会自动断点到这里。
这玩意儿看起来挺方便的,程序员可以用它来调试代码,但是,坏人也可以用它来反调试。
1. debugger
语句的反调试原理
反调试者会在代码中插入大量的 debugger
语句,甚至是在循环中插入。这样,当你尝试调试这段代码时,浏览器就会频繁地断点,让你烦不胜烦,根本无法正常调试。
举个例子:
function antiDebug() {
for (let i = 0; i < 1000; i++) {
debugger;
console.log("防止调试" + i);
}
}
antiDebug();
这段代码,一旦你打开开发者工具,就会疯狂触发断点,让你根本没法干活。
2. 绕过 debugger
语句的方法
-
禁用断点: 最简单粗暴的方法,直接在开发者工具里禁用所有断点。不同浏览器的操作方式略有不同,但一般都能找到禁用断点的选项。
-
条件断点: 如果你只想在特定条件下跳过
debugger
语句,可以使用条件断点。比如,你可以设置一个条件,让断点只在某个变量的值为特定值时才生效。// 举例:只在变量 flag 为 true 时才断点 let flag = false; function someFunction() { if (flag) { debugger; // 设置条件断点:flag === true } console.log("继续执行"); }
-
Hook
debugger
语句: 这是一种更高级的方法,可以通过修改 JavaScript 引擎的行为来阻止debugger
语句的执行。// 重写 debugger 语句,使其无效 (function() { 'use strict'; var originalDebugger = eval; eval = function(arg) { if (arg === 'debugger') { console.log("debugger语句被拦截!"); return; // 阻止 debugger 语句执行 } return originalDebugger(arg); }; })(); function test() { debugger; // 尝试触发 debugger console.log("debugger 之后的代码"); } test(); // 调用函数,测试 debugger 是否被拦截
这段代码的核心在于重写
eval
函数。当eval
函数接收到的参数是字符串'debugger'
时,我们直接返回,阻止了debugger
语句的执行。注意: 这种方法可能会影响其他代码的执行,需要谨慎使用。并且现代浏览器对此类hook的检测越来越严格,效果可能不佳。
3. 更高级的 debugger
反调试
有些反调试会使用更高级的技巧,比如使用 setInterval
或 setTimeout
来定期执行 debugger
语句,让你防不胜防。
// 每隔一段时间执行 debugger 语句
setInterval(function() {
debugger;
}, 50);
对于这种反调试,我们可以使用类似的方法来 Hook setInterval
和 setTimeout
,阻止它们执行 debugger
语句。
// 重写 setInterval 和 setTimeout,阻止 debugger 语句的执行
(function() {
'use strict';
var originalSetInterval = window.setInterval;
var originalSetTimeout = window.setTimeout;
window.setInterval = function(callback, delay) {
if (typeof callback === 'function') {
let originalCallback = callback;
callback = function() {
try {
return originalCallback.apply(this, arguments);
} catch (e) {
if (e.toString().includes("Debugger statement")) {
console.log("setInterval中的debugger被拦截");
return;
} else {
throw e;
}
}
};
}
return originalSetInterval(callback, delay);
};
window.setTimeout = function(callback, delay) {
if (typeof callback === 'function') {
let originalCallback = callback;
callback = function() {
try {
return originalCallback.apply(this, arguments);
} catch (e) {
if (e.toString().includes("Debugger statement")) {
console.log("setTimeout中的debugger被拦截");
return;
} else {
throw e;
}
}
};
}
return originalSetTimeout(callback, delay);
};
})();
// 测试代码
setInterval(function() {
debugger;
console.log("这个信息不应该被打印");
}, 100);
setTimeout(function() {
debugger;
console.log("这个信息也不应该被打印");
}, 500);
这段代码重写了 setInterval
和 setTimeout
函数,在回调函数执行之前,对回调函数进行了包装。如果回调函数中包含 debugger
语句,并且执行时抛出了包含 "Debugger statement" 的错误,则会捕获该错误并阻止 debugger
语句的执行。
二、 console.log
重写:让你眼瞎的反调试
console.log
是我们调试代码时最常用的工具之一。但是,有些反调试会重写 console.log
函数,让你看不到想看的信息,或者输出一些误导性的信息。
1. console.log
重写的反调试原理
反调试者会重写 console.log
函数,使其:
- 不输出任何信息。
- 输出一些垃圾信息,干扰你的判断。
- 检测调试器是否打开,并采取相应的措施。
举个例子:
// 重写 console.log 函数,使其不输出任何信息
console.log = function() {};
// 重写 console.log 函数,输出垃圾信息
console.log = function(message) {
//生成随机字符串作为干扰
let randomString = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
originalConsoleLog("垃圾信息:" + randomString);
};
// 检测调试器是否打开,并采取相应的措施
console.log = function(message) {
if (/* 检测调试器是否打开 */) {
// 执行反调试操作
alert("不要调试我!");
} else {
// 正常输出信息
originalConsoleLog(message);
}
};
2. 绕过 console.log
重写的方法
-
保存原始的
console.log
函数: 在代码的开头,保存原始的console.log
函数,以便在需要的时候使用。// 保存原始的 console.log 函数 var originalConsoleLog = console.log; // 重写 console.log 函数 console.log = function(message) { // ... }; // 使用原始的 console.log 函数 originalConsoleLog("这是一条重要的信息");
-
使用其他调试工具: 除了
console.log
,浏览器还提供了其他的调试工具,比如console.dir
、console.table
等。这些工具可能没有被重写,可以使用它们来查看信息。 -
使用浏览器开发者工具的 "Logpoints": Logpoints 允许你在代码的特定位置记录信息,而无需修改代码本身。这可以绕过一些简单的
console.log
重写。 -
重写重写后的
console.log
: 没错,就是这么套娃。你可以再次重写已经被重写过的console.log
函数,恢复其原始功能。// 假设 console.log 已经被重写 console.log = function(message) { // 一些反调试逻辑 // ... }; // 再次重写 console.log 函数,恢复其原始功能 (function() { var iframe = document.createElement('iframe'); iframe.style.display = 'none'; document.body.appendChild(iframe); console.log = iframe.contentWindow.console.log; })(); // 现在 console.log 应该可以正常工作了 console.log("这是一条重要的信息");
这段代码创建了一个隐藏的
iframe
元素,然后将console.log
函数指向iframe
的console.log
函数。由于iframe
中的console.log
函数没有被重写,因此可以恢复其原始功能。
3. 更高级的 console.log
反调试
有些反调试会使用更高级的技巧,比如检测开发者工具是否打开,并根据情况选择是否重写 console.log
函数。
// 检测开发者工具是否打开
function isDevtoolsOpen() {
// 一些检测逻辑
// ...
}
// 重写 console.log 函数
console.log = function(message) {
if (isDevtoolsOpen()) {
// 开发者工具已打开,执行反调试操作
// ...
} else {
// 开发者工具未打开,正常输出信息
originalConsoleLog(message);
}
};
对于这种反调试,我们需要先绕过开发者工具的检测,然后再重写 console.log
函数。开发者工具的检测方法有很多种,比如检测 window.outerWidth
和 window.innerWidth
的差异,或者检测 console.firebug
是否存在。针对不同的检测方法,我们需要采取不同的绕过策略。
三、 时间检测:让你慢下来的反调试
时间检测是一种比较隐蔽的反调试手段。它通过检测代码执行的时间来判断是否被调试。
1. 时间检测的反调试原理
调试器会影响代码的执行速度。当代码在调试器中运行时,执行速度会变慢。反调试者会利用这一点,通过检测代码执行的时间来判断是否被调试。
举个例子:
// 检测代码执行的时间
var startTime = new Date().getTime();
// 执行一些代码
for (let i = 0; i < 1000000; i++) {
// ...
}
var endTime = new Date().getTime();
// 计算代码执行的时间
var elapsedTime = endTime - startTime;
// 判断是否被调试
if (elapsedTime > 1000) {
// 代码执行的时间超过了 1000 毫秒,可能被调试
alert("不要调试我!");
}
这段代码会计算循环执行的时间。如果在调试器中运行,由于调试器的影响,循环执行的时间会变长,超过 1000 毫秒,从而触发反调试操作。
2. 绕过时间检测的方法
-
禁用断点: 断点会显著影响代码的执行速度,因此禁用断点可以减少时间检测的误差。
-
提高代码执行速度: 可以通过优化代码来提高代码的执行速度,减少时间检测的误差。比如,可以减少循环的次数,或者使用更高效的算法。
-
修改时间检测的代码: 如果能够找到时间检测的代码,可以直接修改它,使其失效。比如,可以修改时间阈值,或者直接删除时间检测的代码。
-
使用性能分析工具: Chrome 开发者工具的 "Performance" 面板可以帮助你分析代码的性能瓶颈,找出导致代码执行速度变慢的原因。通过优化这些瓶颈,可以减少时间检测的误差。
3. 更高级的时间检测
有些反调试会使用更高级的技巧,比如:
-
使用
performance.now()
函数:performance.now()
函数提供更高精度的时间戳,可以更准确地检测代码执行的时间。 -
结合多种时间检测方法: 同时使用多种时间检测方法,可以提高检测的准确性。
-
动态调整时间阈值: 根据代码的运行环境,动态调整时间阈值,可以提高反调试的适应性。
对于这些更高级的时间检测,我们需要更深入地分析代码,找出时间检测的具体实现方式,然后采取相应的绕过策略。
总结
今天我们聊了三种常见的 JS 反调试技术:debugger
语句、console.log
重写和时间检测。这些反调试技术各有特点,但都旨在阻止你调试代码。
反调试技术 | 原理 | 绕过方法 |
---|---|---|
debugger 语句 |
强制启动调试器,频繁断点,干扰调试。 | 禁用断点,条件断点,Hook debugger 语句,阻止其执行。对于 setInterval 和 setTimeout 触发的 debugger ,可以 Hook 这两个函数,阻止其执行 debugger 。 |
console.log 重写 |
重写 console.log 函数,使其不输出信息、输出垃圾信息或检测调试器状态。 |
保存原始的 console.log 函数,使用其他调试工具(console.dir 、console.table ),使用 Logpoints,重写重写后的 console.log 。对于检测开发者工具状态的反调试,需要先绕过开发者工具检测,再重写 console.log 。 |
时间检测 | 通过检测代码执行的时间来判断是否被调试。调试器会影响代码执行速度,导致时间变长。 | 禁用断点,提高代码执行速度,修改时间检测的代码(修改阈值或删除代码),使用性能分析工具。对于使用 performance.now() 、结合多种时间检测方法或动态调整时间阈值的反调试,需要更深入地分析代码,找出具体实现方式,再采取相应策略。 |
记住,反调试和反反调试是一场永无止境的猫鼠游戏。新的反调试技术层出不穷,我们需要不断学习,不断提升自己的技能,才能更好地应对这些挑战。
希望今天的讲座对大家有所帮助! 祝大家调试愉快,早日摆脱反调试的困扰!