背景
booster
是一款专门为移动应用设计的易用、轻量级且可扩展的质量优化框架,其目标主要是为了解决随着 APP 复杂度的提升而带来的性能、稳定性、包体积等一系列质量问题。
booster
项目地址:didi/booster: 🚀Optimizer for mobile applications (github.com)
booster
项目提供了非常多的拿来即用的好用的工具,包括通用 api
封装、性能优化插件、包体积插件、系统 bug 修复插件等等,具体详情可以参考项目 wiki
。 而我们今天,就将会使用过 booster
封装的 ASM api
来二次封装个简单的 hook
框架。
无处不在的 hook 需求
不管项目大小,总归有一些通过 hook
技术来进行切面编程的需求,比如:
- 检查或者禁用掉项目对于隐私敏感
API
的调用 - 特殊日子全局置灰,
Activity
可以通过ActivityLifecycleCallbacks
切入,但是像Dialog
、PopupWindow
散落各地且没法简单同意注入的就可以简单的hook
一下关键方法就行 - 想让项目中的
logcat
在线上不打印,也可以通过hook android.util.Log
来实现 so
文件不内置且需要动态下发,就可以通过hook System.load
进入到自定义的下载、检查、加载逻辑
当然,上面提到的这些只是一些比较基础的 hook
需求举例,甚至部分可能不通过 hook
也可以做到,这里只是作为需求引入。
好了,那么当我们有了 hook
的需求后,就要去想法子进行实现了,这很容易让人想到 Transform API
,gradle
比较远古的版本就支持了 Transform API
,允许第三方插件在将已编译的 class
文件转换为 dex
文件之前对其进行操作。所以我们今天的 hook 框架就用 Transform API
来实现。
那就会有同学问,你为啥不自己写个 Transform 插件,而要用 booster 呢?答案很简单,booster 封装过一层 API 后,使用起来更简单哈,「懒」
。
怎样进行 hook 更加简单
接下来,我们可以开始考虑考虑,我们的 hook
工具需要变成什么样子才会使用起来更加简单,结论显而易见:敲最少次数的键盘 + 容易阅读,那就是所谓的配置化了,配置化的优点有以下:
- 添加一个
hook
需求只需要做个简单配置 - 能一眼看全配置了哪些
hook
条目
给大家举个例子,当大家在项目中想要 hook
所有的Dialog#show()
方法,在弹任何 Dialog
的时候,全局进行计数,如果只需要写下面这些代码,是不是非常简单:
ini
HookClass(
needHookClass = "android/app/Dialog",
afterHookClass = "com/test/instrument/TestDialog",
hookMethod = HookMethod(methodName = "show")
)
#TestDialog.java
public static dialogCnt = 0;
public static void show(Dialog dialog) {
dialog.show();
dialogCnt++;
}
我们上面只干了以下几个事情:
- 指定要
hook
的类和方法 - 实现
hook
后的逻辑及指定hook
后的替换类和方法
这样大家就可以开心的进行想要的 hook
,而不需要思考如何去实现 hook
过程。
配置化的 hook 我们希望指定哪些条目可以配置
接下来我们可以开脑洞想想,我们在配置 hook
项时需要有哪些配置,其实大体可以分为两部分:
- 指定
hook
处的配置项 - 排除不想
hook
处的filter
那么我们可以大概列出来:
needHookClass
- 需要hook
的class
afterHookClass
-hook
后逻辑实现的class
hookMethods
- 需要被hook
的方法们
methodName
- 方法名methodDesc
- 方法描述,包括参数类型、返回值类型,比如(Ljava/lang/String;)Ljava/lang/String;
就表明入参是string
类型,返回值也是string
类型methodFilter
- 提供一个自定义的过滤器
doNotHookPackages
- 哪些包下面的被hook
类的指定方法调用不进行hook
,当然也可以实现成类似filter
的东西
当然,如果要做的更加完善,肯定还会有其他更多的配置项,我这里做演示只加这些哈
如何描述 hook 配置
一般来说,在 Android 项目中经常能见到的描述配置化的东西有以下几种方式:
- 类似
build.gradle
中的DSL
方案
groovy dsl
kotlin dsl
-
XML
配置解析 -
json
配置文件 -
其他
在这篇文章里,我们将一个不采用,为啥呢?嫌麻烦,由于 kotlin
支持命名参数,我们直接把配置写在 kt
代码里,key
用命名参数描述就行了,虽然用 dsl
方案会更加优雅。
那我们将这样描述我们的 hook
配置:
ini
//用 kotlin 的命名参数来描述 key-value,先突出一个格式整齐
val hookConfig = HookConfig(
listOf(
HookClass(
needHookClass = "android/app/ActivityManager",
afterHookClass = "com/test/instrument/ShadowActivityManager",
hookMethod = HookMethod(methodName = "getRunningAppProcesses")
),
HookClass(
needHookClass = "android/app/Dialog",
afterHookClass = "com/test/instrument/TestDialog",
hookMethod = HookMethod(methodName = "show")
),
HookClass(
needHookClass = "java/lang/System",
afterHookClass = "com/test/instrument/ShadowSystem",
hookMethods = listOf(
HookMethod(methodName = "load"),
HookMethod(methodName = "loadLibrary"),
),
doNotHookPackages = listOf("com/test/base/tools/soloader")
),
HookClass(
needHookClass = "android/util/Log",
afterHookClass = "com/test/instrument/ShadowLog",
hookMethods = listOf(
HookMethod(methodName = "v"),
HookMethod(methodName = "d"),
HookMethod(methodName = "i"),
HookMethod(methodName = "w"),
HookMethod(methodName = "e"),
HookMethod(methodName = "wtf"),
HookMethod(methodName = "println")
)
),
HookClass(
needHookClass = "android/content/pm/PackageManager",
afterHookClass = "com/taou/instrument/ShadowPackageManager",
hookMethods = listOf(
HookMethod(methodName = "getInstalledPackages"),
HookMethod(methodName = "getInstalledApplications"),
HookMethod(methodName = "queryIntentActivities"),
HookMethod(methodName = "getPackagesForUid"),
),
doNotHookPackages = listOf("com/tencent/")
),
)
)
上述这份配置表里将会涉及以下几个 class
文件。一个是 HookConfig
,就是我们所谓的配置,HookClass
是我们需要 hook
的目标类,HookMethod
是我们需要 hook
的目标方法。
kotlin
class HookConfig(val hookClassList: List<HookClass>)
data class HookClass(
val needHookClass: String,
val afterHookClass: String,
val hookMethods: List<HookMethod>,
val doNotHookPackages: List<String> = emptyList()
) {
constructor(
needHookClass: String,
afterHookClass: String,
hookMethod: HookMethod,
doNotHookPackages: List<String> = emptyList()
) : this(
needHookClass,
afterHookClass,
listOf(hookMethod),
doNotHookPackages
)
}
data class HookMethod(
val methodName: String,
val methodDesc: String = "",
val methodFilter: ((methodNode: MethodInsnNode) -> Boolean) = { _ -> true },
)
如何实现配置下就能完成 hook
我们参考How to Create Customized Transformer · didi/booster Wiki (github.com) 来自定义 Transformer
,具体实现步骤为:
-
自定义一个
HookTransformer
来实现ClassTransformer
接口,同时加上注解@AutoService(ClassTransformer::class)
-
将
HookTransformer
放到buildSrc
包中 -
没了。。
在我们实际的项目中,我们可能大部分的 hook
都是在 hook
某个对象方法或者静态方法,所以我们这篇文章里 只介绍针对对象方法、静态方法的 hook ,而且这两种场景也是最容易实现的,因此作为演示内容。
大家可以想一想,当我们要 hook
某个方法并且转到另一个实现方法时,怎么做会更简单呢?假如是一个对象方法:
ini
Dialog dialog = new Dialog(context);
dialog.show();
我们要 hook
它的话,有两种方式:
1. 继承 Dialog
重写 show
方法,同时将对象构造给替换掉,即:
scala
Dialog dialog = new DialogTest(context);
dialog.show();
public class DialogTest extends Dialog{
@override
publich void show(){
xxx
}
}
2. 替换 show 方法
替换掉 show
方法,转成静态方法实现,即:
java
Dialog dialog = new Dialog(context);
DialogUtils.show(dialog);
public class DialogUtils{
publich static void show(dialog){
xxx
dialog.show();
}
}
出于以下两点考虑,本文将采用转静态方法的 hook 方式来进行 hook:
- 实现简单,只需两步:
- 修改
opcode
为Opcodes.INVOKESTATIC
- 如果是对象方法,在方法的参数列表中加上
hookClass
本身,就可以将调用对象加在参数里进行传递
- 对象方法和静态方法的
hook
实现逻辑几乎没有区别
好,那就开整。
我们先描述一下我们的 hook 实现大体流程:
- 针对每个
class
文件,遍历class
的每个method
- 针对每个
method
,遍历操作指令,找出所有的方法指令 - 针对这些方法指令进行我们的
hook
过滤
owner
是我们的hookClass
owner
不能是我们的afterHookClass
owner
不能在我们的doNotHookPackages
里opcode
必须是INVOKESTATIC、INVOKEVIRTUAL、INVOKEINTERFACE
,opcode 参考methodName
匹配methodDesc
匹配methodFilter
不能过滤
- 针对每个需要
hook
的method
进行处理
-
opcode
统一替换成INVOKESTATIC
-
owner
统一替换成afterHookClass
-
如果是对象方法,在
desc
上,加上该对象参数
具体代码如下:
kotlin
//1. build.gradle 中 implementation 下 booster-api 的依赖
implementation "com.didiglobal.booster:booster-transform-asm:$boosterVersion"
//2. 实现自定义的 Transformer
@AutoService(ClassTransformer::class)
class HookTransformer : ClassTransformer {
override fun transform(context: TransformContext, klass: ClassNode): ClassNode {
klass.methods.forEach { method ->
method.instructions?.iterator()?.asIterable()
?.filterIsInstance(MethodInsnNode::class.java)?.let { methodInsnNode ->
for (i in methodInsnNode.indices) {
val methodNode = methodInsnNode[i]
//检查owner,先匹配当前方法调用的 owner,根据 owner 找到 HookClass,找不到则说明不需要hook
val hookClass = hookConfig.hookClassList.firstOrNull {
//1. owner要匹配,2. 当前处理的 class 不能是 afterHookClass,3. 当前处理的 class 不能在过滤的包中
(methodNode.owner == it.needHookClass || context.klassPool[it.needHookClass].isAssignableFrom(
methodNode.owner
)) &&
klass.name != it.afterHookClass &&
it.doNotHookPackages.none { doNotHookPackage ->
klass.name.startsWith(
doNotHookPackage
)
}
}
hookClass ?: continue
//检查方法类型不匹配,https://blog.csdn.net/LuoZheng4698729/article/details/104971966
if (methodNode.opcode != Opcodes.INVOKESTATIC && methodNode.opcode != Opcodes.INVOKEVIRTUAL && methodNode.opcode != Opcodes.INVOKEINTERFACE) {
continue
}
//检查方法名、方法描述不匹配,或者方法名、方法描述被过滤
var methodNeedHook = false
for (index in hookClass.hookMethods.indices) {
val hookMethod = hookClass.hookMethods[index]
if (hookMethod.methodName == methodNode.name
&& (hookMethod.methodDesc == methodNode.desc || hookMethod.methodDesc.isEmpty())
&& hookMethod.methodFilter(methodNode)
) {
methodNeedHook = true
break
}
}
if (!methodNeedHook) {
continue
}
//开始hook
methodNode.owner = hookClass.afterHookClass
//对象方法、接口方法需要调整为static方法
if (methodNode.opcode != Opcodes.INVOKESTATIC) {
methodNode.desc =
methodNode.desc.replace("(", "(L${hookClass.needHookClass};")
}
methodNode.opcode = Opcodes.INVOKESTATIC
methodNode.itf = false
}
}
}
return klass
}
}
写在最后
至此,我们实现了一个「实现容易,使用容易」
的 hook 框架,想要 hook
一个方法仅需要如下两步:
ini
1. 在指定配置文件里加上 hook 配置
HookClass(
needHookClass = "android/app/Dialog",
afterHookClass = "com/test/instrument/TestDialog",
hookMethod = HookMethod(methodName = "show")
)
2. 实现一下 hook 后的替换方法
#TestDialog.java
public static dialogCnt = 0;
public static void show(Dialog dialog) {
dialog.show();
dialogCnt++;
}
当然,由于是基础版本,实际上并没有实现的非常完善,比如:
- 未实现构造函数的
hook
- 只能
hook
项目代码,无法hook Framework
代码 - 无法
hook native
代码
但是,这并不影响这个 hook
框架非常好用哈。
展望
后续我会基于本文中描述的 hook 框架:
- 进行功能扩展,支持 hook 构造方法
- 分享相关的使用案例,如 so 动态下载、全局置灰等,当支持 hook 构造方法后,就可以用来做线程优化了。
- 其他...