Hook知识点记录

Hook 直译为中文就是“钩子”。在 Android 平台上,Hook 的目标事件的粒度一般为函数级别,即在指定函数被调用时,根据我们的逻辑拦截修改原有的函数。


通过 Hook 实现 AOP 编程、插桩性能监控、热补丁、在线修复等

hook技术:
1.反射:虚拟机提供的能力,稳定高但范围小(java部分)
2.ClassLoader:修改类的加载顺序,需要准备好dex(java全部)
3.Xposed:适配性差,每个版本需要改动(java全部)
当然还有其他技术。。。


反射(android9.0限制@hide被反射)

Android P 9.0 引入了针对非 SDK 接口(俗称为隐藏API @hide)的使用限制。这是继 Android N上针对 NDK 中私有库的链接限制之后的又一次重大调整。从今以后,不论是native层的NDK还是 Java层的SDK,我们只能使用Google提供的、公开的标准接口。

如果不是系统类,且API处于黑名单,禁止调用

android 9.0 10.0 可以使用【元反射】方法:
通过反射去获取Class.class类上getDeclaredMethod方法,获取到的Method可以称为“元反射方法”,通过“元反射方法” 去调用隐藏API,就意味着调用者是java.lang.Class,这个类属于系统类,可以正常调用

比如:

1
2
3
4
5
6
7
8
9
10
public static Field getDeclaredField(Class<?> cls, String name) {
try {
Method dMethod = Class.class.getDeclaredMethod("getDeclaredField", String.class);

return (Field) dMethod.invoke(cls, name);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

https://github.com/tiann/FreeReflection

android 11.0 上【元反射】开始失效,因为增加了 调用者上下文判断 机制。机制给 调用者 跟 被调用的方法 增加了domian,调用者domain要等于或者小于 被调用的方法domain,才能允许访问。

1
2
3
4
5
enum class Domain : char { //Domain范围
kCorePlatform = 0, // 0
kPlatform, // 1
kApplication, // 2
};

https://bbs.kanxue.com/thread-268936.htm

https://github.com/whulzz1993/RePublic


Hook 选择的关键点

Hook 的选择点:

尽量静态变量和单例,因为一旦创建对象,它们不容易变化,非常容易定位

Hook 过程:

寻找 Hook 点,原则是尽量静态变量或者单例对象,尽量 Hook public 的对象和方法。
选择合适的代理方式,如果是接口可以用动态代理。
偷梁换柱——用代理对象替换原始对象。
Android 的 API 版本比较多,方法和类可能不一样,所以要做好 API 的兼容工作。

Hook 必须掌握的知识

反射

需要了解反射操作

java 的动态代理(动态代理机制InvocationHandler)

动态代理是指在运行时动态生成代理类,不需要我们像静态代理那个去手动写一个个的代理类。在 java 中,我们可以使用 InvocationHandler 实现动态代理,有兴趣的,可以查看我的这一篇博客 java 代理模式详解


Hook举例

Hook Click(不过这种方式没有做到普遍性,需要自己手动取每个地方设置)
1
2
3
4
5
6
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}

反射getListenerInfo方法,在反射获取mOnClickListener变量

//先设置ClickListener,再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
TextView tx = findViewById(R.id.tx);
tx.setOnClickListener(…);
HookClickManager.hook(tx);

//其实就是给ClickListener外加一层自己的逻辑,内部还是ClickListener执行
public static void hook(View v) {
try {
//获取listnerInfo实例
Method method = View.class.getDeclaredMethod("getListenerInfo");
method.setAccessible(true);
Object listnerInfo = method.invoke(v);

//获取listnerInfo类的View.OnClickListener变量
Class c = Class.forName("android.view.View$ListenerInfo");
Field field = c.getDeclaredField("mOnClickListener");
View.OnClickListener clickListener = (View.OnClickListener) field.get(listnerInfo);

//给listnerInfo的View.OnClickListener变量设置新值
ProxyOnClickListener proxy = new ProxyOnClickListener(clickListener);
field.set(listnerInfo, proxy);

} catch (Exception e) {
e.printStackTrace();
}
}

public class ProxyOnClickListener implements View.OnClickListener {
View.OnClickListener listener;
public ProxyOnClickListener(View.OnClickListener l) {
this.listener = l;
}

@Override
public void onClick(View v) {
Log.d("ProxyOnClickListener", "this is hook");
if (listener != null) {
listener.onClick(v);
}
}
}
HooK Activity

hook mInstrumentation即可

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
HookActivityManager.hook(this);
startActivity(new Intent(this, xxxActivity.class))

public class HookActivityManager {
public static void hook(Activity activity) {
try {
Field mInstrumentation = Activity.class.getDeclaredField("mInstrumentation");
mInstrumentation.setAccessible(true);
Instrumentation instrumentation = (Instrumentation) mInstrumentation.get(activity);
ProxyInstrumentation proxy = new ProxyInstrumentation(instrumentation);
mInstrumentation.set(activity, proxy);
} catch (Exception e) {
e.printStackTrace();
}
}
}


public class ProxyInstrumentation extends Instrumentation {
private Instrumentation instrumentation;
public ProxyInstrumentation(Instrumentation i) {
this.instrumentation = i;
}
@SuppressLint("DiscouragedPrivateApi")
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
Log.d("ProxyInstrumentation", "Proxy Instrumentation success !");
try {
Class<?> c = Class.forName("android.app.Instrumentation");
Method execStartActivity = c.getDeclaredMethod(
"execStartActivity",
Context.class, IBinder.class, IBinder.class, Activity.class,
Intent.class, int.class, Bundle.class);
return (ActivityResult) execStartActivity.invoke(instrumentation, who, contextThread, token, target, intent, requestCode, options);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

Lancet本质上是 Gradle Plugin,通过依赖 Android 的打包插件提供的 Transform API,在打包过程中获取到所有的代码。依赖 ASM 提供的字节码注入能力,通过我们解析自定义的注解,在目标点注入相应的代码。通过注解进行 AOP 这点和 AspectJ 很相似,但是更加轻量和简洁,使用方式也有所不同。

这里要区分一个概念,编译期注入和运行期注入。

编译期:即在编译时对字节码做插桩修改,达到 AOP 的目的。优点是运行时无额外的性能损耗,但因为编译时的限制,只能修改最终打包到APK中的代码,即 Android Framework 的代码是固化在 ROM 中的,无法修改。运行期:是只在运行时动态的修改代码的执行,因而可以修改 Framework 中代码的执行流程,在hook点上执行性能上有所损耗。

Lancet,编译期注入
AspectJ,既支持编译期也支持运行期的注入,运行期的注入一般是依赖JVM提供的AttachAPI,因为Android 没有 JVM 的环境,实际上 class 还会继续转换成 dex,因此 AspectJ 在 Android 平台是只能做到编译期注入
Dexposed,运行期注入


参考文章:
https://juejin.cn/post/7023717952180617247