1. 什么是 Hook
Hook 英文翻译过来就是「钩子」的意思,那我们在什么时候使用这个「钩子」呢?在 Android 操作系统中系统维护着自己的一套事件分发机制。应用程序,包括应用触发事件和后台逻辑处理,也是根据事件流程一步步地向下执行。而「钩子」的意思,就是在事件传送到终点前截获并监控事件的传输,像个钩子钩上事件一样,并且能够在钩上事件时,处理一些自己特定的事件。
Hook 的这个本领,使它能够将自身的代码「融入」被勾住(Hook)的程序的进程中,成为目标进程的一个部分。API Hook 技术是一种用于改变 API 执行结果的技术,能够将系统的 API 函数执行重定向。在 Android 系统中使用了沙箱机制,普通用户程序的进程空间都是独立的,程序的运行互不干扰。这就使我们希望通过一个程序改变其他程序的某些行为的想法不能直接实现,但是 Hook 的出现给我们开拓了解决此类问题的道路。当然,根据 Hook 对象与 Hook 后处理的事件方式不同,Hook 还分为不同的种类,比如消息 Hook、API Hook 等。
2. Hook的应用场景。
Hook的应用非常广泛,不仅开发人员会用到,攻击者也会用到。
开发有:对程序的执行记录日志、防止应用重复启动等。
攻击有:使用hook拦截用户输入信息,获取键盘数据等。
3. Hook的技术方式或框架。
- inline hook方式:目标函数执行指令中插入Jump跳转指令实现重定向
- 动态代理方式:思路应该是类似于设计模式中的代理模式,代理原本的函数的执行
- Method Swizzle方式:动态改变SEL(方法编号)与IMP(方法实现)的对应关系
- Cydia Substrate方式:适用于iOS和andriod,定义了一系列的函数和宏,底层调用了objc的runtime和fishHook来替代目标函数或者系统方法
- fishHook方式:是Facebook提供一种动态修改链接Mach-O文件的工具。此利用Mach-O文件加载原理,通过修改非懒加载和懒加载两个表的指针达到C函数的Hook的目的
- Xposed框架:目标函数为native,利用JNI hook重定向表中的函数指针
- Legend框架:Android 免 Root 环境下的一个 Apk Hook 框架,该框架代码设计简洁,通用性高,适合逆向工程时一些 Hook 场景。大部分的功能都放到了 Java 层,兼容性非常好。原理是直接构造出新旧方法对应的虚拟机数据结构,然后替换信息写到内存中即可
4. Hook的一般步骤和技巧。
- 寻找 Hook 点。原则是尽可能是静态变量或者单例对象,因为它们容易定位,其次是尽量 Hook public 的对象和方法。
- 选择适当的hook方式或框架。
- 将hook代码注入到目标程序的运行内存中。
实战
我们自己的代码里面,给一个view设置了点击事件,现在要求在不改动这个点击事件的情况下,添加额外的点击事件逻辑.
View v = findViewById(R.id.tv);
v.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "别点啦,再点我咬你了...", Toast.LENGTH_SHORT).show();
}
});
这是view的点击事件,toast了一段话,现在要求,不允许改动这个OnClickListener,要在toast之前添加日志打印 Log.d(…).
按照上面的思路来:
第一步:根据需求 确定要hook的对象;
我们的目的是在OnClickListener中,插入自己的逻辑.所以要hook的是v.setOnClickListener()方法的实参。
第二步:寻找要hook的对象的持有者,拿到要hook的对象
进入v.setOnClickListener源码:发现我们创建的OnClickListener对象被赋值给了getListenerInfo().mOnClickListener
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
继续索引:getListenerInfo() 是个什么玩意?继续追查:
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
结果发现这个其实是一个伪单例,一个View对象中只存在一个ListenerInfo对象. 进入ListenerInfo内部:发现OnClickListener对象 被ListenerInfo所持有.
static class ListenerInfo {
...
public OnClickListener mOnClickListener;
...
}
到这里为止,完成第二步,找到了点击事件的实际持有者:ListenerInfo .
第三步:定义“要hook的对象”的代理类,并且创建该类的对象
我们要hook的是View.OnClickListener对象,所以,创建一个类 实现View.OnClickListener接口.
static class ProxyOnClickListener implements View.OnClickListener {
View.OnClickListener oriLis;
public ProxyOnClickListener(View.OnClickListener oriLis) {
this.oriLis = oriLis;
}
@Override
public void onClick(View v) {
Log.d("HookSetOnClickListener", "点击事件被hook到了");
if (oriLis != null) {
oriLis.onClick(v);
}
}
}
然后,创建出一个代理对象
ProxyOnClickListener proxyOnClickListener = new ProxyOnClickListener(onClickListenerInstance);
可以看到,这里传入了一个View.OnClickListener对象,它存在的目的,是让我们可以有选择地使用到原先的点击事件逻辑。一般hook,都会保留原有的源码逻辑.
另外提一句:当我们要创建的代理类,是被接口所约束的时候,比如现在,我们创建的ProxyOnClickListener implements View.OnClickListener,只实现了一个接口,则可以使用JDK提供的Proxy类来创建代理对象
Object proxyOnClickListener = Proxy.newProxyInstance(context.getClass().getClassLoader(),
new Class[]>>{View.OnClickListener.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Log.d("HookSetOnClickListener", "点击事件被hook到了");//加入自己的逻辑
return method.invoke(onClickListenerInstance, args);//执行被代理的对象的逻辑
}
});
到这里为止,第三步:定义“要hook的对象”的代理类,并且创建该类的对象 完成。
第四步:使用上一步创建出来的对象,替换掉要hook的对象,达成 偷梁换柱的最终目的. 利用反射,将我们创建的代理点击事件对象,传给这个view field.set(mListenerInfo, proxyOnClickListener);
这里,贴出最终代码:
辅助类
/**
* hook的辅助类
* hook的动作放在这里
*/
public class HookSetOnClickListenerHelper {
/**
* hook的核心代码
* 这个方法的唯一目的:用自己的点击事件,替换掉 View原来的点击事件
*
* @param v hook的范围仅限于这个view
*/
public static void hook(Context context, final View v) {//
try {
// 反射执行View类的getListenerInfo()方法,拿到v的mListenerInfo对象,这个对象就是点击事件的持有者
Method method = View.class.getDeclaredMethod("getListenerInfo");
method.setAccessible(true);//由于getListenerInfo()方法并不是public的,所以要加这个代码来保证访问权限
Object mListenerInfo = method.invoke(v);//这里拿到的就是mListenerInfo对象,也就是点击事件的持有者
//要从这里面拿到当前的点击事件对象
Class<?> listenerInfoClz = Class.forName("android.view.View$ListenerInfo");// 这是内部类的表示方法
Field field = listenerInfoClz.getDeclaredField("mOnClickListener");
final View.OnClickListener onClickListenerInstance = (View.OnClickListener) field.get(mListenerInfo);//取得真实的mOnClickListener对象
//2. 创建我们自己的点击事件代理类
// 方式1:自己创建代理类
// ProxyOnClickListener proxyOnClickListener = new ProxyOnClickListener(onClickListenerInstance);
// 方式2:由于View.OnClickListener是一个接口,所以可以直接用动态代理模式
// Proxy.newProxyInstance的3个参数依次分别是:
// 本地的类加载器;
// 代理类的对象所继承的接口(用Class数组表示,支持多个接口)
// 代理类的实际逻辑,封装在new出来的InvocationHandler内
Object proxyOnClickListener = Proxy.newProxyInstance(context.getClass().getClassLoader(), new Class[]{View.OnClickListener.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Log.d("HookSetOnClickListener", "点击事件被hook到了");//加入自己的逻辑
return method.invoke(onClickListenerInstance, args);//执行被代理的对象的逻辑
}
});
//3. 用我们自己的点击事件代理类,设置到"持有者"中
field.set(mListenerInfo, proxyOnClickListener);
//完成
} catch (Exception e) {
e.printStackTrace();
}
}
// 还真是这样,自定义代理类
static class ProxyOnClickListener implements View.OnClickListener {
View.OnClickListener oriLis;
public ProxyOnClickListener(View.OnClickListener oriLis) {
this.oriLis = oriLis;
}
@Override
public void onClick(View v) {
Log.d("HookSetOnClickListener", "点击事件被hook到了");
if (oriLis != null) {
oriLis.onClick(v);
}
}
}
}
具体调用
v.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "别点啦,再点我咬你了...", Toast.LENGTH_SHORT).show();
}
});
HookSetOnClickListenerHelper.hook(this, v);//这个hook的作用,是 用我们自己创建的点击事件代理对象,替换掉之前的点击事件。
ok,目的达成v.setOnClickListener已经被hook.
以上就是HOOK技术的基本解析还附加了实战演练;想要在Android领域发展的可以参考《Android核心技术手册》这个技术文档,点击查看即可!
文末
关于 Android 中的 Hook 机制,大致有两个方式:
- 要 root 权限,直接 Hook 系统,可以干掉所有的 App。
- 免 root 权限,但是只能 Hook 自身,对系统其它 App 无能为力。