Android Hook技术分析

一.简介

      Hook技术是一种用于改变API执行结果的技术,Android系统中有一套自己的事件分发机制,所有的代码调用和回调都是按照一定顺序执行的,Hook技术存在的意义就在于,Hook可以帮助我们在Android中在SDK源代码逻辑执行过程中,通过代码手动拦截执行该逻辑,加入自己的代码逻辑。
      为了保证hook的稳定性,一般拦截的点都会选择比较容易找到并且不易发生变化的对象,比如静态变量和单例。
      提到Hook,就不得不说一下Java的反射机制。

二.反射

      Java反射机制主要提供了以下功能:
            在运行时判断任意一个对象所属的类
            在运行时构造任意一个类的对象
            在运行时判断任意一个类所具有的成员变量和方法
            在运行时调用任意一个对象的方法
            生成动态代理
      在运行状态中,对于任意一个类,都能够获取到这个类的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性(包括私有的方法和属性),这种动态获取的信息以及动态调用对象的方法的功能就称为java语言的反射机制。

反射.png

三.Hook使用案例分析

1.实现启动未注册的activity

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

1-25 10:44:54.811 E/AndroidRuntime(25309): Caused by: android.content.ActivityNotFoundException: Unable to 
find explicit activity class {com.hly.learn/com.hly.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.hly.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);
}
1-25 11:28:37.627 D/Seven   ( 1852): ------Hook打印execStartActivity------
1-25 11:28:37.665 D/Seven   ( 1852): ------newActivity内进行替换------

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

2.拦截点击事件统计点击次数
      a.寻找hook点

      对一个view设置点击事件的调用流程如下:

    hkBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                
            }
        });

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

    static class ListenerInfo {
        ......
        /**
         * Listener used to dispatch click events.
         * This field should be made private, so it is hidden from the SDK.
         * {@hide}
         */
        public OnClickListener mOnClickListener;
        ......
}

      Hook操作onClickListener实例,需要首先获取到onClickListener的拥有者,即:ListenerInfo,最后通过ListenerInfo获取到原始的mOnClickListener。

      b.选择合适的代理方式

      从上面的流程可以发现,要将ListenerInfo类里面的mOnClickListener替换成修改过的代理对象;要替换ListenerInfo里面的字段,得先拿到ListenerInfo,View类中有一个方法getListenerInfo() ,通过它可以拿到这个ListenerInfo,实现如下:

    public static void hookOnClickListener(View view) {
        //step1:反射执行View类的getListenerInfo()方法,拿到view的mListenerInfo对象,这个对象是点击事件mOnClickListener的持有者
        try {
            Class<?> viewClz = Class.forName("android.view.View");
            Method method = viewClz.getDeclaredMethod("getListenerInfo");
            //由于getListenerInfo()方法并不是public的,所以要加这个代码来保证访问权限
            method.setAccessible(true);
            //拿到mListenerInfo,也就是点击事件的持有者
            Object listenerInfo = method.invoke(view);

            //step2:找到mListenerInfo持有的点击事件对象mOnClickListener
            //内部类的表示方法:android.view.View$ListenerInfo
            Class<?> listenerInfoClz = Class.forName("android.view.View$ListenerInfo");
            Field onClickListener = listenerInfoClz.getDeclaredField("mOnClickListener");
            final View.OnClickListener onClickListenerInstance = (View.OnClickListener) onClickListener.get(
                    listenerInfo);

            //step3:创建自己点击事件的OnClickListener代理类
             动态或静态创建OnClickListener的代理类proxyOnClickListener

            //step4:将持有者拥有的点击事件替换成代理对象[将listenerInfo里面的onClickListener变量替换为proxyOnClickListener]
            onClickListener.set(listenerInfo, proxyOnClickListener);
        } catch (NoSuchMethodException | IllegalAccessException | ClassNotFoundException | NoSuchFieldException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }
    }

      接下来要创建OnClickListener的代理对象,由于OnClickListener是一个接口,因此可以使用JDK动态代理方式Java动态代理,也可以用静态代理类实现,实现方式如下:

    //方式1:自己实现代理类,将原始的View.OnClickListener对象onClickListenerInstance作为参数传入
    ProxyOnClickListener proxyOnClickListener = new ProxyOnClickListener(onClickListenerInstance);
    static class ProxyOnClickListener implements View.OnClickListener{

        private View.OnClickListener listener;
        private int clickCount = 0;
        ProxyOnClickListener(View.OnClickListener listener){
            this.listener = listener;
        }

        @Override
        public void onClick(View v) {
            clickCount++;
            Log.d("Seven", "Hook OnClickListener 2 click count " + clickCount);
            if(this.listener != null){
                this.listener.onClick(v);
            }
    }
            
    //方式2:由于View.OnClickListener是一个接口,所以可以直接用动态代理模式
    //参数:类的加载器,要代理实现的接口(用Class数组表示,支持多接口),代理类的实际逻辑封装在new出来的InvocationHandler内
    Object proxyOnClickListener = Proxy.newProxyInstance(View.OnClickListener.class.getClassLoader(),
                    new Class[]{View.OnClickListener.class}, new InvocationHandler() {
                private int clickCount = 0;
                @Override
                 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                         clickCount++;
                         Log.d("Seven", "Hook OnClickListener 1 click count " + clickCount);
                         return method.invoke(onClickListenerInstance, args);
                }
    });
      c.启动运行
hkBtn.setOnClickListener(this);
HookUtils.hookonClickListener(hkBtn);

      运行结果如下:

1-25 10:51:12.190 D/Seven   (26125): Hook OnClickListener 1 click count 1
1-25 10:51:21.428 D/Seven   (26125): Hook OnClickListener 1 click count 2
1-25 10:51:21.607 D/Seven   (26125): Hook OnClickListener 1 click count 3
1-25 10:51:21.770 D/Seven   (26125): Hook OnClickListener 1 click count 4
1-25 10:51:21.918 D/Seven   (26125): Hook OnClickListener 1 click count 5
1-25 10:51:22.068 D/Seven   (26125): Hook OnClickListener 1 click count 6
1-25 10:51:22.217 D/Seven   (26125): Hook OnClickListener 1 click count 7
1-25 10:51:22.394 D/Seven   (26125): Hook OnClickListener 1 click count 8
1-25 10:51:22.543 D/Seven   (26125): Hook OnClickListener 1 click count 9
1-25 10:51:22.722 D/Seven   (26125): Hook OnClickListener 1 click count 10
1-25 10:51:22.854 D/Seven   (26125): Hook OnClickListener 1 click count 11
1-25 10:51:23.009 D/Seven   (26125): Hook OnClickListener 1 click count 12
1-25 10:51:23.158 D/Seven   (26125): Hook OnClickListener 1 click count 13

      至此已经实现了对点击事件的hook。

3.总结

      针对以上两个案例分析,我们可以看到在进行hook时主要分为以下几步:
      a.寻找合适的hook点;
      b.找到Hook点对应的class;
      c.直接通过class或class内部的method找到对应的filed;
      d.替换对象是interface的话,直接用动态代理方式创建对象;否则,直接写个类继承要替换的类;
      e.调用filed.set(obj, proxy)来对变量进行替换;
      f.针对hook点进行相应的处理;

      hook技术涉及到的知识点主要有反射、代理及android源码的熟练程度。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容