206 lines
7.4 KiB
Python
206 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()
|