From cf636c426353f79322a2fa64baf3e7ba47095f6b Mon Sep 17 00:00:00 2001 From: inliver233 Date: Sat, 19 Sep 2026 22:18:16 +0800 Subject: [PATCH] Add Dart AOT depth (reference + 3 scripts); fix encoding rule and add P26 New reference: - dart-aot.md: pinning the Dart version, building a matching decompiler, the object-pool model and reference indexes, register/boolean conventions, the three assembly signatures that identify most business logic, the locating workflow, patch strategy, and the traps specific to this layer (including ambiguous identifiers that resolve to protocol fields). New scripts (parameterized, no machine-specific paths): - dart_pprefs.py: object-pool offset -> code-site index, decoded arithmetically. Avoids the full-capstone pass that costs minutes and 1-2 GB and reads as a hang. - dart_pool_strings.py: framed literal recovery with file offsets. Defaults to one-byte because automatic two-byte (UTF-16) detection is unreliable; --find searches both encodings and is the reliable way to locate a known literal. - dart_disasm.py: annotated windowed disassembly (pool + boolean annotations) and a B/BL caller index that needs no disassembler. Fixes and integration: - framework-runtimes.md: point the Dart depth at dart-aot.md, and make the string-encoding rule precise (ASCII is one-byte/UTF-8; CJK is UTF-16LE) instead of leaving it as 'try both'. - pitfalls.md: P26 -- self-built tooling that fails in a way that reads as a target finding (silent arithmetic, helper shadowing a stdlib module, measuring against an incomplete reference, and a knob that does not move the metric). - SKILL.md / README.md: index the new reference and scripts. Validated by running all three scripts against a real Dart AOT sample: the pool-ref index agrees with an independently built index to 99.2%, and the literal extractor's one-byte output was verified string-by-string against a decompiler's pool listing. --- README.md | 7 + SKILL.md | 4 + references/dart-aot.md | 254 ++++++++++++++++++++++++++++ references/framework-runtimes.md | 14 +- references/pitfalls.md | 38 +++++ scripts/dart_disasm.py | 212 ++++++++++++++++++++++++ scripts/dart_pool_strings.py | 273 +++++++++++++++++++++++++++++++ scripts/dart_pprefs.py | 172 +++++++++++++++++++ 8 files changed, 970 insertions(+), 4 deletions(-) create mode 100644 references/dart-aot.md create mode 100644 scripts/dart_disasm.py create mode 100644 scripts/dart_pool_strings.py create mode 100644 scripts/dart_pprefs.py diff --git a/README.md b/README.md index e8940a0..898673f 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,9 @@ references/ loaded on demand, one topic each boundary with single-variable tests, choosing a native host framework-runtimes.md Flutter / React Native / Unity: which layer owns the UI, and how to find logic when there are no symbols (string encoding traps) + dart-aot.md Dart AOT in depth: version pinning and building a matching decompiler, + the object pool and reference indexes, register/boolean conventions, + the three signatures that identify business logic, locating, patching native-and-so.md .so hosts, DT_NEEDED vs JNI_OnLoad, relocation limits, relocation-free bootstrapping, replacing Java methods natively, and which ABI/library is *actually loaded and executing* @@ -67,6 +70,10 @@ scripts/ parameterized, path-agnostic dex_strpatch.py byte-level string patch with a string_ids ordering guard dex_classdiff.py prove a dex edit was surgical dex_strings.py strings/URLs/SDK markers without a decompiler + dart_pool_strings.py recover literals from a Dart AOT snapshot (framed entries, the + one-byte vs UTF-16 split, file offsets, run-length noise filter) + dart_pprefs.py build/query the object-pool -> code-site index for a Dart snapshot + dart_disasm.py annotated windowed disassembly of Dart AOT code + B/BL caller index find_refs.py count callers of a method before patching it repack.py rebuild APK, strip only signatures, sign, verify devsh.py quoting-safe ADB shell helper diff --git a/SKILL.md b/SKILL.md index 87b02bb..4dfef95 100644 --- a/SKILL.md +++ b/SKILL.md @@ -87,6 +87,7 @@ Load only what the current step needs. | `references/recon.md` | Starting any new sample; identifying packer, SDKs, code location, ABI | | `references/packers.md` | The app is packed/hardened, or an edit makes it die before your code runs. Also load before discarding any route as "blocked by the shell" | | `references/framework-runtimes.md` | The UI is not native (Flutter / React Native / Unity / Cordova), or Java-layer hooks fire zero times while the UI clearly works | +| `references/dart-aot.md` | The logic lives in a Dart AOT snapshot (`libapp.so`): pinning the Dart version, building a matching decompiler, the object pool and reference indexing, register/boolean conventions, locating and patching Dart code | | `references/native-and-so.md` | Patching in a `.so`, needing code to run before the app's own code, hand-built native payloads that crash inside the linker, or **deciding which library/ABI is actually loaded and executing** | | `references/ad-removal.md` | Task involves ads, trackers, sponsored cards, splash/interstitial/reward | | `references/membership-and-limits.md` | Task involves VIP, subscription, paid content, unlock, "fully cracked" | @@ -113,6 +114,9 @@ All scripts are parameterized and path-agnostic; pass paths explicitly. Run `--h | `scripts/dex_strpatch.py` | Byte-level string constant patch with **string_ids ordering guard** | | `scripts/dex_classdiff.py` | Compare two dex class tables (set + access flags) to prove a patch was surgical | | `scripts/dex_strings.py` | Dump/extract strings and endpoints from dex without a decompiler | +| `scripts/dart_pprefs.py` | Build/query the object-pool offset -> code-site index for a Dart AOT snapshot (arithmetic decode; seconds, not minutes) | +| `scripts/dart_pool_strings.py` | Recover string literals from a Dart AOT snapshot: framed entries, the one-byte vs UTF-16 split, file offsets, and a run-length noise filter | +| `scripts/dart_disasm.py` | Annotated windowed disassembly of Dart AOT code (pool + boolean annotations) plus a B/BL caller index | | `scripts/find_refs.py` | Count and list callers of a method/field (blast-radius check) | | `scripts/repack.py` | Rebuild APK with replaced dex, strip only signatures, sign | | `scripts/dexpatch/` | dexlib2 method-level surgical rewriter (the preferred patch tool) + build notes | diff --git a/references/dart-aot.md b/references/dart-aot.md new file mode 100644 index 0000000..9844b3b --- /dev/null +++ b/references/dart-aot.md @@ -0,0 +1,254 @@ +# Dart AOT (Flutter) — reaching the layer that actually owns the logic + +Load this when: the target is a Flutter app (`libflutter.so` + `libapp.so`) and the behaviour you +must change is decided **inside** the Dart snapshot, not in the dex. The Java side of a Flutter app +is a thin plugin shell; a dex-only plan will stall (`framework-runtimes.md` §the layer trap). + +Everything below assumes you already confirmed the runtime is Flutter and that Java hooks fire +**zero times** while the UI plainly works. + +--- + +## 1. Pin the Dart version before choosing any tool + +The snapshot format is version-specific. A decompiler built for another Dart version may run and +produce plausible-but-wrong output, which is worse than failing. + +```python +import re +blob = open('libflutter.so', 'rb').read() # the engine, not libapp.so +m = re.search(rb'(\d+\.\d+\.\d+ \(stable\)[^\x00]{0,60})', blob) +print(m.group(1).decode() if m else 'not found') # e.g. b'3.9.0 (stable) (Mon ...) on "android_arm64"' +``` + +If the string is absent, fall back to the snapshot hash the loader reports at runtime, or to the +engine build id in `libflutter.so`'s version string. + +## 2. Get a decompiler that matches that version + +**blutter** is the practical choice for Android `libapp.so` (arm64). It reconstructs the object pool +and per-class declarations. It is a *source tree you build*, not a binary you download — and its +prebuilt outputs only cover the Dart versions someone already built. + +The version question is the first obstacle, and it has a clean answer: + +- blutter's git HEAD typically supports newer Dart than its shipped `dartsdk/` directory. +- When the target's version is missing, blutter fetches it itself: it sparse-clones + `dart-lang/sdk` at that version tag (only `runtime`, `tools`, `third_party/double-conversion`), + generates a source list, and builds a `dartvm` static library for the target ABI. +- That build needs a real toolchain: `cmake` (>=3.20 is fine, including 4.x), `ninja`, `git`, and a + C++ compiler with C++20 `` support (VS 2022 on Windows, gcc>=13 / clang>=16 elsewhere). + A `vcvars64`-style environment must be active before the build, and `CMakeLists` in the tree pins + `cmake_minimum_required(3.20)`. +- Budget **tens of minutes** for the first build of a new version and run it as a background job; + the compiler log is enormous (per-file include trees), so read only its tail. + +Outputs of interest: + +| Output | What it gives you | What it does *not* give | +|---|---|---| +| `pp.txt` | every object-pool entry with its `pp+0x…` offset: strings, types, closures, fields, stubs | who references what | +| `objs.txt` | reconstructed object shapes with type annotations | code | +| `asm/` | per-class declarations | **instructions — there are none here** | +| `ida_script/`, `blutter_frida.js` | symbol/annotation helpers | a finished analysis | + +> `asm/` having one file per (obfuscated) class invites the assumption that it holds disassembly. +> It does not. Do not plan around it. + +**`product` builds carry no debug info.** Expect `no-code_comments`, no function names, and +obfuscated identifiers. That is normal; the strings still survive (see §7). + +## 3. The object pool is the whole game + +Dart AOT uses **compressed pointers**: heap references are 32-bit offsets from a base held in a +dedicated register. Constants, strings, types, closures and field metadata live in one contiguous +**object pool**, reached through that register. + +Two offset spaces exist and they are **not the same number** — mixing them wastes hours: + +- **PP offsets** (`pp+0x…`) — what `pp.txt` and every pool-load in disassembly use. +- **file offsets** — where a byte actually sits in `libapp.so` on disk. + +Keep the mapping explicit in your notes. When a tool reports "this string is at X", state which +space X is in. + +## 4. Build your own reference index (blutter will not give you this) + +`pp.txt` tells you what is in the pool. To *find the code that uses it* you need pool-offset → +referencing-instruction-address. Generate it once, then query it constantly: + +``` +python dart_pprefs.py libapp.so pp_refs.json +python dart_pprefs.py --lookup pp_refs.json 0x1d1a8 0xc9d0 +``` + +**Do not build this index with a full-capstone pass.** Decoding every instruction of a multi-MB +`.text` with `detail=True` and querying `regs_access()` per instruction costs minutes of CPU and +1–2 GB of peak memory, and on a 16 GB host it presents as a hang with no progress output. Only three +encodings can read the pool, so decode them from the raw 32-bit words instead — the shipped script +does exactly that and finishes in seconds. The same argument applies to building a call graph +(§9): decode `B`/`BL` arithmetically. + +## 5. What the registers mean + +Stable across Dart 3.x arm64 AOT. Verify once on a known function, then rely on it. + +| Register | Role | +|---|---| +| `x27` | **object pool base (PP)** — every constant/string/type load goes through it | +| `x26` | current thread (`[x26,#0x38]` stack limit, `[x26,#0x68]` isolate group) | +| `x28` | heap base — how compressed pointers are decompressed | +| `x22` | **null/base for booleans** (see §6) | +| `x15` | Dart's own stack pointer (not the system SP) | +| `x21` | class dispatch table (virtual calls load a target from it) | + +## 6. Booleans are not 0/1 + +`true` and `false` are objects immediately adjacent to null, so they appear as small fixed offsets +from `x22`: + +```asm +add x0, x22, #0x20 ; construct TRUE +add x0, x22, #0x30 ; construct FALSE +tbnz w0, #4,