sots-engine/docs/M0.md

5.1 KiB
Raw Permalink Blame History

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.