Android Hook技术分析1

367 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第25天,点击查看活动详情

一.简介

      Hook技术是一种用于改变API执行结果的技术,Android系统中有一套自己的事件分发机制,所有的代码调用和回调都是按照一定顺序执行的,Hook技术存在的意义就在于,Hook可以帮助我们在Android中在SDK源代码逻辑执行过程中,通过代码手动拦截执行该逻辑,加入自己的代码逻辑。

      为了保证hook的稳定性,一般拦截的点都会选择比较容易找到并且不易发生变化的对象,比如静态变量和单例。

      提到Hook,就不得不说一下Java的反射机制。

二.反射

      Java反射机制主要提供了以下功能:

            在运行时判断任意一个对象所属的类

            在运行时构造任意一个类的对象

            在运行时判断任意一个类所具有的成员变量和方法

            在运行时调用任意一个对象的方法

            生成动态代理

      在运行状态中,对于任意一个类,都能够获取到这个类的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性(包括私有的方法和属性),这种动态获取的信息以及动态调用对象的方法的功能就称为java语言的反射机制。

三.Hook使用案例分析

1.实现启动未注册的activity

      比如我们想启动一个activity,如果未在AndroidManifest.xml里面注册的话,调用Context.startActivity()时会出现以下异常:

10:44:54.811 E/AndroidRuntime(25309): Caused by: android.content.ActivityNotFoundException: Unable to 
find explicit activity class {com.xxx.learn/com.xxx.learn.HookActivity}; have you declared this activity in 
your AndroidManifest.xml?

      下面就一起来实现启动一个未在AndroidManifest.xml注册的activity。

      a.寻找hook点

      对于Context.startActivity,由于Context的实现类为ContextImpl,因此直接分析ContextImpl类的startActivity()的方法:

   @Override
    public void startActivity(Intent intent) {
        warnIfCallingFromSystemProcess();
        startActivity(intent, null);
    }

    @Override
    public void startActivity(Intent intent, Bundle options) {
        ......
        mMainThread.getInstrumentation().execStartActivity(
                getOuterContext(), mMainThread.getApplicationThread(), null,
                (Activity) null, intent, -1, options);
    }

      从上面可以看到,最终会调用mMainThread.getInstrumentation().execStartActivity(),mMainThread是ActivityThread实例,getInstrumentation()返回的是Instrumentation实例,实际上使用了ActivityThread类的mInstrumentation成员的execStartActivity方法;而ActivityThread 实际上是主线程,因为主线程一个进程只有一个,所以这里是一个良好的Hook点,即Hook主线程对象。

      b.选择合适代理方式

      要将这个主线程对象里面的mInstrumentation替换成修改过的代理对象;要替换主线程对象里面的字段,得先拿到主线程对象的引用,如何获取呢?
ActivityThread类里面有一个静态方法currentActivityThread,通过它可以拿到这个对象类;但ActivityThread是一个隐藏类,需用反射去获取拿到currentActivityThread后,要修改它的mInstrumentation字段为修改后的代理对象。实现如下:

    public static void hookInstrumentation(Context context) {
        try {
            //step1:先获取到当前的ActivityThread对象, 该对象是mInstrumentation的持有者
            Class<?> activityThreadClz = Class.forName("android.app.ActivityThread");
            Method currentActivityThreadMethod = activityThreadClz.getDeclaredMethod("currentActivityThread");
            currentActivityThreadMethod.setAccessible(true);
            Object currentActivityThread = currentActivityThreadMethod.invoke(null);

            //step2:从ActivityThread里面拿到原始的mInstrumentation
            Field instrumentation = activityThreadClz.getDeclaredField("mInstrumentation");
            instrumentation.setAccessible(true);
            Instrumentation mInstrumentation = (Instrumentation) instrumentation.get(currentActivityThread);

            //step3:创建Instrumentation的代理对象[接下来会讲到]
            Instrumentation proxyInstrumentation = new ProxyInstrumentation(mInstrumentation, context.getPackageManager());

            //step4:将持有的Instrumentation原始对象替换成代理对象[将currentActivityThread里面的instrumentation变量替换为proxyInstrumentation]
            instrumentation.set(currentActivityThread, proxyInstrumentation);
        } catch (ClassNotFoundException | IllegalAccessException | NoSuchFieldException | NoSuchMethodException | InvocationTargetException e) {
            e.printStackTrace();
        }

    }

      接下来实现这个代理对象,由于JDK动态代理只支持接口,而这个Instrumentation是一个类,因此只能手写一个静态代理类,用来覆盖掉原始的方法,实现如下:

public class ProxyInstrumentation extends Instrumentation {

    private Instrumentation mBase;
    private PackageManager mPm;

    public ProxyInstrumentation(Instrumentation base, PackageManager pm) {
        mBase = base;
        mPm = pm;
    }

    public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
        Log.d("Seven", "------Hook打印execStartActivity------");
        List<ResolveInfo> resolveInfos = mPm.queryIntentActivities(intent, PackageManager.MATCH_ALL);

        //检查要跳转的activity是否在Manifest.xml里面注册
        if (resolveInfos == null || resolveInfos.size() == 0) {
            //把要跳转的activity记录下来,在接下来newActivity还原的时候要拿来还原
            intent.putExtra("intent_name", intent.getComponent().getClassName());
            //把要跳转的activity改成已经在Manifest.xml里面注册过的MainActivity
            intent.setClassName(who, "com.xxx.learn.MainActivity");
        }

        // 开始调用Instrumentation原始的方法,由于这个方法是隐藏的,因此需要使用反射调用;
        try {
            //找到这个方法
            Method execStartActivity = Instrumentation.class.getDeclaredMethod(
                    "execStartActivity", Context.class, IBinder.class, IBinder.class, Activity.class,
                    Intent.class, int.class, Bundle.class);
            execStartActivity.setAccessible(true);
            //通过反射调用Instrumentation的execStartActivity方法
            return (ActivityResult) execStartActivity.invoke(mBase, who,
                    contextThread, token, target, intent, requestCode, options);
        } catch (Exception e) {
            throw new RuntimeException("do not support!");
        }
    }

    @Override
    public Activity newActivity(ClassLoader cl, String className, Intent intent)
            throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        //取出上步记录下来的要跳转的activity并进行替换,来启动未注册的activity
        String intentName = intent.getStringExtra("intent_name");
        Log.d("Seven", "------newActivity内进行替换------");
        if (!TextUtils.isEmpty(intentName)) {
            return super.newActivity(cl, intentName, intent);
        }
        return super.newActivity(cl, className, intent);
    }
}
      c.启动运行
HookUtils.hookInstrumentation(mContext);
private void hookActivity() {
    Intent i = new Intent();
    i.setClass(mContext, HookActivity.class);
    i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    mContext.getApplicationContext().startActivity(i);
}
11:28:37.627 D/Seven   ( 1852): ------Hook打印execStartActivity------
11:28:37.665 D/Seven   ( 1852): ------newActivity内进行替换------

      至此,一个未在AndroidManifest.xml里面注册的activity通过hook技术方式就可以启动了。