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.
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import org.jf.dexlib2.DexFileFactory;
|
||||
import org.jf.dexlib2.Opcode;
|
||||
import org.jf.dexlib2.Opcodes;
|
||||
import org.jf.dexlib2.iface.ClassDef;
|
||||
import org.jf.dexlib2.iface.Method;
|
||||
import org.jf.dexlib2.iface.MethodImplementation;
|
||||
import org.jf.dexlib2.builder.MutableMethodImplementation;
|
||||
import org.jf.dexlib2.immutable.ImmutableMethod;
|
||||
import org.jf.dexlib2.writer.pool.DexPool;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* dexlib2 定点 patch:把指定方法的实现整体替换为 return-void(或 return 常量)。
|
||||
*
|
||||
* 为什么不用 baksmali/smali 整树往返:实测本项目的 classes7.dex 往返重建后运行时报
|
||||
* IncompatibleClassChangeError: Found interface io.ktor.client.engine.HttpClientEngine,
|
||||
* but class was expected
|
||||
* (R8 生成的 synthetic access bridge 被破坏)。本工具只改目标方法的 code_item,
|
||||
* 其余类/方法/字符串表原样保留,因此不会有这类损伤。
|
||||
*
|
||||
* 用法: java PatchMethod <in.dex> <out.dex> <descriptor> <methodName> <returnType>
|
||||
* 例: java PatchMethod classes7.dex out.dex Lx6; d V
|
||||
*/
|
||||
public class PatchMethod {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
if (args.length < 5) {
|
||||
System.err.println("usage: PatchMethod <in.dex> <out.dex> <descriptor> <methodName> <returnType>");
|
||||
System.exit(2);
|
||||
}
|
||||
String inPath = args[0], outPath = args[1], desc = args[2], mName = args[3], ret = args[4];
|
||||
|
||||
org.jf.dexlib2.iface.DexFile dex =
|
||||
DexFileFactory.loadDexFile(new File(inPath), Opcodes.forApi(34));
|
||||
|
||||
List<ClassDef> outClasses = new ArrayList<ClassDef>();
|
||||
int patched = 0;
|
||||
|
||||
for (ClassDef cd : dex.getClasses()) {
|
||||
List<Method> direct = new ArrayList<Method>();
|
||||
for (Method m : cd.getDirectMethods()) direct.add(m);
|
||||
List<Method> virtual = new ArrayList<Method>();
|
||||
for (Method m : cd.getVirtualMethods()) virtual.add(m);
|
||||
boolean isTargetClass = cd.getType().equals(desc);
|
||||
boolean changed = false;
|
||||
|
||||
if (isTargetClass) {
|
||||
for (int pass = 0; pass < 2; pass++) {
|
||||
List<Method> list = (pass == 0) ? direct : virtual;
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
Method m = list.get(i);
|
||||
if (!m.getName().equals(mName) || !m.getReturnType().equals(ret)) continue;
|
||||
// 用 return-void 替换整个方法体。
|
||||
// 寄存器数必须保留原值:Compose/协程方法体里可能仍被其它指令引用同一寄存器窗口,
|
||||
// 且 dex 校验要求 registers >= 参数寄存器数。
|
||||
int regs = m.getImplementation() != null ? m.getImplementation().getRegisterCount() : 0;
|
||||
MethodImplementation impl = new org.jf.dexlib2.immutable.ImmutableMethodImplementation(
|
||||
regs,
|
||||
java.util.Collections.singletonList(
|
||||
new org.jf.dexlib2.immutable.instruction.ImmutableInstruction10x(Opcode.RETURN_VOID)),
|
||||
null,
|
||||
null);
|
||||
Method nm = new ImmutableMethod(
|
||||
m.getDefiningClass(), m.getName(), m.getParameters(), m.getReturnType(),
|
||||
m.getAccessFlags(), m.getAnnotations(), m.getHiddenApiRestrictions(),
|
||||
impl);
|
||||
list.set(i, nm);
|
||||
patched++;
|
||||
changed = true;
|
||||
System.out.println("[patch] " + desc + "->" + mName + m.getReturnType()
|
||||
+ " registers=" + regs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
outClasses.add(cd);
|
||||
} else {
|
||||
outClasses.add(new org.jf.dexlib2.immutable.ImmutableClassDef(
|
||||
cd.getType(), cd.getAccessFlags(), cd.getSuperclass(),
|
||||
cd.getInterfaces(), cd.getSourceFile(), cd.getAnnotations(),
|
||||
cd.getStaticFields(), cd.getInstanceFields(), direct, virtual));
|
||||
}
|
||||
}
|
||||
|
||||
DexFileFactory.writeDexFile(outPath,
|
||||
new org.jf.dexlib2.immutable.ImmutableDexFile(Opcodes.forApi(34), outClasses));
|
||||
System.out.println("[done] patched=" + patched + " -> " + outPath);
|
||||
if (patched == 0) {
|
||||
System.err.println("[warn] no method matched!");
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import org.jf.dexlib2.DexFileFactory;
|
||||
import org.jf.dexlib2.Opcode;
|
||||
import org.jf.dexlib2.Opcodes;
|
||||
import org.jf.dexlib2.iface.ClassDef;
|
||||
import org.jf.dexlib2.iface.Method;
|
||||
import org.jf.dexlib2.iface.instruction.Instruction;
|
||||
import org.jf.dexlib2.immutable.ImmutableClassDef;
|
||||
import org.jf.dexlib2.immutable.ImmutableDexFile;
|
||||
import org.jf.dexlib2.immutable.ImmutableMethod;
|
||||
import org.jf.dexlib2.immutable.ImmutableMethodImplementation;
|
||||
import org.jf.dexlib2.immutable.instruction.ImmutableInstruction11x;
|
||||
import org.jf.dexlib2.immutable.instruction.ImmutableInstruction22c;
|
||||
import org.jf.dexlib2.immutable.instruction.ImmutableInstruction35c;
|
||||
import org.jf.dexlib2.immutable.instruction.ImmutableInstruction51l;
|
||||
import org.jf.dexlib2.immutable.reference.ImmutableFieldReference;
|
||||
import org.jf.dexlib2.immutable.reference.ImmutableMethodReference;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* classes7.dex 定点改动(单次读写,绝不要连续重写两次)。
|
||||
*
|
||||
* 只做一件事:把免广告到期时间的读取 lambda 改成恒返回"未过期"。
|
||||
*
|
||||
* VideoDataStore$currentAdFreeExpiresAt$$inlined$map$1$2.emit(Object; Continuation)Object
|
||||
* -> 无条件向下游 collector 发一个超大 Long
|
||||
*
|
||||
* 效果等价于 DataStore 里 ad_free_expires_at 永远没有过期,
|
||||
* 「看广告领特权」弹窗因此不再出现,而且这个改动写死在代码里,
|
||||
* 全新安装同样生效(DataStore 那份是运行时数据,随 APK 分发不了)。
|
||||
*
|
||||
* ⚠️ 不要顺手 patch Lx6;->d:那是通用的图片卡片 Composable,
|
||||
* 内容和广告共用,改成 return-void 会导致正常图片/播放链路一起失效。
|
||||
*/
|
||||
public class PatchClasses7v2 {
|
||||
|
||||
private static final String AD_CLASS =
|
||||
"Lcyc/data/datastore/VideoDataStore$currentAdFreeExpiresAt$$inlined$map$1$2;";
|
||||
private static final long BIG = 4102444800000L; // ~2100-01-01 (ms)
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
String in = args[0], out = args[1];
|
||||
org.jf.dexlib2.iface.DexFile dex =
|
||||
DexFileFactory.loadDexFile(new File(in), Opcodes.forApi(34));
|
||||
|
||||
List<ClassDef> outClasses = new ArrayList<ClassDef>();
|
||||
int p = 0;
|
||||
|
||||
for (ClassDef cd : dex.getClasses()) {
|
||||
List<Method> direct = new ArrayList<Method>();
|
||||
for (Method m : cd.getDirectMethods()) direct.add(m);
|
||||
List<Method> virtual = new ArrayList<Method>();
|
||||
for (Method m : cd.getVirtualMethods()) virtual.add(m);
|
||||
boolean touched = false;
|
||||
|
||||
if (cd.getType().equals(AD_CLASS)) {
|
||||
for (int pass = 0; pass < 2; pass++) {
|
||||
List<Method> list = (pass == 0) ? direct : virtual;
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
Method m = list.get(i);
|
||||
if (!m.getName().equals("emit")
|
||||
|| !m.getReturnType().equals("Ljava/lang/Object;")
|
||||
|| m.getImplementation() == null) {
|
||||
continue;
|
||||
}
|
||||
List<Instruction> body = new ArrayList<Instruction>();
|
||||
// v0 = collector, v1:v2 = long, p0=3 p1=4 p2=5
|
||||
body.add(new ImmutableInstruction22c(Opcode.IGET_OBJECT, 0, 3,
|
||||
new ImmutableFieldReference(AD_CLASS, "b", "Lkw2;")));
|
||||
body.add(new ImmutableInstruction51l(Opcode.CONST_WIDE, 1, BIG));
|
||||
body.add(new ImmutableInstruction35c(Opcode.INVOKE_STATIC, 2, 1, 2, 0, 0, 0,
|
||||
new ImmutableMethodReference("Ljava/lang/Long;", "valueOf",
|
||||
Collections.singletonList("J"), "Ljava/lang/Long;")));
|
||||
body.add(new ImmutableInstruction11x(Opcode.MOVE_RESULT_OBJECT, 1));
|
||||
body.add(new ImmutableInstruction35c(Opcode.INVOKE_INTERFACE, 3, 0, 1, 5, 0, 0,
|
||||
new ImmutableMethodReference("Lkw2;", "emit",
|
||||
Arrays.asList("Ljava/lang/Object;", "Lwj1;"),
|
||||
"Ljava/lang/Object;")));
|
||||
body.add(new ImmutableInstruction11x(Opcode.MOVE_RESULT_OBJECT, 0));
|
||||
body.add(new ImmutableInstruction11x(Opcode.RETURN_OBJECT, 0));
|
||||
|
||||
list.set(i, new ImmutableMethod(m.getDefiningClass(), m.getName(),
|
||||
m.getParameters(), m.getReturnType(), m.getAccessFlags(),
|
||||
m.getAnnotations(), m.getHiddenApiRestrictions(),
|
||||
new ImmutableMethodImplementation(6, body, null, null)));
|
||||
p++;
|
||||
touched = true;
|
||||
System.out.println("[patch] emit -> BIG=" + BIG + " (list=" + pass + ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!touched) outClasses.add(cd);
|
||||
else outClasses.add(new ImmutableClassDef(
|
||||
cd.getType(), cd.getAccessFlags(), cd.getSuperclass(), cd.getInterfaces(),
|
||||
cd.getSourceFile(), cd.getAnnotations(), cd.getStaticFields(),
|
||||
cd.getInstanceFields(), direct, virtual));
|
||||
}
|
||||
|
||||
DexFileFactory.writeDexFile(out, new ImmutableDexFile(Opcodes.forApi(34), outClasses));
|
||||
System.out.println("[done] patched=" + p);
|
||||
if (p == 0) System.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
# 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`).
|
||||
Reference in New Issue
Block a user