理解 Android Hook 技术以及简单实战

作者:落英坠露 链接:https://www.jianshu.com/p/4f6d20076922 著作权归作者所有,转载请联系作者获得授权。

1. 什么是 Hook

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

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

2. 常用的 Hook 框架

关于 Android 中的 Hook 机制,大致有两个方式:

  • 要 root 权限,直接 Hook 系统,可以干掉所有的 App。
  • 免 root 权限,但是只能 Hook 自身,对系统其它 App 无能为力。 几种 Hook 方案:

Xposed 通过替换 /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 层,这样的兼容性就非常好。 原理是这样的,直接构造出新旧方法对应的虚拟机数据结构,然后替换信息写到内存中即可。

3. 使用 Java 反射实现 API Hook

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

下面通过 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);

4. 使用 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 的选择点:静态变量和单例,因为一旦创建对象,它们不容易变化,非常容易定位。 Hook 过程: 寻找 Hook 点,原则是静态变量或者单例对象,尽量 Hook public 的对象和方法。 选择合适的代理方式,如果是接口可以用动态代理。 偷梁换柱——用代理对象替换原始对象。 Android 的 API 版本比较多,方法和类可能不一样,所以要做好 API 的兼容工作。

原文发布于微信公众号 - 刘望舒(liuwangshuAndroid)

原文发表时间:2018-04-17

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏冷冷

Spring Cloud Gateway 聚合swagger文档

关于pigX:全网最新的微服务脚手架,Spring Cloud Finchley、oAuth2的最佳实践

3714
来自专栏互联网开发者交流社区

SpringBoot与Web开发

1214
来自专栏JadePeng的技术博客

使用SpringBoot开发REST服务

本文介绍如何基于Spring Boot搭建一个简易的REST服务框架,以及如何通过自定义注解实现Rest服务鉴权 搭建框架 pom.xml 首先,引入相关依赖,...

4755
来自专栏飞雪无情的博客

Android注解支持(Support Annotations)

原文地址 http://tools.android.com/tech-docs/support-annotations

953
来自专栏Ryan Miao

在dropwizard中使用feign,使用hystrix

前言 用惯了spring全家桶之后,试试dropwizard的Hello World也别有一帆风味。为了增强对外访问API的能力,需要引入open feign...

40112
来自专栏青蛙要fly的专栏

Android技能树 — 多进程相关小结

这次是讲Android存储路径及IO的基本操作。因为我们在开发的时候会经常这种方便的需求。这篇文章的内容我写的可能很少,都没有细写。别吐槽。o( ̄︶ ̄)o

691
来自专栏24K纯开源

inno setup读取注册表遇到的一个坑

一、背景       目前,公司针对PR开发的一个插件需要发布到64位系统上。该插件包括一个prm格式的文件和若干个DLL文件。其中,prm文件需要复制到PR公...

4196
来自专栏pangguoming

如何使用Retrofit获取服务器返回来的JSON字符串

有关Retrofit的简单集成攻略,大家可以参考我此前的一篇文章 有关更多API文档的查阅请大家到Retrofit官网查看。 在大家使用网络请求的时候,往往会出...

33910
来自专栏向治洪

Widget简介

Widget简介 可以使用AppWidgetManager更新Widget中的数据,但这样最短也要半个小时才能更新一次,一般不用他更新,而是自己定义一个服务去更...

2097
来自专栏xiaoheike

Spring MVC 上下文(ApplicationContext)初始化入口

应该来说是很少使用这种方法用于生产开发,常常在学习Spring做demo的时候会使用到。更有可能出现在Spring项目的代码测试,不过呢,单元测试的框架(比如 ...

5661

扫码关注云+社区