Up until now, I’ve been studying JavaScript related anti-debugging techniques. But when I searched online, I found that there were not many articles on the subject, and if there were any, they were very incomplete. So in this article, I’m going to give you a summary of JavaScript anti-debugging techniques. It’s worth noting that some of these methods are already widely used by cybercriminals in malware.

With JavaScript, you only need to spend a little time debugging and analyzing, and you can understand the functional logic of the JavaScript code snippet. What we’re going to talk about can make it difficult for anyone who wants to analyze your JavaScript code. But our technique isn’t about obfuscation, it’s about making active debugging of code difficult.

The technical methods to be introduced in this paper are as follows:

1. Detect unknown execution environment (our code only wants to be executed in the browser);

2. Check debugging tools, such as DevTools.

3. Code integrity control;

4. Flow integrity control;

5. Reverse simulation;

In short, if we detect an “abnormal” condition, the flow of the program will change and jump to a fake code block and “hide” the real functional code. 1. Function redefinition

This is one of the most basic and common techniques for undebugging code. In JavaScript, we can redefine the functions used to gather information. For example, the console.log() function can be used to collect information about functions and variables and display it in the console. If we redefine this function, we can modify its behavior and hide specific information or display bogus information.

We can run this function directly in DevTools to see what it does:

console.log("HelloWorld"); var fake = function() {}; window['console']['log']= fake; console.log("Youcan't see me!" );Copy the code

After running, we should see:

VM48:1 Hello WorldCopy the code

You will notice that the second message is not displayed because we have redefined the function to “disable” its original functionality. But we can also make it display fake information. Like this:

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!" );Copy the code

If all goes well:

Normal function
VM117:11 This is unaltered
VM117:9 Spoofed!
VM117:11 Bye bye!Copy the code

In fact, in order to control how the code executes, we can also change the functionality of the function in more intelligent ways. For example, we could build a code snippet based on the above code and redefine the eval function. We can pass JavaScript code to the eval function, which will then be evaluated and executed. If we redefined this function, we could run different code:

//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! ') ");Copy the code

The running results are as follows:

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:19Copy the code

As mentioned earlier, while this method is very clever, it is also very basic and common, so it is easy to detect. Second, the breakpoint

To help us understand what code does, JavaScript debugging tools such as DevTools can prevent script code from executing by setting breakpoints, which are fundamental to code debugging.

If you’ve studied debuggers or x86 architectures, you’re probably familiar with the 0xCC directive. In JavaScript, we have a similar instruction called the Debugger. After we declare the debugger function in the code, the script will stop running at the debugger instruction. Such as:

console.log("Seeme!" ); debugger; console.log("Seeme!" );Copy the code

Many commercial products define an infinite loop of debugger instructions in their code, but some browsers block it and some don’t. The main purpose of this approach is to annoy people who want to debug your code, because an infinite loop means that the code keeps popping up asking if you want to continue running the script:

setTimeout(function(){while (true) {eval("debugger")Copy the code

3. Time difference

This is a time-based anti-debugging technique borrowed from traditional anti-reverse techniques. When executed in a tool environment such as DevTools, scripts run very slowly (and for a long time), so we can determine if the script is currently being debugged based on the running time. For example, we can measure the run time between two set points in the code and use this value as a reference. If the run time exceeds this value, the script is currently running in the debugger.

The demo code is as follows:

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);Copy the code

4. DevTools Detection (Chrome)

This technique leverages the ID attribute in div elements, which the browser automatically tries to retrieve when a div element is sent to the console (such as console.log(div)). If the code calls the getter method after console.log, the console is currently running.

The simple proof-of-concept code is as follows:

let div = document.createElement('div'); let loop = setInterval(() => { console.log(div); console.clear(); }); Object.defineProperty(div,"id", {get: () => { clearInterval(loop); alert("Dev Tools detected!" ); }});Copy the code

Implicit flow integrity control

When we try to de-obfuscate code, we first try to rename some function or variable, but in JavaScript we can detect if the function name has been changed, or we can get the original name or call order directly from the stack trace.

Arguments.callee.caller can help us create a stack trace to store previously executed functions.

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();Copy the code

Note: The more obfuscated the source code, the better this technique works. Proxy object

Proxy objects are one of the most useful tools in JavaScript today. They help you learn about other objects in your code, including modifying their behavior and triggering object activity in a particular context. For example, we could create a dia object and track every document.createelemen call, then record the relevant information:

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');Copy the code

Next, we can record related parameters and information in the console:

VM64:3 Intercepted a call to createElement with args: divCopy the code

We can use this information and debug code by intercepting specific functions, but the main purpose of this article is to introduce anti-debugging techniques, so how do we detect if the “other side” is using proxy objects? It’s a cat-and-mouse game, for example, we could use the same code snippet and try to call toString and catch the exception:

//Call a "virgin" createElement: try { document.createElement.toString(); }catch(e){ console.log("I saw your proxy!" ); }Copy the code

The information is as follows:

"function createElement() { [native code] }"Copy the code

But when we use the proxy:

//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!" ); }Copy the code

Yes, we can indeed detect agents:

VM391:13 I saw your proxy!Copy the code

We can also add the toString method:

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!" ); }Copy the code

Now we can’t detect it:

"function createElement() { [native code] }"Copy the code

As I said, it’s a game of cat and mouse. conclusion

I hope the tips I’ve collected will be of some help, but if you have a better one to share, feel free to leave it in the comments section below, or tweet @thexc3ll.