
Analyseur statique pour les instantanés AOT Flutter/Dart — récupère les noms de fonctions, les hiérarchies de classes, les graphes d'appel et les signaux comportementaux de libapp.so sans embarquer ni exécuter la machine virtuelle Dart. Prend en charge ARM64 et x86_64, Dart 2.10–3.12.
# AOTopsy
[](https://github.com/BroNils/aotopsy/actions/workflows/ci.yml)
[](https://github.com/BroNils/aotopsy/releases)
[](LICENSE)
Un analyseur de snapshots AOT Dart. Transforme `libapp.so` — le code Dart compilé à l'intérieur d'un APK Flutter release — en noms de fonctions, dispositions de classes, graphes d'appels, signaux comportementaux et pseudocode lisible. Pas de Dart VM, pas de compilation SDK, pas de repli à l'exécution.
> **Avis de fork :** AOTopsy est un fork de **unflutter**, créé à l'origine par **Anthony Zboralski**. Le dépôt original `zboralski/unflutter` n'est plus disponible (supprimé par l'auteur) ; une continuation communautaire existe à [`KristijanZic/unflutter`](https://github.com/KristijanZic/unflutter). Tout le crédit pour le parseur de snapshot original, le désérialiseur de clusters, le pipeline de désassemblage ARM64 et l'intégration Ghidra/IDA revient à l'auteur original. AOTopsy l'étend avec le support x86_64, un décompilateur natif, l'inférence de types à l'échelle du programme, la génération de scripts Frida et une documentation complète.
## Ce qu'il récupère
| Sortie | Ce que c'est |
|--------|-----------|
| Noms de fonctions | Le nom Dart original de chaque fonction compilée |
| Structures de classes | Noms de champs, décalages d'octets, chaînes d'héritage |
| Graphe d'appels | Arêtes d'appel directes (BL) et indirectes (BLR/dispatch) avec provenance |
| Références de chaînes | Quelles fonctions chargent quels littéraux de chaîne depuis le pool d'objets |
| Signaux comportementaux | Classification par mots-clés : crypto, réseau, jeux d'argent, SIM, localisation, WebView, blockchain |
| Pseudocode | Sortie décompilée indépendante de l'architecture à partir de code machine ARM64 ou x86_64 |
| Export de source Dart | Fichiers `.dart` modulaires de tout le projet reconstruits avec classes, champs et méthodes |
Supporte **ARM64** et **x86_64**. Couvre **Dart 2.10 à 3.13** (3.13.2 est la frontière stable actuelle) — chaque version modélisée s'analyse proprement sur les deux architectures, voir [COVERAGE.md](https://github.com/bronils/aotopsy/blob/main/COVERAGE.md) (93 builds d'échantillons, 0 échec).
## Précision et honnêteté
AOTopsy est mesuré par rapport à une vérité terrain, non affirmé. Deux propriétés sont imposées par
la suite de tests à chaque changement :
| Métrique | Valeur | Ce que cela signifie |
|--------|-------|---------------|
| **Concordance de récupération des noms** | **90,2 %** global (81,3 % pire bande, plancher de seuil ≥ 0,81) sur 44 builds de vérité terrain | Noms de fonctions récupérés comparés au `.symtab` ELF propre à chaque build, la vérité terrain externe — `TestSymtabDifferential`. Tableau de bord complet par build : [BENCHMARK.md](https://github.com/bronils/aotopsy/blob/main/BENCHMARK.md) (`make bench`). |
| **Validité syntaxique du décompilateur** | **100 %** Dart valide | Chaque fonction de pseudocode émise s'analyse comme du Dart — `TestDecompileQualityCorpus`. |
| **Taux de fabrication** | **0 %** | La règle §2 : ne jamais émettre un nom, type ou cible d'appel deviné comme un fait. Les inconnues sont rendues honnêtement (`indirectCall`, `<unknown>`, `dynamic`). |
Les jumeaux de vérité terrain sont de vrais builds de production que nous ne pouvons pas redistribuer, donc ces
seuils différentiels s'exécutent **localement** ; la CI publique valide le build, vet, `gofmt`,
`staticcheck`, les tests unitaires (mélangés) et un plancher de couverture sur la matrice
de plateformes. `samples/` n'est pas redistribuable et est ignoré par git, donc les tests dépendant des échantillons
sont ignorés là — mais uniquement lorsque le corpus est entièrement absent ; un corpus
présent mais auquel il manque un échantillon enregistré échoue, car ces situations signifient des choses opposées.
Comment chaque chiffre ici est mesuré (vérité terrain, définitions des métriques, reproduction) : [METHODOLOGY.md](https://github.com/bronils/aotopsy/blob/main/METHODOLOGY.md). Voir
[SECURITY.md](https://github.com/bronils/aotopsy/blob/main/SECURITY.md) pour la vérification des binaires de release et la portée honnête ci-dessous.
## Démarrage rapide
```bash
make build
./aotopsy libapp.so # full pipeline
./aotopsy doctor libapp.so # quick diagnostic
./aotopsy export-dart --lib libapp.so --out ./lib # reconstruct entire Dart source project
./aotopsy _debug decompile-native --lib libapp.so --find MyClass # find and decompile a function
```
Consultez [DEMO.md](https://github.com/bronils/aotopsy/blob/main/DEMO.md) pour une démonstration de cinq minutes sur un instantané réel, ou `WORKFLOW.md` pour la méthodologie étape par étape lorsque vous disposez d'un APK brut et ne savez pas par où commencer.
## Fonctionnement
AOTopsy traite l'instantané AOT Dart comme une grammaire binaire déterministe. Chaque octet a exactement une interprétation correcte étant donné les bonnes contraintes (structure ELF, magie de l'instantané, hachage de version, table CID, encodage de cluster). Le parseur applique des contraintes jusqu'à ce qu'une seule interprétation subsiste — aucune heuristique, aucune supposition.
Le pipeline s'exécute par étapes, chacune étant une fonction pure des octets vers des données structurées :
```mermaid
flowchart TD
A[libapp.so] --> B[ELF parse]
B --> C[snapshot region extraction]
C --> D[version detection]
D --> E[cluster alloc<br/>object census]
E --> F[cluster fill<br/>field values, names, strings]
F --> G[instructions table<br/>code ranges, stub boundaries]
G --> H{architecture?}
H -->|ARM64| I[ARM64 disassembly]
H -->|x86_64| J[x86_64 disassembly]
I --> K[CFG + call edges<br/>register provenance]
J --> K
K --> L[type inference<br/>BLR receiver type resolution]
L --> M[signal classification<br/>behavioral keyword matching]
M --> N[JSONL + HTML + DOT<br/>pseudocode output]
```
Deux backends indépendants partagent la même moitié avant (ELF jusqu'au remplissage des clusters), puis se séparent selon l'architecture : `internal/disasm` pour ARM64, `internal/disasm/x86.go` pour x86_64. Le décompilateur (`internal/decompiler`) gère les deux architectures via une IR unifiée.
```mermaid
flowchart LR
subgraph "Shared front half"
A[elfx] --> B[snapshot]
B --> C[cluster]
end
subgraph "ARM64 backend"
C --> D1[disasm ARM64]
D1 --> E1[callgraph]
E1 --> F1[signal]
end
subgraph "x86_64 backend"
C --> D2[disasm x86_64]
D2 --> E2[callgraph]
E2 --> F2[signal]
end
subgraph "Decompiler (both archs)"
C --> G[decompiler IR]
G --> H[pseudocode]
end
```
## Comparaison avec Blutter
```mermaid
flowchart LR
subgraph Blutter
direction TB
B1[libapp.so] --> B2[Compile matching<br/>Dart SDK]
B2 --> B3[Embed Dart VM]
B3 --> B4[Deserialize via<br/>VM internal APIs]
B4 --> B5[Perfect fidelity]
end
subgraph AOTopsy
direction TB
A1[libapp.so] --> A2[Parse binary format<br/>directly]
A2 --> A3[No VM, no SDK]
A3 --> A4[Version-specific<br/>format modeling]
A4 --> A5[Portability + speed]
end
```
[Blutter](https://github.com/worawit/blutter) intègre la VM Dart pour désérialiser le snapshot via ses propres chemins de code. Une fidélité parfaite, mais nécessite de compiler un SDK Dart correspondant pour chaque version cible — et il est limité à ARM64, sans prise en charge statique de x86_64. AOTopsy est le seul analyseur statique et indépendant de la version doté d'un décompilateur de pseudocode natif et d'une précision de référence publiée.
AOTopsy analyse directement le format binaire. Pas de VM, pas de SDK. Le compromis : chaque changement de format entre les versions de Dart doit être modélisé explicitement. Il n'y a pas de runtime pour le gérer automatiquement.
## Commandes
### Pipeline complet
```bash
aotopsy libapp.so # disasm + call edges + signal + metadata (ARM64: + Ghidra/IDA)
aotopsy signal libapp.so # same, skip metadata
aotopsy libapp.so --graph # also build call graph DOT files
```
Options : `--out <dir>` (par défaut : `<basename>.aotopsy/`), `--quiet`, `--strict`, `--max-steps <n>`, `--k <n>` (profondeur du contexte de signal, par défaut 2), `--decompile`.
`--decompile` écrit du pseudocode Dart par fonction dans `<out>/dart/`. Il est désactivé par
défaut car il triple environ le répertoire de sortie ; chaque exécution sans cette option
le signale, afin que la fonctionnalité soit découvrable plutôt que simplement présente.
### Diagnostic
```bash
aotopsy doctor libapp.so # Dart version, pointer size, support status, build features
aotopsy find-libapp --apk app.apk # locate libapp.so inside an APK
```
### Exportation de la source Dart de l'ensemble du projet
Reconstruit toutes les classes, champs, méthodes, getters, setters et constructeurs dans des fichiers `.dart` modulaires mappés par les URI de bibliothèque d'origine :
```bash
aotopsy export-dart --lib libapp.so --out ./reconstructed_lib/ # full project export
aotopsy export-dart --lib libapp.so --out ./lib/ --app-only # filter out core/flutter framework
aotopsy export-dart --lib libapp.so --out ./lib/ --filter Auth # targeted export by name
```
### Décompilateur natif de haut niveau (ARM64 + x86_64)
Produit du code Dart propre et idiomatique directement à partir des instructions machine binaires :
- **Flux de contrôle structurel** : itérateurs `for-in`, `while`, `for`, `try-catch-finally` avec délimitation exacte des PC.
- **Linéarisation async/await** : Déroule les machines à états `_SuspendState` en `await future` et `await for` linéaires.
- **Inlining des lambdas** : Synthétise les callbacks fléchés `(item) => process(item)` directement aux sites d'appel.
- **Treillis de types** : Propage les types Dart concrets (`String`, `int`, `UserModel`) à travers les valeurs SSA sans exécuter de VM active.
- **Idiomes Dart** : Null-aware (`?.`, `??`, `??=`), cascade (`..`), littéraux Set/List/Map, interpolation de chaînes (`"${a}${b}"`).
```bash
aotopsy _debug decompile-native --lib libapp.so --find MyClass # locate by name
aotopsy _debug decompile-native --lib libapp.so --func 0x1a92728 # one function at a VA
aotopsy _debug decompile-native --lib libapp.so --from-main --out out/ # reachability from app entry
aotopsy _debug decompile-native --lib libapp.so --all --filter MyClass # bulk, filtered
```
Avertissement : `--all` sans un petit `--max` peut nécessiter ~64 Go de RAM sur une application réelle. Préférez `--find`/`--func`/`--from-main`.
### Ghidra / IDA (ARM64 uniquement)
```bash
aotopsy ghidra libapp.so # headless Ghidra with metadata injection
aotopsy ida libapp.so # headless IDA via idalib
```
Les deux rejettent l'entrée x86_64. Utilisez `decompile-native` pour le pseudocode x86_64.
### Génération de scripts Frida
```bash
aotopsy _debug decompile-native --lib libapp.so --func 0x1a92728 --gen-frida --gen-frida-out hooks.js
frida -U -f com.example.app -l hooks.js --no-pause
```
Voir `FRIDA.md` pour le guide complet.
### Références croisées et traçage
```bash
aotopsy _debug strings --lib libapp.so --find "X-Signature" --xref # which function loads this string?
aotopsy _debug ffi-trace --lib libapp.so --filter MyClass # dart:ffi call sites
aotopsy _debug dispatch-table --lib libapp.so --filter MyClass # dispatch table entries
aotopsy _debug fingerprint --lib libapp.so # build-id and version markers
aotopsy _debug funcdiff --old old.so --new new.so # function set diff
aotopsy _debug symbolmap --stripped lib.so --unstripped debug.so # resolve stripped targets
```
### Outils de corpus
```bash
aotopsy inventory --dir samples/ # catalog APKs
aotopsy parity --samples samples/ --out out/ # cross-version parse report
aotopsy _debug thr-audit -lib libapp.so -out thr.jsonl # THR access scan
```
## Artefacts de sortie
| Fichier | Contenu |
|------|----------|
| `functions.jsonl` | Nom, adresse, taille, propriétaire, nombre de paramètres par fonction |
| `call_edges.jsonl` | Arêtes BL/BLR avec cibles résolues et provenance |
| `classes.jsonl` | Noms de champs, offsets, tailles d'instance par classe |
| `string_refs.jsonl` | Références de chaînes issues des chargements du pool d'objets |
| `dispatch_table.jsonl`| Types de récepteurs de tables de dispatch inférés et mappage des cibles |
| `signal.html` | Rapport de signaux comportementaux avec graphe de contexte |
| `dart_meta.json` | Métadonnées du snapshot, indicateur de pointeurs compressés, disposition THR |
| `flutter_meta.json` | Métadonnées unifiées pour Ghidra/IDA (ARM64 uniquement) |
| `aotopsy.sarif` | Rapport de résultats de sécurité SARIF 2.1.0 (pour GitHub Code Scanning) |
| `evidence.jsonl` | Modèle de preuves unifié avec niveau de confiance et provenance par site d'appel |
| `asm/*.txt` | Désassemblage annoté par fonction |
| `asm/*.bin` | Octets d'instructions bruts par fonction (les deux architectures) |
| `dart/*.dart` | Pseudocode décompilé par fonction (avec `--decompile`) |
| `cfg/*.dot` | CFG par fonction (avec `--graph`) |
## Organisation du paquet
```
cmd/aotopsy/ CLI entry point and command handlers
internal/
analysis/ Pipeline orchestration, snapshot loader, and analysis engines
sdk/ Dart VM ground-truth facts, register mappings, and predicates
vmtables/ Versioned THR field offsets, stub names, and stub orders
thraudit/ Thread-relative memory access audit and classification
arch/arm64/ Centralized ARM64 bitmask instruction decoders
arch/x86/ Centralized x86_64 decode primitives and register helpers
naming/ Central pool lookups, name resolution, and stub builders
elfx/ ELF validation and symbol extraction
snapshot/ Snapshot region extraction, version profiles
dartfmt/ Dart VM variable-length integer encoding
cluster/ Two-phase snapshot deserialization (alloc + fill)
disasm/ ARM64 + x86_64 decode, CFG, call-edge provenance
callgraph/ Call graph construction and DOT rendering
signal/ Behavioral string and malware signal classification
render/ HTML/DOT/SVG visualization
output/ JSONL and SARIF 2.1.0 serialization
decompiler/ Dart-AOT pseudocode decompiler (both architectures)
typetrack/ Whole-program type inference and receiver recovery
fingerprint/ Build-id and version marker identification
funcdiff/ Function-set diffing between builds
symbolmap/ Stripped-vs-unstripped symbol resolution
ffitrace/ Static dart:ffi call-site tracing
strxref/ String-to-function cross-referencing
strutil/ Dart syntax sanitization and metadata serialization
jsonutil/ Generic JSONL stream readers and writers
evidence/ Unified evidence model with confidence and provenance
frida/ Frida runtime hook and probe generation
cli/ ANSI color helpers for CLI output
tools/ Standalone utilities (THR table extractor and validator)
ghidra_scripts/ Ghidra integration (Python)
ida_scripts/ IDA integration (Python)
```
Voir `ARCHITECTURE.md` pour une analyse approfondie de chaque package.
## Build
Nécessite Go 1.25+.
```bash
make build # build ./aotopsy
make install # install to ~/.aotopsy/bin, plus the Ghidra/IDA integration
# assets into ~/.aotopsy/ghidra_scripts and ~/.aotopsy/ida_scripts
make test # run tests
make bench # regenerate BENCHMARK.md (needs local ground-truth twins)
make coverage # regenerate COVERAGE.md (needs local corpus samples)
make analyze # cross-check export-dart output against `dart analyze`
```
Les tests d'intégration résolvent leur entrée depuis `internal/samplecorpus`, qui recherche un répertoire `samples/` ; il n'y a rien à configurer. `samples/` est ignoré par git, donc une copie de travail qui n'en possède pas ignore ces tests — mais une copie de travail qui *possède* un corpus et à laquelle il manque un échantillon enregistré échoue, car un corpus dérivé et un corpus absent signifient des choses différentes.
La CI publique exécute `gofmt`, `staticcheck`, le build, vet et des tests unitaires mélangés sur linux/amd64, darwin/arm64 et windows/amd64, ainsi qu'une tâche race+couverture avec un seuil de couverture minimal.
## Versions & Branches
AOTopsy utilise un modèle à deux branches :
| Branche | Rôle |
|--------|------|
| `main` | **Stable.** Chaque commit est une release candidate fusionnée en squash et vérifiée par les gates. Les versions étiquetées (avec binaires multiplateformes précompilés) sont découpées à partir d'ici. |
| `develop` | **Rolling / nightly.** Là où atterrissent d'abord les petits commits quotidiens, les fonctionnalités et la recherche. Peut être instable entre les fusions. Regroupée dans `main` via une PR de fusion en squash une fois les gates au vert. |
Contribuez sur `develop` ; ouvrez une PR vers `main` uniquement lorsqu'un lot de travail est vérifié par les gates.
Des binaires précompilés pour Linux, macOS et Windows (amd64/arm64) sont joints à chaque
[version GitHub](../../releases). AOTopsy est du pur Go, donc `make build` effectue une compilation croisée proprement pour n'importe quelle cible.
Chaque archive de version contient également `ghidra_scripts/` et `ida_scripts/` à côté du
binaire, ce qui correspond à la disposition que découvrent `aotopsy ghidra` et `aotopsy ida` — ainsi ces
commandes fonctionnent directement depuis un téléchargement décompressé, sans copie de travail nécessaire.
## Limitations & Périmètre
AOTopsy énonce clairement ce qu'il récupère et ne récupère pas. Certaines limites relèvent du périmètre
d'ingénierie ; d'autres sont des **planchers durs du format AOT** — des informations que le compilateur Dart supprime dans
les builds release (PRODUCT), vérifiées contre le code source du SDK. Nous documentons les planchers plutôt que de fabriquer par-dessus.
**Planchers AOT durs (vérifiés — ne vous attendez pas à ce qu'ils s'améliorent) :**
- **Les noms de champs d'instance sont absents à ~97–99 %.** `Precompiler::DropFields` ne conserve les noms de champs
que sous `#if !defined(PRODUCT)` ; une vraie application possède ~233 objets `Field` nommés contre
~16k synthétiques. La récupération basée sur les accesseurs (`get:`/`set:` portent encore le nom) perce
partiellement ce voile (−11–22 %, déterministe, jamais deviné) ; le reste est véritablement perdu.
- **Les noms de variables locales et capturées sont perdus.** Rendu sous la forme `local_*` / `tN`.
- **Les cibles de dispatch véritablement polymorphes ne sont pas résolvables statiquement.** Un BLR à travers une
table de dispatch / un objet `Closure` dynamique est une limite de l'AOT, pas une lacune de l'analyse ; ceux-ci
sont rendus honnêtement sous la forme `indirectCall` / `dynamicCall`. Voir [METHODOLOGY.md](https://github.com/bronils/aotopsy/blob/main/METHODOLOGY.md#limits-of-this-method-honestly).
**Périmètre d'ingénierie :**
- **AOT uniquement.** Pas de support JIT.
- **La décompilation Ghidra/IDA est réservée à ARM64.** x86_64 est rejeté avec une erreur claire — utilisez plutôt `decompile-native` (le seul décompilateur Flutter statique pour x86_64).
- **La décompilation `--all` peut faire planter l'hôte.** Une vraie application de taille complète nécessite ~64 Go de RAM pour un `--all` non borné. Utilisez des modes ciblés (`--find`, `--func`, `--from-main`) ou limitez avec `--max`.
- **Le dispatch virtuel est invisible pour l'accessibilité de `--from-main`.** Les callbacks du cycle de vie des widgets passent par le dispatch du framework Flutter, et non par des instructions d'appel directes. Une heuristique de touche de classe en récupère une partie, mais c'est une sur-approximation. Utilisez Frida pour le reste.
- **Chaque changement de version de Dart doit être modélisé.** Il n'y a pas de VM pour gérer automatiquement les changements de format. Le snapshot porte un hash dérivé de git, pas un numéro de version, donc le support est basé sur la structure. Actuellement modélisé : Dart 2.10 → 3.13.
- **Les jumeaux de référence ne sont pas redistribuables** (vrais builds de production), donc le
différentiel de récupération de noms s'exécute localement, pas dans la CI publique.
## Licence
BSD-3-Clause. Les tables CID, les décalages de champs THR et les noms de stubs sont dérivés du [Dart SDK](https://github.com/dart-lang/sdk) (également BSD-3-Clause). Voir `LICENSE` et `NOTICE`.