When to use / when not to use
- Use: found inside an Android package
lib/<abi>/libapp.so(Dart AOT snapshot) andlibflutter.so(engine) — business logic is in libapp.so - Use: iOS
App.framework/App(AOT snapshot embedded in Mach-O,Flutter.frameworkis the engine) - Use: debug build's
assets/flutter_assets/kernel_blob.bin(Dart kernel binary, contains class/function names and AST) - Use: need to recover a Flutter App's Dart business logic (network interfaces, signing/encryption algorithms, protocols, MethodChannel channels)
- Don't use: pure native Android App (no libapp.so / kernel_blob, go to [[re-apk]])
- Don't use: pure native iOS App (no App.framework, go to [[re-ios]])
- Don't use: Flutter Web (output is JS/WASM rather than a snapshot, go to [[re-wasm]])
- Don't use: the engine layer itself (internal logic of libflutter.so / Flutter.framework, see step 4)
- Note: dynamic steps are executed within a controlled device / emulator snapshot ([[re-analyze/platform-tips]] highest principle); static first (large sample principle)
Tool preparation
See [[re-analyze/platform-tips]] — Flutter artifacts are large (libapp.so starts at tens of MB), follow "static first (large samples)": locate statically first, supplement dynamically as needed; dynamic (running / repackaging the App) defaults to within a controlled device / emulator snapshot.
python3 (main workhorse for snapshot header / section parsing)
- Linux:
apt install python3/dnf install python3/pacman -S python - macOS: built-in; Windows: official installer or
choco install python - Verify:
python3 --version
binutils (readelf / strings / objcopy, snapshot locating and segment extraction)
- Debian/Ubuntu:
apt install binutils;Fedora:dnf install binutils;Arch:pacman -S binutils - macOS: built into Xcode command line tools (
xcode-select --install) - Windows: no built-in binutils — use WSL, or Ghidra's built-in parsing instead
- Verify:
readelf --version && strings --version
Ghidra (disassembly / decompilation and cross-references for instruction segments)
- Installation and verification see [[re-ghidra]]; without Ghidra, IDA / radare2 are equivalent ([[re-ida]] / [[re-radare2]])
- Verify: after importing libapp.so, can locate the
_kDart*symbol
frida (dynamic side, Dart VM entry hook)
- Installation, frida-server deployment and version matching see [[re-frida]] tool preparation (host and device versions must match)
- Verify:
frida --version;frida-ps -Ulists device processes
blutter (automatic AOT snapshot recovery, optional)
- Automatic recovery of classes/functions/object pool for release libapp.so, installation and usage see [[re-hybrid-app]] tool preparation; step 2 of this skill uses manual parsing as the core method, blutter serves as an automated alternative and cross-check
- Verify: output directory shows
pp.txtand contains target App class names
Operating steps
Execute in order; archive each step's artifacts (segment extraction / parsing scripts / hook scripts + sha256).
-
Identify build mode (debug / release):
unzip -l app.apk | grep -E 'kernel_blob|libapp|libflutter' # Android 容器内定位 file lib/arm64-v8a/libapp.so readelf -sW lib/arm64-v8a/libapp.so | grep _kDart # release: AOT 快照符号 xxd -l 32 assets/flutter_assets/kernel_blob.bin # debug: 前 4 字节应为 ef cd ab 90(kernel magic 0x90abcdef 的小端) readelf -sW libapp.so | grep -c _kDartIsolateSnapshot # AOT 段符号(data/instructions),比按 ASCII 串搜可靠- debug:
assets/flutter_assets/kernel_blob.binexists (header is 8 bytes total = magic0x90abcdef+ formatVersion, Dart kernel binary, class/function names and AST preserved, low recovery cost) → go directly to step 3 - release: no kernel_blob, business code is in libapp.so's
_kDartIsolateSnapshotData(data) /_kDartIsolateSnapshotInstructions(instructions) → Step 2 - iOS:
Payload/<App>.app/Frameworks/App.framework/App(business) andFlutter.framework/Flutter(engine), same segment name within Mach-O,otool -l/ strings locate
- debug:
-
Dart AOT snapshot partition parsing (release):
objcopy --dump-section .rodata=snap.rodata libapp.so # 数据段(含 VM + isolate 两份快照) objcopy --dump-section .text=snap.text libapp.so # 指令段 python3 - <<'PY' import struct data = open('snap.rodata', 'rb').read() MAGIC = 0xDCDCF5F5 # Dart VM snapshot magic(int32;文件里是小端 f5 f5 dc dc) KINDS = {0: 'full', 1: 'full-jit', 2: 'full-aot'} off = 0 while True: off = data.find(struct.pack('<I', MAGIC), off) if off < 0: break length, kind = struct.unpack('<qq', data[off+4:off+20]) # 基础头共 20 字节 print('snapshot @', hex(off), 'kind:', KINDS.get(kind, kind), 'length:', length) off += 4 PY- Header format:
magic(4, 0xdcdcf5f5)+length(8)+kind(8)= 20-byte base header(followed by fields such as hashes).No ASCII"SNAPSHOT", and no fixed 12-byte version string—search by stringSNAPSHOTon real AOT samples finds nothing - After the header is the version-dependent internal serialization layout(the order and structure of sub-blobs change with the Dart version),do not treat it as a stable format promise: for precise parsing, read against the target SDK version's
runtime/vm/snapshot.h, serializer, and image snapshot implementation, rather than applying a hardcoded partition table - The machine code itself is ina separate instruction section(
_kDartIsolateSnapshotInstructions), separate from the data section;kindisfull-aotmeans an AOT snapshot _kDartVmSnapshot*is the VM snapshot (standard libraries such as dart:core/io, shared by all Flutter apps); business code is only in_kDartIsolateSnapshot*- Extract business strings from the strings blob:
strings -n 6 -t x snap.rodata | grep -E 'https?://|token|secret|api/|MethodChannel' - Import the instruction section into Ghidra: automatic analysis often fails to recognize AOT code objects, so function boundaries must be divided according to the target version's code object metadata (the sub-blob layout changes with the version, see the previous item); after decompilation, locate business functions by string cross-references
- Automatic alternative: blutter (tool preparation) restores asm/object pool/class names; manual parsing is used for verification and version fallback (pitfall 5)
- Header format:
-
Symbol restoration:
- The build side left a mapping (
--obfuscate --split-debug-info):# 构建命令: flutter build apk --obfuscate --split-debug-info=symbols/ ls symbols/ # app.android-arm64.symbols 等(每 ABI 一个) python3 -c "import json; d=json.load(open('symbols/app.android-arm64.symbols')); print(len(d['data']))" # JSON 结构: {"data": {"混淆名": "原名"}} —— 反向映射表,查表即得原名 - When there is no mapping, infer by string cross-references: UI copy / error messages / URLs in the strings blob → Ghidra cross-references to find referencing instructions → expand upstream and downstream along the call chain (methods correspond to [[re-binary-core]] cross-references)
- debug kernel_blob.bin: the kernel format contains class/function names and the AST; strings + structure parsing directly restores call relationships, without needing a symbol table
- The build side left a mapping (
-
Distinguishing engine and business code:
libflutter.so/Flutter.framework: Flutter engine (C++: Dart VM, Skia, channel implementation), basically identical across apps—do not analyze, only use it to get VM exported symbols (Step 5)libapp.so/App.framework/App: Dart business code AOT snapshot—the analysis target_kDartVmSnapshot*(VM snapshot / standard library) ≠_kDartIsolateSnapshot*(business): business strings and class names are only in the isolate snapshot- Native plugins and the native side of MethodChannel are outside the business so, use [[re-binary-core]]
-
Dynamic side (inside a controlled device / emulator snapshot, [[re-analyze/platform-tips]] default sandbox):
- Hook the Dart VM class library registration point—exported by libflutter.so
Dart_CreateRootLibrary(root library registration entry, onEnter reads the library URI):var f = Process.getModuleByName("libflutter.so").findExportByName("Dart_CreateRootLibrary"); if (f) Interceptor.attach(f, { onEnter: function (args) { var uri = args[1].readCString(); // 库 URI(C 字符串) console.log("root library: " + uri); } });- Under release AOT this C API may not trigger (the library is deserialized directly from the snapshot)—instead hook the VM internal
dart::Library::New(symbol within libflutter.so, trimmed depending on version) or directly use the VM Service enumeration below
- Under release AOT this C API may not trigger (the library is deserialized directly from the snapshot)—instead hook the VM internal
- Dart VM Service enumerate isolate / libraries (release requires enabling the VM service first: hook
Dart_SetVMFlags/Dart_InitIsolateinject--enable-vm-service, or switch to a debug build to verify dynamic behavior):adb forward tcp:8181 tcp:8181 curl http://127.0.0.1:8181/getVM curl "http://127.0.0.1:8181/getIsolate?isolateId=isolates/1" curl "http://127.0.0.1:8181/getInstalledLibraries?isolateId=isolates/1"getInstalledLibrariesList all business libraries (library name = package name / path);getObjectEnumerate classes/fields by objectId; class names and strings in the VM service metadata are still readable
- After locating the target function, hook by address (
_kDartIsolateSnapshotInstructionsbase address + code object offset); for the instrumentation foundation see [[re-frida]]
- Hook the Dart VM class library registration point—exported by libflutter.so
Cross-domain combination
- [[re-mobile]]: Mobile App analysis gateway—the Flutter branch is handled by [[re-hybrid-app]] and this skill (container-side static and dynamic run-through goes through the gateway)
- [[re-hybrid-app]]: Flutter snapshot automatic restoration (blutter / reFlutter) and MethodChannel interception—complements this skill (manual snapshot parsing + VM-layer dynamic)
- [[re-apk]] / [[re-ios]]: Android / iOS container-side static (unpacking, manifest, signature, DEX/ObjC native bridging)
- [[re-frida]]: Dynamic instrumentation foundation (version matching, spawn/attach, anti-detection, script templates)
- [[re-binary-core]]: Native layer of libapp.so / libflutter.so ([[re-format-elf]] + [[re-ghidra]] decompilation engine and JNI bridge)
Common Pitfalls and Traps
- release has no kernel_blob.bin: Symptom—following a debug tutorial to find
assets/flutter_assets/kernel_blob.bincannot be found; Cause—the build mode determines the artifacts:Debug → JIT (kernel binary, business code in kernel_blob),Profile and Release are both AOT(business code in the snapshot section of libapp.so)—Profile does not rely on kernel_blob to execute business code, and its difference from Release is profiling/service/optimization configuration rather than the execution model; Countermeasure—first determine the mode according to step 1: no kernel_blob → go to step 2 snapshot partition parsing, do not keep searching in assets - Short names after obfuscate/tree-shaking: Symptom—function/class names in the snapshot are all
a/band other short names or are directly missing; Cause—--obfuscateobfuscates names + tree-shaking removes unreferenced code; Countermeasure—if the build side has a--split-debug-infomapping file, look it up directly (step 3 JSON); if there is no mapping, use the strings blob's copy/URLs as anchors to cross-reference and infer the call chain in reverse; on the dynamic side, hook key functions to observe behavior as a complement - Engine and business code confusion: Symptom—analysis gets stuck deep in libflutter.so's
dart::internal functions or the_kDartVmSnapshot*section (dart:core/io standard library), wasting time; Cause—the engine layer and VM snapshot are shared code across all Flutter Apps, not business code; Countermeasure—analyze only the_kDartIsolateSnapshot*of libapp.so for business; from libflutter.so take only the VM exports (steps 4/5) - frida version mismatch: Symptom—
frida-ps -Ureports a protocol error / unable to communicate; Cause—the host frida-tools and device frida-server version numbers are inconsistent, or the architecture does not match; Countermeasure—strictly prepare according to [[re-frida]] tooling:frida --versiondownload the same version of frida-server by comparison, choose the correct architecture (arm64/arm/x86_64) - Snapshot version does not match the parsing script: Symptom—partition offsets are misaligned, strings blob contents are garbled, or parsing is interrupted; Cause—snapshot layouts differ between Dart versions (the version string is visible at the header), and manual/third-party scripts only cover specific versions; Countermeasure—read the header version string first; branch parsing by version; when the version is too new, fall back to generic methods (magic scanning + partition semantic inference)
- hook reads Dart strings as garbled text: Symptom — hooking a Dart object pointer but printing garbled text; Cause — Dart strings are not C strings (at object pointer +7 offset there are 4 bytes of Smi-encoded length, right-shift by 1 bit gives the real length, from +15 onward is UTF-8 data; this offset is a community-measured value); Countermeasure — write a readDartString utility function that reads according to this layout and add a length cap (for layout details see the [[re-hybrid-app]] pitfall entry);the offset drifts with the Dart version— rely on the authoritative offset exported by
gen_snapshot --print-object-layout-toor on measurement; don't copy hardcoded offsets on a different version - Object pool deserialization treated as fixed-width read (VLE): Symptom — blutter fails on snapshots of custom engine / structurally rearranged builds (
No cluster defined for cid Nor empty output), and afterward a self-written parser reads refs misaligned and all strings garbled; Cause — the Dart AOT object pool is a tagged array of entries (X27 holds the pool base address,LDR Xn,[X27,#off]loads slot number off/8), and in the fill area each entry has 1 byte of type (older versionsTypeBits = eb & 0x7f,PatchableBit = 0x80), type 0 (kTaggedObject) carries a VLE ref (variable-length),type 1 (kImmediate) payload is a fixed-width integer (intptr_t, not VLE)(a common pitfall); unsigned VLE has 7 data bits per byte, with the last segment having bit7 set (5→0x85,27673→19 58 81);Note the above type numbering/bit widths are the layout from the Dart 2.9 era; in current versions the TypeBits bit count and enum order have changed — before parsing, obtain the layout table according to the snapshot version, don't apply it across versions; Countermeasure — after VLE-reading the length at the start, decode entry by entry by type, slot_offset = idx × 8; first validate with 2–4 known ref→offset anchors before trusting the full set, and when an anchor doesn't match, narrow the scan window and retry; the object pool is the symbol table of unsigned binaries —pool_offset → {加载它的所有 PC} → {所属函数}, enabling "which functions touch string ref N" to be answered statically (source: reverse-skills (inliver233), MIT) - strings cannot find Chinese text: Symptom — running
strings/grep on libapp.so cannot find Chinese/CJK/emoji business text, and the lead goes dead; Cause — Dart snapshot strings have two storage forms: OneByteString (Latin-1/UTF-8, ordinary grep works) and TwoByteString (UTF-16LE); Chinese/CJK/emoji are basically all in TwoByteString, so matching a UTF-8 byte sequence against UTF-16LE storage of course won't hit;CID numbers drift with the Dart version(older versions 85/86, current 3.x is 93/94/95 etc.), don't hardcode the CID — read it dynamically from the snapshot class table; Countermeasure — for the target string, search both UTF-8 and UTF-16LE encodings at the same time, recording offset + encoding + occurrence count;--dump-utf16fully export CJK strings then grep; in the object pool a function's Code is adjacent to the strings it loads, so when unsigned you can guess string↔function attribution by proximity within the pool (source: reverse-skills (inliver233), MIT) - Pool slot strings cannot find cross-references: Symptom — you know a string's offset in the pool, but the decompiler's cross-reference list is empty and you can't locate the referencing function; Cause — there are only two standard idioms for fetching from the pool:
ADD xR,x27,#page,LSL#12followed byLDR xD,[xR,#off](offset=(page<<12)+off), and when page==0 the directLDR xD,[x27,#off]; generic xref analysis often misses this kind of indirect addressing; Countermeasure — write a pattern scanner using raw bytes to match the two idioms (no decompiler needed), and for function attribution use prologue features (fd 79 bf a9 fd 03 0f aa) to reverse-scan and locate the function start; first calibrate with one known load point before trusting the full set;"scanner didn't find it" ≠ "no loader exists"— Loads with non-standard encodings will be missed, and negative results require cross-validation with a second method; a string loaded by exactly one function is feature-dedicated and is a strong clue for a functional UI builder (source: reverse-skills (inliver233), MIT) - build/virtual method callers=0 misjudged as dead code: Symptom—performing a BL xref scan on build or virtual methods yields 0 callers, and they are misjudged as dead code and abandoned; Cause—Dart build/virtual methods are not called via BL, but via dispatch stubs: read object cid → look up dispatch table →
blr x30virtual dispatch, which static BL scanning naturally cannot see; Countermeasure—callers=0 is the norm and is not evidence of dead code; finding callers must be dynamic: capture at the dispatch stub or the target function's onEnterlr+Thread.backtrace(Backtracer.ACCURATE)to locate the dispatch point (the "while rebuilding dirty elements" frame is the framework rendering path; the business builder has already returned); the stub puts cid in x0 (low 16 bits) and the object pointer in x1; the object field @1 also stores the cid as a fallback; after obtaining the cid, statically searchmovk xR,#<cid>,lsl#16for the constructor, whose BL caller is the parent build; X21 = dispatch table base; when the whole table needs to be covered, combine it with a static cid→constructor mapping (source: reverse-skills (inliver233), MIT) - Dart bool treated as 0/1: Symptom—when decompiling AOT code, bool is read as an ordinary 0/1 integer, and the logic never lines up no matter how you go back and forth; Cause—Dart AOT has fixed register conventions: X15=shadow-stack, X26=Thread, X27=Pool, X22=null sentinel, X28=heap base (compressed pointers), X21=dispatch table base;Dart bool is a tagged pointer to a canonical object, not 0/1—True is at a low offset from null, False at a higher offset (with uncompressed pointers, True = null+0x20 (the low 16 bits often look like …8061), False = null+0x30 (…8071);with compressed pointers (the Flutter release default), True = null+0x10 (…8051), False = null+0x18 (…8059); the consumer side
tbz w0,#4(with compressed pointers,#3)bit=0 goes to the true branch—the one whose test bit is 0 is True; Countermeasure—read the disassembly according to the fixed register semantics (mov x0,x22returns Dart null; the standard prologuestp x29,x30,[x15,#-16]!; mov x29,x15, bytefd 79 bf a9 fd 03 0f aa); the bool encoding can be cracked by instruction frequency:add xR,x22,#0x20(with compressed pointers,#0x10) appears thousands of times (True),add xR,x22,#0x30(compressed#0x18) likewise (False); offsets/test bits fluctuate with pointer compression and version, so rely on actual measurement or--print-object-layout-to; the Thread globalG = Thread[x26+0x80], fields are read by offset; the allocation thunk patternmov x2,#imm; movk x2,#cid,lsl16; ldr x4,[x26,#0x228]; br x4(source: reverse-skills (inliver233), MIT) - Patching to materialize false with movz w0,#0 crashes (SIGILL): Symptom—using a static patch or a Frida runtime memory write with
movz w0,#0(byte 52800000) to materialize "false" causes a runtime crash with signal 4 (SIGILL); Cause—Dart bool is a tagged pointer, and 0 is an illegal immediate tag; Countermeasure—materialize True withadd x0,x22,#0x20(byte c0820091), False with+0x30(under compressed pointers this is True+0x10、False+0x18); this rule applies to both static patches and Frida runtime memory writes; when writing a Dart bool, always write the canonical address; the encoding fluctuates with the pointer compression mode and version, so rely on actual measurement (source: reverse-skills (inliver233), MIT) - Popup patch ineffective (three-source triage): Symptom — after NOP-ing out all showDialog calls, the dialog still appears as before, or the dialog appears on a timer of about 60 seconds, and repeated retries become a time sink; Cause — Flutter dialogs have only three sources, and mis-triage is the biggest waste: (a) explicit showDialog BL; (b) uncaught exceptions — MissingPluginException → FlutterError.onError/zone onError → asynchronous dialog, not going through any showDialog BL; (c) integrity aggregation gate — one aggregation function ORs about 7 checks, and if any is true it shows a dialog; Countermeasure — if logcat + Dart stack frames show MissingPluginException/Unhandled Exception and it appears on a timer → source (b), and the correct fix is to truncate the fault-body constructor (change the conditional branch that builds the body to an unconditional skip), so the global handler has no content to display,stop NOP-ing the dialog; dialog text → pool ref → body builder → caller tracing, callers=0 (cid dispatch) and no showDialog BL → also source (b); for source (c), find the aggregation function (references many check*/get* channel method strings + conditions → dialog construction), NOP its aggregation failure branch (
CMP W0,W22; B.EQ fail) or the entry-null dialog construction helper; when a patch works on one device but not another, use the text content to locate the newly triggered check category (e.g. validation prompt text corresponds to environment/emulator checks) (Source: reverse-skills (inliver233), MIT) - Treating the kernel format magic as ASCII
KERNEL: Symptom — judging by grepping/xxd-ing on the basis that "the file header is KERNEL"kernel_blob.bin, or searching the container for kernel files by that string, yields no hits or hits something else; Cause — the magic of the Dart kernel binary isthe numeric value0x90abcdef(in the sourceruntime/vm/kernel_binary.hofkMagicProgramFile), which appears in the file as little-endian bytesef cd ab 90,and is not an ASCII string at all; the header is 8 bytes total (magic + formatVersion), followed by the format version number, which can be used for version discrimination; Countermeasure — judge by the 4-byte magic number, do not use string search - Finding the AOT snapshot by ASCII
SNAPSHOT: Symptom —: Symptom —grep SNAPSHOT/data.find(b'SNAPSHOT')gets zero hits on a real libapp.so, so you conclude "there is no snapshot" or switch to guessing offsets by string; Cause — the header of a Dart VM snapshot isthe numeric magic0xdcdcf5f5(4B) + length(8B) + kind(8B) 20-byte base header,there is no ASCIISNAPSHOT, nor a fixed 12-byte version string; the sub-blob layout after the header isa version-dependent internal serialization implementation, which cannot be treated as a stable format; Countermeasure — locate by0xdcdcf5f5(little-endianf5 f5 dc dc) and read length/kind (kindbeingfull-aotmeans AOT snapshot); for precise parsing, consult the target SDK version'sruntime/vm/snapshot.hand serializer implementation; the machine code itself is in a separate instruction section - Tens of thousands of unsigned functions, no idea where to start: Symptom — libapp.so is unsigned, with about twenty thousand functions, so directly digging through the disassembly is like looking for a needle in a haystack; Cause — when unsigned, function boundaries, string ownership, and call relationships all depend on static reconstruction, lacking priority signals; Countermeasure — use prologue signature bytes (
fd 79 bf a9 fd 03 0f aa) to reverse-scan within .text (window about 0x6000–0x8000 bytes) to first recover function boundaries; build a feature vector per function: the set of strings accessed (dominant signal), BL targets/caller fan-in ((word>>26)==0b100101decode imm26), branch point after gate (TBZ/TBNZ/B.EQ), null comparison (CMP W0,W22/MOV X0,X22), bool return flag; the four sorters reduce 20,000 → shortlist: keyword string shortlist → total access count (keyword-class strings ≥2 → orchestrator) → dual-anchor set intersection (intersection of the access function sets of the title string + body string → precise builder, 0 or 1 function) → BL call fan-in upward iteration;caller count = risk metric——high counts (shared-state functions) are avoided as patch points, dedicated handlers with a count of 1 are safe targets; the aggregate score of high cmp + conditional branch count is used to surface integrity gates/aggregate predicates (source: reverse-skills (inliver233), MIT)