When to use / when not to use
- Use: when libflutter.so / libapp.so (Flutter) or assets/index.android.bundle (React Native) is found inside the app
- Use: when the business logic of the hybrid framework needs to be recovered (dart snapshot / JS bundle)
- Use: when sensitive data exchanged over the JS<->native bridge (MethodChannel / NativeModules) needs to be located
- Don't use: pure Java/Kotlin native apps (go to [[re-apk]])
- Don't use: pure native so logic (go to [[re-binary-core]])
- Don't use: when runtime hooking is needed—locate statically first, then [[re-frida]]
Tool preparation
Static analysis can be done without a sandbox ([[re-analyze/platform-tips]] highest principle); dynamic (reFlutter repackaging / running) is done inside a controlled device / emulator snapshot. Verify every tool before use.
Engine identification — jadx / unzip (reuse [[re-apk]])
unzip -l app.apkJust look at the assets/ and lib/ structure to determine the engine; for cross-OS installation see [[re-apk]] tool preparation
blutter — Flutter AOT snapshot static recovery (libapp.so)
- Install:
git clone https://github.com/worawit/blutter && cd blutter—C++20 project, requires a fairly new compiler (g++>=13 / Clang>=16) + cmake/ninja/pkg-config/libicu/libcapstone + pyelftools/requests- Debian/Ubuntu:
apt install python3-pyelftools python3-requests git cmake ninja-build build-essential pkg-config libicu-dev libcapstone-dev - Fedora:
dnf install gcc-c++ clang cmake ninja-build pkgconf-pkg-config libicu-devel capstone-devel python3-pyelftools python3-requests git - Arch:
pacman -S gcc cmake ninja pkgconf icu capstone python-pyelftools python-requests git - macOS:
brew install cmake ninja pkg-config icu4c capstone+pip3 install pyelftools requests
- Debian/Ubuntu:
- Usage:
python3 blutter.py libapp.so <输出目录>(auto-detects the Dart version, downloads and compiles the engine from Dart source if necessary)—currently supports Android arm64 libapp.so and newer Dart versions - Outputs:
asm/(disassembly with symbols),objs.txt(complete dump of object pool objects),pp.txt(all Dart objects in the object pool),blutter_frida.js(Frida script template) - Verification: the output directory contains
pp.txtand includes the target app's class names
reFlutter — Flutter dynamic (repackaging + traffic interception)
- Install:
pip3 install reflutter(Python 3, depends on frida-tools) - Usage:
reflutter main.apk→ enter the Burp proxy IP → producesrelease.RE.apk→ re-sign with uber-apk-signer (java -jar uber-apk-signer.jar --allowResign -a release.RE.apk) → install and run - Outputs: after running,
adb -d shell "cat /data/data/<包名>/dump.dart" > dump.dart(class/function list + code offsets) - Verification:
reflutter -houtputs the version and usage; after repackaging it can be installed and run and produce dump.dart - Note: Flutter >= 3.24 requires manually setting the proxy on the device (
adb shell settings put global http_proxy <ip:port>)
dart toolchain (to understand the snapshot structure, optional)
- Arch:
pacman -S dart(extra official repository) - Debian/Ubuntu / Fedora: no dart package in the official repositories → use the one bundled with the Flutter SDK (
snap install flutteror the official website SDK) - macOS:
brew install dart(orbrew install --cask flutter);Windows: Flutter SDK zip - Verification:
dart --version
RN bundle recovery — hermes-dec (Hermes bytecode)
- Install:
pip3 install hermes-dec(Python 3, pure standard library); Ubuntu also hassnap install hermes-dec(the snap version commands have thehermes-dec.prefix, e.g.hermes-dec.hbc-disassembler, the pip version has no prefix); Arch AURyay -S hermes-dec - Commands:
hbc-file-parser(parses the HBC header / version / function table),hbc-disassembler(bytecode → .hasm assembly),hbc-decompiler(→ pseudo JS) - Verification:
hbc-decompiler --helpavailable; the three steps can run through on the sample file - Plaintext JS bundle beautification:
pip install jsbeautifier(Python 3,js-beautify bundle.js)
Procedure
Execute in order; archive each step's artifacts (so / bundle / hasm / decompiled output + sha256).
-
Engine identification:
unzip -l app.apk | grep -E 'libapp|libflutter|assets/|bundle' file lib/arm64-v8a/libapp.so file assets/index.android.bundle # Hermes: "Hermes JavaScript bytecode, version XX"- Flutter:
lib/<abi>/libapp.so(AOT snapshot) +libflutter.so(engine) +assets/flutter_assets/ - RN:
assets/index.android.bundle(plaintext JS or Hermes HBC); RN 0.70+ defaults to Hermes - Other: WebView shell (assets/*.html) / Cordova / uni-app—handle via the webview path
- The identification conclusion determines the branch: Flutter → step 2; RN → step 3
- Flutter:
-
Flutter: libapp.so snapshot analysis (blutter):
python3 blutter.py lib/arm64-v8a/libapp.so ./flutter_out grep -r "目标类名" ./flutter_out/pp.txt # 类/函数名在快照字符串表里 grep -rn "https://\|api\|token\|secret" ./flutter_out/asm/ | head- The blutter artifacts provide Dart classes/functions and object pool structure—track business logic by class name (release AOT preserves dart-layer names, see pitfall 1)
- Dynamic supplement: repackage with reFlutter → dump.dart to get runtime classes/functions and offsets →
frida -U -f <包名> -l frida.jscombined with_kDartIsolateSnapshotInstructions(readelf -Ws libapp.so) to locate hook points
-
RN: bundle.js extraction and restoration:
# 明文 JS unzip -p app.apk assets/index.android.bundle > bundle.js js-beautify bundle.js > bundle.pretty.js # Hermes 字节码 hbc-file-parser index.android.bundle > meta.txt hbc-disassembler index.android.bundle out.hasm hbc-decompiler index.android.bundle out.js # 线索提取 grep -aoE 'https?://[^"'"'"' ]+' out.js | sort -u # 端点半程 grep -aoE '"[A-Za-z0-9_/+=]{16,}"' out.js | sort -u # 疑似密钥 / base64- Examine function call relationships at the hasm level (
LoadConstStringpaired withCallN); when the pseudo-JS loops/conditions are incomplete, rely on hasm + the string table (see pitfall 2)
- Examine function call relationships at the hasm level (
-
The native part goes through [[re-binary-core]]:
libflutter.so/libapp.so(ELF) → [[re-format-elf]] parsing → [[re-ghidra]] decompiling engine-layer C++ (dart:: VM, channel registration, Skia) and JNI bridging (FlutterEngine / PlatformChannel)- RN:
libhermes.so(engine, optional), JNI bridge (com.facebook.reactpackage) → native module logic - Anti-debugging / encryption / certificate verification are often at this layer (e.g., the BoringSSL verify function in libflutter.so)—for hook points see step 2
-
Logic location (bridge boundary):
- Flutter: globally grep MethodChannel channel names (
flutter_prefix is common); the PlatformChannel send/receive points are the sensitive data (token / config) exchange points - RN: NativeModules registry and
requireNativeComponent/NativeModules.xxx—encryption/decryption not found on the JS side is in native modules (go to step 4) - Reverse-lookup from UI text / network strings: string table (Flutter snapshot / JS bundle) → corresponding function → logic
- Flutter: globally grep MethodChannel channel names (
Flutter MethodChannel dynamic interception
The Flutter platform channel (PlatformChannel) is the main artery of Dart ↔ native communication; interception allows observing all native capability calls:
- Java/Kotlin side:hook
io.flutter.plugin.common.MethodChannel's MethodCallHandler—record channel name / method name / parameter JSON - channel name acquisition:hook
MethodChannel.setMethodCallHandler(onEnter captures the channel instance, including its name) orMethodChannelconstructor directly obtains the channel name; at a lower level you can hookBinaryMessenger.send(the first argument is the channel name) - engine messenger layer:
io.flutter.embedding.engine.FlutterEngine's messenger messages (low-level fallback) - Complements static observation on the Dart side: statically find channel names and call sites (string literals), dynamically confirm actual traffic
- Output: structured JSON (channel / method / args), for consumption by the [[re-analyze/analysis-contract]] data contract (evidence archive)
For Flutter specifics see [[re-flutter]]
React Native / Hermes (RN bytecode and bridging)
Identification: HBC and RN artifacts
.hbc/ Hermes bytecode: file header 4-byte magicc61fbc03class (little-endian),fileoutput takes the form "Hermes JavaScript bytecode, version XX"; one without a magic number and starting withvar __BUNDLE_START_TIME__and other JS source code is a plaintext bundle- RN app structure:
assets/index.android.bundle(business JS / HBC packaged by Metro, compiled with Hermes by default in RN 0.70+),lib/<abi>/libhermes.so(Hermes engine),libreactnativejni.so(JNI bridge) - Differences between Hermes and standard JS engines: Hermes is AOT precompiled bytecode (VM instructions, no JIT compilation at runtime), while the output of standard engines (JSC / V8 class) is a plaintext JS source bundle; the two determine whether to subsequently go with bytecode disassembly or pure JS recovery
Hermes bytecode: disassembly approach
- Structure: file header (magic number + version number) → bytecode section → function table / string table; the version number is in the file header and directly determines tool compatibility
- Disassembly: hermesc-type tools (
-dump-bytecodeseries) or hbc disassembly-type Python implementations (see "Tool Preparation") → produce hasm assembly; the string table can be extracted as a whole (grab printable strings across sections) to get clues first - Function table characteristics: each function contains id / offset / instruction count / parameter count—track business entry points by call relationships (LoadConstString paired with CallN)
JS bundle extraction: Metro structure and recovery
- Metro packaging structure: module factories are registered into the global registry via
__d(factory, moduleId, deps), and require is used at__r(moduleId)runtime; the top of the bundle has__BUNDLE_START_TIME__and a polyfill prefix - Recover business JS: beautify (jsbeautify-type tools) → split module boundaries by
__d(→ restore each module independently (factory body + dependency array) → obfuscation / string encryption connects to [[re-script-deob]] - Module correspondence: large modules (long strings / many dependencies) are often the main business package, and the scope can be narrowed by comparing with page functionality
Native bridging: NativeModules / TurboModule
- JS-side locating:
NativeModules.xxxaccess points /TurboModuleRegistry.get('Xxx')—the bridge name is the module name - Native-side mapping: the module registry under the JNI bridge
com.facebook.reactpackage (@ReactMethodannotated class methods /getNativeModules()list); TurboModule interface method signatures correspond one-to-one with JS call names - The bridge boundary is the sensitive data exchange point: encryption and decryption that cannot be found on the JS side are in native modules (switch to [[re-binary-core]]), the same principle as Flutter MethodChannel
Dynamic: hook the JS runtime ([[re-frida]])
- Module loading points:hook
__d/__rcan enumerate all loaded modules and ids, and dump module factory source code; modules registered again at runtime are a characteristic of remote delivery (hot updates) - console output: hook the console.log / warn implementation to obtain business logs and error stacks
- Bridge layer fallback: hook NativeModules method calls (input parameters / return values) to observe data exchanged between JS and native
Pitfalls and traps
- Hermes bytecode does not cross versions: symptom—the same sample disassembles normally in one environment, but when the version is changed or the tool is changed the output is garbled or errors out directly; cause—the HBC format evolves with the engine version and backward compatibility is not guaranteed, and disassembly-type tools only cover specific version ranges; countermeasure—first obtain the version (
fileOutput / version string inside libhermes.so / build time), select the corresponding version of hermesc-like tools or update the disassembly tool; when versions do not match, prefer switching tools rather than manually patching bytes - bundle encryption / obfuscation (Metro encryption layer): Symptom—after the bundle is extracted, it is ciphertext / garbled, or the module function names are unreadable; Cause—the build pipeline encrypts at the Metro output layer (a decryptor is embedded in the bundle and decrypts at runtime) or enables an obfuscation plugin; Countermeasure—find the decryptor's characteristics inside the bundle (XOR loop / self-decrypting function / hardcoded key), statically restore the decryptor and decrypt the whole thing, or use [[re-frida]] to dump plaintext at the return point of the decryption function; for the obfuscated part, continue with [[re-script-deob]]
- Mixture of bytecode and plaintext JS (some pages use remote bundles): Symptom—the local bundle is plaintext JS and its content is normal, but the key page logic is missing, or modules not present locally appear at runtime; Cause—hybrid delivery: some pages are packaged into assets, while sensitive / updated pages use remotely delivered bundles (hot updates, canary releases), and the local package only contains the shell and loading logic; Countermeasure—first verify the completeness of the local bundle (
__dmodule count versus page functionality), capture packets / take file system snapshots to archive the remote bundle, then process it via the plaintext or HBC path
Cross-domain joint
- [[re-mobile]]: the hybrid app branch fixedly invokes this skill (after engine identification, route by framework)
- [[re-apk]] / [[re-ios]]: container-side static (manifest, signature, iOS equivalents)
- [[re-binary-core]]: native part—[[re-format-elf]] + [[re-ghidra]] to decompile libapp.so / libflutter.so and the RN JNI bridge
- [[re-frida]]: runtime hook (bridge layer, certificate validation, anti-detection)
- [[re-analyze/platform-tips]]: default sandbox, tool parsing ≠ loader view (snapshot offsets / tool version differences)
- This skill is referenced by the triage "Mobile App Analysis" path of [[re-analyze]] (re-mobile → re-hybrid-app)
Common pitfalls and traps
- Flutter AOT snapshots have no source-level symbols: Symptom—when looking directly at libapp.so in IDA/Ghidra, you only see dart:: internal functions and bare offsets, and business function names are not visible; Cause—the AOT snapshot has no DWARF symbols, but Dart class/function names are actually retained in the snapshot string table; Countermeasure—use blutter to restore (pp.txt / asm/), grep the string table to locate class names, then analyze against asm; do not look for business symbols in the ELF symbol table
- RN Hermes bytecode is hard to restore: Symptom—the pseudo-JS produced by hbc-decompiler is missing all loops/conditions, variable names become register numbers, and the logic cannot be read through; Cause—HBC is VM instructions, and decompilation to source level is incomplete; Countermeasure—use hbc-disassembler's hasm to inspect call relationships (LoadConstString + CallN to locate string usage points), use string tables / plaintext inside the bundle (URLs, error prompts) to infer semantics, and when necessary dynamically capture plaintext traffic for comparison
- Engine version differences: Symptom—blutter reports that the Dart version is unsupported / hermes-dec fails to parse or is misaligned; Cause—the tool only supports a specific version range, while the app uses a new engine (new snapshot format in Dart 3.x, new bytecode version in Hermes); Countermeasure—first obtain the version (version string inside libflutter.so /
fileoutput HBC version / build time), update the tool (blutter will download and recompile the corresponding Dart source as needed) or switch to a dedicated tool that supports that version - JS and native bridge boundary: Symptom — the encryption/decryption and key logic cannot be found in the JS/bundle; Cause — the sensitive logic is in native modules (RN NativeModules / Flutter MethodChannel native side); Countermeasure — follow step 5 to find the bridge registration (channel name / native module table) → switch to [[re-binary-core]] to analyze the corresponding so; don't keep grinding away at the JS layer
- Obfuscated build: Symptom — function names are all a/b/c or hashes; Cause — Flutter
--obfuscate/ RN Hermes + obfuscation-option build; Countermeasure — driven by still-usable strings (error messages / API paths) and behavioral observation, combined with dynamic [[re-frida]] hooking of the bridge layer for locating - Cannot capture packets through Flutter SSL verification: Symptom — the packet capture tool cannot see Flutter app traffic or reports a certificate error; Cause — Flutter has its own SSL verification (it does not go through the system proxy/certificate trust chain), and a universal bypass solution has open-source code on GitHub; Countermeasure — first use a universal Flutter SSL verification bypass solution or frida to bypass and capture packets; after blutter restores it, libapp.so symbols become visible, and you can directly hook the target function based on its address (e.g. the input parameters/return value of the signature function generateMD5 to obtain the salt value and plaintext) — full algorithm recovery is not needed
- Dart string memory layout (key to reading strings with frida): Symptom — hooking a libapp.so function gets a pointer but the string cannot be read out; Cause — a Dart string is not a C string:at pointer +7 offset there are 4 bytes of Smi-encoded length (shift right by 1 bit for the real length), and at +15 offset is the UTF-8 data; Countermeasure — write the readDartStringExact utility function according to this layout (add a length cap to prevent misreading), and both hook input parameters/return values can recover plaintext
- The Hermes function table header is bitfield-compressed rather than a struct: Symptom — reading the function table as an ordinary struct causes function boundaries/names/parameter counts to be garbled, or causes the large header offset of an overflowed function to be read as an ordinary field; Cause — the HBC function header packs 10 fields into a single 128-bit word (legacy 16 bytes, version <97) or 96-bit word (modern 12 bytes, version >=97, the default form for RN 0.75+), and the bit-width allocation differs by version; Countermeasure — get the version number first, then choose the bitfield diagram; the flags bits can aid semantic inference (bit0 strict, bit3 hasExceptionHandler, bit5 overflowed), and when overflowed is set the inline header fields are untrustworthy, so assemble from the large header instead (legacy is (info_offset<<16)|offset, modern is (function_name<<24)|(offset&0xffffff)); function boundary = in-header offset + bytecode_size, and the global code is function 0 (Source: hermes-decomp (SymbioticSec), MIT)
- Hermes small string table: u32 bitfield + sentinel overflow table + run encoding + packed shared storage: Symptom — string length/content is misaligned, and xref reverse lookup finds the wrong function; Cause — each entry is 1 u32: bit0 is the UTF-16 flag, bits1-23 store the storage offset, bits24-31 store the length (max 255), and when the length or offset hits the sentinel value (0xff / 0x800000) the real value is supplemented in the overflow table as an (offset,length) pair; the string kind table is run-length encoded (the highest bit 0=ordinary String, 1=Identifier, and the low 31 bits are the count of consecutive same-kind entries); Countermeasure — parse by bitfield and go through the overflow table, always locate entries by table-entry offset and never by substring search (multiple entries may share the same piece of packed storage), and the write-back path is the same; expanding by run can distinguish code strings from identifiers (identifiers are used for property-access resolution and naming hints) (Source: hermes-decomp (SymbioticSec), MIT)
- opcode definitions drift by version, and builtin indices change only at release boundaries: Symptom — the same bytecode, when fed to a different disassembly/decompilation tool or version, produces misaligned output and crashes on unknown opcodes; Cause — each HBC version has its own instruction table (40-99): opcode numbers and operand type sequences (Reg8/Reg32/UInt8/Addr8, etc.) differ from version to version; Countermeasure — take a per-version table (for a missing version, degrade using the table of the "nearest <= target version" and report the actual version); fall back on unknown opcodes to rather than going out of bounds; the disassembly layer first normalizes semantic variants to abstract definitions (the two Mov encodings, the three GetById variants all merge into the same abstract operation), and the IR layer only handles abstract semantics; jump targets are computed by Addr8/Addr32 relative to their own offset (Source: hermes-decomp (SymbioticSec), MIT)
- Do not bring in full SSA+Phi for variable recovery: Symptom — decompiled output is full of temporary variables and edge copies, poor readability; Cause — the target language JS has no Phi, and the De-SSA stage of full SSA instead creates noise; Countermeasure — use live-range splitting-style recovery: reaching definitions + union-find, when two definitions converge at the same use site (if/else merge points, loop back edges) they are merged into one class, otherwise they become independent variables, no Phi; for naming, first do register role inference (use property/method vocabulary to infer types backwards: length/push/map/filter→Array, then/catch/finally→Promise, next/done/value→Iterator, toString/split/trim→String), name priority: debug-segment scope name > destructuring key name > named function name > parameter > type fallback > number, function names go through a "meaningfulness" filter (reject pure numbers/generated names/generic core names) (Source: hermes-decomp (SymbioticSec), MIT)
- The Metro module factory parameter count is a version signal: Symptom — scanning __d registrations fails to recognize factories, factory parameter roles are misidentified, and module skeleton recovery fails; Cause — classic bundle factories have 4 params (require/module/exports/deps), modern bundles have 7 params (including importDefault/importAll); Countermeasure — param_count-1 (removing this) is the basis for role determination; match the __d(undefined, factory, moduleId, deps) pattern, and when parameters are forwarded through registers, maintain an in-flow map of "register→function/integer/array" for value tracking; export analysis has three modes: direct assignment/bulk assignment/Object.defineProperty; module naming goes through propagation chains (reverse require naming→re-export thin wrapper inheritance→single dependency chain) and is more reliable than single-point inference (Source: hermes-decomp (SymbioticSec), MIT)
- Bytecode write-back patching: same-length in place, variable-length relocation, overlap rejection: Symptom — after directly modifying .hbc bytes, the bundle crashes at runtime and structure tables are misaligned; Cause — string storage allows packed (multiple entries sharing the same segment), offsets are referenced in multiple places, and variable-length modifications must correspondingly fix all subsequent segments; Countermeasure — only modify bytecode without recompiling JS (HASM round trip: emit-hasm out, modify, asm back); string patches are dispatched by length: same-length→overwrite in place without touching the table, variable-length→rebuild the string table + storage and relocate all trailing segments (with corresponding offset fixes); UTF-16 strings and packed overlapping strings are always refused for patching; modern (>=97) only supports same-length string patches, and variable-length strings/function resize/injected stub expansion/create from scratch all explicitly error out without writing corruption; after patching, re-parse to verify structural consistency, and if inconsistent, refuse to output (Source: hermes-decomp (SymbioticSec), MIT)
- The capability boundaries of Hermes decompilation:Phenomenon—the decompilation result lacks variable names and has distorted control flow, or you expect to get the original source code back directly; Cause—decompilers have no deobfuscation capability (after obfuscation, only the obfuscated semantics can be obtained); release packages often strip the debug sections (offset=0 or u32::MAX), leaving no variable names or line numbers at all; disassembly ≠ decompilation—the instruction stream can only provide precise control flow and operands, while variable/structure/module semantics depend on IR + whole-program analysis; Countermeasure—"no debug information" is itself an analysis conclusion, and naming can only rely on heuristics; prioritize looking in the string table for source code/JSON/mappings embedded at compile time (for example, RN Reanimated's Babel plugin compiles the worklet's original source code into the .hbc string table as ordinary strings, and joining by function name yields the true source code), which is less laborious than decompilation and preserves fidelity (source: hermes-decomp (SymbioticSec), MIT)