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>
This commit is contained in:
commit
adb318f57c
5 files changed
+957
No files matched your search
+206
@@ -0,0 +1,206 @@
|
||||
#!/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()
|
||||
Reference in new issue
Block a user