
aotopsy v1.1.0
Analizzatore statico per snapshot AOT Flutter/Dart — recupera i nomi delle funzioni, le gerarchie delle classi, i grafi di chiamata e i segnali comportamentali da libapp.so senza incorporare o eseguire la Dart VM. Supporta ARM64 e x86_64, Dart 2.10–3.12.
AOTopsy
Un analizzatore di snapshot AOT per Dart. Trasforma libapp.so — il codice Dart compilato all'interno di un APK Flutter in versione release — in nomi di funzioni, layout di classi, grafi di chiamata, segnali comportamentali e pseudocodice leggibile. Nessuna Dart VM, nessuna compilazione SDK, nessun fallback a runtime.
Nota sul fork: AOTopsy è un fork di unflutter, originariamente creato da Anthony Zboralski. Il repository originale
zboralski/unflutternon è più disponibile (rimosso dall'autore); esiste una continuazione della community suKristijanZic/unflutter. Tutto il merito per il parser di snapshot originale, il deserializzatore di cluster, la pipeline di disassemblaggio ARM64 e l'integrazione con Ghidra/IDA appartiene all'autore originale. AOTopsy lo estende con supporto x86_64, un decompilatore nativo, inferenza di tipi a livello di intero programma, generazione di script Frida e documentazione completa.
Cosa Recupera
| Output | Cosa è |
|---|---|
| Nomi di funzioni | Il nome Dart originale per ogni funzione compilata |
| Strutture di classi | Nomi dei campi, offset in byte, catene di ereditarietà |
| Grafo di chiamata | Archi di chiamata diretti (BL) e indiretti (BLR/dispatch) con provenienza |
| Riferimenti a stringhe | Quali funzioni caricano quali letterali stringa dal pool di oggetti |
| Segnali comportamentali | Classificazione per parole chiave: crittografia, rete, gioco d'azzardo, SIM, posizione, WebView, blockchain |
| Pseudocodice | Output decompilato neutro rispetto all'architettura da codice macchina ARM64 o x86_64 |
| Esportazione sorgente Dart | File .dart modulari a livello di intero progetto ricostruiti con classi, campi e metodi |
Supporta ARM64 e x86_64. Copre Dart 2.10 fino a 3.13 (3.13.2 è l'attuale frontiera stabile).
Accuratezza e Onestà
AOTopsy è misurato rispetto alla verità di base, non semplicemente dichiarato. Due proprietà sono imposte dalla suite di test a ogni modifica:
| Metrica | Valore | Cosa significa |
|---|---|---|
| Accordo sul recupero dei nomi | 89,8% complessivo (81,3% nella banda peggiore, soglia minima ≥ 0,81) su 44 build di verità di base | Nomi di funzioni recuperati confrontati con il .symtab ELF di ogni build, la verità di base esterna — TestSymtabDifferential. Classifica completa per build: BENCHMARK.md (make bench). |
| Validità sintattica del decompilatore | 100% Dart valido | Ogni funzione in pseudocodice emessa viene parsata come Dart — TestDecompileQualityCorpus. |
| Tasso di fabbricazione | 0% | La regola §2: non emettere mai un nome, tipo o target di chiamata indovinato come fatto. Gli sconosciuti vengono resi onestamente (indirectCall, <unknown>, dynamic). |
I gemelli di verità di base sono build di produzione reali che non possiamo ridistribuire, quindi quelle porte differenziali vengono eseguite localmente; la CI pubblica valida build + test unitari sulla matrice di piattaforme (i test dipendenti da campioni vengono saltati correttamente quando il binario è assente). Vedi SECURITY.md per la verifica dei binari di release e l'ambito onesto di seguito.
Avvio Rapido
make build
./aotopsy libapp.so # pipeline completa
./aotopsy doctor libapp.so # diagnostica rapida
./aotopsy export-dart --lib libapp.so --out ./lib # ricostruisce l'intero progetto sorgente Dart
./aotopsy _debug decompile-native --lib libapp.so --find MyClass # trova e decompila una funzione
Vedi WORKFLOW.md per la metodologia passo-passo quando hai un APK grezzo e non sai da dove iniziare.
Come Funziona
AOTopsy tratta lo snapshot AOT di Dart come una grammatica binaria deterministica. Ogni byte ha esattamente un'interpretazione corretta dati i vincoli giusti (struttura ELF, magic dello snapshot, hash di versione, tabella CID, codifica dei cluster). Il parser applica i vincoli finché sopravvive una sola interpretazione — nessuna euristica, nessuna supposizione.
La pipeline procede per fasi, ciascuna una funzione pura da byte a dati strutturati:
flowchart TD
A[libapp.so] --> B[Parsing ELF]
B --> C[Estrazione regione snapshot]
C --> D[Rilevamento versione]
D --> E[Allocazione cluster<br/>censimento oggetti]
E --> F[Riempimento cluster<br/>valori campi, nomi, stringhe]
F --> G[Tabella istruzioni<br/>intervalli codice, confini stub]
G --> H{Architettura?}
H -->|ARM64| I[Disassemblaggio ARM64]
H -->|x86_64| J[Disassemblaggio x86_64]
I --> K[CFG + archi di chiamata<br/>provenienza registri]
J --> K
K --> L[Inferenza tipi<br/>risoluzione tipo ricevente BLR]
L --> M[Classificazione segnali<br/>corrispondenza parole chiave comportamentali]
M --> N[Output JSONL + HTML + DOT<br/>pseudocodice]
Due backend indipendenti condividono la stessa metà anteriore (ELF fino al riempimento dei cluster), poi si dividono per architettura: internal/disasm per ARM64, internal/disasm/x86.go per x86_64. Il decompilatore (internal/decompiler) gestisce entrambe le architetture tramite un IR unificato.
flowchart LR
subgraph "Metà anteriore condivisa"
A[elfx] --> B[snapshot]
B --> C[cluster]
end
subgraph "Backend ARM64"
C --> D1[disasm ARM64]
D1 --> E1[callgraph]
E1 --> F1[signal]
end
subgraph "Backend x86_64"
C --> D2[disasm x86_64]
D2 --> E2[callgraph]
E2 --> F2[signal]
end
subgraph "Decompilatore (entrambe le architetture)"
C --> G[IR decompilatore]
G --> H[pseudocodice]
end
Confronto con Blutter
flowchart LR
subgraph Blutter
direction TB
B1[libapp.so] --> B2[Compila SDK Dart<br/>corrispondente]
B2 --> B3[Integra Dart VM]
B3 --> B4[Deserializza tramite<br/>API interne VM]
B4 --> B5[Fedeltà perfetta]
end
subgraph AOTopsy
direction TB
A1[libapp.so] --> A2[Parsing formato binario<br/>direttamente]
A2 --> A3[Nessuna VM, nessun SDK]
A3 --> A4[Modellazione formato<br/>specifico per versione]
A4 --> A5[Portabilità + velocità]
end
Blutter integra la Dart VM per deserializzare lo snapshot attraverso i propri percorsi di codice. Fedeltà perfetta, ma richiede la compilazione di un SDK Dart corrispondente per ogni versione target — ed è solo ARM64, senza supporto statico x86_64. AOTopsy è l'unico analizzatore statico e indipendente dalla versione con un decompilatore nativo in pseudocodice e accuratezza pubblicata rispetto alla verità di base.
AOTopsy analizza direttamente il formato binario. Nessuna VM, nessun SDK. Il compromesso: ogni modifica al formato tra le versioni di Dart deve essere modellata esplicitamente. Non c'è un runtime che la gestisca automaticamente.
Comandi
Pipeline completa
aotopsy libapp.so # disasm + archi di chiamata + segnali + metadati (ARM64: + Ghidra/IDA)
aotopsy signal libapp.so # come sopra, salta i metadati
aotopsy libapp.so --graph # crea anche file DOT del grafo di chiamata
Flag: --out <dir> (default: <basename>.aotopsy/), --quiet, --strict, --max-steps <n>, --k <n> (profondità contesto segnali, default 2).
Diagnostica
aotopsy doctor libapp.so # versione Dart, dimensione puntatore, stato supporto, funzionalità build
aotopsy find-libapp --apk app.apk # individua libapp.so all'interno di un APK
Esportazione Sorgente Dart a Livello di Intero Progetto
Ricostruisce tutte le classi, i campi, i metodi, i getter, i setter e i costruttori in file .dart modulari mappati dagli URI delle librerie originali:
aotopsy export-dart --lib libapp.so --out ./reconstructed_lib/ # esportazione progetto completo
aotopsy export-dart --lib libapp.so --out ./lib/ --app-only # filtra il framework core/flutter
aotopsy export-dart --lib libapp.so --out ./lib/ --filter Auth # esportazione mirata per nome
Decompilatore Nativo ad Alto Livello (ARM64 + x86_64)
Produce codice Dart pulito e idiomatico direttamente dalle istruzioni macchina binarie:
- Flusso di controllo strutturale: iteratori
for-in,while,for,try-catch-finallycon delimitazione PC esatta. - Linearizzazione Async/Await: Disfa le macchine a stati
_SuspendStateinawait futureeawait forlineari. - Inlining di Lambda: Sintetizza callback a freccia
(item) => process(item)direttamente nei siti di chiamata. - Reticolo di tipi: Propaga tipi Dart concreti (
String,int,UserModel) attraverso i valori SSA senza eseguire una VM live. - Idiomi Dart: Null-aware (
?.,??,??=), cascade (..), letterali Set/List/Map, interpolazione di stringhe ("${a}${b}").
aotopsy _debug decompile-native --lib libapp.so --find MyClass # individua per nome
aotopsy _debug decompile-native --lib libapp.so --func 0x1a92728 # una funzione a un VA
aotopsy _debug decompile-native --lib libapp.so --from-main --out out/ # raggiungibilità dall'entry dell'app
aotopsy _debug decompile-native --lib libapp.so --all --filter MyClass # bulk, filtrato
Attenzione: --all senza un --max piccolo può richiedere ~64GB di RAM su un'app reale. Preferisci --find/--func/--from-main.
Ghidra / IDA (solo ARM64)
aotopsy ghidra libapp.so # Ghidra headless con iniezione di metadati
aotopsy ida libapp.so # IDA headless tramite idalib
Entrambi rifiutano input x86_64. Usa decompile-native per lo pseudocodice x86_64.
Generazione script Frida
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
Vedi FRIDA.md per la guida completa.
Riferimenti incrociati e tracciamento
aotopsy _debug strings --lib libapp.so --find "X-Signature" --xref # quale funzione carica questa stringa?
aotopsy _debug ffi-trace --lib libapp.so --filter MyClass # siti di chiamata dart:ffi
aotopsy _debug dispatch-table --lib libapp.so --filter MyClass # voci della tabella di dispatch
aotopsy _debug fingerprint --lib libapp.so # build-id e marcatori di versione
aotopsy _debug funcdiff --old old.so --new new.so # diff del set di funzioni
aotopsy _debug symbolmap --stripped lib.so --unstripped debug.so # risolve i target stripped
Strumenti per corpus
aotopsy inventory --dir samples/ # catalogo APK
aotopsy parity --samples samples/ --out out/ # report di parsing cross-versione
aotopsy _debug thr-audit -lib libapp.so -out thr.jsonl # scansione accessi THR
Artefatti di Output
| File | Contenuto |
|---|---|
functions.jsonl | Nome, indirizzo, dimensione, proprietario, numero di parametri per funzione |
call_edges.jsonl | Archi BL/BLR con target risolti e provenienza |
classes.jsonl | Nomi dei campi, offset, dimensioni delle istanze per classe |
string_refs.jsonl | Riferimenti a stringhe dai caricamenti del pool di oggetti |
signal.html | Report dei segnali comportamentali con grafo di contesto |
flutter_meta.json | Metadati unificati per Ghidra/IDA (solo ARM64) |
asm/*.txt | Disassemblaggio annotato per funzione |
cfg/*.dot | CFG per funzione (con --graph) |
Struttura dei Pacchetti
cmd/aotopsy/ Punto di ingresso CLI e gestori dei comandi
internal/
elfx/ Validazione ELF ed estrazione simboli
snapshot/ Estrazione regione snapshot, profili di versione
dartfmt/ Codifica interi a lunghezza variabile della Dart VM
cluster/ Deserializzazione snapshot in due fasi (alloc + fill)
disasm/ Decodifica ARM64 + x86_64, CFG, provenienza archi di chiamata
callgraph/ Costruttori di grafi a reticolo per rendering DOT
signal/ Classificazione comportamentale delle stringhe
render/ Visualizzazione HTML/DOT/SVG
output/ Serializzazione JSONL
decompiler/ Decompilatore pseudocodice Dart-AOT (entrambe le architetture)
typetrack/ Inferenza di tipi a livello di intero programma per risoluzione BLR
fingerprint/ Identificazione build-id e marcatori di versione
funcdiff/ Diff del set di funzioni tra build
symbolmap/ Risoluzione simboli stripped-vs-unstripped
ffitrace/ Tracciamento statico dei siti di chiamata dart:ffi
strxref/ Riferimenti incrociati stringa-funzione
strutil/ Utility condivise per stringhe
pipeline/ Orchestrazione della pipeline e risoluzione dei nomi
tools/ Utility standalone (estrattore tabella THR)
ghidra_scripts/ Integrazione Ghidra (Python)
ida_scripts/ Integrazione IDA (Python)
Vedi ARCHITECTURE.md per l'analisi approfondita di ogni pacchetto.
Build
Richiede Go 1.25+.
make build # compila ./aotopsy
make install # installa in ~/.aotopsy/bin
make test # esegue i test
I test di integrazione usano variabili d'ambiente (AOTOPSY_TEST_SAMPLE_*) per individuare i binari di esempio — vengono saltati automaticamente se non impostate.
Release e Branch
AOTopsy usa un modello a due branch:
| Branch | Ruolo |
|---|---|
main | Stabile. Ogni commit è un candidato alla release verificato dai gate e unito con squash. Le release taggate (con binari precompilati cross-platform) vengono tagliate da qui. |
develop | Rolling / nightly. Dove arrivano prima i piccoli commit quotidiani, le funzionalità e la ricerca. Può essere instabile tra i merge. Viene raggruppato in main tramite una PR con squash-merge una volta che i gate sono verdi. |
Contribuisci su develop; apri una PR verso main solo quando un lotto di lavoro è verificato dai gate.
Binari precompilati per Linux, macOS e Windows (amd64/arm64) sono allegati a ogni
release GitHub. AOTopsy è Go puro, quindi make build compila in modo incrociato senza problemi per qualsiasi target.
Limitazioni e Ambito
AOTopsy dichiara chiaramente cosa recupera e cosa non recupera. Alcuni limiti sono questione di ambito ingegneristico; altri sono limiti strutturali del formato AOT — informazioni che il compilatore Dart rimuove nelle build di release (PRODUCT), verificate contro il sorgente SDK. Documentiamo i limiti strutturali piuttosto che fabbricare sopra di essi.
Limiti strutturali AOT (verificati — non aspettarti che migliorino):
- I nomi dei campi di istanza sono assenti per ~97–99%.
Precompiler::DropFieldsmantiene i nomi dei campi solo sotto#if !defined(PRODUCT); un'app reale ha ~233 oggettiFieldnominati contro ~16k sintetici. Il recupero basato sugli accessor (get:/set:mantengono ancora il nome) penetra parzialmente questo limite (−11–22%, deterministico, mai indovinato); il resto è genuinamente perso. - I nomi delle variabili locali e catturate sono persi. Resi come
local_*/tN. - I target di dispatch realmente polimorfici non sono risolvibili staticamente. BLR attraverso una
tabella di dispatch / oggetto
Closuredinamico è un limite AOT, non una lacuna di analisi; questi vengono resi onestamente comeindirectCall/dynamicCall. Vedidocs/roadmap/20-invariants-and-non-defects.md.
Ambito ingegneristico:
- Solo AOT. Nessun supporto JIT.
- La decompilazione Ghidra/IDA è solo ARM64. x86_64 viene rifiutato con un errore chiaro — usa
decompile-nativeinvece (l'unico decompilatore statico x86_64 per Flutter). - La decompilazione
--allpuò mandare in crash l'host. Un'app reale a grandezza naturale richiede ~64GB di RAM per--allsenza limiti. Usa le modalità mirate (--find,--func,--from-main) o limita con--max. - Il dispatch virtuale è invisibile alla raggiungibilità di
--from-main. I callback del ciclo di vita dei widget passano attraverso il dispatch del framework Flutter, non istruzioni di chiamata dirette. Un'euristica di contatto con le classi ne recupera alcuni, ma è una sovra-approssimazione. Usa Frida per il resto. - Ogni modifica alla versione di Dart deve essere modellata. Non c'è una VM per gestire automaticamente i cambi di formato. Lo snapshot porta un hash derivato da git, non un numero di versione, quindi il supporto è basato sulla struttura. Attualmente modellato: Dart 2.10 → 3.13.
- I gemelli di verità di base non sono ridistribuibili (build di produzione reali), quindi il differenziale sul recupero dei nomi viene eseguito localmente, non nella CI pubblica.
Licenza
BSD-3-Clause. Le tabelle CID, gli offset dei campi THR e i nomi degli stub derivano dal Dart SDK (anch'esso BSD-3-Clause). Vedi LICENSE e NOTICE.