Files
Aidem-Media-DLL-Analysis/README.md
2026-05-31 17:00:14 +02:00

212 lines
11 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
Pełny łańcuch działa: **binarka → (Ghidra worker) → `snapshot.json` → katalog (DB) → diff → UI**.
Ekstraktor pokrywa 5 osi (typy / metody / eventy / pola skryptowe / ciała metod) + bonus layout C++,
zwalidowany na golden pair PIKLIB8 (MSVC6) ↔ bloomoo (MSVC8). Nad tym: FastAPI + katalog (SQLite/Postgres),
diff z normalizacją ciał i miarą „podobnych wersji", Command Center UI, pipeline akwizycji ISO/ZIP
i pełny stack `docker compose` z workerem Ghidry. Zobacz **Quickstart** niżej.
## 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
```
## Quickstart
### 1. Lokalnie, bez Dockera (działa od ręki — golden snapshoty są w repo)
```bash
python -m venv .venv && source .venv/bin/activate
pip install -e ".[api,dev]"
# zasiej katalog dwoma wersjami z golden pair
python -m ams.api.importer --game "Reksio i UFO" snapshots/PIKLIB8.dll.snapshot.json
python -m ams.api.importer --game "Reksio i Kapitan Nemo" snapshots/bloomoodll.dll.snapshot.json
uvicorn ams.api.app:create_app --factory # → http://127.0.0.1:8000/
```
Diff też z CLI: `python -m ams OLD.json NEW.json --owner CMC_Animo --only dispatch`.
### 2. Pełny stack w Dockerze (Ghidra w workerze) — upload ISO/ZIP → snapshot
```bash
docker compose up --build # db(Postgres) + redis + api + worker(Ghidra)
```
Pierwszy build workera **ściąga Ghidrę (~1 GB) + JDK 21** (wolny, ale cache'owany). Potem wgraj grę
przyciskiem **+ wgraj** w UI (http://localhost:8000) albo z CLI:
```bash
curl -F file=@"/sciezka/do/gra.iso" -F game="Reksio i Czarodzieje" http://localhost:8000/jobs
curl http://localhost:8000/jobs/1 # status: queued → started → finished
```
Worker rozpakowuje (bsdtar: ISO/ZIP), content-based znajduje DLL silnika, hashuje, odpala
Ghidra headless + `extract_engine_surface.py` → snapshot → import do Postgresa → UI.
### 3. Ghidra w obrazie — wersja / troubleshooting
Worker (`docker/worker.Dockerfile`, `eclipse-temurin:21-jdk`) pobiera Ghidrę i ustawia
`GHIDRA_HOME=/opt/ghidra`. Wersja jest przypięta w `ARG GHIDRA_URL`. Jeśli build padnie na pobieraniu,
nadpisz URL realnym wydaniem z [releases NSA](https://github.com/NationalSecurityAgency/ghidra/releases)
(nazwa pliku: `ghidra_<wer>_PUBLIC_<data>.zip`):
```bash
docker compose build worker \
--build-arg GHIDRA_URL=https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_11.3.2_build/ghidra_11.3.2_PUBLIC_20250415.zip
docker compose up
```
### 4. Ekstrakcja ręcznie w GUI Ghidry (alternatywa, bez Dockera)
*Script Manager → Manage Script Directories* → wskaż `ghidra_scripts/`, otwórz program (DLL),
uruchom `extract_engine_surface.py`. Snapshot ląduje w `snapshots/<nazwa>.snapshot.json`,
potem zaimportuj go przez `python -m ams.api.importer …`.
## 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.