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:
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
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 );
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 Expressions
Find Identifiers
Find Member Expressions
// Find all unary expressions with void operator
this . jscodeshiftAst . find ( j . UnaryExpression , {
operator: 'void' ,
argument: { value: 0 }
})
. replaceWith ( path => j . identifier ( 'undefined' ));
// Find and replace specific identifiers
this . jscodeshiftAst . find ( j . Identifier , { name: '_0x173146' })
. replaceWith ( path => j . identifier ( 'window' ));
// Convert bracket notation to dot notation
this . jscodeshiftAst . find ( j . MemberExpression , {
property: { value: 'push' }
})
. replaceWith ( path =>
j . memberExpression (
path . node . object ,
j . identifier ( path . node . property . value )
)
);
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
Boolean Literals
String 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
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
Use AST Explorer
Visit astexplorer.net to visualize AST structure and test transformations interactively.
Output Intermediate Steps
Enable intermediate output to see how each transformation affects the code: new ChainedTransformer ( j ( code ), true , "debug" ). transform ();
Write Unit Tests
Test each transformer independently before integrating into the chain.
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.
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