JavaScript Deobfuscation: Unraveling Obfuscated Code Step by Step

CyberSecureFox 🦊

In the previous article, we explored what JavaScript code obfuscation is and why it’s used. Obfuscated code is difficult to read and analyze, which is both an advantage for hiding logic and a disadvantage for debugging and maintaining code. But what if we need to understand how an obfuscated script works? This is where deobfuscation – the process of restoring code to a readable form – comes to the rescue.

Deobfuscation is required in several cases:

  1. Analysis of malicious scripts. Hackers and malware authors often obfuscate their code to hide malicious functionality. To understand how malware works, it’s necessary to restore the code.

  2. Finding and fixing bugs. Obfuscation complicates debugging because it’s difficult to set breakpoints and view variable values in minified code. Deobfuscation simplifies error detection.

  3. Studying hidden feature functionality. Sometimes developers obfuscate code with undocumented capabilities. Through deobfuscation, you can learn about these features.

  4. Modifying proprietary code. In closed-source software, code is usually obfuscated. Deobfuscation opens up possibilities for changing logic to suit your needs.

It’s worth remembering that deobfuscating proprietary code may violate the license agreement. Always check the terms of use before reverse engineering.

Basic deobfuscation methods

There are several main approaches to restoring readable code:

1. Formatting (Beautify)

The first step in deobfuscation is formatting minified code. This won’t give a fully readable result, but it will simplify further analysis. For formatting, you can use:

  • Browser tools, such as “Pretty print” in Chrome DevTools
  • Online services like Prettier or JS Nice
  • Plugins for popular code editors.

2. Renaming variables

The next stage is giving variables and functions meaningful names instead of single-letter ones. Good deobfuscators do this automatically based on the context of name usage.
For example, the variable “a” would be renamed to “userName” if it’s used in the string “Hello ” + a.

3. Removing dead code

Obfuscators often insert unreachable code that never executes but greatly interferes with analysis. During deobfuscation, it needs to be removed. For example:

if (false) {
   x = 10;
}

4. Restoring control flow

Obfuscation confuses the sequence of code execution with meaningless conditions and jumps. The deobfuscator’s task is to restore the normal structure with linear code blocks and minimum conditions.

Example with redundant condition:

// Obfuscated  
function check(x) {
  if (x >= 0) {
    if (x > 0) {
      console.log('Positive');
    } else {
      console.log('Zero');  
    }
  } 
}

// Deobfuscated
function check(x) {
  if (x > 0) {
    console.log('Positive');
  } else if (x === 0) {  
    console.log('Zero');
  }
}

5. Value substitution

Many obfuscators move strings and numbers into separate arrays and then access them by index. During deobfuscation, these values need to be embedded back into the code.

// Obfuscated
const strings = ['Hello', 'world', '!'];
console.log(strings[0] + ' ' + strings[1] + strings[2]);
// Deobfuscated
console.log('Hello' + ' ' + 'world' + '!');

6. Simplifying expressions

Obfuscators complicate arithmetic and logical expressions. For example, x * 1 instead of x or !!x instead of x. When restoring code, such excesses need to be removed.

Before:

if (!!x && y !== undefined) {
  result = x * 1 + y / 2;
}

After:

if (x && y !== undefined) {
  result = x + y / 2; 
}

Tools for deobfuscation

Manual deobfuscation is a painstaking process that can take a lot of time. Fortunately, there are tools to automate code unraveling:

  • JS Nice – an online service that not only formats but also renames variables, removes dead code, and restores control flow.

  • de4js – another online deobfuscator with support for unpacking, string substitution, and control simplification.

  • JavaScript Deobfuscator – a desktop deobfuscator with extensive capabilities for AST (Abstract Syntax Tree) analysis.

  • Browser extensions, such as JavaScript Deobfuscator for Chrome, which allows deobfuscation directly in DevTools.

However, even the most advanced tools don’t give 100% readable code, especially with non-standard obfuscation techniques. Therefore, manual analysis is often required for full logic restoration.

Step-by-step deobfuscation example

Let’s examine the practical restoration of readable code from an obfuscated script.
Suppose we have the following code:

(function() {
  var _0x5d2a = ['log', 'Hello\x20World'];
  (function(_0x25a336, _0x5d2afa) {
    var _0x41fb0f = function(_0x9fdc11) {
      while (--_0x9fdc11) {
        _0x25a336['push'](_0x25a336['shift']());
      }
    };
    _0x41fb0f(++_0x5d2afa);
  }(_0x5d2a, 0x78));
  var _0x41fb = function(_0x25a336, _0x5d2afa) {
    _0x25a336 = _0x25a336 - 0x0;
    var _0x41fb0f = _0x5d2a[_0x25a336];
    return _0x41fb0f;
  };
  console[_0x41fb('0x0')](_0x41fb('0x1'));
})();

Step 1. Formatting

Let’s format the minified code to make it a bit more structured:

(function() {
  var _0x5d2a = ['log', 'Hello\x20World'];
  (function(_0x25a336, _0x5d2afa) {
    var _0x41fb0f = function(_0x9fdc11) {
      while (--_0x9fdc11) {
        _0x25a336['push'](_0x25a336['shift']());
      }
    };
    _0x41fb0f(++_0x5d2afa);
  }(_0x5d2a, 0x78));
  var _0x41fb = function(_0x25a336, _0x5d2afa) {
    _0x25a336 = _0x25a336 - 0x0;
    var _0x41fb0f = _0x5d2a[_0x25a336];
    return _0x41fb0f;
  };
  console[_0x41fb('0x0')](_0x41fb('0x1'));
})();

Step 2. Analyzing the string array

In the first line of code, an array _0x5d2a is declared with two elements: ‘log’ and ‘Hello\x20World’.
Let’s remember this and continue the analysis.

Step 3. Analyzing the IIFE

The next block is an immediately invoked function expression (IIFE).
It takes the array _0x5d2a and the number 0x78 (120 in decimal), which is then incremented by 1.

Inside the IIFE, a function _0x41fb0f is declared, which takes a number _0x9fdc11 and in a loop calls push and shift on the array _0x25a336. Essentially, this is shuffling the array.

After declaring _0x41fb0f, it’s immediately called with the argument 0x79 (121 in decimal).

Step 4. Analyzing the getter function

Next, a function _0x41fb is declared, which takes 2 arguments.

The first argument _0x25a336 is decreased by 0x0 (0 in decimal), which means it remains unchanged.

Then, an element is taken from the array _0x5d2a at index _0x25a336 and returned. So this is a wrapper for accessing the array by index.

Step 5. Value substitution

In the last line, a method with index ‘0x0’ (0) is called on the console object with the argument ‘0x1‘ (1).

In the array _0x5d2a, ‘log’ is stored at index 0, and ‘Hello\x20World’ at index 1.

Substituting these values, we get:

console['log']('Hello\x20World');

Or, simplifying:

console.log('Hello World');

Result

The original obfuscated code was simply outputting ‘Hello World‘ to the console, albeit in a very convoluted way.

Putting all the deobfuscation steps together, we get this code:

(function() {
  console.log('Hello World');
})();

Tips for analyzing obfuscated code

  • Start with formatting and analyzing variable declarations. This will give you a general idea of the code structure.

  • If you see incomprehensible numbers in the code, try converting them to other number systems (decimal, ASCII).

  • Pay attention to vulnerable built-in functions like eval() and Function(). Other parts of obfuscated code might be executed through them.

  • Use a debugger and step-by-step execution to view variable values and the sequence of operations.

  • If you don’t understand the logic of a code section, try temporarily removing it or replacing it with a stub. It might not affect the main functionality.

  • Look for patterns and repetitions. Obfuscators generate templated code, so the same constructions may be used in different parts.

  • Be patient. Deobfuscation can be time-consuming, especially when analyzing malicious scripts.

Conclusion

JavaScript code deobfuscation is a valuable skill that will come in handy for analyzing malicious scripts, finding bugs, hidden features, and customizing closed-source code. The main methods of deobfuscation include formatting, renaming variables, removing dead code, restoring control flow, value substitution, and simplifying expressions.

There are online services like JS Nice and de4js, as well as desktop utilities and browser extensions to automate deobfuscation steps. However, with complex obfuscation, manual analysis is often necessary. The deobfuscation process itself consists of sequentially analyzing code sections, substituting variable values, and simplifying logic. It requires patience, JavaScript knowledge, and deductive abilities.

To more effectively analyze obfuscated code, start with formatting and parsing variable declarations. Pay attention to suspicious built-in functions, use a debugger for step-by-step execution. Look for patterns and repetitions in code generated by obfuscators. Studying deobfuscation develops skills in careful code reading, debugging, and reverse engineering. It’s a superpower for a programmer that will definitely come in handy in your work or participation in CTF competitions.

I wish you success in the non-trivial but exciting process of unraveling the tangles of obfuscated JavaScript code! Remember that behind the complexities always lies the elegance of the author’s original intention.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.