Files
Aidem-Media-DLL-Analysis/README.md
Patryk Gensch 27399a52b1 Method dispatch axis: map id -> body via Runner::run switch
Recovers how a script method id maps to its implementation, the foundation for
body-level normalisation. Each CMC_*_Runner::run is a switch(id) (vtable slot 17);
every case is the method body — inline (MSVC6) or a tail-call to a separate
show()/load() (MSVC8). The extractor parses the jump table at the disassembly
level (Ghidra's decompiler jump-table recovery silently dropped the big runners),
fingerprints each case by its ordered CALL anchors (Class::method / vtbl+0xNN),
and expands thin wrappers one level so MSVC8 lines up with MSVC6.

Validated on the golden pair: Animo SHOW..RESUME (id 1-4) yield identical leaves
(getAnimo + vtbl+0xa0/0xa4/0x4c/0x50) across both compilers. Coverage 30/32
runners; Piklib 475 / BlooMoo 619 dispatch rows.

- extract_engine_surface.py: extract_method_dispatch (schema_version -> 4)
- snapshots regenerated with the method_dispatch axis
- ams: Snapshot.method_dispatch; diff axis keyed (owner,id) on [impl,calls] with
  method-name join; render METHOD BODIES section; cli --only dispatch; owner filter
- UI: "Ciała metod" diff axis + browse tab
- tests: body-change unit + cross-compiler vtbl assertion -> 29/29

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 13:15:58 +02:00

150 lines
7.7 KiB
Markdown

# aidem_media_playground
Narzędzie do **analizy różnicowej** silników gier Aidem Media (Piklib / BlooMoo).
Cel: katalog gier (z ISO/ZIP), wersje silnika z hashami, oraz ekstrakcja i porównywanie
"powierzchni silnika" — typów, metod, eventów i pól klas `CMC_*` — między wersjami.
## Status
Faza 1: **ekstraktor `snapshot.json` z Ghidry** (walidacja na golden pair PIKLIB8 ↔ bloomoo).
Infrastruktura (FastAPI + worker + DB + front) dochodzi dopiero, gdy format snapshotu
będzie sprawdzony na realnych binariach.
## Architektura (docelowa)
Modularny monolit + worker. Backend Python/FastAPI tylko zleca, wersjonuje i diffuje.
Cała ekstrakcja żyje w **workerze = Ghidra headless + ten skrypt**, bo wymaga dostępu
do call-grafu, referencji i vtable. Worker emituje `snapshot.json`, monolit go konsumuje.
```
Front (centrum dowodzenia) ─ FastAPI (katalog/hashe/diff) ─ PostgreSQL
│ kolejka
Worker: Ghidra headless + extract_engine_surface.py
```
## Zasada ekstrakcji
Ekstrakcja stoi na **kotwicach semantycznych** (cele wywołań, referowane literały
stringów, immediaty `PUSH`), a **nie** na tekście dekompilatu. Dzięki temu jeden skrypt
działa na MSVC6 (Piklib) i MSVC8 (BlooMoo) mimo różnego kodu wynikowego.
| Co | Kotwica w Ghidrze | Status |
|----|-------------------|--------|
| Typy | `CMC_ObjectsContainer::resolve`: `operator==("NAME")``operator_new(SIZE)``CMC_X::CMC_X` | ✅ (+ `dispatch_addr`, `via_module_iface`) |
| Metody | `CMC_*_Runner::prepareMthHashSet`: `CInteger(id)` + `CStringHashCode("NAME")` + `CHashtable::put` | ✅ (+ `method_inheritance`) |
| Eventy | `CMC_*::getBehavioursList`: lista literałów `CXString` | ✅ (lista per klasa, bez dziedziczenia) |
| Pola (skryptowe) | ctory `CMC_*`: literały czytane przez `CMElement::getProperty<T>Value` → nazwa + typ pola (FPS, PRELOAD, VISIBLE…) | ✅ (+ `field_inheritance`) |
| Layout C++ (bonus) | ctory `CMC_*`: store'y `this+offset` przez P-code (rozmyte, `is_vtable`) | ✅ pod `struct_layout` |
| Ciała metod | `CMC_*_Runner::run`: `switch(id)` (vtable slot 17) → per case kotwice CALL (`Klasa::metoda` / `vtbl+0xNN`), rozwinięcie wrapperów | ✅ pod `method_dispatch` (id→`impl_addr`+`calls`) |
## Uruchomienie ekstraktora
**W GUI Ghidry** (najszybciej do walidacji): skopiuj `ghidra_scripts/extract_engine_surface.py`
do swojego katalogu skryptów (Script Manager → *Manage Script Directories*), otwórz program,
uruchom skrypt. Wynik trafi do `<NazwaProgramu>.snapshot.json` w katalogu roboczym Ghidry.
**Headless** (tryb docelowy):
```bash
analyzeHeadless <projDir> <projName> -process PIKLIB8.dll \
-postScript extract_engine_surface.py "$(pwd)/snapshots/PIKLIB8.snapshot.json"
```
## Uruchomienie w kontenerach (docker compose)
Pełny stack — modularny monolit + **wydzielony worker Ghidry** — czterema usługami:
```
db (Postgres) ── api (FastAPI + UI) ──┐
├── redis (kolejka)
worker (Ghidra headless) ─────────────┘
```
```bash
docker compose up --build # api na http://localhost:8000
```
`api` i `worker` współdzielą wolumen `uploads`: API streamuje wgrane archiwum na dysk,
worker czyta je po ścieżce (przez Redisa leci tylko ścieżka, nie bajty). Obraz workera
**pobiera Ghidrę (~1 GB) przy pierwszym buildzie** — wersję nadpiszesz przez
`--build-arg GHIDRA_URL=…`. Postgres trzyma katalog trwale (wolumen `pgdata`).
Asynchroniczna akwizycja przez API (zlecenie → kolejka → worker → snapshot w bazie):
```bash
curl -F file=@game.iso -F game="Reksio i UFO" http://localhost:8000/jobs # → 202 {id, status:queued}
curl http://localhost:8000/jobs/1 # poll: queued→started→finished
```
Endpointy joba: `POST /jobs` (upload+enqueue), `GET /jobs`, `GET /jobs/{id}` (status, `snapshot_id`, `error`).
## Akwizycja — ISO/ZIP → katalog
Worker, który domyka łańcuch od *pliku gry* do wpisu w katalogu: rozpakowuje archiwum,
**sam znajduje DLL silnika** (po markerach w binarce — `CMC_ObjectsContainer` w RTTI —
więc działa nawet po zmianie nazwy pliku), liczy hashe (sha256 + md5 + opcjonalnie ssdeep),
odpala Ghidrę headless z ekstraktorem i ląduje snapshotem w bazie.
```bash
pip install -e ".[api,acquire]" # acquire = ppdeep (fuzzy hash, opcjonalny)
export GHIDRA_HEADLESS=/path/to/ghidra/support/analyzeHeadless # albo GHIDRA_HOME
python -m ams.acquire game.iso --game "Reksio i UFO" # ISO/ZIP/katalog/luźny DLL
python -m ams.acquire dump_dir --game "Reksio i UFO" --sink http --post http://127.0.0.1:8000
python -m ams.acquire PIKLIB8.dll --identify-only # tylko unpack+identify+hash, bez Ghidry
```
`--sink db` (domyślnie) importuje wprost do bazy, `--sink http` POST-uje na `/snapshots`,
`--sink none` zostawia sam snapshot. `--identify-only` to suchy bieg do walidacji bez Ghidry.
Rozpakowywanie stoi na `bsdtar` (libarchive — czyta i ISO9660, i ZIP); ZIP ma fallback na
czysty Python. Snapshot dostaje doklejony blok `binary.acquisition` (źródło, nazwa DLL) oraz
`binary.fuzzy/md5/size`.
## Diff engine (CLI)
```bash
python -m ams OLD.snapshot.json NEW.snapshot.json [--owner CMC_Animo] \
[--only types,methods,events,fields,layout,dispatch] [--json]
```
Porównuje dwa snapshoty po osiach (added/removed/changed) + wykrywa metody przeniesione
w hierarchii. Oś `dispatch` (ciała metod, klucz `owner`+`id`) diffuje fingerprint wywołań
każdej metody — wykrywa **zmiany ciała** między wersjami; jak `struct_layout`, najczystsza
między wersjami tego samego kompilatora (cross-compiler proste metody i tak się zgadzają,
np. Animo `SHOW``vtbl+0xa0` na MSVC6 i MSVC8).
## Backend (FastAPI + katalog)
Modularny monolit nad SQLAlchemy — domyślnie SQLite (zero setupu), gotowy pod Postgres
przez `DATABASE_URL`. Pełny snapshot trzymany jest w bazie verbatim; diff czyta go z powrotem
przez `ams.diff`.
```bash
pip install -e ".[api,dev]" # zależności
python -m ams.api.importer --game "Reksio i UFO" snapshots/PIKLIB8.dll.snapshot.json
uvicorn ams.api.app:create_app --factory --reload # serwer
```
Endpointy: `POST/GET /games`, `POST/GET /snapshots` (import deduplikowany po sha256),
`GET /diff?old=&new=[&owner=]`, `GET /snapshots/{id}/similar`, `POST/GET /jobs`, `GET /health`.
Testy: `pytest` (28, w tym integracyjne na golden pair).
### Podobne wersje
`GET /snapshots/{id}/similar[?min=N]` rankuje pozostałe wersje w katalogu po **overlapie
powierzchni** — Jaccard zbiorów tożsamości (te same klucze co diff) per oś, plus pula `overall`.
Miara jest *cross-compiler*: golden pair PIKLIB8 (MSVC6) ↔ bloomoodll (MSVC8) wychodzi **85%**
(types 95% / methods 87% / events 77% / fields 90%), tam gdzie fuzzy-hash binarki daje 0.
Fuzzy (ssdeep) leci jako sygnał poboczny „prawie ten sam plik", gdy snapshot ma `binary.fuzzy`.
## Front — Command Center
Po starcie serwera otwórz **http://127.0.0.1:8000/** (`/``/ui/`). Statyczny UI bez build-stepu
(czysty HTML/CSS/JS w `ams/api/static/`, serwowany przez FastAPI): lista gier/wersji, wybór dwóch
wersji (A/B), wizualny diff po 4 osiach z filtrem klasy i przeglądarka pojedynczej powierzchni.
Przycisk **+ wgraj** w panelu gier otwiera upload ISO/ZIP/DLL → `POST /jobs`; status zadania
(queued→started→finished/failed) jest odpytywany na żywo, a po zakończeniu lista wersji odświeża się sama.
W przeglądarce pojedynczej wersji widać panel **Podobne wersje** (pasek overlapu + `⇄ diff` ustawiający
A/B i odpalający porównanie).
## Format snapshotu
`schema_version`, `binary{name,sha256,engine,compiler,factory_addr}`, oraz listy
`types` / `methods` / `events` / `fields`. Diff = operacje na zbiorach dwóch snapshotów.