#!/usr/bin/env python3
"""Recover the hidden game JavaScript that neon_outrun feeds to its embedded V8.

Usage: python3 -I extract_js.py <neon_outrun> <out.js>

Static recovery of the loader in FUN_00688c64 (file VA; Ghidra VA +0x100000):
  1. two key bytes k0,k1 are produced by control-flow-flattened constant folding
     (0x689b99..0x689de3 -> [rsp+0x7e], [rsp+0x7f]);
  2. buf[i] = rodata[0x21db73 + i] ^ k[i & 1] for i < 0x796;
  3. an LZSS decoder (0x68a242..0x68a4f8):
        flag byte, LSB first, bit set -> literal byte
        else two bytes b0 b1: dist = ((b1 & 0xf0) << 4) | b0, len = (b1 & 0xf) + K,
        copy from out[len(out) - dist] (clamped at 0)
     K (in [rsp]), the start index (rbp) and the initial flag mask are again opaque
     constants computed by flattened state machines;
  4. the LZSS output is a zstd frame (28 b5 2f fd) -> zstd decompress;
  5. which yields a raw DEFLATE stream (flate2) -> inflate;
  6. the result is passed to v8::String::NewFromUtf8 -> v8::Script::Compile/Run.
Only those small pure-register fragments are evaluated, with Unicorn (no OS, no
syscalls, nothing but the .text bytes mapped) - the binary itself is never run.
"""
import sys
import zlib

import zstandard

from unicorn import Uc, UC_ARCH_X86, UC_MODE_64
from unicorn.x86_const import (UC_X86_REG_RSP, UC_X86_REG_RBP, UC_X86_REG_R12,
                               UC_X86_REG_RAX, UC_X86_REG_R14, UC_X86_REG_RBX)

TEXT_LO, TEXT_HI = 0x4c2ac0, 0x4c2ac0 + 0xef0310   # .text: file offset = VA - 0x1000
TEXT_DELTA = 0x1000
STACK = 0x7f0000000000


def emu(raw, start, end, regs=None):
    mu = Uc(UC_ARCH_X86, UC_MODE_64)
    base = start & ~0xfff
    size = ((end - base) + 0x2000) & ~0xfff
    mu.mem_map(base, size)
    mu.mem_write(base, raw[base - TEXT_DELTA: base - TEXT_DELTA + size])
    mu.mem_map(STACK, 0x10000)
    mu.reg_write(UC_X86_REG_RSP, STACK + 0x8000)
    for r, v in (regs or {}).items():
        mu.reg_write(r, v)
    mu.emu_start(start, end, count=2_000_000)
    return mu


def main():
    raw = open(sys.argv[1], "rb").read()
    rsp = STACK + 0x8000

    # 1. key bytes
    mu = emu(raw, 0x689b99, 0x689de3)
    k = bytes(mu.mem_read(rsp + 0x7e, 2))
    print(f"[*] xor key bytes: {k.hex()}")

    # 2. xor-decode the 0x796-byte blob (rodata VA == file offset)
    blob = raw[0x21db73:0x21db73 + 0x796]
    buf = bytes(b ^ k[i & 1] for i, b in enumerate(blob))

    # 3a. K (min match length) -> [rsp] at 0x68a056
    mu = emu(raw, 0x689e50, 0x68a056)
    K = int.from_bytes(mu.mem_read(rsp, 8), "little")
    # 3b. start index rbp at 0x68a242
    mu = emu(raw, 0x68a07c, 0x68a242)
    start = mu.reg_read(UC_X86_REG_RBP)
    # 3c. initial flag mask r12b: run the per-flag-byte state machine once
    mu = emu(raw, 0x68a254, 0x68a3d2, {UC_X86_REG_R12: 0})
    mask0 = mu.reg_read(UC_X86_REG_R12) & 0xff
    print(f"[*] K={K} start={start} mask0={mask0:#x}")

    out = bytearray()
    p = start
    while p < len(buf):
        flags = buf[p]
        p += 1
        mask = mask0
        for _ in range(8):
            if p >= len(buf):
                break
            if mask & flags:
                out.append(buf[p])
                p += 1
            else:
                if p + 1 >= len(buf):
                    p = len(buf)
                    break
                b0, b1 = buf[p], buf[p + 1]
                dist = ((b1 << 4) & 0xf00) | b0
                ln = (b1 & 0xf) + K
                src = max(0, len(out) - dist)
                for _ in range(ln):
                    out.append(out[src] if src < len(out) else 0)
                    src += 1
                p += 2
            mask = (mask << 1) & 0xff
    assert out[:4] == b"\x28\xb5\x2f\xfd", "expected a zstd frame"
    js = zstandard.ZstdDecompressor().decompressobj().decompress(bytes(out))
    out = zlib.decompressobj(-15).decompress(js)
    open(sys.argv[2], "wb").write(bytes(out))
    print(f"[*] {len(out)} bytes of JavaScript -> {sys.argv[2]}")


if __name__ == "__main__":
    main()
