Files
apk-reverse/scripts/dexpatch/README.md
T
inliver233 c66658b6b3 feat: apk-reverse Agent Skill
Progressive-disclosure skill for Android APK reverse engineering:
recon, ad removal, membership limits, dex surgical patching, repack/sign,
runtime data, Frida, environment setup, and a hard-won failure catalogue.
2026-09-19 21:25:35 +08:00

79 lines
3.3 KiB
Markdown

# dexlib2 method-level patcher
The **preferred** way to change app behavior. It replaces only a target method's
implementation and writes a new dex, leaving every other class, method, string and
reference untouched.
Why not whole-tree smali round-trip: `baksmali` → `smali` rebuilds misrepresent R8's
synthetic access bridges. The class table still looks perfect — same class count, zero
`ACC_INTERFACE` mismatches — but at runtime you get
`IncompatibleClassChangeError: Found interface X, but class was expected`.
See `../../references/pitfalls.md` P3.
## Build
Needs the same eight jars `smtool.py` uses:
```
smali-2.5.2.jar antlr-runtime-3.5.2.jar stringtemplate-3.2.1.jar
baksmali-2.5.2.jar util-2.5.2.jar jcommander-1.64.jar
guava-27.1-android.jar dexlib2-2.5.2.jar
```
```bash
# point at your jars (or reuse ../smali_cp.txt)
CP=$(cat ../smali_cp.txt | grep -v '^#' | tr '\n' ';' | sed 's/;$//')
javac -encoding UTF-8 -cp "$CP" PatchMethod.java
```
**Always pass `-encoding UTF-8`** if the source has non-ASCII comments — `javac`
otherwise reads them with the platform default encoding and fails.
## Run
```bash
java -cp "$CP;." PatchMethod <in.dex> <out.dex> <descriptor> <methodName> <returnType>
# example: make Lx6;->d(...)V a no-op
java -cp "$CP;." PatchMethod classes7.dex out.dex 'Lx6;' d V
```
`PatchMethod` replaces the body with `return-void`. It scans **both** `directMethods`
and `virtualMethods` — a method you cannot find is often in the other list
(`static`/`private`/`<init>` are direct; everything else, including `public final`,
is virtual).
Use `PatchMethodExample.java` as the template for anything non-trivial (returning a
constant, emitting a fixed value from a Flow lambda, applying **several** edits to the
same dex in one pass).
## Rules that decide success
1. **One dex, one write pass.** Never chain two patchers over the same dex — the second
serialization drops metadata and ART then refuses to start the process
(`Failure starting process`, no Java stack). Combine all edits into one program.
2. **Preserve the register count.** `registers` must be `>=` the parameter register
count. With `registers = N` and `k` parameters (counting `this` for non-static),
`p0 = N - k`. For `(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;`
on an instance method, `k = 3`, so with `registers = 6` you get `p0=3, p1=4, p2=5`.
3. **`const-wide` needs the 64-bit instruction form** (`ImmutableInstruction51l`), not
the 32-bit one.
4. **`invoke` operand order in `35c`** is `(opcode, registerCount, regC, regD, regE, regF, regG, ref)`.
`invoke-static {v1, v2}, ...` → `registerCount=2, regC=1, regD=2`.
5. **End every body with a return of the correct type.**
6. **Check the blast radius first** — `../find_refs.py`. A generic helper with dozens of
callers is not your patch point (`../../references/pitfalls.md` P6).
## Verify
```bash
python ../dex_classdiff.py <original.dex> <patched.dex>
python ../smtool.py d <patched.dex> <tmp_tree> # confirm it parses
# then read the target method in tmp_tree to confirm it says what you intended
```
Expect `only_in_A=0`, `only_in_B=0`, `ACC_INTERFACE mismatch: 0`. Then install and
launch — table checks cannot see code-item damage, so runtime verification is
mandatory (`../../references/verification.md`).