72 lines
5.1 KiB
Markdown
72 lines
5.1 KiB
Markdown
# M0 — shim bootstrap (proxy `binkw32.dll` + one hook)
|
||
|
||
**Result (2026-09-07): proven end to end on the real game.** The proxy DLL loads, forwards all
|
||
Bink calls, relocates the target RVA against the ASLR'd exe base, hooks `Mars::Application::Initialize`,
|
||
logs the call, and the game plays its intro and reaches the main menu.
|
||
|
||
## What was built
|
||
- `src/shim/binkw32.def` — 66 forwarders (`_BinkOpen@8 = binkw32_real._BinkOpen@8 @41` style),
|
||
names and ordinals mirrored from the real DLL. Note: the real DLL has **66** exports including
|
||
`_RADTimerRead@0`, which earlier lists missed. The exe imports 12 of them **by name** (no ordinals).
|
||
- `src/shim/main.cpp` — `DllMain` → `Shim_Init`: opens `<dll dir>\shim.log`, logs banner (build id,
|
||
exe path, exe base + ASLR delta, provenance line lifted from `sots_addresses.h`), reads
|
||
`shim.cfg` (`hooks=trace|off`), `MH_Initialize`, one MinHook hook on
|
||
`exeBase + sots::addr::Mars_Application_Initialize`, `MH_EnableHook`. Detach → `MH_Uninitialize`.
|
||
- `third_party/minhook/` — MinHook vendored (BSD-2, see `VENDOR.txt` for commit).
|
||
- `CMakeLists.txt`, `cmake/toolchain-mingw-i686.cmake`, `CMakePresets.json` (`shim` = i686 MinGW
|
||
cross, `host` = Linux placeholder test `tests/addr_smoke.cpp`).
|
||
- `tools/build-shim.sh` (build box), `tools/sync-build.sh` (dev box → build box), `tools/deploy.ps1`,
|
||
`tools/undeploy.ps1` (game VM).
|
||
|
||
## Build & deploy
|
||
```
|
||
# dev box: push the tree to spicy:/bulk-storage/re-lab/build/sots-engine (== CT111 /srv/re-lab/...)
|
||
# and run tools/build-shim.sh inside CT111 (needs g++-mingw-w64-i686 binutils-mingw-w64-i686 cmake ninja-build)
|
||
tools/sync-build.sh
|
||
# -> builds build-shim/binkw32.dll, diffs its export-name table against the real DLL (fails on mismatch),
|
||
# stages binkw32.dll + shim.cfg + deploy.ps1 + undeploy.ps1 + BUILD_ID in /srv/re-lab/shim/dist (= Z:\shim\dist)
|
||
|
||
# game VM (re@192.168.10.139): Z: is per-logon, so pass share creds (or set RELAB_USER/RELAB_PASS)
|
||
powershell -ExecutionPolicy Bypass -File deploy.ps1 -ShareUser re -SharePass <pw>
|
||
# stops the game, keeps the original as C:\SOTS\binkw32_real.dll (only if not already a proxy),
|
||
# copies the proxy, writes shim.cfg only if absent, relaunches via scheduled task SOTS
|
||
powershell -ExecutionPolicy Bypass -File undeploy.ps1 # put the original back
|
||
```
|
||
Logs: `C:\SOTS\shim.log` (append mode, one banner per run). Screenshots: `qm monitor 140` screendump.
|
||
|
||
## Evidence (`/bulk-storage/re-lab/shim/logs/`)
|
||
- `m0.log` — the shim log. Key lines from the passing run (build `4a15301-20260907T2118Z`):
|
||
```
|
||
==== sots-engine shim (binkw32 proxy) build 4a15301-20260907T2118Z ====
|
||
exe base=0x00e80000 (link-time image base 0x00400000, ASLR delta +11010048) pid=3808 shim=730c0000
|
||
addresses: Source: sots-re ghidra/addresses.json @ daea98f, generated 2026-09-07 by tools/gen_addresses.py
|
||
hook: Mars_Application_Initialize rva=0x004a0e50 -> va=01320e50
|
||
hook: MH_Initialize -> MH_OK / MH_CreateHook -> MH_OK (trampoline=00e50fe0) / MH_EnableHook -> MH_OK
|
||
Application::Initialize called (this=035c8128)
|
||
```
|
||
- `m0-main-menu.png` — main menu with the hook live (intro skipped with 3× Esc, ~35 s after launch).
|
||
- Export table: `build-shim/exports.txt`; `tools/build-shim.sh` printed
|
||
`exports: 66 names, identical to binkw32.dll`.
|
||
- `hooks=off` run (first banner in `m0.log`, pid 6396): forwarders alone play the Bink intro — proxy proven
|
||
separately from hooking.
|
||
- `tools/clean_room_check.sh` → OK.
|
||
|
||
## Gotchas
|
||
1. **Do not call the original from a C++ `thiscall` detour.** v1 did exactly that
|
||
(`void __thiscall Detour(void* self) { log; orig(self); }`) and the game died inside Initialize with
|
||
"Mars: Application error encountered" (`m0-crash-hookv1.{log,png}`). The prototype in
|
||
`sots_addresses.h` is `[unverified]`; if Initialize takes stack args, returns via EAX or relies on
|
||
EDX (fastcall-like), a calling wrapper corrupts the stack/registers on return. The shipped detour is
|
||
an asm stub: `pushfl/pushal`, call a C logger with ECX, `popal/popfl`, **`jmp` to the trampoline**.
|
||
It's correct for any convention but cannot log the return. Until a prototype is verified, use this
|
||
pattern for every trace hook; only replace-hooks (that never call the original) may be plain C++.
|
||
2. Hooking from `DllMain` worked (process is single-threaded at that point; MinHook only touches
|
||
kernel32). The fallback (install from the first forwarded Bink call) was not needed.
|
||
3. `--kill-at` / `--enable-stdcall-fixup` must NOT be passed to ld — they would strip the `@N`
|
||
suffixes the exe imports by name. With a plain `.def` MinGW exports the names verbatim.
|
||
4. `objdump -p` lists `+base[` rows in both the address table and the name table; compare the
|
||
`[Ordinal/Name Pointer] Table` section only (forwarder rows print `Forwarder RVA -- target`).
|
||
5. rsync `--exclude 'build-*'` also matches `build-shim.sh`; use `/build-*/`.
|
||
6. The CT is unprivileged: files written on the host must be `--chown=100000:100000` or the CT can't
|
||
write its build dir. `Z:` mappings are per logon on the VM; SSH sessions start without it.
|
||
7. `shim.log` is held open by the running game; move/rotate it only after `Stop-Process`.
|