Learn how to create custom jscodeshift transforms to handle new obfuscation patterns in bet365’s JavaScript.
The deobfuscation process uses a chain of transforms that progressively simplify obfuscated code. Each transform extends the AstTransformer base class.
import escodegen from "escodegen";
import * as esprima from "esprima";
import fs from "node:fs";
class AstTransformer {
constructor(stepNumber, jscodeshiftAst, output, outputBaseName) {
this.stepNumber = stepNumber;
this.jscodeshiftAst = jscodeshiftAst;
this.output = output;
this.outputBaseName = outputBaseName;
if (this.outputBaseName == null) {
this.outputFileName = `deobfuscated-output-${this.stepNumber}.js`;
} else {
this.outputFileName = `${outputBaseName}-${this.stepNumber}.js`;
}
}
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);
}
}
The performTransform() method must be implemented by subclasses. It should modify this.jscodeshiftAst in place.
Transforms are executed in sequence by the ChainedTransformer:
import {AstTransformer} from "./refactor-obfuscated-code-jscodeshift-common";
import {Void0Transformer} from "./refactor-obfuscated-code-jscodeshift-0";
import {UnaryExpressionTransformer} from "./refactor-obfuscated-code-jscodeshift-1";
import {RefactorVariableTransformer} from "./refactor-obfuscated-code-jscodeshift-2";
// ... more imports
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();
// ... more transforms
return ast;
}
}
Replaces void 0 expressions with undefined:
import {AstTransformer} from "./refactor-obfuscated-code-jscodeshift-common";
import j from "jscodeshift";
class Void0Transformer extends AstTransformer {
constructor(stepNumber, jscodeshiftAst, output, outputBaseName) {
super(stepNumber, jscodeshiftAst, output, outputBaseName);
}
performTransform() {
this.jscodeshiftAst
.find(j.UnaryExpression, {
operator: 'void',
argument: {value: 0}
})
.replaceWith(path => j.identifier('undefined'));
}
}
module.exports = {Void0Transformer};
Before:
After:
Simplifies unary expressions with constant operands:
import {AstTransformer} from "./refactor-obfuscated-code-jscodeshift-common";
import j from "jscodeshift";
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]));
});
});
}
}
Before:
After:
var a = true;
var b = false;
Renames obfuscated variables to meaningful names:
import {AstTransformer} from "./refactor-obfuscated-code-jscodeshift-common";
var refactorVariables = {
'_0x1e16': 'keywords',
'_0x35ab': 'getKeywordName',
'_0x588211': 'globalState',
'_0x478891': 'tape',
'_0x4951e9': 'functions',
// ... many more mappings
};
class RefactorVariableTransformer extends AstTransformer {
performTransform() {
Object.entries(refactorVariables).forEach(([key, refactoredVariableName]) => {
this.jscodeshiftAst
.findVariableDeclarators(key)
.renameTo(refactoredVariableName);
});
}
}
Before:
var _0x588211 = [];
_0x588211[35] = context;
After:
var globalState = [];
globalState[35] = context;
Create Transform File
Create a new file following the naming convention:touch mitmproxy/src/javascript/refactor-obfuscated-code-jscodeshift-11.js
Import Dependencies
Import the base class and jscodeshift:import {AstTransformer} from "./refactor-obfuscated-code-jscodeshift-common";
import j from "jscodeshift";
Define Your Transformer
Extend AstTransformer and implement performTransform():class MyCustomTransformer extends AstTransformer {
constructor(stepNumber, jscodeshiftAst, output, outputBaseName) {
super(stepNumber, jscodeshiftAst, output, outputBaseName);
}
performTransform() {
// Your transformation logic here
this.jscodeshiftAst
.find(j.CallExpression, {
callee: {
type: 'Identifier',
name: 'obfuscatedFunction'
}
})
.forEach(path => {
// Transform each matching node
});
}
}
module.exports = {MyCustomTransformer};
Add to Chain
Import and add your transformer to the chain:// In refactor-obfuscated-code-jscodeshift-chained.js
import {MyCustomTransformer} from "./refactor-obfuscated-code-jscodeshift-11";
performTransform() {
let ast = this.jscodeshiftAst;
// ... existing transforms
ast = new MyCustomTransformer(11, ast, this.output, this.outputBaseName).transform();
return ast;
}
Test Your Transform
Test with a sample file:node mitmproxy/src/javascript/refactor-obfuscated-code-jscodeshift.js \
test-input.js test-output.js true
Enable intermediate output (true) to see the result at each step.
Pattern 1: Simple Replacement
Replace one AST node with another:
performTransform() {
this.jscodeshiftAst
.find(j.Identifier, {name: 'oldName'})
.replaceWith(path => j.identifier('newName'));
}
Pattern 2: Conditional Replacement
Replace based on context or conditions:
performTransform() {
this.jscodeshiftAst
.find(j.CallExpression)
.filter(path => {
const callee = path.value.callee;
return callee.type === 'Identifier' &&
callee.name.startsWith('_0x');
})
.forEach(path => {
// Transform this call expression
});
}
Pattern 3: Parent-Child Manipulation
performTransform() {
this.jscodeshiftAst
.find(j.MemberExpression)
.forEach(path => {
const object = path.value.object;
const property = path.value.property;
// Access parent node
const parent = path.parent;
// Modify based on parent context
if (parent.value.type === 'CallExpression') {
// This is a method call
}
});
}
Pattern 4: Multi-Node Patterns
Find and transform complex patterns:
performTransform() {
this.jscodeshiftAst
.find(j.BinaryExpression, {
operator: '+',
left: {type: 'StringLiteral'},
right: {type: 'StringLiteral'}
})
.replaceWith(path => {
// Concatenate string literals at transform time
const left = path.value.left.value;
const right = path.value.right.value;
return j.stringLiteral(left + right);
});
}
jscodeshift Query API
Finding Nodes
// Find by node type
ast.find(j.VariableDeclaration)
// Find with property matching
ast.find(j.Identifier, {name: 'specificName'})
// Find nested properties
ast.find(j.MemberExpression, {
object: {name: 'obj'},
property: {name: 'prop'}
})
Filtering Results
ast.find(j.CallExpression)
.filter(path => {
return path.value.arguments.length > 0;
})
Traversing the AST
// Find all functions
ast.find(j.FunctionDeclaration)
.forEach(path => {
// Access the node
const node = path.value;
// Get the function name
const name = node.id.name;
// Get function body
const body = node.body.body;
// Modify the node
node.async = true;
})
Building Node Objects
jscodeshift provides builder functions for creating AST nodes:
// Identifiers
j.identifier('variableName')
// Literals
j.stringLiteral('hello')
j.numericLiteral(42)
j.booleanLiteral(true)
// Expressions
j.callExpression(
j.identifier('func'),
[j.identifier('arg1'), j.identifier('arg2')]
)
j.memberExpression(
j.identifier('object'),
j.identifier('property')
)
// Statements
j.variableDeclaration('const', [
j.variableDeclarator(
j.identifier('x'),
j.numericLiteral(10)
)
])
Unit Testing
Create test files alongside your transforms:
// refactor-obfuscated-code-jscodeshift-11.test.js
import {MyCustomTransformer} from './refactor-obfuscated-code-jscodeshift-11';
import j from 'jscodeshift';
test('transforms obfuscated pattern', () => {
const input = `
obfuscatedFunction(arg1, arg2);
`;
const ast = j(input);
const transformer = new MyCustomTransformer(11, ast, false, null);
const result = transformer.transform();
const output = result.toSource();
expect(output).toContain('deobfuscatedFunction');
});
Integration Testing
Test the entire transform chain with real samples:
node mitmproxy/src/javascript/refactor-obfuscated-code-jscodeshift.js \
mitmproxy/src/javascript/obfuscated/sample-1.js \
test-output.js
# Verify output is valid JavaScript
node --check test-output.js
Best Practices
Order Matters: Place transforms in logical order. Simple replacements should come before complex pattern matching.
Test Incrementally: Enable intermediate output to see the effect of each transform step.
Use AST Explorer: Test your node matching patterns in astexplorer.net before implementing them.
Be careful with replacements that could create invalid JavaScript. Always validate output after transforms.
Handling Obfuscation Updates
bet365 frequently updates their obfuscation patterns. When this happens:
Capture New Obfuscated Code
Use save_obfuscated_code.py to save the latest version:python mitmproxy/src/python/save_obfuscated_code.py
This finds the obfuscated file in /output and saves it to /mitmproxy/src/javascript/obfuscated/. Identify Pattern Changes
Compare the new obfuscated code with previous versions to identify what changed:diff mitmproxy/src/javascript/obfuscated/old-version.js \
mitmproxy/src/javascript/obfuscated-new-raw.js
Update or Create Transforms
Modify existing transforms or create new ones to handle the new patterns.
Update Variable Mappings
If variable names changed, update the refactorVariables mapping in Transform 2.
Transforms run every time a request is intercepted. To optimize:
- Avoid redundant traversals
- Use specific node matching instead of broad searches
- Cache results when possible
- Profile with
--inspect to find bottlenecks
node --inspect --inspect-brk \
mitmproxy/src/javascript/refactor-obfuscated-code-jscodeshift.js \
input.js output.js
Resources