#!/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(" 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(" 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()