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.
3.3 KiB
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
- 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. - Preserve the register count.
registersmust be>=the parameter register count. Withregisters = Nandkparameters (countingthisfor non-static),p0 = N - k. For(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;on an instance method,k = 3, so withregisters = 6you getp0=3, p1=4, p2=5. const-wideneeds the 64-bit instruction form (ImmutableInstruction51l), not the 32-bit one.invokeoperand order in35cis(opcode, registerCount, regC, regD, regE, regF, regG, ref).invoke-static {v1, v2}, ...→registerCount=2, regC=1, regD=2.- End every body with a return of the correct type.
- Check the blast radius first —
../find_refs.py. A generic helper with dozens of callers is not your patch point (../../references/pitfalls.mdP6).
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).