JS 代码实现
/** * 检测到调试时进行的操作 */ let onDebug = function () { //提示 document.write('检测到非法调试!请停止调试后刷新本页面!'); /*卡死*/ // while (true) { // console.log('hi'); // console.clear(); // } }; /*通过 debugger 时间检测*/ setInterval(function () { let st, et; st = new Date().getTime(); debugger; et = new Date().getTime(); if ((et - st) > 1000) { onDebug(); } }, 1000); /*检测 console.log() 在不同环境(F12执行时间久一些)下的运行时间不同*/ setInterval(function(){ let startTime = performance.now(), check, diff; for (check = 0; check < 1000; check++) { console.log(check); console.clear(); } diff = performance.now() - startTime; if (diff > 200) { onDebug(); } },1000); /*监控 F12的按下*/ document.onkeydown = document.onkeyup = document.onkeypress = function (event) { const e = event || window.event || arguments.callee.caller.arguments[0]; if (e && e.keyCode == 123) { onDebug(); } }; /*调试相关的函数重定义*/ // window['console']['log']= function(){}; /** * 注:这招对火狐好像没用 * 当dom被发送至控制台时(例如console.log) * 浏览器会自动通过该dom的getter()获取该 dom 的id * 所以我们可以创建一个dom,然后发送到控制台,并且重定义其getter() */ let div = document.createElement('div'); Object.defineProperty(div, "id", { get: () => { clearInterval(loop); onDebug(); } }); let loop = setInterval(() => { console.log(div); console.clear(); }); /* 还有其他检测方法: 调用链检测 代理对象(自己暂时没学) 不过上面这两种并不可以简单调用的,所以这里没写 */
突破反调试
这里仅列出两种较难反调试的突破方法
感谢 @老虎会游泳 的指导。老虎原话:
要对付这个控制台计时代码,需要使用火狐,然后把“显示分离式控制台”关掉,然后不去看控制台就好了。如果曾经打开过控制台,则计时代码可以检测到。但是如果从未打开过控制台,计时代码就完全检测不到。
Chrome不行,只要打开F12,控制台计时代码就能检测到。
很少有调试工作一定得看控制台才能进行,控制台里只是网站自己输出的一些东西而已,基本上没什么价值,所以完全不看基本上没什么影响。
只要“查看器”、“调试器”和“网络”面板可以正常使用就完全够用了。目前看来在火狐里是完全可以做到的。
进行学习
由于网吧电脑打开火狐就死机,百度也没有关于“显示分离式控制台”的资料,所以这里纯属自己的理解。
如果把“显示分离式控制台”关掉,那么 F12 就不会加载 控制台,console.log() 在 F12 中的执行时间就与正常访问无异。
既然没有加载控制台,那么 【通过发送 dom 到控制台】的反调试自然也可以突破。
被突破代码
/*检测 console.log() 在不同环境(F12执行时间久一些)下的运行时间不同*/ setInterval(function(){ let startTime = performance.now(), check, diff; for (check = 0; check < 1000; check++) { console.log(check); console.clear(); } diff = performance.now() - startTime; if (diff > 200) { onDebug(); } },1000); /** * 当dom被发送至控制台时(例如console.log) * 浏览器会自动通过该dom的getter()获取该 dom 的id * 所以我们可以创建一个dom,然后发送到控制台,并且重定义其getter() */ let div = document.createElement('div'); Object.defineProperty(div, "id", { get: () => { clearInterval(loop); onDebug(); } }); let loop = setInterval(() => { console.log(div); console.clear(); });
转载过来的思路
本文所要介绍的技术方法大致如下:
1. 检测未知的执行环境(我们的代码只想在浏览器中被执行);
2. 检测调试工具(例如DevTools);
3. 代码完整性控制;
4. 流完整性控制;
5. 反模拟;
简而言之,如果我们检测到了“不正常”的情况,程序的运行流程将会改变,并跳转到伪造的代码块,并“隐藏”真正的功能代码。
一、函数重定义
这是一种最基本也是最常用的代码反调试技术了。在JavaScript中,我们可以对用于收集信息的函数进行重定义。比如说,console.log()函数可以用来收集函数和变量等信息,并将其显示在控制台中。如果我们重新定义了这个函数,我们就可以修改它的行为,并隐藏特定信息或显示伪造的信息。
我们可以直接在DevTools中运行这个函数来了解其功能:
console.log(“HelloWorld”);
var fake = function() {};
window[‘console’][‘log’]= fake;
console.log(“Youcan’t see me!”);
运行后我们将会看到:
VM48:1 Hello World
你会发现第二条信息并没有显示,因为我们重新定义了这个函数,即“禁用”了它原本的功能。但是我们也可以让它显示伪造的信息。比如说这样:
console.log(“Normalfunction”);
//First we save a reference to the original console.log function
var original = window[‘console’][‘log’];
//Next we create our fake function
//Basicly we check the argument and if match we call original function with otherparam.
// If there is no match pass the argument to the original function
var fake = function(argument) {
if (argument === “Ka0labs”) {
original(“Spoofed!”);
} else {
original(argument);
}
}
// We redefine now console.log as our fake function
window[‘console’][‘log’]= fake;
//Then we call console.log with any argument
console.log(“Thisis unaltered”);
//Now we should see other text in console different to “Ka0labs”
console.log(“Ka0labs”);
//Aaaand everything still OK
console.log(“Byebye!”);
如果一切正常的话:
Normal function
VM117:11 This is unaltered
VM117:9 Spoofed!
VM117:11 Bye bye!
实际上,为了控制代码的执行方式,我们还能够以更加聪明的方式来修改函数的功能。比如说,我们可以基于上述代码来构建一个代码段,并重定义eval函数。我们可以把JavaScript代码传递给eval函数,接下来代码将会被计算并执行。如果我们重定义了这个函数,我们就可以运行不同的代码了:
//Just a normal eval
eval(“console.log(‘1337’)”);
//Now we repat the process…
var original = eval;
var fake = function(argument) {
// If the code to be evaluated contains1337…
if (argument.indexOf(“1337”) !==-1) {
// … we just execute a different code
original(“for (i = 0; i < 10;i++) { console.log(i);}”);
}
else {
original(argument);
}
}
eval= fake;
eval(“console.log(‘Weshould see this…’)”);
//Now we should see the execution of a for loop instead of what is expected
eval(“console.log(‘Too1337 for you!’)”);
运行结果如下:
1337
VM146:1We should see this…
VM147:10
VM147:11
VM147:12
VM147:13
VM147:14
VM147:15
VM147:16
VM147:17
VM147:18
VM147:19
正如之前所说的那样,虽然这种方法非常巧妙,但这也是一种非常基础和常见的方法,所以比较容易被检测到。
二、断点
为了帮助我们了解代码的功能,JavaScript调试工具(例如DevTools)都可以通过设置断点的方式阻止脚本代码执行,而断点也是代码调试中最基本的了。
如果你研究过调试器或者x86架构,你可能会比较熟悉0xCC指令。在JavaScript中,我们有一个名叫debugger的类似指令。当我们在代码中声明了debugger函数后,脚本代码将会在debugger指令这里停止运行。比如说:
console.log(“Seeme!”);
debugger;
console.log(“Seeme!”);
很多商业产品会在代码中定义一个无限循环的debugger指令,不过某些浏览器会屏蔽这种代码,而有些则不会。这种方法的主要目的就是让那些想要调试你代码的人感到厌烦,因为无限循环意味着代码会不断地弹出窗口来询问你是否要继续运行脚本代码:
setTimeout(function(){while (true) {eval(“debugger”)
三、时间差异
这是一种从传统反逆向技术那里借鉴过来的基于时间的反调试技巧。当脚本在DevTools等工具环境下执行时,运行速度会非常慢(时间久),所以我们就可以根据运行时间来判断脚本当前是否正在被调试。比如说,我们可以通过测量代码中两个设置点之间的运行时间,然后用这个值作为参考,如果运行时间超过这个值,说明脚本当前在调试器中运行。
演示代码如下:
set Interval(function(){
var startTime = performance.now(), check,diff;
for (check = 0; check < 1000; check++){
console.log(check);
console.clear();
}
diff = performance.now() – startTime;
if (diff > 200){
alert(“Debugger detected!”);
}
},500);
四、DevTools检测(Chrome)
这项技术利用的是div元素中的id属性,当div元素被发送至控制台(例如console.log(div))时,浏览器会自动尝试获取其中的元素id。如果代码在调用了console.log之后又调用了getter方法,说明控制台当前正在运行。
简单的概念验证代码如下:
let div = document.createElement(‘div’);
let loop = setInterval(() => {
console.log(div);
console.clear();
});
Object.defineProperty(div,“id”, {get: () => {
clearInterval(loop);
alert(“Dev Tools detected!”);
}});
五、隐式流完整性控制
当我们尝试对代码进行反混淆处理时,我们首先会尝试重命名某些函数或变量,但是在JavaScript中我们可以检测函数名是否被修改过,或者说我们可以直接通过堆栈跟踪来获取其原始名称或调用顺序。
arguments.callee.caller可以帮助我们创建一个堆栈跟踪来存储之前执行过的函数,演示代码如下:
function getCallStack() {
var stack = “#”, total = 0, fn =arguments.callee;
while ( (fn = fn.caller) ) {
stack = stack + “” +fn.name;
total++
}
return stack
}
function test1() {
console.log(getCallStack());
}
function test2() {
test1();
}
function test3() {
test2();
}
function test4() {
test3();
}
test4();
注意:源代码的混淆程度越强,这个技术的效果就越好。
六、代理对象
代理对象是目前JavaScript中最有用的一个工具,这种对象可以帮助我们了解代码中的其他对象,包括修改其行为以及触发特定环境下的对象活动。比如说,我们可以创建一个嗲哩对象并跟踪每一次document.createElemen调用,然后记录下相关信息:
const handler = { // Our hook to keep the track
apply: function (target, thisArg, args){
console.log(“Intercepted a call tocreateElement with args: “ + args);
return target.apply(thisArg, args)
}
}
document.createElement= new Proxy(document.createElement, handler) // Create our proxy object withour hook ready to intercept
document.createElement(‘div’);
接下来,我们可以在控制台中记录下相关参数和信息:
VM64:3 Intercepted a call to createElement with args: div
我们可以利用这些信息并通过拦截某些特定函数来调试代码,但是本文的主要目的是为了介绍反调试技术,那么我们如何检测“对方”是否使用了代理对象呢?其实这就是一场“猫抓老鼠”的游戏,比如说,我们可以使用相同的代码段,然后尝试调用toString方法并捕获异常:
//Call a “virgin” createElement:
try {
document.createElement.toString();
}catch(e){
console.log(“I saw your proxy!”);
}
信息如下:
“function createElement() { [native code] }”
但是当我们使用了代理之后:
//Then apply the hook
consthandler = {
apply: function (target, thisArg, args){
console.log(“Intercepted a call tocreateElement with args: “ + args);
return target.apply(thisArg, args)
}
}
document.createElement= new Proxy(document.createElement, handler);
//Callour not-so-virgin-after-that-party createElement
try {
document.createElement.toString();
}catch(e) {
console.log(“I saw your proxy!”);
}
没错,我们确实可以检测到代理:
VM391:13 I saw your proxy!
我们还可以添加toString方法:
const handler = {
apply: function (target, thisArg, args){
console.log(“Intercepted a call tocreateElement with args: “ + args);
return target.apply(thisArg, args)
}
}
document.createElement= new Proxy(document.createElement, handler);
document.createElement= Function.prototype.toString.bind(document.createElement); //Add toString
//Callour not-so-virgin-after-that-party createElement
try {
document.createElement.toString();
}catch(e) {
console.log(“I saw your proxy!”);
}
现在我们就没办法检测到了:
“function createElement() { [native code] }”
就像我说的,这就是一场“猫抓老鼠“的游戏。