Skip to main content

Overview

The deobfuscation process in bet365-re-js uses a sophisticated multi-stage Abstract Syntax Tree (AST) transformation pipeline to reverse the obfuscated JavaScript code served by bet365.com. Rather than attempting runtime deobfuscation, this approach performs static analysis and code transformation through 11 distinct transformation stages.

How It Works

The deobfuscation pipeline is built on top of jscodeshift, a JavaScript codemods toolkit that provides powerful AST manipulation capabilities. The process follows these high-level steps:
1

Parse Obfuscated Code

The raw obfuscated JavaScript is parsed into an AST using jscodeshift.
2

Apply Transformation Chain

Execute 11 sequential transformation stages, each targeting specific obfuscation patterns.
3

Generate Clean Code

Convert the transformed AST back to JavaScript using escodegen for pretty-printing.

The Transformation Pipeline

The ChainedTransformer orchestrates all transformation stages in sequence:
class ChainedTransformer extends AstTransformer {
    performTransform() {
        let ast = this.jscodeshiftAst;
        ast = new Void0Transformer(0, ast, this.output, this.outputBaseName).transform();
        ast = new UnaryExpressionTransformer(1, ast, this.output, this.outputBaseName).transform();
        ast = new RefactorVariableTransformer(2, ast, this.output, this.outputBaseName).transform();
        ast = new VariableReplacementTransformer(3, ast, this.output, this.outputBaseName).transform();
        ast = new ParameterRefactorTransformer(4, ast, this.output, this.outputBaseName).transform();
        ast = new FunctionRefactorTransformer(5, ast, this.output, this.outputBaseName).transform();
        ast = new RemoveKeywordsTransformer(6, ast, this.output, this.outputBaseName).transform();
        ast = new BracketToDotNotationTransformer(7, ast, this.output, this.outputBaseName).transform();
        ast = new RemovedUnusedParametersTransformer(8, ast, this.output, this.outputBaseName).transform();
        ast = new RemovedClosestTransformer(9, ast, this.output, this.outputBaseName).transform();
        ast = new RemoveStaleIdentifierTransformer(10, ast, this.output, this.outputBaseName).transform();
        return ast;
    }
}

Stage 0: Void Expression Transformation

Replaces void 0 expressions with undefined for readability.
class Void0Transformer extends AstTransformer {
    performTransform() {
        this.jscodeshiftAst.find(j.UnaryExpression, {operator: 'void', argument: {value: 0}})
            .replaceWith(path => j.identifier('undefined'));
    }
}
Example transformation:
// Before
var x = void 0;

// After
var x = undefined;

Stage 1: Unary Expression Simplification

Simplifies unary expressions like !0 and !1 to their boolean equivalents.
const operators = {
    "!": [[0, true], [1, false]]
}

class UnaryExpressionTransformer extends AstTransformer {
    performTransform() {
        Object.entries(operators).forEach(([operator, values]) => {
            values.forEach(valueArray => {
                this.jscodeshiftAst.find(j.UnaryExpression, 
                    {operator: operator, argument: {value: valueArray[0]}})
                    .replaceWith(path => j.booleanLiteral(valueArray[1]));
            });
        });
    }
}
Example transformation:
// Before
if (!0) { doSomething(); }
var flag = !1;

// After
if (true) { doSomething(); }
var flag = false;

Stage 2: Variable Name Refactoring

Renames obfuscated variable names (like _0x1e16, _0x35ab73) to meaningful names based on their usage patterns.
var refactorVariables = {
    '_0x1e16': 'keywords',
    '_0x35ab': 'getKeywordName',
    '_0x35ab73': 'shiftKeywords',
    '_0x588211': 'globalState',
    '_0x478891': 'tape',
    '_0x4951e9': 'functions',
    '_0xfe71f0': 'globalStateWriteIndex',
    '_0x3fb717': 'tapeBytes',
    // ... over 200 variable mappings
};
Example transformation:
// Before
var _0x588211 = [];
_0x478891[_0xfe71f0] = _0x3fb717;

// After
var globalState = [];
tape[globalStateWriteIndex] = tapeBytes;

Stage 3: Variable Replacement

Removes redundant variable declarations and replaces them with direct references.
const replaceVariables = {
    '_0x49b7bf': 'globalStateContextValues',
    '_0x173146': 'window'
}

Stage 7: Bracket to Dot Notation

Converts bracket notation property access to dot notation for common properties, improving readability.
const membershipRefactor = new Set([
    'push', 'shift', 'code', 'length', 'call', 'exports', 
    'charCodeAt', 'fromCharCode', 'toString', 'charAt', 
    'substr', 'indexOf', 'pow', 'pop', 'apply', 'slice'
]);

class BracketToDotNotationTransformer extends AstTransformer {
    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))
            );
        });
    }
}
Example transformation:
// Before
array['push'](item);
string['charAt'](0);

// After
array.push(item);
string.charAt(0);

Entry Point

The main deobfuscation script (refactor-obfuscated-code-jscodeshift.js) orchestrates the entire process:
function transform(rawObfuscatedJsCode) {
    return new ChainedTransformer(j(rawObfuscatedJsCode), outputIntermediateSteps).transform();
}

const rawObfuscatedJsCode = fs.readFileSync(rawObfuscatedJsFileName).toString();
const transformedJscodeshiftAst = transform(rawObfuscatedJsCode);
const ast = esprima.parseScript(transformedJscodeshiftAst.toSource());

// Convert to AST for pretty printing
const refactoredJsCode = escodegen.generate(ast);
fs.writeFile(deobfuscatedJsFileName, refactoredJsCode, error => {
    if (error) {
        console.error(error);
    }
});

Output Options

Each transformation stage can optionally output its intermediate AST to a file, allowing you to inspect the progressive transformation of the code. This is controlled by the outputIntermediateSteps parameter.

Development Workflow

For rapid development and testing of deobfuscation transformations:
watchexec -e js "touch mitmproxy/src/python/download-payload.py && \
node mitmproxy/src/javascript/refactor-obfuscated-code-jscodeshift.js \
mitmproxy/src/javascript/obfuscated-original.js \
mitmproxy/src/javascript/deobfuscated-output.js && \
node mitmproxy/src/javascript/pre-transform-code.js"
This watches for JavaScript file changes and automatically re-runs the deobfuscation pipeline.
Use AST Explorer to visualize and experiment with AST transformations before implementing them in the pipeline.

Key Insights

Static vs Runtime Deobfuscation

The project uses static analysis rather than runtime deobfuscation because:
  • bet365 frequently changes variable names
  • Static transformations are more maintainable
  • Can be integrated into CI/CD pipelines
  • Allows for systematic tracking of obfuscation pattern changes

Pattern Recognition

The deobfuscation relies on recognizing common obfuscation patterns:
  • Hex-encoded variable names (_0x[0-9a-f]+)
  • Boolean obfuscation (!0, !1)
  • Void expressions (void 0)
  • Bracket notation for common properties
  • Redundant variable assignments
The obfuscation patterns change frequently. When bet365 updates their obfuscation scheme, the transformation mappings (especially in Stage 2) need to be updated accordingly.

Next Steps

Build docs developers (and LLMs) love