Skip to main content

Overview

AST (Abstract Syntax Tree) manipulation is the core technique used in bet365-re-js to transform obfuscated JavaScript into readable code. By working at the AST level rather than string manipulation, the tool can perform precise, syntax-aware transformations.

What is an AST?

An Abstract Syntax Tree represents the syntactic structure of source code in a tree format. Each node in the tree represents a construct in the code. Example:
var x = 5 + 3;
This simple statement generates an AST with:
  • A VariableDeclaration node
  • An Identifier node for x
  • A BinaryExpression node for 5 + 3
  • Two NumericLiteral nodes for 5 and 3

Tools Used

bet365-re-js leverages several JavaScript AST manipulation libraries:

jscodeshift

The primary tool for AST transformations. It provides a high-level API for finding and modifying AST nodes.
import j from "jscodeshift";
Key features:
  • jQuery-like API for traversing the AST
  • Built-in methods for common transformations
  • Support for renaming, removing, and replacing nodes

esprima

Parses JavaScript into an AST:
import * as esprima from "esprima";

const ast = esprima.parseScript(transformedJscodeshiftAst.toSource());

escodegen

Generates JavaScript code from an AST with pretty-printing:
import escodegen from "escodegen";

const refactoredJsCode = escodegen.generate(ast);

Base Transformer Class

All transformations inherit from the AstTransformer base class:
class AstTransformer {
    constructor(stepNumber, jscodeshiftAst, output, outputBaseName) {
        if (this.constructor === AstTransformer) {
            throw new Error("not instantiable");
        }
        this.stepNumber = stepNumber;
        this.jscodeshiftAst = jscodeshiftAst;
        this.output = output;
        this.outputBaseName = outputBaseName;
    }

    performTransform() {
        throw new Error("implement");
    }

    transform() {
        this.performTransform();
        if (this.output) {
            this.outputToFile();
        }
        return this.jscodeshiftAst;
    }

    outputToFile() {
        const jsCode = escodegen.generate(
            esprima.parseScript(this.jscodeshiftAst.toSource())
        );
        fs.writeFileSync(`${__dirname}/${this.outputFileName}`, jsCode);
    }
}

Common AST Manipulation Patterns

Finding and Replacing Nodes

// Find all unary expressions with void operator
this.jscodeshiftAst.find(j.UnaryExpression, {
    operator: 'void', 
    argument: {value: 0}
})
.replaceWith(path => j.identifier('undefined'));

Renaming Variables

jscodeshift provides a convenient method for renaming variables across their entire scope:
class RefactorVariableTransformer extends AstTransformer {
    performTransform() {
        Object.entries(refactorVariables).forEach(([key, refactoredVariableName]) => {
            this.jscodeshiftAst.findVariableDeclarators(key)
                .renameTo(refactoredVariableName);
        });
    }
}
Benefits:
  • Handles all references to the variable
  • Respects scope boundaries
  • Updates function parameters, destructuring, etc.

Removing Nodes

Remove variable declarations and clean up redundant code:
class VariableReplacementTransformer extends AstTransformer {
    performTransform() {
        Object.entries(replaceVariables).forEach(([oldVariable, newVariable]) => {
            // Remove the variable declaration
            this.jscodeshiftAst.findVariableDeclarators(oldVariable).remove();
            
            // Replace all references
            this.jscodeshiftAst.find(j.Identifier, {name: oldVariable})
                .replaceWith(path => j.identifier(newVariable));
            
            // Remove redundant assignments like x = x
            this.jscodeshiftAst.find(j.AssignmentExpression, {
                operator: '=',
                left: {name: newVariable},
                right: {name: newVariable}
            })
            .remove();
        });
    }
}

Working with Literals

// Replace !0 with true and !1 with false
this.jscodeshiftAst.find(j.UnaryExpression, {
    operator: '!', 
    argument: {value: 0}
})
.replaceWith(path => j.booleanLiteral(true));

this.jscodeshiftAst.find(j.UnaryExpression, {
    operator: '!',
    argument: {value: 1}
})
.replaceWith(path => j.booleanLiteral(false));

Advanced Patterns

Filtering and Conditional Replacement

Use .filter() to apply conditions before transformation:
this.jscodeshiftAst.find(j.AssignmentExpression, {operator: '='})
    .filter(path => {
        // Only remove assignments where left and right are the same
        return path.value.left.name && 
               path.value.left.name === path.value.right.name;
    })
    .remove();

Iterating Over Collections

Process multiple transformations systematically:
const membershipRefactor = new Set([
    'push', 'shift', 'code', 'length', 'call', 'exports',
    'charCodeAt', 'fromCharCode', 'toString', 'charAt'
]);

performTransform() {
    membershipRefactor.forEach(membership => {
        this.jscodeshiftAst.find(j.MemberExpression, {
            property: {value: membership}
        })
        .replaceWith(path => 
            j.memberExpression(
                path.node.object,
                j.identifier(path.node.property.value)
            )
        );
    });
}

Creating New AST Nodes

jscodeshift provides builder methods for all node types:
// Create identifier
j.identifier('variableName')

// Create boolean literal
j.booleanLiteral(true)

// Create member expression
j.memberExpression(
    j.identifier('object'),
    j.identifier('property')
)

// Create binary expression
j.binaryExpression(
    '+',
    j.literal(5),
    j.literal(3)
)

AST Node Types Reference

Common node types used in deobfuscation:
  • UnaryExpression: !, void, typeof, etc.
  • BinaryExpression: +, -, *, ==, ===, etc.
  • AssignmentExpression: =, +=, -=, etc.
  • MemberExpression: Property access (obj.prop or obj['prop'])
  • CallExpression: Function calls
  • VariableDeclaration: var, let, const
  • FunctionDeclaration: Function definitions
  • VariableDeclarator: Individual variable in a declaration
  • Literal: Strings, numbers, regex
  • BooleanLiteral: true or false
  • NumericLiteral: Number values
  • StringLiteral: String values
  • Identifier: Variable names
  • Program: Root node
  • BlockStatement: Code blocks { }
  • ExpressionStatement: Expression as statement

Testing Transformations

The project includes Jest tests for each transformer:
const {Void0Transformer} = require('./refactor-obfuscated-code-jscodeshift-0');
const j = require('jscodeshift');

test('transforms void 0 to undefined', () => {
    const input = 'var x = void 0;';
    const expected = 'var x = undefined;';
    
    const ast = j(input);
    new Void0Transformer(0, ast, false).transform();
    
    expect(ast.toSource()).toBe(expected);
});

Development Tips

1

Use AST Explorer

Visit astexplorer.net to visualize AST structure and test transformations interactively.
2

Output Intermediate Steps

Enable intermediate output to see how each transformation affects the code:
new ChainedTransformer(j(code), true, "debug").transform();
3

Write Unit Tests

Test each transformer independently before integrating into the chain.
4

Inspect Node Structure

Use console.log(JSON.stringify(path.node, null, 2)) to inspect node structure during development.
Be careful with AST transformations that modify the tree while iterating. Use .forEach() or collect nodes first, then transform them.

Performance Considerations

  • Minimize AST passes: Each .find() traverses the entire tree. Combine related transformations when possible.
  • Use specific node types: Searching for j.Identifier is more efficient than searching all node types.
  • Cache results: If you need to find the same nodes multiple times, cache the result.
// Less efficient
this.jscodeshiftAst.find(j.Identifier).forEach(/* ... */);
this.jscodeshiftAst.find(j.Identifier).forEach(/* ... */);

// More efficient
const identifiers = this.jscodeshiftAst.find(j.Identifier);
identifiers.forEach(/* ... */);
identifiers.forEach(/* ... */);

Resources

Next Steps

Build docs developers (and LLMs) love