Files
boxsim-fileformat-rev/bpj2ini.py
T
magdevandClaude Fable 5 adb318f57c Reverse engineer Boxsim MyIni2CK container, add bpj2ini decoder
Format: 8-byte magic, XOR layer (table-shuffled 3-LCG PRNG, seeds 123/456),
custom LZ+Huffman codec (A2F). Decompression runs the original ExpandA2F
code from Boxsim.exe under Unicorn. Decodes .bpj and .bch files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-28 19:55:19 +02:00

207 lines
7.4 KiB
Python

#!/usr/bin/env python3
"""Decode Boxsim .bpj / .bch (MyIni2CK) files to plain INI text.
Pipeline (reverse engineered from Boxsim 2.1, TMyIniFile by UG):
1. 8-byte magic "MyIni2CK" (plaintext)
2. body XOR-encrypted with a table-shuffled 3-LCG PRNG, seeds (123, 456)
3. result is a custom canonical-Huffman + LZ stream ("A2F" codec)
4. decompressed: tab-separated INI text, CRLF line ends, German decimals
Step 3 is executed by emulating Boxsim's own ExpandA2F function (VA 0xa11880)
with Unicorn, so Boxsim.exe must be available (BOXSIM_EXE env or default path).
Usage: bpj2ini.py input.bpj [output.ini]
"""
import os
import struct
import sys
import pefile
from unicorn import *
from unicorn.x86_const import *
EXE = os.environ.get(
"BOXSIM_EXE",
os.path.join(os.path.dirname(os.path.abspath(__file__)),
"boxsim_dl", "BoxSim 2.1 DE (Stand 12.2025)", "Boxsim.exe"))
MAGIC = b"MyIni2CK"
ENTRY = 0xA11880 # ExpandA2F(obj, instream, outstream)
ERR_FUNC = 0x97E584 # Boxsim error logger
RAISE_FUNC = 0x80ACC4 # Delphi RaiseExcept
OBJ_BASE = 0x20000000 # decoder object (zero-init, ~11 MB of tables)
OBJ_SIZE = 0x01000000
FAKE_BASE = 0x40000000 # fake TStream objects, vtables, trap stubs
STACK_BASE = 0x60000000
STACK_SIZE = 0x100000
GDT_BASE = 0x50000000
FS_SEG = 0x50002000 # Delphi SEH chain scratch
SENTINEL = 0x7F00DEAD
IN_OBJ, OUT_OBJ = FAKE_BASE, FAKE_BASE + 0x100
IN_VT, OUT_VT = FAKE_BASE + 0x1000, FAKE_BASE + 0x2000
STUB = FAKE_BASE + 0x3000
S_IN_SIZE, S_IN_READ, S_IN_SEEK, S_OUT_WRITE, S_OUT_SIZE, S_OUT_SEEK = range(6)
M32 = 1 << 32
class KryptPRNG:
"""Boxsim file scrambler PRNG (Init at VA 0xa12be4, next at 0xa12c9c)."""
def __init__(self, s1=123, s2=456, threshold=0xFFFFFFFF):
self.s1, self.s2, self.thr = s1, s2, threshold
self.s3 = (s1 % 0xFFFE) * (s2 % 0x7F23)
self.table = [0] * 256
for i in range(256):
self._step()
self.table[i] = (self.s1 + self.s2) % M32
def _step(self):
self.s1 = (self.s1 * 0x10B95 + 1) % M32
self.s2 = (self.s2 * 0x10BA9 + 1) % M32
if self.s2 >= self.thr:
self.s2 = 0
self.s3 = (self.s3 * 0x1155D + 1) % M32
def next_byte(self):
self._step()
idx = self.s3 & 0xFF
out = self.table[idx]
self.table[idx] = (self.s1 + self.s2) % M32
return out & 0xFF
def _gdt_entry(base, limit, access, gran):
e = limit & 0xFFFF
e |= (base & 0xFFFFFF) << 16
e |= (access & 0xFF) << 40
e |= ((limit >> 16) & 0xF) << 48
e |= (gran & 0xF) << 52
e |= ((base >> 24) & 0xFF) << 56
return struct.pack("<Q", e)
def expand_a2f(data):
"""Decompress a decrypted MyIni2CK stream by emulating Boxsim's decoder."""
pe = pefile.PE(EXE)
base = pe.OPTIONAL_HEADER.ImageBase
img = pe.get_memory_mapped_image(ImageBase=base)
uc = Uc(UC_ARCH_X86, UC_MODE_32)
uc.mem_map(base, (len(img) + 0xFFF) & ~0xFFF)
uc.mem_write(base, img)
for addr, size in ((OBJ_BASE, OBJ_SIZE), (FAKE_BASE, 0x10000),
(STACK_BASE, STACK_SIZE), (GDT_BASE, 0x4000)):
uc.mem_map(addr, size)
uc.mem_write(IN_OBJ, struct.pack("<I", IN_VT))
uc.mem_write(OUT_OBJ, struct.pack("<I", OUT_VT))
slot_map = ((IN_VT, {0x00: S_IN_SIZE, 0x0C: S_IN_READ, 0x28: S_IN_SEEK}),
(OUT_VT, {0x00: S_OUT_SIZE, 0x0C: S_IN_READ,
0x10: S_OUT_WRITE, 0x28: S_OUT_SEEK}))
for vt, stubs in slot_map:
for off in range(0, 0x30, 4):
sid = stubs.get(off, 15)
uc.mem_write(vt + off, struct.pack("<I", STUB + sid * 16))
for sid in range(16):
body = b"\xc2\x08\x00" if sid in (S_IN_SEEK, S_OUT_SEEK) else b"\xc3"
uc.mem_write(STUB + sid * 16, body)
uc.mem_write(GDT_BASE + 8, _gdt_entry(0, 0xFFFFF, 0x9B, 0xC))
uc.mem_write(GDT_BASE + 16, _gdt_entry(0, 0xFFFFF, 0x93, 0xC))
uc.mem_write(GDT_BASE + 24, _gdt_entry(FS_SEG, 0xFFF, 0x93, 0x4))
uc.reg_write(UC_X86_REG_GDTR, (0, GDT_BASE, 0x1F, 0))
uc.reg_write(UC_X86_REG_CS, 8)
for seg in (UC_X86_REG_DS, UC_X86_REG_ES, UC_X86_REG_SS, UC_X86_REG_GS):
uc.reg_write(seg, 16)
uc.reg_write(UC_X86_REG_FS, 24)
state = {"pos": 0, "out": bytearray(), "err": None}
def hook_code(u, addr, _sz, _ud):
if STUB <= addr < STUB + 256:
sid = (addr - STUB) // 16
eax = u.reg_read(UC_X86_REG_EAX)
edx = u.reg_read(UC_X86_REG_EDX)
ecx = u.reg_read(UC_X86_REG_ECX)
if sid == S_IN_READ and eax == IN_OBJ:
n = min(ecx, len(data) - state["pos"])
if n > 0:
u.mem_write(edx, data[state["pos"]:state["pos"] + n])
state["pos"] += n
u.reg_write(UC_X86_REG_EAX, n)
elif sid == S_IN_SIZE:
u.reg_write(UC_X86_REG_EAX, len(data))
u.reg_write(UC_X86_REG_EDX, 0)
elif sid == S_IN_SEEK:
esp = u.reg_read(UC_X86_REG_ESP)
off = struct.unpack("<i", u.mem_read(esp + 4, 4))[0]
origin = edx & 0xFF
state["pos"] = (off if origin == 0 else
state["pos"] + off if origin == 1 else
len(data) + off)
u.reg_write(UC_X86_REG_EAX, state["pos"] & 0xFFFFFFFF)
u.reg_write(UC_X86_REG_EDX, 0)
elif sid == S_OUT_WRITE:
state["out"] += bytes(u.mem_read(edx, ecx))
u.reg_write(UC_X86_REG_EAX, ecx)
elif sid in (S_OUT_SIZE, S_OUT_SEEK):
u.reg_write(UC_X86_REG_EAX, len(state["out"]))
u.reg_write(UC_X86_REG_EDX, 0)
else:
state["err"] = f"unexpected vtable stub {sid}"
u.emu_stop()
elif addr == ERR_FUNC:
eax = u.reg_read(UC_X86_REG_EAX)
msg = bytes(u.mem_read(eax, 64)).split(b"\x00")[0]
state["err"] = f"boxsim error: {msg.decode('latin1')}"
u.emu_stop()
elif addr == RAISE_FUNC:
state["err"] = "delphi exception"
u.emu_stop()
uc.hook_add(UC_HOOK_CODE, hook_code, None)
esp = STACK_BASE + STACK_SIZE - 0x1000
uc.mem_write(esp, struct.pack("<I", SENTINEL))
uc.reg_write(UC_X86_REG_ESP, esp)
# codec config as set by the compressor entry (0xa1013c)
uc.mem_write(OBJ_BASE + 0x04, b"\x01\x01")
uc.mem_write(OBJ_BASE + 0x08, struct.pack("<I", 0x20))
uc.mem_write(OBJ_BASE + 0x0C, struct.pack("<I", 0x47))
uc.mem_write(OBJ_BASE + 0x10, b"\x01")
uc.reg_write(UC_X86_REG_EAX, OBJ_BASE)
uc.reg_write(UC_X86_REG_EDX, IN_OBJ)
uc.reg_write(UC_X86_REG_ECX, OUT_OBJ)
uc.emu_start(ENTRY, SENTINEL, timeout=120 * 1000000)
if state["err"]:
raise RuntimeError(state["err"])
if state["pos"] != len(data):
raise RuntimeError(f"decoder stopped early: {state['pos']}/{len(data)}")
return bytes(state["out"])
def decode_bpj(raw):
if raw[:8] != MAGIC:
raise ValueError("not a MyIni2CK file")
prng = KryptPRNG()
body = bytes(c ^ prng.next_byte() for c in raw[8:])
return expand_a2f(MAGIC + body)
def main():
if len(sys.argv) < 2:
sys.exit(__doc__)
src = sys.argv[1]
dst = sys.argv[2] if len(sys.argv) > 2 else os.path.splitext(src)[0] + ".ini"
text = decode_bpj(open(src, "rb").read())
open(dst, "wb").write(text)
print(f"{src}: {len(text)} bytes -> {dst}")
if __name__ == "__main__":
main()