一、认识hook机制
Hook又叫“钩子”,它可以在事件传送的过程中截获并监控事件的传输,将自身的代码与系统方法进行融入。这样
当这些方法被调用时,也就可以执行我们自己的代码,这也是面向切面编程的思想(AOP)。当然,根据 Hook 对
象与 Hook 后处理的事件方式不同,Hook 还分为不同的种类,比如消息 Hook、API Hook 等。
1、Java API Hook:
通过对 Android 平台的虚拟机注入与 Java 反射的方式,来改变 Android 虚拟机调用函数的方式(ClassLoader),从而达到 Java 函数重定向的目的,这里我们将此类操作称为 Java API Hook。
2、Hook动态代理:
代理可以增强原始对象的方法能力,比如帮人跑腿买东西,可以坑钱坑货;那么很自然,如果我们动态创建代理对象,然后把原始对象替换为我们的代理对象,对这个代理对象为所欲为。这样动态拦截对象修改参数,替换返回值,我们称之为Hook动态代理。
如何使用:
1、Hook 的选择点:静态变量和单例,因为一旦创建对象,它们不容易变化,非常容易定位。
2、Hook 过程:
- 寻找 Hook 点,原则是静态变量或者单例对象,尽量 Hook public 的对象和方法。
- 选择合适的代理方式,如果是接口可以用动态代理。
- 偷梁换柱——用代理对象替换原始对象。
3、Android 的 API 版本比较多,方法和类可能不一样,所以要做好 API 的兼容工作。
二、Hook使用实例
1、简单案例: 使用 Hook 修改 View.OnClickListener 事件
首先,我们先分析 View.setOnClickListener 源码,找出合适的Hook点。可以看到 OnClickListener 对象被保存在了一个叫做 ListenerInfo 的内部类里,其中 mListenerInfo 是 View 的成员变量。ListeneInfo 里面保存了 View 的各种监听事件。
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
接下来,创建一个Hook代理,当给 View 设置监听事件后,替换 OnClickListener 对象,添加自己的操作。
class HookedProxyClass implements View.OnClickListener {
private View.OnClickListener origin;
HookedProxyClass(View.OnClickListener origin) {
this.origin = origin;
}
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "搞事情!", Toast.LENGTH_SHORT).show();
if (origin != null) {
origin.onClick(v);
}
}
}
public void hookOnClickListener(View view) throws Exception {
// 反射 得到 ListenerInfo 对象
Method getListenerInfo = View.class.getDeclaredMethod("getListenerInfo");
getListenerInfo.setAccessible(true);
Object listenerInfo = getListenerInfo.invoke(view);
// 得到 原始的 OnClickListener事件方法
Class<?> listenerInfoClz = Class.forName("android.view.View$ListenerInfo");
Field mOnClickListener = listenerInfoClz.getDeclaredField("mOnClickListener");
mOnClickListener.setAccessible(true);
View.OnClickListener originOnClickListener = (View.OnClickListener) mOnClickListener.get(listenerInfo);
// 用Hook代理类 替换原始的 OnClickListener
View.OnClickListener hookedOnClickListener = new HookedProxyClass(originOnClickListener);
mOnClickListener.set(listenerInfo, hookedOnClickListener);
}
具体调用:
TextView click_me = (TextView) findViewById(R.id.click_me);
click_me.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d("log", "原始点击事件内容");
}
});
hookOnClickListener(click_me);
最后,我们触发点击事件,在弹出原始点击事件内我们的toast之前,会弹出“搞点自己的事情”的toast。
2、Hook修改startAcivity启动其他页面
首先,我们依然先去分析一下startActivity的源代码寻找合适的Hook点。由于Context.startActivity与Activity.startActivity有所不同,我们先来看Context的实现是 ContextImpl 类的startActivity方法源码。实际上是使用了ActivityThread类的mMainThread来获取mInstrumententation对象,调用execStartActivity方法执行。因为ActivityThread是主线程,进程中的唯一线程,因此这里是一个合适的Hook点。
@Override
public void startActivity(Intent intent, Bundle options) {
warnIfCallingFromSystemProcess();
if ((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0) {
throw new AndroidRuntimeException(".....省略");
}
mMainThread.getInstrumentation().execStartActivity(//这里主线程执行启动
getOuterContext(), mMainThread.getApplicationThread(), null,
(Activity)null, intent, -1, options);
}
接下来,我们就是把这个mMainThread的mInstrumententation对象给替换成新的代理对象。由于JDK动态代理只支持接口,而这个Instrumentation是一个类,没办法,我们只有手动写静态代理类,覆盖掉原始的方法即可。
public static class HookedInstrumentation extends Instrumentation {
private Instrumentation origin;
public HookedInstrumentation(Instrumentation base) {
origin = base;
}
public ActivityResult execStartActivity(
Context who, IBinder contextThread,
IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
Log.d("log", "Hook拦截了"+who+"执行的startActivity。跳转到页面"+target);
try {
// 使用反射调用execStartActivity构造方法
Method execStartActivity = Instrumentation.class.getDeclaredMethod(
"execStartActivity", Context.class, IBinder.class,
IBinder.class, Activity.class,
Intent.class, int.class, Bundle.class);
execStartActivity.setAccessible(true);
return (ActivityResult) execStartActivity.invoke(origin, who,
contextThread, token, target, intent, requestCode, options);
} catch (Exception e) {
Log.d("log", "某该死的rom修改了 需要手动适配 ");
throw new RuntimeException("do not support!!! pls adapt it");
}
}
}
public static void hookContextStartActivity() throws Exception{
// 获取当前 ActivityThread 对象
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
Object currentActivityThread = currentActivityThreadMethod.invoke(null);
// 获取原始 mInstrumentation 字段
Field mInstrumentationField = activityThreadClass.getDeclaredField("mInstrumentation");
mInstrumentationField.setAccessible(true);
Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(currentActivityThread);
// 创建代理对象
Instrumentation evilInstrumentation = new HookedInstrumentation(mInstrumentation);
// 偷梁换柱
mInstrumentationField.set(currentActivityThread, evilInstrumentation);
}
我们运行测试看看结果,在跳转的时候,会自动打印出一句“hahaha”的toast出来。
private void mStartActivity(Context context){
context.startActivity(new Intent(MainActivity.this,MainActivity.class));
try {
hookContextStartActivity();
} catch (Exception e) {
e.printStackTrace();
}
}
总结整个Hook过程:
- 寻找Hook点,原则是静态变量或者单例对象,尽量Hook pulic的对象和方法;
- 选择合适的代理方式,如果是接口可以用动态代理;如果是类可以手动写代理;
- 偷梁换柱——用代理对象替换原始对象。
以上的demo地址:https://github.com/buder-cp/DesignPattern/tree/master/Hook_demo