Files
Aidem-Media-DLL-Analysis/README.md
Patryk Gensch b0d3d22445 Body normalisation: per-method similarity score + leaf delta
Turns the dispatch axis from a binary changed/unchanged into a "how much" measure
of code change — the original goal. ams.normalize compares two body fingerprints
(the ordered leaf-call anchors) with difflib after collapsing consecutive-duplicate
anchors (a load-twice codegen artefact), yielding a 0-100 similarity and the exact
leaves that appeared/vanished.

Every dispatch `changed` entry now carries body={similarity, added, removed}, and the
block carries a summary={shared, identical, changed, mean_similarity}.

Golden pair (cross-compiler): 470 shared bodies, 131 identical, mean 66% similar;
Animo SHOW/HIDE/PAUSE/RESUME come out 100% despite MSVC6 vs MSVC8, LOAD 50% with the
swapped leaves spelled out.

- normalize.py: canonical / body_similarity / body_delta
- diff: _dispatch_diff enriches changed with body + adds summary
- render: METHOD BODIES shows %, leaf delta, summary line
- UI: similarity % + leaf delta + axis summary
- tests: 5 new -> 34/34

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

157 lines
8.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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).
**Normalizacja ciał** (`ams.normalize`): każda zmiana w osi `dispatch` niesie wynik
`body = {similarity, added, removed}` — podobieństwo 0100% sekwencji liści (`difflib`,
po zwinięciu sąsiednich duplikatów = artefaktów codegenu) oraz *które* wywołania doszły/zniknęły.
Blok dostaje też `summary` (wspólne / identyczne / zmienione / średnie podobieństwo). Na golden
pair (cross-compiler): 470 wspólnych ciał, 131 identycznych, średnio 66% — a `SHOW/HIDE/PAUSE/
RESUME` Animo wychodzą 100% mimo MSVC6↔MSVC8. To jest miara „na ile się zmieniło" na poziomie kodu.
## 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.