一、认识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。

Android调用Go so文件_API

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();
        }
    }

Android调用Go so文件_反射_02

总结整个Hook过程:

  • 寻找Hook点,原则是静态变量或者单例对象,尽量Hook pulic的对象和方法;
  • 选择合适的代理方式,如果是接口可以用动态代理;如果是类可以手动写代理;
  • 偷梁换柱——用代理对象替换原始对象。

以上的demo地址:https://github.com/buder-cp/DesignPattern/tree/master/Hook_demo