Files
apk-reverse/scripts/dexpatch
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
..
2026-09-19 21:25:35 +08:00
2026-09-19 21:25:35 +08:00

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
# 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

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

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).