Preface
This article assumes a preliminary understanding of Abstract Syntax Tree structure and BabelJS. Click Here to read my introductory article on the usage of Babel.
Definition of a Constant Variable
For our purposes, a constant variable is any variable that meets all three of the following conditions:
- The variable is declared AND initialized at the same time.
- The variable is initialized to a literal value, e.g. StringLiteral, NumericLiteral, BooleanLiteral, etc.
- The variable is never reassigned another value in the script
Therefore, a variable’s declaration keyword (let
,var
,const
) has no bearing on whether or not it is a constant.
Here is a quick example:
javascript 1const a = [1, 2, 3];
2var d = 12;
3let e = "String!";
4let f = 13;
5let g;
6
7f += 2;
8
9console.log(a, b, d, e, f);
10g = 14;
In this example:
a
is not a constant, since it’s initialized as an ArrayExpression, not a Literald
is a constant, as it is declared and initialized to a NumericLiteral. Declaration and initialization happen at the same time. It is also never reassigned.e
is a constant, as it is declared and initialized to a StringLiteral. Declaration and initialization happen at the same time. It is also never reassigned.f
is not a constant, since it is reassigned after initialization:f+=2
g
is not a constant, since it is not declared and initialized at the same time.
The reasoning for declared but uninitialized variables not counting as a constant is an important concept to understand. Take the following script as an example:
javascript1let foo; // Initialization
2
3console.log(foo); // => undefined
4
5foo = 2;
6
7console.log(foo); // => 2
Console Output:
undefined
2
If, in this case, we tried to substitute foo
’s initialization value (2
) for each reference offoo
:
1let foo; // Initialization
2
3console.log(2); // => 2, NOT undefined!
4
5foo = 2;
6
7console.log(2); // => 2
Console Output:
2
2
Which clearly breaks the original functionality of the script due to not accounting for the state of the variable at certain points in the script. Therefore, we must follow the 3 conditions when determining a constant variable.
I’ll now discuss an example where substituting in constant variables can be useful for deobfuscation purposes.
Examples
Let’s say we have a very simple, unobfuscated script that looks like this:
javascript 1/**
2 * "Input.js"
3 * Original, unobfuscated code.
4 *
5 */
6var url = "https://api.n0tar3als1t3.dev:1423/getData";
7const req = function () {
8 let random = Math.random() * 1000;
9 var xhr = new XMLHttpRequest();
10 xhr.open("GET", url);
11 xhr.setRequestHeader("RandomInt", random);
12 xhr.setRequestHeader("Accept", "text/html");
13 xhr.setRequestHeader("Accept-Encoding", "gzip, deflate, br");
14 xhr.setRequestHeader("Accept-Language", "en-US,en;q=0.9");
15 xhr.setRequestHeader("Cache-Control", "no-cache");
16 xhr.setRequestHeader("Connection", "keep-alive");
17 xhr.setRequestHeader("Host", "n0tar3als1t3.dev");
18 xhr.setRequestHeader("Pragma", "no-cache");
19 xhr.setRequestHeader("Referer", "https://n0tar3als1t3.dev");
20 xhr.setRequestHeader(
21 `sec-ch-ua", "" Not A;Brand";v="99", "Chromium";v="101", "Google Chrome";v="101"`
22 );
23 xhr.setRequestHeader("sec-ch-ua-mobile", "?0");
24 xhr.setRequestHeader("sec-ch-ua-platform", `"Windows"`);
25 xhr.setRequestHeader("Sec-Fetch-Dest", "empty");
26 xhr.setRequestHeader("Sec-Fetch-Mode", "cors");
27 xhr.setRequestHeader("Sec-Fetch-Site", "same-origin");
28 xhr.setRequestHeader(
29 "User-Agent",
30 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.0.0 Safari/537.36"
31 );
32
33 xhr.onreadystatechange = function () {
34 if (xhr.readyState === 4) {
35 console.log(xhr.status);
36 console.log(xhr.responseText);
37 }
38 };
39
40 xhr.send();
41};
We can obfuscate it by replacing all references to the string literals with references to constant variables:
javascript 1/**
2 * "constantReferencesObfuscated.js"
3 * This is the resulting code after obfuscation.
4 *
5 */
6
7const QY$e_yOs = "https://api.n0tar3als1t3.dev:1423/getData";
8let apNykoxUn = "sec-ch-ua-mobile";
9const zgDT = "Connection";
10let A$E =
11 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.0.0 Safari/537.36";
12const XVyy$qGVDc = "Sec-Fetch-Dest";
13var EkoMLkb = "Cache-Control";
14let $jAONLEC = "Host";
15var PGOSDhGVlcd = "https://n0tar3als1t3.dev";
16const m$ua = "Accept-Encoding";
17var Hw$seiMEes = "Pragma";
18const ZHCx = "Sec-Fetch-Site";
19var PfxQUj = "Referer";
20const e_WXHbgheSe = "Accept";
21const _VTGows = "GET";
22var kphzJIkbgb = "gzip, deflate, br";
23
24const req = function () {
25 const SNgfg = "no-cache";
26 let vOqEy = "text/html";
27 const uugBXYcdsHp = "same-origin";
28 const AH$HwC = "Accept-Language";
29 var PnAJsD =
30 'sec-ch-ua", "" Not A;Brand";v="99", "Chromium";v="101", "Google Chrome";v="101"';
31 const Svno = "n0tar3als1t3.dev";
32 let OTCqIvdmed = '"Windows"';
33 let mVu = "RandomInt";
34 const UgLln = "empty";
35 const HwjBe = "?0";
36 var QnXFnewjh = "Sec-Fetch-Mode";
37 var lGhlU$gqPoK = "cors";
38 const GcictYiOQ = "User-Agent";
39 const AfYNl = "no-cache";
40 var cLAVjnFa = "keep-alive";
41 var V$lt = "en-US,en;q=0.9";
42 const TlMBXe = "sec-ch-ua-platform";
43 let random = Math.random() * 1000;
44 var xhr = new XMLHttpRequest();
45 var url = QY$e_yOs;
46 xhr.open(_VTGows, url);
47 xhr.setRequestHeader(mVu, random);
48 xhr.setRequestHeader(e_WXHbgheSe, vOqEy);
49 xhr.setRequestHeader(m$ua, kphzJIkbgb);
50 xhr.setRequestHeader(AH$HwC, V$lt);
51 xhr.setRequestHeader(EkoMLkb, SNgfg);
52 xhr.setRequestHeader(zgDT, cLAVjnFa);
53 xhr.setRequestHeader($jAONLEC, Svno);
54 xhr.setRequestHeader(Hw$seiMEes, AfYNl);
55 xhr.setRequestHeader(PfxQUj, PGOSDhGVlcd);
56 xhr.setRequestHeader(PnAJsD);
57 xhr.setRequestHeader(apNykoxUn, HwjBe);
58 xhr.setRequestHeader(TlMBXe, OTCqIvdmed);
59 xhr.setRequestHeader(XVyy$qGVDc, UgLln);
60 xhr.setRequestHeader(QnXFnewjh, lGhlU$gqPoK);
61 xhr.setRequestHeader(ZHCx, uugBXYcdsHp);
62 xhr.setRequestHeader(GcictYiOQ, A$E);
63
64 xhr.onreadystatechange = function () {
65 if (xhr.readyState === 4) {
66 console.log(xhr.status);
67 console.log(xhr.responseText);
68 }
69 };
70
71 xhr.send();
72};
Analysis Methodology
Obviously, the obfuscated script is much more difficult to read. If you were to manually deobfuscate it, you’d have to search up each referenced variable and replace each occurrence of it with the actual variable. That could get tedious for a large number of variables, so we’re going to do it the Babel way. As always, let’s start by pasting the code into AST Explorer.
Our targets of interest are the extra variable declarations. Let’s take a closer look at one of them:
So, the target node type appears to be of type VariableDeclaration. However, each of these VariableDeclarations contains an array of VariableDeclarators. It is the VariableDeclarator that actually contains the information of the variables, including its id
and init
values. So, the actual node type we should focus on is VariableDeclarator.
Recall that we want to identify all constant variables, then replace all their references with their actual value. It’s important to note that variables in different scopes (e.g. local vs. global), may share the same name but have different values. So, the solution isn’t as simple as blindly replacing all matching identifiers with their initial value.
This would be a convoluted process if not for Babel’s ‘Scope’ API. I won’t dive too deep into the available scope APIs, but you can refer to the Babel Plugin Handbook to learn more about them. In our case, the scope.getBinding(${identifierName})
method will be incredibly useful for us, as it directly returns information regarding if a variable is constant and all of its references.
Putting all this knowledge together, the steps for creating the deobfuscator are as follows:
- Traverse the ast in search of VariableDeclarators. If one is found:
- Check if the variable is initialized. If it is, check that the initial value is a Literal type. If not, skip the node by returning.
- Use the
path.scope.getBinding(${identifierName})
method with the name of the current variable as the argument. - Store the returned
constant
andreferencedPaths
properties in their own respective variables. - Check if the
constant
property istrue
. If it isn’t, skip the node by returning. - Loop through all NodePaths in the
referencedPaths
array, and replace them with the current VariableDeclarator ’s initial value (path.node.init
) - After finishing the loop, remove the original VariableDeclarator node since it has no further use.
The babel implementation is shown below:
Babel Deobfuscation Script
Javascript 1
2/**
3 * Deobfuscator.js
4 * The babel script used to deobfuscate the target file
5 *
6 */
7const parser = require("@babel/parser");
8const traverse = require("@babel/traverse").default;
9const t = require("@babel/types");
10const generate = require("@babel/generator").default;
11const beautify = require("js-beautify");
12const { readFileSync, writeFile } = require("fs");
13
14/**
15 * Main function to deobfuscate the code.
16 * @param source The source code of the file to be deobfuscated
17 *
18 */
19function deobfuscate(source) {
20 //Parse AST of Source Code
21 const ast = parser.parse(source);
22
23 // Visitor for replacing constants
24
25 const replaceRefsToConstants = {
26 VariableDeclarator(path) {
27 const { id, init } = path.node;
28 // Ensure the the variable is initialized to a Literal type.
29 if (!t.isLiteral(init)) return;
30 let {constant, referencePaths} = path.scope.getBinding(id.name);
31 // Make sure it's constant
32 if (!constant) return;
33 // Loop through all references and replace them with the actual value.
34 for (let referencedPath of referencePaths) {
35 referencedPath.replaceWith(init);
36 }
37 // Delete the now useless VariableDeclarator
38 path.remove();
39 },
40 };
41
42 // Execute the visitor
43 traverse(ast, replaceRefsToConstants);
44
45 // Code Beautification
46 let deobfCode = generate(ast, { comments: false }).code;
47 deobfCode = beautify(deobfCode, {
48 indent_size: 2,
49 space_in_empty_paren: true,
50 });
51 // Output the deobfuscated result
52 writeCodeToFile(deobfCode);
53}
54/**
55 * Writes the deobfuscated code to output.js
56 * @param code The deobfuscated code
57 */
58function writeCodeToFile(code) {
59 let outputPath = "output.js";
60 writeFile(outputPath, code, (err) => {
61 if (err) {
62 console.log("Error writing file", err);
63 } else {
64 console.log(`Wrote file to ${outputPath}`);
65 }
66 });
67}
68
69deobfuscate(readFileSync("./constantReferencesObfuscated.js", "utf8"));
After processing the obfuscated script with the babel plugin above, we get the following result:
Post-Deobfuscation Result
javascript 1const req = function () {
2 let random = Math.random() * 1000;
3 var xhr = new XMLHttpRequest();
4 xhr.open("GET", "https://api.n0tar3als1t3.dev:1423/getData");
5 xhr.setRequestHeader("RandomInt", random);
6 xhr.setRequestHeader("Accept", "text/html");
7 xhr.setRequestHeader("Accept-Encoding", "gzip, deflate, br");
8 xhr.setRequestHeader("Accept-Language", "en-US,en;q=0.9");
9 xhr.setRequestHeader("Cache-Control", "no-cache");
10 xhr.setRequestHeader("Connection", "keep-alive");
11 xhr.setRequestHeader("Host", "n0tar3als1t3.dev");
12 xhr.setRequestHeader("Pragma", "no-cache");
13 xhr.setRequestHeader("Referer", "https://n0tar3als1t3.dev");
14 xhr.setRequestHeader(
15 'sec-ch-ua", "" Not A;Brand";v="99", "Chromium";v="101", "Google Chrome";v="101"'
16 );
17 xhr.setRequestHeader("sec-ch-ua-mobile", "?0");
18 xhr.setRequestHeader("sec-ch-ua-platform", '"Windows"');
19 xhr.setRequestHeader("Sec-Fetch-Dest", "empty");
20 xhr.setRequestHeader("Sec-Fetch-Mode", "cors");
21 xhr.setRequestHeader("Sec-Fetch-Site", "same-origin");
22 xhr.setRequestHeader(
23 "User-Agent",
24 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.0.0 Safari/537.36"
25 );
26
27 xhr.onreadystatechange = function () {
28 if (xhr.readyState === 4) {
29 console.log(xhr.status);
30 console.log(xhr.responseText);
31 }
32 };
33
34 xhr.send();
35};
And the code is restored. Even better than the original actually, since we substituted in the url
variable too!
Conclusion
Substitution of constant variables is a must-know deobfuscation technique. It’ll usually be one of your first steps in the deobfuscation, combined with constant folding. If you would like to learn about constant folding, you can read my article about it here.
This article also gave a nice introduction to one of the useful Babel API methods. Unfortunately, there isn’t much good documentation out there aside from the Babel Plugin Handbook. However, you can discover a lot more useful features Babel has to offer by reading its source code, or using the debugger of an IDE to list and test helper methods (the latter of which I personally prefer 😄).
If you’re interested, you can find the source code for all the examples in this repository.
Okay, that’s all I have for you today. I hope that this article helped you learn something new. Thanks for reading, and happy reversing!