Java基础:基于反射和动态代理的Hook

一. 什么是 Hook

Hook 英文翻译过来就是「钩子」的意思,那我们在什么时候使用这个「钩子」呢?在 Android 操作系统中系统维护着自己的一套事件分发机制。应用程序,包括应用触发事件和后台逻辑处理,也是根据事件流程一步步地向下执行。而「钩子」的意思,就是在事件传送到终点前截获并监控事件的传输,像个钩子钩上事件一样,并且能够在钩上事件时,处理一些自己特定的事件。

image

Hook 的这个本领,使它能够将自身的代码「融入」被勾住(Hook)的程序的进程中,成为目标进程的一个部分。API Hook 技术是一种用于改变 API 执行结果的技术,能够将系统的 API 函数执行重定向。在 Android 系统中使用了沙箱机制,普通用户程序的进程空间都是独立的,程序的运行互不干扰。这就使我们希望通过一个程序改变其他程序的某些行为的想法不能直接实现,但是 Hook 的出现给我们开拓了解决此类问题的道路。

什么是沙箱机制

沙箱是一个虚拟系统程序,沙箱提供的环境相对于每一个运行的程序的进程空间都是独立的,程序的运行互不干扰,而且不会对现有的系统产生影响。

二、Hook 分类

1、根据Android开发模式

Java层级的Hook;
Native层级的Hook;

2、根 Hook 对象与 Hook 后处理事件方式

消息Hook;
API Hook;

3、针对Hook的不同进程上来说

全局Hook;
单个进程Hook

四. API Hook 原理

通过对 Android 平台的虚拟机注入与 Java 反射的方式,来改变 Android 虚拟机调用函数的方式(ClassLoader),从而达到 Java 函数重定向的目的,这里我们将此类操作称为 Java API Hook。

基础知识

由此可见,Hook的基础知识,是反射及基于反射的动态代理

  • 反射
    反射(Reflection)是什么呢?

反射有时候也被称为内省(Introspection),事实上,反射,就是一种内省的方式,Java不允许在运行时改变程序结构或类型变量的结构,但它允许在运行时去探知、加载、调用在编译期完全未知的class,可以在运行时加载该class,生成实例对象(instance object),调用method,或对field赋值。这种类似于“看透”了class的特性被称为反射(Reflection),我们可以将反射直接理解为:可以看到自己在水中的倒影,这种操作与直接操作源代码效果相同,但灵活性高得多。

在之前学习热更新的时候有介绍过反射,详见 Android热更新二:理解Java反射

  • java 的动态代理
    首先了解一些代理模式的定义。

为其他对象提供一种代理以控制这个对象的访问。

从代码的角度来分,代理可以分为两种:一种是静态代理,另一种是动态代理。

之前讲设计模式的时候,也讲过动态代理,详见 Android常见设计模式五:代理模式

五、Hook Activity 的 startActivity

原理知道后,还是实战来得舒畅,下面以startActivity启动一个activity为例,在Activity中启动另一个Activity,首先我们需要了解activity的启动流程,一步步跟踪,发现最终在Activity.startActivityForResult中以如下方式启动:

 Instrumentation.ActivityResult ar =
                mInstrumentation.execStartActivity(
                    this, mMainThread.getApplicationThread(), mToken, this,
                    intent, requestCode, options);
            if (ar != null) {
                mMainThread.sendActivityResult(
                    mToken, mEmbeddedID, requestCode, ar.getResultCode(),
                    ar.getResultData());
            }

如果你对Activity启动熟悉的话会发现,此处的mInstrumentation就是ActivityThread通过ativity.attach传过来的,而ActivityThread一个app唯一的,而mInstrumentation就是在ActivityThread创建后马上创建的,此时,是不是感觉这个mInstrumentation符合hook点,ok,先hook一把

public static void hookCurrentThread(){
        try {
            Class<?> activityThreadCls = Class.forName("android.app.ActivityThread");
            //1.获取ActivityThread对象
            //hook点,有public的方法或属性,优先
            Method currentActThreadMethod = activityThreadCls.getDeclaredMethod("currentActivityThread");
            Object curThreadObj = currentActThreadMethod.invoke(null);
            //获取mInstrumentation
            Field instrumentationField = curThreadObj.getClass().getDeclaredField("mInstrumentation");
            instrumentationField.setAccessible(true);
            instrumentationField.set(curThreadObj,new InstrumentProxy((Instrumentation) instrumentationField.get(curThreadObj)));
        } catch (Exception e) {
            e.printStackTrace();
        }

其中InstrumentProxy如下:

public class InstrumentProxy extends Instrumentation{
    private Instrumentation realObj;
    public InstrumentProxy(Instrumentation obj){
        this.realObj = obj;
    }
    public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
       
        // 开始调用原始的方法, 调不调用随你,但是不调用的话, 所有的startActivity都失效了.
        // 由于这个方法是隐藏的,因此需要使用反射调用;首先找到这个方法
        try {
            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(realObj, who,
                    contextThread, token, target, intent, requestCode, options);
        } catch (Exception e) {
            throw new RuntimeException("do not support!!! pls adapt it");
        }
    }
}

此处,先获取ActivityThread中的mInstrumentation属性,然后再采用静态代理将其替换掉,这样就hook住系统的方法,我们也就可以在InstrumentProxy中任意插桩。

倘若你没有发现mInstrumentation符合hook点,你可以继续跟踪Instrumentation.execStartActivity方法,里面有个非常明显的hook点:

try {
    intent.migrateExtraStreamToClipData();
    intent.prepareToLeaveProcess();
    int result = ActivityManagerNative.getDefault().startActivity(whoThread, who.getBasePackageName(), intent,                    intent.resolveTypeIfNeeded(who.getContentResolver()), token, target != null ? target.mEmbeddedID : null,  requestCode, 0, null, options);
    checkStartActivityResult(result, intent);
} catch (RemoteException e) {

}

对,就是ActivityManagerNative.getDefault(),我们可以进入ActivityManagerNative类,发现getDefault方法实现如下:

static public IActivityManager getDefault() {
    return gDefault.get();
}

其中gDefault是个static属性,完全符合hook要求,具体hook如下:

public static void hookAMNative(){
        try {
            Class<?> actManagerNativeCls = Class.forName("android.app.ActivityManagerNative");
            //获取gDefault
            Field gDefaultField = actManagerNativeCls.getDeclaredField("gDefault");
            gDefaultField.setAccessible(true);
            Object gDefaultObj = gDefaultField.get(null);
//            Method getField = gDefaultObj.getClass().getDeclaredMethod("get");
//            Object activityImpl = getField.invoke(null);
            Class<?> singleton = Class.forName("android.util.Singleton");
            Field mInstanceField = singleton.getDeclaredField("mInstance");
            mInstanceField.setAccessible(true);
 
            Object activityImpl = mInstanceField.get(gDefaultObj);
 
//            Method activityManagerMethod= actManagerNativeCls.getMethod("getDefault");
//            Object actManagerImpl = activityManagerMethod.invoke(null);
 
            Object actProxy = Proxy.newProxyInstance(activityImpl.getClass().getClassLoader(),
                    activityImpl.getClass().getInterfaces(),new ProxyHandler(activityImpl,null));
            mInstanceField.set(gDefaultObj,actProxy);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

其中ProxyHandler如下:

public class ProxyHandler implements InvocationHandler{
    private Object realObj;
    public ProxyHandler(Object obj,Object d){
        this.realObj = obj;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        if("startActivity".equals(methodName)){
            android.util.Log.e("hooktest",">>>>proxyhandler>>>startActivityBefore");
            Object retur = method.invoke(realObj,args);
            android.util.Log.e("hooktest",">>>>proxyhandler>>>startActivityAfter");
            return retur;
        }
 
        return method.invoke(realObj,args);
    }
}

采用动态代理的方法对IActivityManager进行了接管,同样完成了startActivity的hook。

其实不选单例、静态属性或共有属性,整个private的也是可以的,还以启动startActivity为例,考虑到Activity是继承ContextWrapper,而ContextWrapper中有个属性mBase,如果我们能对mBase hook也是可以的,这样就需要对ContextImpl来个代理就可以了,代码可以如下:

public static void hookContextWrapper(ContextWrapper wrapper) {
        try {
            Field mBaseFiled;
            Class<?> wrapperClass = ContextWrapper.class;
 
            mBaseFiled = wrapperClass.getDeclaredField("mBase");
            mBaseFiled.setAccessible(true);
            mBaseFiled.set(wrapper,new HookWrapper((Context) mBaseFiled.get(wrapper)));
 
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

这样尽管可以,但是启动activity就需要注意了,只要能走到mBase.startActivity(intent)接口才生效,如果没走它,就hook失效啰,所以hook点选择很关键,尽管都hook到了东西,但是是不是hook住了全部,还需要验证。

六、Hook View 的 OnClickListener

下面通过 Hook View 的 OnClickListener 来说明 Hook 的使用方法。

首先进入 View 的 setOnClickListener 方法,我们看到 OnClickListener 对象被保存在了一个叫做 ListenerInfo 的内部类里,其中 mListenerInfo 是 View 的成员变量。ListeneInfo 里面保存了 View 的各种监听事件,比如 OnClickListener、OnLongClickListener、OnKeyListener 等等。

    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 OnClickListener,所以就要在给 View 设置监听事件后,替换 OnClickListener 对象,注入自定义的操作。

    private void hookOnClickListener(View view) {
        try {
            // 得到 View 的 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);
            // 用自定义的 OnClickListener 替换原始的 OnClickListener
            View.OnClickListener hookedOnClickListener = new HookedOnClickListener(originOnClickListener);
            mOnClickListener.set(listenerInfo, hookedOnClickListener);
        } catch (Exception e) {
            log.warn("hook clickListener failed!", e);
        }
    }

    class HookedOnClickListener implements View.OnClickListener {
        private View.OnClickListener origin;

        HookedOnClickListener(View.OnClickListener origin) {
            this.origin = origin;
        }

        @Override
        public void onClick(View v) {
            Toast.makeText(MainActivity.this, "hook click", Toast.LENGTH_SHORT).show();
            log.info("Before click, do what you want to to.");
            if (origin != null) {
                origin.onClick(v);
            }
            log.info("After click, do what you want to to.");
        }
    }

到这里,我们成功 Hook 了 OnClickListener,在点击之前和点击之后可以执行某些操作,达到了我们的目的。下面是调用的部分,在给 Button 设置 OnClickListener 后,执行 Hook 操作。点击按钮后,日志的打印结果是:Before click → onClick → After click。

        Button btnSend = (Button) findViewById(R.id.btn_send);
        btnSend.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                log.info("onClick");
            }
        });
        hookOnClickListener(btnSend);

七、使用 Hook 拦截应用内的通知

当应用内接入了众多的 SDK,SDK 内部会使用系统服务 NotificationManager 发送通知,这就导致通知难以管理和控制。现在我们就用 Hook 技术拦截部分通知,限制应用内的通知发送操作。

发送通知使用的是 NotificationManager 的 notify 方法,我们跟随 API 进去看看。它会使用 INotificationManager 类型的对象,并调用其 enqueueNotificationWithTag 方法完成通知的发送。

    public void notify(String tag, int id, Notification notification)
    {
        INotificationManager service = getService();
        …… // 省略部分代码
        try {
            service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id,
                    stripped, idOut, UserHandle.myUserId());
            if (id != idOut[0]) {
                Log.w(TAG, "notify: id corrupted: sent " + id + ", got back " + idOut[0]);
            }
        } catch (RemoteException e) {
        }
    }

    private static INotificationManager sService;

    /** @hide */
    static public INotificationManager getService()
    {
        if (sService != null) {
            return sService;
        }
        IBinder b = ServiceManager.getService("notification");
        sService = INotificationManager.Stub.asInterface(b);
        return sService;
    }

INotificationManager 是跨进程通信的 Binder 类,sService 是 NMS(NotificationManagerService) 在客户端的代理,发送通知要委托给 sService,由它传递给 NMS,具体的原理在这里不再细究,感兴趣的可以了解系统服务和应用的通信过程。

我们发现 sService 是个静态成员变量,而且只会初始化一次。只要把 sService 替换成自定义的不就行了么,确实如此。下面用到大量的 Java 反射和动态代理,特别要注意代码的书写。

    private void hookNotificationManager(Context context) {
        try {
            NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
            // 得到系统的 sService
            Method getService = NotificationManager.class.getDeclaredMethod("getService");
            getService.setAccessible(true);
            final Object sService = getService.invoke(notificationManager);

            Class iNotiMngClz = Class.forName("android.app.INotificationManager");
            // 动态代理 INotificationManager
            Object proxyNotiMng = Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{iNotiMngClz}, new InvocationHandler() {

                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    log.debug("invoke(). method:{}", method);
                    if (args != null && args.length > 0) {
                        for (Object arg : args) {
                            log.debug("type:{}, arg:{}", arg != null ? arg.getClass() : null, arg);
                        }
                    }
                    // 操作交由 sService 处理,不拦截通知
                    // return method.invoke(sService, args);
                    // 拦截通知,什么也不做
                    return null;
                    // 或者是根据通知的 Tag 和 ID 进行筛选
                }
            });
            // 替换 sService
            Field sServiceField = NotificationManager.class.getDeclaredField("sService");
            sServiceField.setAccessible(true);
            sServiceField.set(notificationManager, proxyNotiMng);
        } catch (Exception e) {
            log.warn("Hook NotificationManager failed!", e);
        }
    }

Hook 的时机还是尽量要早,我们在 attachBaseContext 里面操作。

    @Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(newBase);
        hookNotificationManager(newBase);
    }

这样我们就完成了对通知的拦截,可见 Hook 技术真的是非常强大,好多插件化的原理都是建立在 Hook 之上的。

八、结语

以上,我们知道,hook技术涉及到的知识点主要有反射、代理及android的一些底层知识,如果想要较好地掌握好hook相关的内容,就需要花更多的时间去学习和总结。

下面总结一下注意点:

  1. Hook 的选择点:静态变量和单例,因为一旦创建对象,它们不容易变化,非常容易定位。
  1. Hook 过程:

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

  1. Android 的 API 版本比较多,方法和类可能不一样,所以要做好 API 的兼容工作。

另外,市面上游几个比较成熟的Hook 方案,如果有需要大量使用此技术的不妨参考参考:

通过替换 /system/bin/app_process 程序控制 Zygote 进程,使得 app_process 在启动过程中会加载 XposedBridge.jar 这个 Jar 包,从而完成对 Zygote 进程及其创建的 Dalvik 虚拟机的劫持。
Xposed 在开机的时候完成对所有的 Hook Function 的劫持,在原 Function 执行的前后加上自定义代码。

  • Cydia Substrate

    Cydia Substrate 框架为苹果用户提供了越狱相关的服务框架,当然也推出了 Android 版 。Cydia Substrate 是一个代码修改平台,它可以修改任何进程的代码。不管是用 Java 还是 C/C++(native代码)编写的,而 Xposed 只支持 Hook app_process 中的 Java 函数。

  • Legend

    Legend 是 Android 免 Root 环境下的一个 Apk Hook 框架,该框架代码设计简洁,通用性高,适合逆向工程时一些 Hook 场景。大部分的功能都放到了 Java 层,这样的兼容性就非常好。
    原理是这样的,直接构造出新旧方法对应的虚拟机数据结构,然后替换信息写到内存中即可。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容