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,