Android逆向学习(九) Xposed快速上手(下)

一、写在前面

这是吾爱破解正己大大教程的第九个作业,然后我的系统还是ubuntu,android测试机器用的是已经root的Redmi K30su。

上个博客介绍了xposed的配置方法和简单举例,本博客主要讲解关于xposed的其他用法。

二、任务目标

实现学习使用xposed常用API,并上手尝试,查看效果,了解xposed基本功能

本次博客量大管饱

三、实现方法

Xposed常用API

这些hook的实例来自于正己大大给出的apk中的com.zj.wuaipojie.Demo文件,可以通过APKLab逆向得到具体信息,APKLab的使用方法可以自行google或者看我的第一篇博客。

APKlab解码之后的代码是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package com.zj.wuaipojie;

import android.util.Log;
import java.util.ArrayList;
import java.util.HashMap;
import kotlin.Metadata;
import kotlin.jvm.internal.DefaultConstructorMarker;

/* compiled from: Demo.kt */
@Metadata(d1 = {"\u00006\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u000e\n\u0002\b\u0002\n\u0002\u0010\b\n\u0002\b\u0006\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\b\t\u0018\u0000 \u001c2\u00020\u0001:\u0003\u001b\u001c\u001dB\u0007\b\u0016¢\u0006\u0002\u0010\u0002B\u000f\b\u0002\u0012\u0006\u0010\u0003\u001a\u00020\u0004¢\u0006\u0002\u0010\u0005J\u001a\u0010\r\u001a\u00020\u000e2\n\u0010\u000f\u001a\u00060\u0010R\u00020\u00002\u0006\u0010\u0003\u001a\u00020\u0004J\u000e\u0010\u0011\u001a\u00020\u00042\u0006\u0010\u0003\u001a\u00020\u0004J4\u0010\u0012\u001a\u00020\u000e2\u0006\u0010\u0003\u001a\u00020\u00042\"\u0010\u0013\u001a\u001e\u0012\u0004\u0012\u00020\u0001\u0012\u0004\u0012\u00020\u00010\u0014j\u000e\u0012\u0004\u0012\u00020\u0001\u0012\u0004\u0012\u00020\u0001`\u0015H\u0002J\u0010\u0010\u0016\u001a\u00020\u000e2\u0006\u0010\u0003\u001a\u00020\u0004H\u0002J\b\u0010\u0017\u001a\u00020\u000eH\u0002J\b\u0010\u0018\u001a\u00020\u000eH\u0002J\u0010\u0010\u0019\u001a\u00020\u000e2\u0006\u0010\u0003\u001a\u00020\u0004H\u0002J\u0006\u0010\u001a\u001a\u00020\u000eR\u000e\u0010\u0006\u001a\u00020\u0007X\u0082D¢\u0006\u0002\n\u0000R\u001a\u0010\b\u001a\u00020\u0007X\u0086\u000e¢\u0006\u000e\n\u0000\u001a\u0004\b\t\u0010\n\"\u0004\b\u000b\u0010\f¨\u0006\u001e"}, d2 = {"Lcom/zj/wuaipojie/Demo;", "", "()V", "str", "", "(Ljava/lang/String;)V", "privateInt", "", "publicInt", "getPublicInt", "()I", "setPublicInt", "(I)V", "Inner", "", "animal", "Lcom/zj/wuaipojie/Demo$Animal;", "a", "complexParameterFunc", "map", "Ljava/util/HashMap;", "Lkotlin/collections/HashMap;", "privateFunc", "refl", "repleaceFunc", "staticPrivateFunc", "test", "Animal", "Companion", "InnerClass", "app_release"}, k = 1, mv = {1, 7, 1}, xi = 48)
/* loaded from: /tmp/jadx-14754525062353381193.dex */
public final class Demo {
private static final String Tag = "zj2595";
private final int privateInt;
private int publicInt;
public static final Companion Companion = new Companion((DefaultConstructorMarker) null);
private static final String staticField = "我是静态变量";

private Demo(String str) {
this.privateInt = 300;
this.publicInt = 200;
Log.d(Tag, "这是有参构造函数 || " + str);
}

public final int getPublicInt() {
return this.publicInt;
}

public final void setPublicInt(int i) {
this.publicInt = i;
}

public Demo() {
this(Tag);
Log.d(Tag, "这是无参构造函数");
}

public final String a(String str) {
return "这是一个" + str + "方法";
}

public final void test() {
Log.d(Tag, a("普通"));
Log.d(Tag, "staticInt = " + staticField);
Log.d(Tag, "publicInt = " + this.publicInt);
Log.d(Tag, "privateInt = " + this.privateInt);
Log.d(Tag, "privateInt = " + this.privateInt);
privateFunc("wuaipojie");
staticPrivateFunc("wuaipojie");
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("key", "value");
new ArrayList().add("listValue");
complexParameterFunc("wuaipojie", hashMap);
repleaceFunc();
Inner((Animal) new test.1(this), "wuaipojie");
new InnerClass(this).innerFunc("wuaipojie");
}

private final void privateFunc(String str) {
Log.d(Tag, "这是私有变量方法 || " + str);
}

private final void staticPrivateFunc(String str) {
Log.d(Tag, "这是静态私有变量方法 || " + str);
}

private final void complexParameterFunc(String str, HashMap<Object, Object> map) {
Log.d(Tag, "这是复杂参数方法 || " + str);
}

private final void repleaceFunc() {
Log.d(Tag, "这是替换函数");
}

public final void Inner(Animal animal, String str) {
Log.d(Tag, "这是自定义参数 ||" + str);
animal.eatFunc("wuaipojie");
}

private final void refl() {
Log.d(Tag, "this is fanshe publicInt " + this.publicInt);
}
}

1. hook静态变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class HookEntry : IXposedHookLoadPackage {
override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
if (lpparam.packageName != "com.zj.wuaipojie") return // 只处理目标应用

XposedBridge.log("程序开始执行")

try {
val clazz = XposedHelpers.findClass(
"com.zj.wuaipojie.Demo", // 目标类名
lpparam.classLoader
)

// 读取静态字段 staticField 的原始值
val originalValue = XposedHelpers.getStaticObjectField(clazz, "staticField")
XposedBridge.log("原始 staticField 值为: $originalValue")

// 修改静态字段的值
XposedHelpers.setStaticObjectField(clazz, "staticField", "我被 Hook 改掉了")

// 再次读取确认已修改
val newValue = XposedHelpers.getStaticObjectField(clazz, "staticField")
XposedBridge.log("修改后 staticField 值为: $newValue")
} catch (e: Throwable) {
XposedBridge.log("处理 staticField 失败: ${e.message}")
}
}
}

img

2. hook实例变量

需要注意的是,hook实例变量的话需要等类加载后才可以hook到

通过对源码的查询发现,是在实例中第六关中import了这个类,所以在hook的过程中,需要点击第六关才可以hook到这个变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.example.lsposedhook

import de.robv.android.xposed.IXposedHookLoadPackage
import de.robv.android.xposed.callbacks.XC_LoadPackage
import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.XposedHelpers

class HookEntry : IXposedHookLoadPackage {
override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
if (lpparam.packageName != "com.zj.wuaipojie") return

try {
XposedHelpers.findAndHookConstructor(
"com.zj.wuaipojie.Demo",
lpparam.classLoader,
object : de.robv.android.xposed.XC_MethodHook() {
override fun afterHookedMethod(param: MethodHookParam) {
val instance = param.thisObject
val clazz = instance.javaClass
val fields = clazz.declaredFields

XposedBridge.log("Demo 实例创建完成,打印字段:")

for (field in fields) {
try {
field.isAccessible = true
val value = field.get(instance)
XposedBridge.log("${field.name} = $value")
} catch (e: Throwable) {
XposedBridge.log("读取字段 ${field.name} 失败: ${e.message}")
}
}
}
}
)
} catch (e: Throwable) {
XposedBridge.log("Hook Demo 构造函数失败: ${e.message}")
}
}
}

同样查看LSposed日志,可以发现已经hook成功了。

img

3. Hook multiDex方法

首先解释一下什么是multiDex

android apk文件中,会有一个classes.dex文件,这个文件里面会包含所有的可执行代码,也就是在编写android应用的时候,所有的java或kotlin代码都会被编译成classes.dex文件,但是classes.dex文件有个特性,就是每个classes.dex里面只能包含65536个方法,如果代码太多了,构建工具gradle在构建的时候会把代码拆分成多个dex文件,这就是multiDex。

multiDex文件的特点就是,多个dex文件不一定会同时加载,比如现在只加载第一个classes.dex文件,剩下的classes2.dex还没加载,如果此时我们直接hook一个方法的话,不一定能找到这个方法,因为它很有可能在classes2.dex中,而这个没有加载,系统不知道这个方法在哪里。

为了应对这种情况,可以使用延迟hook的方式,也就是等到类实际加载时再进行 Hook。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.example.lsposedhook
import android.app.Application
import android.content.Context
import de.robv.android.xposed.IXposedHookLoadPackage
import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.XposedHelpers
import de.robv.android.xposed.callbacks.XC_LoadPackage

class HookEntry : IXposedHookLoadPackage {
override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
if (lpparam.packageName != "com.zj.wuaipojie") return

XposedBridge.log("开始 Hook ${lpparam.packageName}")

XposedHelpers.findAndHookMethod(
Application::class.java,
"attach",
Context::class.java,
object : XC_MethodHook() {
override fun afterHookedMethod(param: MethodHookParam) {
val context = param.args[0] as Context
val cl = context.classLoader
try {
val clazz = cl.loadClass("com.zj.wuaipojie.Demo")
XposedBridge.log("找到 Demo 类,准备 Hook 方法 a()")

XposedHelpers.findAndHookMethod(
clazz,
"a",
String::class.java,
object : XC_MethodHook() {
override fun beforeHookedMethod(param: MethodHookParam) {
XposedBridge.log("调用前参数: ${param.args[0]}")
}

override fun afterHookedMethod(param: MethodHookParam) {
param.result = "Hook 返回值"
XposedBridge.log("返回值已修改")
}
}
)
} catch (e: Exception) {
XposedBridge.log("找不到 Demo 类: ${e.message}")
}
}
}
)

}
}

img

4. 主动调用

主动调用就是通过LSposed插件直接调用应用中的一个函数

主要是通过getDeclaredConstructor().newInstance()创建demo实例

通过getDeclaredMethod(“a”, String::class.java)获取方法

通过invoke(demoInstance, “主动调用”)去调用这个方法,后面哪个是方法使用的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.example.lsposedhook

import android.app.Application
import android.content.Context
import de.robv.android.xposed.IXposedHookLoadPackage
import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XposedHelpers
import de.robv.android.xposed.callbacks.XC_LoadPackage

class HookEntry : IXposedHookLoadPackage {
override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
if (lpparam.packageName != "com.zj.wuaipojie") return

XposedBridge.log("开始 Hook ${lpparam.packageName}")

XposedHelpers.findAndHookMethod(
Application::class.java,
"attach",
Context::class.java,
object : XC_MethodHook() {
override fun afterHookedMethod(param: MethodHookParam) {
val context = param.args[0] as Context
val classLoader = context.classLoader

try {
val clazz = classLoader.loadClass("com.zj.wuaipojie.Demo")
XposedBridge.log("找到 Demo 类,准备调用方法")

// 创建 Demo 实例
val demoInstance = clazz.getDeclaredConstructor().newInstance()

// 获取方法
val method = clazz.getDeclaredMethod("a", String::class.java)

// 调用方法
val result = method.invoke(demoInstance, "主动调用")
XposedBridge.log("调用结果: $result")
} catch (e: Throwable) {
XposedBridge.log("主动调用失败: ${e.message}")
}
}
}
)
}
}

img

5. Hook内部类

内部类就是定义在一个类里面的类,下图是逆向出来的代码,可以看到出现类InnerClass,对应的smali代码就是Demo$InnerClass.smali,

img

innterClass逆向过来的源码是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.zj.wuaipojie;

import android.util.Log;
import kotlin.Metadata;

/* compiled from: Demo.kt */
@Metadata(d1 = {"\u0000 \n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\b\n\u0002\b\u0006\n\u0002\u0010\u0002\n\u0000\n\u0002\u0010\u000e\n\u0000\b\u0080\u0004\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\u000e\u0010\n\u001a\u00020\u000b2\u0006\u0010\f\u001a\u00020\rR\u000e\u0010\u0003\u001a\u00020\u0004X\u0082D¢\u0006\u0002\n\u0000R\u001a\u0010\u0005\u001a\u00020\u0004X\u0086\u000e¢\u0006\u000e\n\u0000\u001a\u0004\b\u0006\u0010\u0007\"\u0004\b\b\u0010\t¨\u0006\u000e"}, d2 = {"Lcom/zj/wuaipojie/Demo$InnerClass;", "", "(Lcom/zj/wuaipojie/Demo;)V", "innerPrivateInt", "", "innerPublicInt", "getInnerPublicInt", "()I", "setInnerPublicInt", "(I)V", "innerFunc", "", "str", "", "app_release"}, k = 1, mv = {1, 7, 1}, xi = 48)
/* loaded from: /tmp/jadx-11594512525937970463.dex */
public final class Demo$InnerClass {
final /* synthetic */ Demo this$0;
private int innerPublicInt = 1000;
private final int innerPrivateInt = 2000;

public Demo$InnerClass(Demo demo) {
this.this$0 = demo;
Log.d("zj2595", "这是内部类构造函数");
}

public final int getInnerPublicInt() {
return this.innerPublicInt;
}

public final void setInnerPublicInt(int i) {
this.innerPublicInt = i;
}

public final void innerFunc(String str) {
Log.d("zj2595", "这是内部类方法 || " + str);
Log.d("zj2595", "内部类变量 = " + this.innerPublicInt);
Log.d("zj2595", "内部类私有变量 = " + this.innerPrivateInt);
}
}

hook的方法就是下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.example.lsposedhook

import android.app.Application
import android.content.Context
import de.robv.android.xposed.IXposedHookLoadPackage
import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.XposedHelpers
import de.robv.android.xposed.callbacks.XC_LoadPackage

class HookEntry : IXposedHookLoadPackage {
override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
if (lpparam.packageName != "com.zj.wuaipojie") return

XposedBridge.log("Hook 开始执行")

XposedHelpers.findAndHookMethod(
Application::class.java,
"attach",
Context::class.java,
object : XC_MethodHook() {
override fun afterHookedMethod(param: MethodHookParam) {
val context = param.args[0] as Context
val cl = context.classLoader
try {
// 1. 获取内部类 class
val clazz = cl.loadClass("com.zj.wuaipojie.Demo\$InnerClass")
// 2. Hook 方法 innerFunc(String)
XposedHelpers.findAndHookMethod(
clazz,
"innerFunc",
String::class.java,
object : XC_MethodHook() {
override fun beforeHookedMethod(param: MethodHookParam) {
XposedBridge.log("innerFunc 调用前: ${param.args[0]}")
}

override fun afterHookedMethod(param: MethodHookParam) {
XposedBridge.log("innerFunc 调用后")
}
}
)
} catch (e: Throwable) {
XposedBridge.log("Hook 内部类失败: ${e.message}")
}
}
}
)
}
}

通过hook这个,可以获得这个方法调用时的输入。

img

6. 反射

Xposed 的 findAndHookMethod是利用底层 ART(Android Runtime)机制,在方法调用入口处“插钩子”(Hook),它默认不会搜索private方法,所以如果想Hook这类方法就要使用反射的操作,在正己大大给出的例子中就包含了这样一个私有方法。

1
2
3
private final void refl() {
Log.d(Tag, "this is fanshe publicInt " + this.publicInt);
}

如果没有使用反射的话,日志会输出:

查询日志的方法是在电脑上输入

1
adb logcat -s zj2595

img

如果使用反射的话,LSposed反射的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.example.lsposedhook

import de.robv.android.xposed.IXposedHookLoadPackage
import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.XposedHelpers
import de.robv.android.xposed.callbacks.XC_LoadPackage


class HookEntry : IXposedHookLoadPackage {
override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
if (lpparam.packageName != "com.zj.wuaipojie") return

XposedBridge.log("Hook 开始执行")

XposedHelpers.findAndHookMethod(
"com.zj.wuaipojie.Demo\$InnerClass",
lpparam.classLoader,
"innerFunc",
String::class.java,
object : XC_MethodHook() {
@Throws(Throwable::class)
override fun beforeHookedMethod(param: MethodHookParam) {
try {
// 1. 获取 Demo 类
val demoClass =
Class.forName("com.zj.wuaipojie.Demo", false, lpparam.classLoader)
// 2. 获取构造方法并创建实例
val demoInstance = demoClass.getDeclaredConstructor().newInstance()
// 3. 获取并调用私有方法
val reflMethod = demoClass.getDeclaredMethod("refl")
reflMethod.isAccessible = true
reflMethod.invoke(demoInstance)
} catch (t: Throwable) {
XposedBridge.log("反射失败: " + t.message)
}
}
}
)

}
}

这时候查看日志会发现:

img

出现了反射的调用例子。

7. 历所有类下的所有方法

正己大大的代码不知为啥一跑就报错,所以自己写了个,可以输出加载的所有类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.example.lsposedhook

import android.util.Log
import dalvik.system.DexFile
import de.robv.android.xposed.IXposedHookLoadPackage
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam


class HookEntry : IXposedHookLoadPackage {
@Throws(Throwable::class)
override fun handleLoadPackage(lpparam: LoadPackageParam) {
if (lpparam.packageName != "com.zj.wuaipojie") return
Log.d(TAG, "Loaded package: " + lpparam.packageName)
// 加载 Dex 文件
val dexFile = DexFile(lpparam.appInfo.sourceDir)
val classNames = dexFile.entries()

while (classNames.hasMoreElements()) {
val className = classNames.nextElement()

try {
val clazz = lpparam.classLoader.loadClass(className)
Log.d(TAG, "类: $className")

val methods = clazz.declaredMethods
for (method in methods) {
method.isAccessible = true // 反射访问私有方法
Log.d(TAG, " ↳ 方法: " + method.name + " | 参数: " + method.parameterCount)
}
} catch (t: Throwable) {
// 某些系统类加载会失败,忽略它们
Log.e(TAG, "加载类失败: " + className + ", 原因: " + t.message)
}
}
}

companion object {
private const val TAG = "XposedHook"
}
}

查看的命令是通过adb:

1
adb logcat | grep XposedHook

img

xposed其他妙用

字符串赋值定位:

加入我们遇到这种情况,就是发现了一个Text,想知道到底这个Text是怎么出现的,如何定位到这个Text的位置。那就可以通过字符串赋值的方法进行定位:

以挑战6为例,想要知道这个到期时间是怎么来的:

img

hook的代码如下,这串代码可以检测到文本,并且打印出相应的堆栈。:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.example.lsposedhook

import android.util.Log
import de.robv.android.xposed.IXposedHookLoadPackage
import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.XposedHelpers
import de.robv.android.xposed.callbacks.XC_LoadPackage


class HookEntry : IXposedHookLoadPackage {
override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
if (lpparam.packageName != "com.zj.wuaipojie") return

try {
XposedHelpers.findAndHookMethod(
"android.widget.TextView",
lpparam.classLoader,
"setText",
CharSequence::class.java,
object : XC_MethodHook() {
@Throws(Throwable::class)
override fun beforeHookedMethod(param: MethodHookParam) {
if (param.args[0] == null) return

val text = param.args[0].toString()
Log.d("XposedHook", "TextView.setText -> $text") //这个可以删掉,不然有点乱
// 关键词触发堆栈打印
if (text.contains("2023年03月16日")) {
Log.d("XposedHook", "检测到可疑文本: $text")
val ex = Throwable()
for (element in ex.stackTrace) {
Log.d(
"XposedHook", "↳ at " + element.className + "." +
element.methodName + "(" + element.fileName + ":" +
element.lineNumber + ")"
)
}
}
}
}
)

} catch (e: Throwable) {
XposedBridge.log("Hook Demo 构造函数失败: ${e.message}")
}
}
}

启动后还是在电脑上使用这个命令:

1
adb logcat | grep XposedHook

之后的结果是:

img

就可以看到这个文本是怎么来的了

点击事件监听

如果我们想要知道点击的按钮都触发了那些操作,就可以通过这种方式实现,代码如下:

原理就是hook “performClick” 这个方法,看会进行那些操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.example.lsposedhook

import android.util.Log
import de.robv.android.xposed.IXposedHookLoadPackage
import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.XposedHelpers
import de.robv.android.xposed.callbacks.XC_LoadPackage


class HookEntry : IXposedHookLoadPackage {
override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
if (lpparam.packageName != "com.zj.wuaipojie") return

try {
val viewClass = XposedHelpers.findClass("android.view.View", lpparam.classLoader)
XposedBridge.hookAllMethods(viewClass, "performClick", object : XC_MethodHook() {
@Throws(Throwable::class)
override fun afterHookedMethod(param: MethodHookParam) {
val viewObj = param.thisObject

try {
val listenerInfo = XposedHelpers.getObjectField(viewObj, "mListenerInfo")
val onClickListener =
XposedHelpers.getObjectField(listenerInfo, "mOnClickListener")

if (onClickListener != null) {
val callbackClass = onClickListener.javaClass.name
Log.d("XposedHook", "点击事件监听器: $callbackClass")

// 打印堆栈以分析点击来源
val ex = Throwable()
for (element in ex.stackTrace) {
Log.d(
"XposedHook", "↳ at " + element.className + "." +
element.methodName + "(" + element.fileName + ":" +
element.lineNumber + ")"
)
}
}
} catch (t: Throwable) {
Log.w("XposedHook", "获取点击监听器失败: $t")
}
}
})


} catch (e: Throwable) {
XposedBridge.log("Hook Demo 构造函数失败: ${e.message}")
}
}
}

效果就是:

img

改写布局

通过Hook onCreate,然后使用afterHookedMethod是指等页面 onCreate() 执行完之后我们再插手干预。

通过反射调用findViewById()方法,查找 ID 为 0x7f0800de 的 View

将这个控件设为“不可见”状态,即隐藏掉。

代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package com.example.lsposedhook

import android.os.Bundle
import android.util.Log
import android.view.View
import de.robv.android.xposed.IXposedHookLoadPackage
import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.XposedHelpers
import de.robv.android.xposed.callbacks.XC_LoadPackage


class HookEntry : IXposedHookLoadPackage {
override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
if (lpparam.packageName != "com.zj.wuaipojie") return

try {
val viewClass = XposedHelpers.findClass("android.view.View", lpparam.classLoader)
XposedBridge.hookAllMethods(viewClass, "performClick", object : XC_MethodHook() {
@Throws(Throwable::class)
override fun afterHookedMethod(param: MethodHookParam) {
val viewObj = param.thisObject

try {
XposedHelpers.findAndHookMethod(
"com.zj.wuaipojie.ui.ChallengeSixth",
lpparam.classLoader,
"onCreate",
Bundle::class.java,
object : XC_MethodHook() {
@Throws(Throwable::class)
override fun afterHookedMethod(param: MethodHookParam) {
try {
val activity = param.thisObject

val view = XposedHelpers.callMethod(
activity,
"findViewById",
0x7f08005b
) as View
if (view != null) {
Log.d("XposedHook", "找到View: " + view.javaClass.name)
view.visibility = View.GONE
} else {
Log.w("XposedHook", "未找到指定 View")
}
} catch (t: Throwable) {
Log.e("XposedHook", "隐藏控件失败: " + t.message)
}
}
}
)
} catch (t: Throwable) {
Log.w("XposedHook", "获取点击监听器失败: $t")
}
}
})
} catch (e: Throwable) {
XposedBridge.log("Hook Demo 构造函数失败: ${e.message}")
}
}
}

adb查询日志:

img

实现的效果前后对比:

原图:

img

hook后:

img

其中一个问题是,如何获得对应的viewID?

其实方法很简单:

逆向出来后我们会发现这个资源的代码:

img

直接转化过来就可以:

img

或者通过查询res资源下的代码,也可以找到对应的ID

img