
Flutter/Dart AOT 스냅샷용 정적 분석기 — Dart VM을 임베드하거나 실행하지 않고 libapp.so에서 함수 이름, 클래스 계층 구조, 호출 그래프, 동작 신호를 복구합니다. ARM64 및 x86_64, Dart 2.10–3.12를 지원합니다.
Dart AOT 스냅샷 분석기입니다. Flutter 릴리스 APK 내부의 컴파일된 Dart 코드인 libapp.so를 함수 이름, 클래스 레이아웃, 호출 그래프, 행위 신호, 그리고 읽을 수 있는 의사코드로 변환합니다. Dart VM도, SDK 컴파일도, 런타임 폴백도 필요 없습니다.
포크 공지: AOTopsy는 원래 Anthony Zboralski가 만든 unflutter의 포크입니다. 원본
zboralski/unflutter저장소는 더 이상 사용할 수 없으며(작성자가 제거함), 커뮤니티 후속 저장소가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 참조(샘플 빌드 93개, 실패 0건).
AOTopsy는 주장이 아니라 정답(ground truth)에 대해 측정됩니다. 두 가지 속성이 모든 변경 시 테스트 스위트에 의해 강제됩니다:
| 지표 | 값 | 의미 |
|---|---|---|
| 이름 복원 일치율 | 44개 정답 빌드 전반에서 전체 90.2% (최악 구간 81.3%, ≥ 0.81 게이트 하한) | 복원된 함수 이름을 각 빌드 자체의 ELF .symtab(외부 정답)과 비교 — TestSymtabDifferential. 빌드별 전체 점수표: BENCHMARK.md (make bench). |
| 디컴파일러 구문 유효성 | 100% 유효한 Dart | 출력된 모든 의사코드 함수가 Dart로 파싱됨 — TestDecompileQualityCorpus. |
| 날조율 | 0% | §2 규칙: 추측한 이름, 타입, 호출 대상을 사실로 출력하지 않음. 미지의 항목은 정직하게 표시됨(indirectCall, <unknown>, dynamic). |
정답 트윈은 재배포할 수 없는 실제 프로덕션 빌드이므로, 해당
차등 게이트는 로컬에서 실행됩니다. 공개 CI는 빌드, vet, gofmt,
staticcheck, 단위 테스트(셔플됨), 그리고 플랫폼
매트릭스 전반의 커버리지 하한을 검증합니다. samples/는 재배포할 수 없으며 gitignore되어 있으므로, 샘플에 의존하는
테스트는 그곳에서 건너뜁니다 — 단, 코퍼스가 완전히 없을 때만 그렇습니다. 코퍼스가 존재하는데 등록된 샘플이 누락된 경우는 실패합니다. 이 둘은 정반대의
의미이기 때문입니다. 여기의 모든 수치가 어떻게 측정되는지(정답, 지표 정의, 재현): METHODOLOGY.md. 릴리스 바이너리 검증과 아래의 정직한 범위는
SECURITY.md를 참조하세요.
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를 참조하거나, 원시 APK가 있고 어디서부터 시작해야 할지 모를 때 단계별 방법론은 WORKFLOW.md를 참조하세요.
AOTopsy는 Dart AOT 스냅샷을 결정론적 바이너리 문법으로 취급합니다. 올바른 제약 조건(ELF 구조, 스냅샷 매직, 버전 해시, CID 테이블, 클러스터 인코딩)이 주어지면 모든 바이트는 정확히 하나의 올바른 해석을 가집니다. 파서는 단 하나의 해석만 남을 때까지 제약 조건을 적용합니다 — 휴리스틱도, 추측도 없습니다.
파이프라인은 여러 단계로 실행되며, 각 단계는 바이트에서 구조화된 데이터로 가는 순수 함수입니다:
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을 통해 두 아키텍처를 모두 처리합니다.
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
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는 자체 코드 경로를 통해 스냅샷을 역직렬화하기 위해 Dart VM을 내장합니다. 완벽한 충실도를 제공하지만, 대상 버전마다 일치하는 Dart SDK를 컴파일해야 하며, ARM64 전용으로 정적 x86_64 지원이 없습니다. AOTopsy는 네이티브 의사코드 디컴파일러와 공개된 정답(ground-truth) 정확도를 갖춘 유일한 정적·버전 독립적 분석기입니다.
AOTopsy는 바이너리 형식을 직접 파싱합니다. VM도, SDK도 필요 없습니다. 그 대가로, Dart 버전 간의 모든 형식 변경을 명시적으로 모델링해야 합니다. 이를 자동으로 처리해 줄 런타임이 없습니다.
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/에 기록합니다. 출력 디렉터리 크기가 대략 세 배로 늘어나기 때문에 기본적으로 꺼져 있습니다. 이 옵션 없이 실행할 때마다 그 사실을 알려주므로, 이 기능은 단순히 존재하는 데 그치지 않고 발견 가능합니다.
aotopsy doctor libapp.so # Dart version, pointer size, support status, build features
aotopsy find-libapp --apk app.apk # locate libapp.so inside an APK
모든 클래스, 필드, 메서드, getter, setter, 생성자를 원본 라이브러리 URI에 매핑된 모듈식 .dart 파일로 재구성합니다:
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
바이너리 기계 명령어로부터 깔끔하고 관용적인 Dart 코드를 직접 생성합니다:
for-in 반복자, while, for, try-catch-finally._SuspendState 상태 머신을 선형적인 await future 및 await for로 풀어냅니다.(item) => process(item)을 직접 합성합니다.String, int, UserModel)을 전파합니다.?., ??, ??=), 캐스케이드(..), Set/List/Map 리터럴, 문자열 보간("${a}${b}").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을 선호하세요.
aotopsy ghidra libapp.so # headless Ghidra with metadata injection
aotopsy ida libapp.so # headless IDA via idalib
둘 다 x86_64 입력을 거부합니다. x86_64 의사코드에는 decompile-native를 사용하세요.