Skip to main content

Overview

bet365 actively rotates their obfuscated JavaScript code, presenting a unique challenge for reverse engineering efforts. The obfuscation patterns, variable names, and even control flow change periodically, requiring systematic tracking and adaptive deobfuscation strategies.

What is Code Rotation?

Code rotation refers to bet365’s practice of periodically changing their obfuscated JavaScript while maintaining the same underlying functionality. This creates multiple “versions” of the same code, each with different obfuscation patterns.
Code rotation is a defensive measure against reverse engineering. By frequently changing the obfuscation, bet365 makes it harder to maintain working deobfuscation tools.

Rotation Factors

bet365’s obfuscated code appears to rotate based on several factors:

Geographic Location (IP Address)

Different geographic regions may receive different versions of the obfuscated code:
# Different code versions based on IP location
# Location A (IP: 203.0.113.1) → Version 1
# Location B (IP: 198.51.100.1) → Version 2
Evidence:
  • Users from different countries report seeing different variable names
  • Same request from different IPs yields different obfuscated code

Time-based Rotation

The code appears to rotate over time, possibly on a schedule:
# Observed rotation timestamps
mitmproxy/src/javascript/obfuscated/
├── 1746447434.4952288-received-14.js  # Jan 5, 2025
├── 1746854061.1840258-received-14.js  # Jan 10, 2025
├── 1747199742.5981588-received-14.js  # Jan 14, 2025
├── 1747977327.0128024-received-14.js  # Jan 23, 2025
└── ... (more timestamps)
Observations:
  • New versions appear every few days to weeks
  • Not strictly scheduled - appears somewhat random
  • Major updates seem to correlate with bet365 deployments

User Agent / Browser Version

Different browsers or browser versions might receive different code:
  • Desktop vs Mobile
  • Chrome vs Firefox vs Safari
  • Different browser versions

Session-based Variation

Some rotation may be session-specific or user-specific:
  • A/B testing different obfuscation strategies
  • Gradual rollout of new obfuscation versions

Tracking Rotation Strategy

The project implements a systematic approach to tracking code rotation:

Timestamped Archive

All intercepted obfuscated code is saved with Unix timestamps:
mitmproxy/src/javascript/obfuscated/
├── 1746447434.4952288-received-14.js
├── 1746854061.1840258-received-14.js
├── 1747199742.5981588-received-14.js
├── 1747977327.0128024-received-14.js
├── 1748409220.998982-received-14.js
└── readme.md
The readme.md documents the purpose:
Keep track of all obfuscated code with timestamps

Automatic Saving

The save_obfuscated_code.py script automatically archives new obfuscated code:
import os
import shutil
from pathlib import Path

project_dir = (Path(__file__).parent / "../../..").resolve()
output_dir = project_dir / "output"
obfuscated_dir = project_dir / "mitmproxy/src/javascript/obfuscated"
obfuscated_file = project_dir / "mitmproxy/src/javascript/obfuscated-new-raw.js"

def find_first_obfuscated_js():
    """Find first JavaScript file containing '(function(){' pattern."""
    try:
        for file in output_dir.glob("*.js"):
            with open(file, "r", encoding="utf-8") as f:
                content = f.read()
                if content.startswith("(function(){"):
                    return file
    except Exception as e:
        print(f"Error processing files: {e}")
    return None

def copy_obfuscated_js():
    target_file = obfuscated_dir / first_obfuscated_js_file.name
    shutil.copy2(first_obfuscated_js_file, target_file)
    if os.path.lexists(obfuscated_file):
        os.unlink(obfuscated_file)
    os.symlink(os.path.relpath(target_file, obfuscated_file.parent), obfuscated_file)

if __name__ == "__main__":
    first_obfuscated_js_file = find_first_obfuscated_js()
    if first_obfuscated_js_file:
        print(f"Found matching file: {first_obfuscated_js_file}")
        copy_obfuscated_js()
    else:
        print("No JavaScript files with '(function(){' pattern found in output directory")
Workflow:
  1. Intercept new obfuscated code in output/ directory
  2. Copy to timestamped archive in obfuscated/ directory
  3. Create symlink obfuscated-new-raw.js pointing to latest version
  4. Allows quick access to latest version while preserving history

Impact on Deobfuscation

Variable Name Changes

The most common rotation involves changing obfuscated variable names:
var _0x588211 = [];
var _0x478891 = tape;
_0x478891[_0xfe71f0] = _0x3fb717;
The hex patterns change (_0x588211_0xa83f42) but the functionality remains the same. Deobfuscation Challenge: The variable mapping in Stage 2 needs constant updates:
// refactor-obfuscated-code-jscodeshift-2.js
var refactorVariables = {
    // Must be updated for each rotation
    '_0x588211': 'globalState',  // Version 1
    '_0xa83f42': 'globalState',  // Version 2 - same semantic meaning
    '_0x478891': 'tape',
    '_0xc91fd8': 'tape',
    // ... over 200 mappings
};

Pattern Changes

Sometimes the obfuscation patterns themselves change:
// Boolean obfuscation with unary operators
var flag = !0;
var disabled = !1;
Each pattern requires a different transformation strategy.

CI/CD for Rotation Detection

The README outlines a plan for automated rotation detection:
We understand that bet365 frequently updates their obfuscated code. To address this, we will:
  • Establish a CI/CD pipeline to identify precisely when changes are made.
  • Maintain a functional backup script to ensure the deobfuscation process continues to work, even when bet365 updates their code.

Proposed CI/CD Pipeline

1

Scheduled Fetching

Periodically fetch JavaScript from bet365 from multiple locations:
# .github/workflows/check-rotation.yml
schedule:
  - cron: '0 */6 * * *'  # Every 6 hours
2

Compare with Archive

Hash the fetched code and compare with known versions:
import hashlib

def get_code_hash(code):
    return hashlib.sha256(code.encode()).hexdigest()

new_hash = get_code_hash(fetched_code)
if new_hash not in known_hashes:
    alert_new_rotation()
3

Test Deobfuscation

Run deobfuscation pipeline and check for errors:
node refactor-obfuscated-code-jscodeshift.js \
  latest.js \
  deobfuscated.js
4

Alert on Failure

If deobfuscation fails or produces errors, notify maintainers:
  • GitHub issue creation
  • Slack/Discord notification
  • Email alert

Handling Rotation: Best Practices

1. Maintain a Version Database

Keep structured metadata about each version:
{
  "versions": [
    {
      "timestamp": 1746447434.4952288,
      "file": "1746447434.4952288-received-14.js",
      "hash": "a3f8c9d2e1b4f6a8c9d2e1b4f6a8c9d2",
      "variable_mappings": {
        "_0x588211": "globalState",
        "_0x478891": "tape"
      },
      "patterns": ["void0", "unary_boolean", "bracket_notation"],
      "location": "US",
      "user_agent": "Chrome/120.0.0.0"
    }
  ]
}

2. Automated Mapping Discovery

Develop heuristics to automatically discover variable mappings:
// Heuristic: Variables assigned to 'tape' are likely tape-related
function discoverTapeVariable(ast) {
    const candidates = ast.find(j.VariableDeclarator, {
        init: {
            type: 'Identifier',
            name: 'tape'
        }
    });
    return candidates.map(path => path.value.id.name);
}

3. Differential Analysis

Compare new versions against known versions to identify changes:
def diff_obfuscated_versions(old_code, new_code):
    old_vars = extract_variable_names(old_code)
    new_vars = extract_variable_names(new_code)
    
    return {
        'removed_vars': old_vars - new_vars,
        'added_vars': new_vars - old_vars,
        'common_vars': old_vars & new_vars
    }

4. Fallback Strategies

Implement graceful degradation when encountering unknown versions:
If deobfuscation fails with new code, serve the last successfully deobfuscated version:
if deobfuscation_failed:
    return last_known_good_deobfuscated_code
Apply only the transformation stages that succeed:
try {
    ast = new Void0Transformer(ast).transform();
    ast = new UnaryExpressionTransformer(ast).transform();
    // ... continue with more stages
} catch (error) {
    // Return partially transformed code
    return ast.toSource();
}
Use pattern matching to identify semantic equivalents across versions:
// Pattern: variable holds array with specific initialization
function findGlobalStateVariable(ast) {
    // Look for: var X = []; ... X[35] = ...
    // Regardless of variable name
}

Development Workflow for Rotation

When a new rotation is detected:
1

Capture New Version

Run mitmproxy and capture the new obfuscated code:
./mitmproxy.sh
# Visit bet365.com in proxied browser
# New code saved to output/ directory
2

Archive the Version

Run the save script:
python mitmproxy/src/python/save_obfuscated_code.py
3

Test Current Deobfuscation

Try deobfuscating the new version:
node mitmproxy/src/javascript/refactor-obfuscated-code-jscodeshift.js \
  mitmproxy/src/javascript/obfuscated-new-raw.js \
  test-output.js
4

Compare with Previous Versions

Use diff tools to identify changes:
diff -u obfuscated/previous-version.js obfuscated/new-version.js
5

Update Variable Mappings

Modify refactor-obfuscated-code-jscodeshift-2.js with new mappings based on semantic analysis.
6

Update Pattern Matchers

If new obfuscation patterns are found, create new transformer stages or update existing ones.
7

Test and Validate

Run tests and verify deobfuscation quality:
npm test

Monitoring Rotation Frequency

Track rotation patterns over time:
import os
from pathlib import Path
from datetime import datetime

obfuscated_dir = Path("mitmproxy/src/javascript/obfuscated")
files = sorted(obfuscated_dir.glob("*.js"))

for i in range(len(files) - 1):
    current_time = float(files[i].name.split("-")[0])
    next_time = float(files[i+1].name.split("-")[0])
    
    time_diff = next_time - current_time
    days = time_diff / 86400
    
    print(f"Rotation interval: {days:.1f} days")
Example output:
Rotation interval: 4.7 days
Rotation interval: 3.9 days
Rotation interval: 9.0 days
Rotation interval: 5.0 days
Rotation interval: 1.8 days

Future Improvements

Planned enhancements to handle rotation more effectively:
  • Machine learning to predict variable mappings
  • Semantic analysis to identify functionally equivalent code patterns
  • Automated variable mapping generation from execution traces
  • Multi-version deobfuscation support (handle multiple versions simultaneously)
  • Real-time A/B testing detection

Challenges

Variable Explosion

With over 200 variable mappings needed and frequent rotation, maintenance becomes challenging:
// refactor-obfuscated-code-jscodeshift-2.js (304 lines!)
var refactorVariables = {
    '_0x1e16': 'keywords',
    '_0x35ab': 'getKeywordName',
    // ... 200+ more lines
};
Potential solution: Generate mappings programmatically based on usage patterns.

Breaking Changes

Occasionally, bet365 makes structural changes that break the entire deobfuscation pipeline:
  • New control flow obfuscation
  • Different function wrapping
  • Changed module system
  • Additional encoding layers
These require significant rework of transformation stages.

Geographic Testing

Testing from multiple geographic locations requires:
  • VPN/proxy infrastructure
  • Coordinated testing
  • Version correlation across locations

Resources

Rotation Archive

All historical versions in mitmproxy/src/javascript/obfuscated/

Save Script

save_obfuscated_code.py for archiving new versions

Variable Mappings

refactor-obfuscated-code-jscodeshift-2.js contains all mappings

CI/CD Plan

See README.md for planned automation

Next Steps

Build docs developers (and LLMs) love