
Flutter/Dart AOT 스냅샷용 정적 분석기 — Dart VM을 임베드하거나 실행하지 않고 libapp.so에서 함수 이름, 클래스 계층 구조, 호출 그래프, 동작 신호를 복구합니다. ARM64 및 x86_64, Dart 2.10–3.12를 지원합니다.
# AOTopsy
[](https://github.com/BroNils/aotopsy/actions/workflows/ci.yml)
[](https://github.com/BroNils/aotopsy/releases)
[](LICENSE)
Dart AOT 스냅샷 분석기입니다. Flutter 릴리스 APK 내부의 컴파일된 Dart 코드인 `libapp.so`를 함수 이름, 클래스 레이아웃, 호출 그래프, 행위 신호, 그리고 읽을 수 있는 의사코드로 변환합니다. Dart VM도, SDK 컴파일도, 런타임 폴백도 필요 없습니다.
> **포크 공지:** AOTopsy는 원래 **Anthony Zboralski**가 만든 **unflutter**의 포크입니다. 원본 `zboralski/unflutter` 저장소는 더 이상 사용할 수 없으며(작성자가 제거함), 커뮤니티 후속 저장소가 [`KristijanZic/unflutter`](https://github.com/KristijanZic/unflutter)에 존재합니다. 원본 스냅샷 파서, 클러스터 역직렬화기, ARM64 디스어셈블리 파이프라인, 그리고 Ghidra/IDA 통합에 대한 모든 공로는 원저자에게 있습니다. AOTopsy는 여기에 x86_64 지원, 네이티브 디컴파일러, 전체 프로그램 타입 추론, Frida 스크립트 생성, 그리고 포괄적인 문서를 추가로 확장했습니다.
## 복원하는 정보
| 출력 | 설명 |
|--------|-----------|
| 함수 이름 | 각 컴파일된 함수의 원래 Dart 이름 |
| 클래스 구조 | 필드 이름, 바이트 오프셋, 상속 체인 |
| 호출 그래프 | 출처 정보가 포함된 직접(BL) 및 간접(BLR/디스패치) 호출 엣지 |
| 문자열 참조 | 어떤 함수가 객체 풀에서 어떤 문자열 리터럴을 로드하는지 |
| 행위 신호 | 암호화, 네트워크, 도박, SIM, 위치, WebView, 블록체인 키워드 분류 |
| 의사코드 | ARM64 또는 x86_64 기계어로부터 생성된 아키텍처 중립적 디컴파일 출력 |
| Dart 소스 내보내기 | 클래스, 필드, 메서드가 재구성된 전체 프로젝트 모듈식 `.dart` 파일 |
**ARM64**와 **x86_64**를 지원합니다. **Dart 2.10부터 3.13까지**를 다룹니다(3.13.2가 현재 안정 프런티어) — 모델링된 모든 버전이 두 아키텍처 모두에서 깔끔하게 파싱됩니다. [COVERAGE.md](https://github.com/bronils/aotopsy/blob/main/COVERAGE.md) 참조(샘플 빌드 93개, 실패 0건).
## 정확성과 정직성
AOTopsy는 주장이 아니라 정답(ground truth)에 대해 측정됩니다. 두 가지 속성이
모든 변경 시 테스트 스위트에 의해 강제됩니다:
| 지표 | 값 | 의미 |
|--------|-------|---------------|
| **이름 복원 일치율** | 44개 정답 빌드 전반에서 전체 **90.2%** (최악 구간 81.3%, ≥ 0.81 게이트 하한) | 복원된 함수 이름을 각 빌드 자체의 ELF `.symtab`(외부 정답)과 비교 — `TestSymtabDifferential`. 빌드별 전체 점수표: [BENCHMARK.md](https://github.com/bronils/aotopsy/blob/main/BENCHMARK.md) (`make bench`). |
| **디컴파일러 구문 유효성** | **100%** 유효한 Dart | 출력된 모든 의사코드 함수가 Dart로 파싱됨 — `TestDecompileQualityCorpus`. |
| **날조율** | **0%** | §2 규칙: 추측한 이름, 타입, 호출 대상을 사실로 출력하지 않음. 미지의 항목은 정직하게 표시됨(`indirectCall`, `<unknown>`, `dynamic`). |
정답 트윈은 재배포할 수 없는 실제 프로덕션 빌드이므로, 해당
차등 게이트는 **로컬에서** 실행됩니다. 공개 CI는 빌드, vet, `gofmt`,
`staticcheck`, 단위 테스트(셔플됨), 그리고 플랫폼
매트릭스 전반의 커버리지 하한을 검증합니다. `samples/`는 재배포할 수 없으며 gitignore되어 있으므로, 샘플에 의존하는
테스트는 그곳에서 건너뜁니다 — 단, 코퍼스가 완전히 없을 때만 그렇습니다. 코퍼스가 존재하는데 등록된 샘플이 누락된 경우는 실패합니다. 이 둘은 정반대의
의미이기 때문입니다. 여기의 모든 수치가 어떻게 측정되는지(정답, 지표 정의, 재현): [METHODOLOGY.md](https://github.com/bronils/aotopsy/blob/main/METHODOLOGY.md). 릴리스 바이너리 검증과 아래의 정직한 범위는
[SECURITY.md](https://github.com/bronils/aotopsy/blob/main/SECURITY.md)를 참조하세요.
## 빠른 시작
```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
```
실제 스냅샷에서 5분 동안 진행되는 워크스루는 [DEMO.md](https://github.com/bronils/aotopsy/blob/main/DEMO.md)를 참조하거나, 원시 APK가 있고 어디서부터 시작해야 할지 모를 때 단계별 방법론은 `WORKFLOW.md`를 참조하세요.
## 작동 방식
AOTopsy는 Dart AOT 스냅샷을 결정론적 바이너리 문법으로 취급합니다. 올바른 제약 조건(ELF 구조, 스냅샷 매직, 버전 해시, CID 테이블, 클러스터 인코딩)이 주어지면 모든 바이트는 정확히 하나의 올바른 해석을 가집니다. 파서는 단 하나의 해석만 남을 때까지 제약 조건을 적용합니다 — 휴리스틱도, 추측도 없습니다.
파이프라인은 여러 단계로 실행되며, 각 단계는 바이트에서 구조화된 데이터로 가는 순수 함수입니다:
```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]
```
두 개의 독립적인 백엔드는 동일한 전반부(ELF부터 클러스터 채우기까지)를 공유한 다음 아키텍처별로 분기됩니다: ARM64용 `internal/disasm`, x86_64용 `internal/disasm/x86.go`. 디컴파일러(`internal/decompiler`)는 통합 IR을 통해 두 아키텍처를 모두 처리합니다.
```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
```
## 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)는 자체 코드 경로를 통해 스냅샷을 역직렬화하기 위해 Dart VM을 내장합니다. 완벽한 충실도를 제공하지만, 대상 버전마다 일치하는 Dart SDK를 컴파일해야 하며, ARM64 전용으로 정적 x86_64 지원이 없습니다. AOTopsy는 네이티브 의사코드 디컴파일러와 공개된 정답(ground-truth) 정확도를 갖춘 유일한 정적·버전 독립적 분석기입니다.
AOTopsy는 바이너리 형식을 직접 파싱합니다. VM도, SDK도 필요 없습니다. 그 대가로, Dart 버전 간의 모든 형식 변경을 명시적으로 모델링해야 합니다. 이를 자동으로 처리해 줄 런타임이 없습니다.
## 명령어
### 전체 파이프라인
```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
```
플래그: `--out <dir>` (기본값: `<basename>.aotopsy/`), `--quiet`, `--strict`, `--max-steps <n>`, `--k <n>` (시그널 컨텍스트 깊이, 기본값 2), `--decompile`.
`--decompile`은 함수별 Dart 의사코드를 `<out>/dart/`에 기록합니다. 출력 디렉터리 크기가 대략 세 배로 늘어나기 때문에 기본적으로 꺼져 있습니다. 이 옵션 없이 실행할 때마다 그 사실을 알려주므로, 이 기능은 단순히 존재하는 데 그치지 않고 발견 가능합니다.
### 진단
```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
```
### 전체 프로젝트 Dart 소스 내보내기
모든 클래스, 필드, 메서드, getter, setter, 생성자를 원본 라이브러리 URI에 매핑된 모듈식 `.dart` 파일로 재구성합니다:
```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
```
### 고수준 네이티브 디컴파일러 (ARM64 + x86_64)
바이너리 기계 명령어로부터 깔끔하고 관용적인 Dart 코드를 직접 생성합니다:
- **구조적 제어 흐름**: 정확한 PC 경계를 갖춘 `for-in` 반복자, `while`, `for`, `try-catch-finally`.
- **Async/Await 선형화**: `_SuspendState` 상태 머신을 선형적인 `await future` 및 `await for`로 풀어냅니다.
- **람다 인라이닝**: 호출 지점에서 화살표 콜백 `(item) => process(item)`을 직접 합성합니다.
- **타입 격자**: 라이브 VM을 실행하지 않고도 SSA 값 전반에 구체적인 Dart 타입(`String`, `int`, `UserModel`)을 전파합니다.
- **Dart 관용구**: Null-aware(`?.`, `??`, `??=`), 캐스케이드(`..`), Set/List/Map 리터럴, 문자열 보간(`"${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
```
경고: 작은 `--max` 없이 `--all`을 사용하면 실제 앱에서 약 64GB의 RAM이 필요할 수 있습니다. `--find`/`--func`/`--from-main`을 선호하세요.
### Ghidra / IDA (ARM64 전용)
```bash
aotopsy ghidra libapp.so # headless Ghidra with metadata injection
aotopsy ida libapp.so # headless IDA via idalib
```
둘 다 x86_64 입력을 거부합니다. x86_64 의사코드에는 `decompile-native`를 사용하세요.
### 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
```
`FRIDA.md`에서 전체 가이드를 확인하세요.
### 상호 참조 및 추적
```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
```
### 코퍼스 도구
```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
```
## 출력 아티팩트
| 파일 | 내용 |
|------|----------|
| `functions.jsonl` | 함수별 이름, 주소, 크기, 소유자, 매개변수 개수 |
| `call_edges.jsonl` | 확인된 대상과 출처가 포함된 BL/BLR 엣지 |
| `classes.jsonl` | 클래스별 필드 이름, 오프셋, 인스턴스 크기 |
| `string_refs.jsonl` | 객체 풀 로드에서의 문자열 참조 |
| `dispatch_table.jsonl`| 추론된 디스패치 테이블 수신자 유형 및 대상 매핑 |
| `signal.html` | 컨텍스트 그래프가 포함된 행위 신호 보고서 |
| `dart_meta.json` | 스냅샷 메타데이터, 압축 포인터 플래그, THR 레이아웃 |
| `flutter_meta.json` | Ghidra/IDA용 통합 메타데이터 (ARM64 전용) |
| `aotopsy.sarif` | SARIF 2.1.0 보안 발견 보고서 (GitHub Code Scanning용) |
| `evidence.jsonl` | 호출 지점별 신뢰도와 출처가 포함된 통합 증거 모델 |
| `asm/*.txt` | 함수별 주석이 달린 디스어셈블리 |
| `asm/*.bin` | 함수별 원시 명령어 바이트 (두 아키텍처 모두) |
| `dart/*.dart` | 함수별 디컴파일된 의사코드 (`--decompile` 사용 시) |
| `cfg/*.dot` | 함수별 CFG (`--graph` 사용 시) |
## 패키지 레이아웃
```
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)
```
각 패키지에 대한 자세한 내용은 `ARCHITECTURE.md`를 참조하세요.
## 빌드
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`
```
통합 테스트는 `internal/samplecorpus`에서 입력을 확인하며, 이는 `samples/` 디렉터리를 찾습니다. 설정할 것은 없습니다. `samples/`는 gitignore되어 있으므로, 해당 디렉터리가 없는 체크아웃에서는 이 테스트들이 건너뛰어집니다. 하지만 코퍼스가 *있는* 체크아웃에서 등록된 샘플이 없으면 실패합니다. 코퍼스가 변경된 것과 아예 없는 것은 서로 다른 의미이기 때문입니다.
공개 CI는 linux/amd64, darwin/arm64, windows/amd64 전반에 걸쳐 `gofmt`, `staticcheck`, 빌드, vet, 셔플된 단위 테스트를 실행하며, 추가로 커버리지 하한이 있는 race+coverage 작업도 실행합니다.
## 릴리스 및 브랜치
AOTopsy는 두 브랜치 모델을 사용합니다:
| 브랜치 | 역할 |
|--------|------|
| `main` | **안정.** 모든 커밋은 스쿼시 병합되고 게이트 검증을 거친 릴리스 후보입니다. 태그된 릴리스(크로스 플랫폼 사전 빌드 바이너리 포함)는 여기서 생성됩니다. |
| `develop` | **롤링 / 나이틀리.** 일상적인 소규모 커밋, 기능, 연구가 가장 먼저 반영되는 곳입니다. 병합 사이에 불안정할 수 있습니다. 게이트가 통과되면 스쿼시 병합 PR을 통해 `main`으로 일괄 반영됩니다. |
`develop`을 대상으로 기여하세요. 게이트 검증을 거친 작업 묶음이 있을 때만 `main`으로 PR을 여세요.
Linux, macOS, Windows(amd64/arm64)용 사전 빌드 바이너리가 각
[GitHub 릴리스](../../releases)에 첨부됩니다. AOTopsy는 순수 Go이므로 `make build`로 어떤 타깃이든 깔끔하게 크로스 컴파일할 수 있습니다.
각 릴리스 아카이브에는 바이너리 옆에 `ghidra_scripts/`와 `ida_scripts/`도 함께 포함되며, 이는 `aotopsy ghidra`와 `aotopsy ida`가 탐색하는 레이아웃입니다. 따라서 이 명령들은 체크아웃 없이 압축을 푼 다운로드에서 바로 작동합니다.
## 제한 사항 및 범위
AOTopsy는 무엇을 복구하고 무엇을 복구하지 않는지 명확히 밝힙니다. 일부 제한은 엔지니어링 범위이고, 다른 것들은 **AOT 포맷의 하드 플로어**입니다. 즉, Dart 컴파일러가 릴리스(PRODUCT) 빌드에서 제거하는 정보로, SDK 소스에 대해 검증된 것입니다. 우리는 플로어를 지어내지 않고 문서화합니다.
**하드 AOT 플로어 (검증됨 — 개선을 기대하지 마세요):**
- **인스턴스 필드 이름은 약 97–99% 존재하지 않습니다.** `Precompiler::DropFields`는 `#if !defined(PRODUCT)`일 때만 필드 이름을 유지합니다. 실제 앱에는 약 233개의 명명된 `Field` 객체가 있는 반면 약 16k개의 합성 객체가 있습니다. 접근자 기반 복구(`get:`/`set:`는 여전히 이름을 가짐)는 이를 부분적으로 관통합니다(−11–22%, 결정적, 절대 추측하지 않음). 나머지는 진정으로 사라졌습니다.
- **로컬 및 캡처된 변수 이름은 사라졌습니다.** `local_*` / `tN`으로 렌더링됩니다.
- **진정한 다형 디스패치 대상은 정적으로 해석할 수 없습니다.** 디스패치 테이블 / 동적 `Closure` 객체를 통한 BLR은 분석 격차가 아니라 AOT 한계입니다. 이들은 `indirectCall` / `dynamicCall`로 정직하게 렌더링됩니다. [METHODOLOGY.md](https://github.com/bronils/aotopsy/blob/main/METHODOLOGY.md#limits-of-this-method-honestly)를 참조하세요.
**엔지니어링 범위:**
- **AOT 전용.** JIT 지원 없음.
- **Ghidra/IDA 디컴파일은 ARM64 전용입니다.** x86_64는 명확한 오류와 함께 거부됩니다 — 대신 `decompile-native`를 사용하세요(유일한 정적 x86_64 Flutter 디컴파일러).
- **`--all` 디컴파일은 호스트를 크래시할 수 있습니다.** 실제 풀사이즈 앱은 무제한 `--all`에 약 64GB RAM이 필요합니다. 대상 지정 모드(`--find`, `--func`, `--from-main`)를 사용하거나 `--max`로 제한하세요.
- **가상 디스패치는 `--from-main` 도달성에 보이지 않습니다.** 위젯 라이프사이클 콜백은 직접 호출 명령이 아니라 Flutter의 프레임워크 디스패치를 거칩니다. 클래스 터치 휴리스틱이 일부를 복구하지만, 이는 과대 근사입니다. 나머지는 Frida를 사용하세요.
- **모든 Dart 버전 변경은 모델링되어야 합니다.** 포맷 변경을 자동으로 처리하는 VM이 없습니다. 스냅샷은 버전 번호가 아닌 git에서 파생된 해시를 담고 있으므로, 지원은 구조 기반입니다. 현재 모델링됨: Dart 2.10 → 3.13.
- **정답 트윈은 재배포할 수 없습니다**(실제 프로덕션 빌드). 따라서
이름 복구 차분은 공개 CI가 아닌 로컬에서 실행됩니다.
## 라이선스
BSD-3-Clause. CID 테이블, THR 필드 오프셋, 스텁 이름은 [Dart SDK](https://github.com/dart-lang/sdk)(역시 BSD-3-Clause)에서 파생되었습니다. `LICENSE`와 `NOTICE`를 참조하세요.