
aotopsy v1.1.0
Analizador estático para instantáneas AOT de Flutter/Dart — recupera nombres de funciones, jerarquías de clases, grafos de llamadas y señales de comportamiento de libapp.so sin incrustar ni ejecutar la VM de Dart. Compatible con ARM64 y x86_64, Dart 2.10–3.12.
AOTopsy
Un analizador de snapshots AOT de Dart. Convierte libapp.so — el código Dart compilado dentro de un APK de Flutter en versión release — en nombres de funciones, diseños de clases, grafos de llamadas, señales de comportamiento y pseudocódigo legible. Sin VM de Dart, sin compilación del SDK, sin respaldo en tiempo de ejecución.
Aviso de fork: AOTopsy es un fork de unflutter, originalmente creado por Anthony Zboralski. El repositorio original
zboralski/unflutterya no está disponible (eliminado por el autor); existe una continuación comunitaria enKristijanZic/unflutter. Todo el crédito por el parser de snapshots original, el deserializador de clústeres, el pipeline de desensamblado ARM64 y la integración con Ghidra/IDA pertenece al autor original. AOTopsy lo extiende con soporte para x86_64, un descompilador nativo, inferencia de tipos a nivel de programa completo, generación de scripts de Frida y documentación exhaustiva.
Qué Recupera
| Salida | Qué es |
|---|---|
| Nombres de funciones | El nombre Dart original de cada función compilada |
| Estructuras de clases | Nombres de campos, desplazamientos de bytes, cadenas de herencia |
| Grafo de llamadas | Aristas de llamadas directas (BL) e indirectas (BLR/dispatch) con procedencia |
| Referencias a cadenas | Qué funciones cargan qué literales de cadena desde el pool de objetos |
| Señales de comportamiento | Clasificación de palabras clave: criptografía, red, juegos de azar, SIM, ubicación, WebView, blockchain |
| Pseudocódigo | Salida descompilada neutral respecto a la arquitectura desde código máquina ARM64 o x86_64 |
| Exportación de código fuente Dart | Archivos .dart modulares de todo el proyecto reconstruidos con clases, campos y métodos |
Soporta ARM64 y x86_64. Cubre Dart 2.10 hasta 3.13 (3.13.2 es la frontera estable actual).
Precisión y Honestidad
AOTopsy se mide contra la verdad de referencia, no se asume. Dos propiedades se verifican mediante el conjunto de pruebas en cada cambio:
| Métrica | Valor | Qué significa |
|---|---|---|
| Concordancia en la recuperación de nombres | 89.8% en general (81.3% en la peor banda, umbral mínimo de 0.81) en 44 compilaciones de verdad de referencia | Nombres de funciones recuperados comparados con el .symtab ELF de cada compilación, la verdad de referencia externa — TestSymtabDifferential. Marcador completo por compilación: BENCHMARK.md (make bench). |
| Validez sintáctica del descompilador | 100% Dart válido | Cada función de pseudocódigo emitida se analiza como Dart — TestDecompileQualityCorpus. |
| Tasa de fabricación | 0% | La regla §2: nunca emitir un nombre, tipo o destino de llamada adivinado como un hecho. Los desconocidos se muestran honestamente (indirectCall, <unknown>, dynamic). |
Las gemelas de verdad de referencia son compilaciones de producción reales que no podemos redistribuir, por lo que esas compuertas diferenciales se ejecutan localmente; el CI público valida la compilación y las pruebas unitarias en la matriz de plataformas (las pruebas dependientes de muestras se omiten limpiamente cuando el binario está ausente). Consulta SECURITY.md para la verificación de binarios de release y el alcance honesto a continuación.
Inicio Rápido
make build
./aotopsy libapp.so # pipeline completo
./aotopsy doctor libapp.so # diagnóstico rápido
./aotopsy export-dart --lib libapp.so --out ./lib # reconstruir todo el proyecto de código fuente Dart
./aotopsy _debug decompile-native --lib libapp.so --find MyClass # encontrar y descompilar una función
Consulta WORKFLOW.md para la metodología paso a paso cuando tienes un APK crudo y no sabes por dónde empezar.
Cómo Funciona
AOTopsy trata el snapshot AOT de Dart como una gramática binaria determinista. Cada byte tiene exactamente una interpretación correcta dadas las restricciones adecuadas (estructura ELF, magic del snapshot, hash de versión, tabla CID, codificación de clústeres). El parser aplica restricciones hasta que solo sobrevive una interpretación — sin heurísticas, sin adivinanzas.
El pipeline se ejecuta en etapas, cada una una función pura de bytes a datos estructurados:
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]
Dos backends independientes comparten la misma mitad frontal (ELF hasta el llenado de clústeres), y luego se dividen por arquitectura: internal/disasm para ARM64, internal/disasm/x86.go para x86_64. El descompilador (internal/decompiler) maneja ambas arquitecturas a través de un IR unificado.
flowchart LR
subgraph "Mitad frontal compartida"
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 "Descompilador (ambas arquitecturas)"
C --> G[decompiler IR]
G --> H[pseudocode]
end
Comparación con Blutter
flowchart LR
subgraph Blutter
direction TB
B1[libapp.so] --> B2[Compilar SDK de Dart<br/>coincidente]
B2 --> B3[Incrustar VM de Dart]
B3 --> B4[Deserializar mediante<br/>APIs internas de la VM]
B4 --> B5[Fidelidad perfecta]
end
subgraph AOTopsy
direction TB
A1[libapp.so] --> A2[Analizar formato binario<br/>directamente]
A2 --> A3[Sin VM, sin SDK]
A3 --> A4[Modelado de formato<br/>específico de versión]
A4 --> A5[Portabilidad + velocidad]
end
Blutter incrusta la VM de Dart para deserializar el snapshot a través de sus propias rutas de código. Fidelidad perfecta, pero requiere compilar un SDK de Dart coincidente para cada versión objetivo — y es solo ARM64, sin soporte estático para x86_64. AOTopsy es el único analizador estático e independiente de versión con un descompilador de pseudocódigo nativo y precisión de verdad de referencia publicada.
AOTopsy analiza el formato binario directamente. Sin VM, sin SDK. La compensación: cada cambio de formato entre versiones de Dart debe modelarse explícitamente. No hay tiempo de ejecución que lo maneje automáticamente.
Comandos
Pipeline completo
aotopsy libapp.so # desensamblado + aristas de llamadas + señales + metadatos (ARM64: + Ghidra/IDA)
aotopsy signal libapp.so # igual, omitir metadatos
aotopsy libapp.so --graph # también construir archivos DOT del grafo de llamadas
Banderas: --out <dir> (predeterminado: <basename>.aotopsy/), --quiet, --strict, --max-steps <n>, --k <n> (profundidad de contexto de señales, predeterminado 2).
Diagnóstico
aotopsy doctor libapp.so # versión de Dart, tamaño de puntero, estado de soporte, características de compilación
aotopsy find-libapp --apk app.apk # localizar libapp.so dentro de un APK
Exportación de código fuente Dart de todo el proyecto
Reconstruye todas las clases, campos, métodos, getters, setters y constructores en archivos .dart modulares mapeados por URIs de biblioteca originales:
aotopsy export-dart --lib libapp.so --out ./reconstructed_lib/ # exportación completa del proyecto
aotopsy export-dart --lib libapp.so --out ./lib/ --app-only # filtrar framework core/flutter
aotopsy export-dart --lib libapp.so --out ./lib/ --filter Auth # exportación dirigida por nombre
Descompilador nativo de alto nivel (ARM64 + x86_64)
Produce código Dart limpio e idiomático directamente desde instrucciones de máquina binarias:
- Flujo de control estructural: iteradores
for-in,while,for,try-catch-finallycon delimitación exacta de PC. - Linealización de Async/Await: Desenvuelve máquinas de estado
_SuspendStateenawait futureyawait forlineales. - Inserción de lambdas: Sintetiza callbacks de flecha
(item) => process(item)directamente en los sitios de llamada. - Retícula de tipos: Propaga tipos Dart concretos (
String,int,UserModel) a través de valores SSA sin ejecutar una VM en vivo. - Modismos de Dart: Consciente de nulos (
?.,??,??=), cascada (..), literales de Set/List/Map, interpolación de cadenas ("${a}${b}").
aotopsy _debug decompile-native --lib libapp.so --find MyClass # localizar por nombre
aotopsy _debug decompile-native --lib libapp.so --func 0x1a92728 # una función en una VA
aotopsy _debug decompile-native --lib libapp.so --from-main --out out/ # alcanzabilidad desde la entrada de la app
aotopsy _debug decompile-native --lib libapp.so --all --filter MyClass # masivo, filtrado
Advertencia: --all sin un --max pequeño puede requerir ~64GB de RAM en una app real. Prefiere --find/--func/--from-main.
Ghidra / IDA (solo ARM64)
aotopsy ghidra libapp.so # Ghidra sin interfaz gráfica con inyección de metadatos
aotopsy ida libapp.so # IDA sin interfaz gráfica mediante idalib
Ambos rechazan entrada x86_64. Usa decompile-native para pseudocódigo x86_64.
Generación de scripts de 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
Consulta FRIDA.md para la guía completa.
Referencias cruzadas y rastreo
aotopsy _debug strings --lib libapp.so --find "X-Signature" --xref # ¿qué función carga esta cadena?
aotopsy _debug ffi-trace --lib libapp.so --filter MyClass # sitios de llamada dart:ffi
aotopsy _debug dispatch-table --lib libapp.so --filter MyClass # entradas de tabla de dispatch
aotopsy _debug fingerprint --lib libapp.so # marcadores de build-id y versión
aotopsy _debug funcdiff --old old.so --new new.so # diferencia de conjuntos de funciones
aotopsy _debug symbolmap --stripped lib.so --unstripped debug.so # resolver objetivos sin símbolos
Herramientas de corpus
aotopsy inventory --dir samples/ # catalogar APKs
aotopsy parity --samples samples/ --out out/ # informe de análisis entre versiones
aotopsy _debug thr-audit -lib libapp.so -out thr.jsonl # escaneo de acceso THR
Artefactos de Salida
| Archivo | Contenido |
|---|---|
functions.jsonl | Nombre, dirección, tamaño, propietario, número de parámetros por función |
call_edges.jsonl | Aristas BL/BLR con destinos resueltos y procedencia |
classes.jsonl | Nombres de campos, desplazamientos, tamaños de instancia por clase |
string_refs.jsonl | Referencias a cadenas desde cargas del pool de objetos |
signal.html | Informe de señales de comportamiento con grafo de contexto |
flutter_meta.json | Metadatos unificados para Ghidra/IDA (solo ARM64) |
asm/*.txt | Desensamblado anotado por función |
cfg/*.dot | CFGs por función (con --graph) |
Estructura de Paquetes
cmd/aotopsy/ Punto de entrada CLI y manejadores de comandos
internal/
elfx/ Validación ELF y extracción de símbolos
snapshot/ Extracción de regiones de snapshot, perfiles de versión
dartfmt/ Codificación de enteros de longitud variable de la VM de Dart
cluster/ Deserialización de snapshot en dos fases (alloc + fill)
disasm/ Decodificación ARM64 + x86_64, CFG, procedencia de aristas de llamada
callgraph/ Constructores de grafos de retícula para renderizado DOT
signal/ Clasificación de cadenas de comportamiento
render/ Visualización HTML/DOT/SVG
output/ Serialización JSONL
decompiler/ Descompilador de pseudocódigo Dart-AOT (ambas arquitecturas)
typetrack/ Inferencia de tipos a nivel de programa completo para resolución BLR
fingerprint/ Identificación de build-id y marcadores de versión
funcdiff/ Diferencia de conjuntos de funciones entre compilaciones
symbolmap/ Resolución de símbolos con y sin símbolos
ffitrace/ Rastreo estático de sitios de llamada dart:ffi
strxref/ Referencias cruzadas de cadena a función
strutil/ Utilidades compartidas de cadenas
pipeline/ Orquestación del pipeline y resolución de nombres
tools/ Utilidades independientes (extractor de tabla THR)
ghidra_scripts/ Integración con Ghidra (Python)
ida_scripts/ Integración con IDA (Python)
Consulta ARCHITECTURE.md para el análisis profundo de cada paquete.
Compilación
Requiere Go 1.25+.
make build # compilar ./aotopsy
make install # instalar en ~/.aotopsy/bin
make test # ejecutar pruebas
Las pruebas de integración usan variables de entorno (AOTOPSY_TEST_SAMPLE_*) para localizar binarios de muestra — se omiten automáticamente si no están configuradas.
Releases y Ramas
AOTopsy usa un modelo de dos ramas:
| Rama | Rol |
|---|---|
main | Estable. Cada commit es un candidato a release verificado por compuertas y fusionado con squash. Los releases etiquetados (con binarios multiplataforma precompilados) se cortan desde aquí. |
develop | Continua / nocturna. Donde aterrizan primero los commits pequeños diarios, características e investigación. Puede ser inestable entre fusiones. Se agrupa en main mediante un PR de fusión con squash una vez que las compuertas están en verde. |
Contribuye contra develop; abre un PR hacia main solo cuando un lote de trabajo esté verificado por compuertas.
Los binarios precompilados para Linux, macOS y Windows (amd64/arm64) se adjuntan a cada release de GitHub. AOTopsy es Go puro, por lo que make build compila de forma cruzada limpiamente para cualquier objetivo.
Limitaciones y Alcance
AOTopsy declara claramente qué recupera y qué no. Algunos límites son de alcance de ingeniería; otros son pisos duros del formato AOT — información que el compilador de Dart elimina en compilaciones release (PRODUCT), verificada contra el código fuente del SDK. Documentamos los pisos en lugar de fabricar sobre ellos.
Pisos duros del AOT (verificados — no esperes que mejoren):
- Los nombres de campos de instancia están ausentes en ~97–99%.
Precompiler::DropFieldsconserva los nombres de campos solo bajo#if !defined(PRODUCT); una app real tiene ~233 objetosFieldnombrados frente a ~16k sintéticos. La recuperación basada en accesores (get:/set:aún llevan el nombre) atraviesa esto parcialmente (−11–22%, determinista, nunca adivinado); el resto está genuinamente perdido. - Los nombres de variables locales y capturadas han desaparecido. Se muestran como
local_*/tN. - Los destinos de dispatch verdaderamente polimórficos no son resolubles estáticamente. BLR a través de una tabla de dispatch / objeto
Closuredinámico es un límite del AOT, no una brecha de análisis; estos se muestran honestamente comoindirectCall/dynamicCall. Consultadocs/roadmap/20-invariants-and-non-defects.md.
Alcance de ingeniería:
- Solo AOT. Sin soporte JIT.
- La descompilación Ghidra/IDA es solo ARM64. x86_64 se rechaza con un error claro — usa
decompile-nativeen su lugar (el único descompilador estático de Flutter para x86_64). - La descompilación
--allpuede bloquear el host. Una app de tamaño completo real necesita ~64GB de RAM para--allsin límite. Usa modos dirigidos (--find,--func,--from-main) o limita con--max. - El dispatch virtual es invisible para la alcanzabilidad de
--from-main. Los callbacks del ciclo de vida de widgets pasan por el dispatch del framework de Flutter, no por instrucciones de llamada directas. Una heurística de toque de clase recupera algunos, pero es una sobre-aproximación. Usa Frida para el resto. - Cada cambio de versión de Dart debe modelarse. No hay VM para manejar los cambios de formato automáticamente. El snapshot lleva un hash derivado de git, no un número de versión, por lo que el soporte se basa en la estructura. Modelado actualmente: Dart 2.10 → 3.13.
- Las gemelas de verdad de referencia no son redistribuibles (compilaciones de producción reales), por lo que el diferencial de recuperación de nombres se ejecuta localmente, no en el CI público.
Licencia
BSD-3-Clause. Las tablas CID, los desplazamientos de campos THR y los nombres de stubs se derivan del Dart SDK (también BSD-3-Clause). Consulta LICENSE y NOTICE.