Android Hook技术

Stella981
• 阅读 1116

1. 什么是 Hook

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

Hook 原理图

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

2. 常用的 Hook 框架

  1. 关于 Android 中的 Hook 机制,大致有两个方式:
  • 要 root 权限,直接 Hook 系统,可以干掉所有的 App。
  • 免 root 权限,但是只能 Hook 自身,对系统其它 App 无能为力。
  1. 几种 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 层,这样的兼容性就非常好。
    原理是这样的,直接构造出新旧方法对应的虚拟机数据结构,然后替换信息写到内存中即可。

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 等等。    

 1   public void setOnClickListener(@Nullable OnClickListener l) {
 2         if (!isClickable()) {
 3             setClickable(true);
 4         }
 5         getListenerInfo().mOnClickListener = l;
 6     }
 7 
 8     ListenerInfo getListenerInfo() {
 9         if (mListenerInfo != null) {
10             return mListenerInfo;
11         }
12         mListenerInfo = new ListenerInfo();
13         return mListenerInfo;
14     }

我们的目标是 Hook OnClickListener,所以就要在给 View 设置监听事件后,替换 OnClickListener 对象,注入自定义的操作。

 1 private void hookOnClickListener(View view) {
 2         try {
 3             // 得到 View 的 ListenerInfo 对象
 4             Method getListenerInfo = View.class.getDeclaredMethod("getListenerInfo");
 5             getListenerInfo.setAccessible(true);
 6             Object listenerInfo = getListenerInfo.invoke(view);
 7             // 得到 原始的 OnClickListener 对象
 8             Class<?> listenerInfoClz = Class.forName("android.view.View$ListenerInfo");
 9             Field mOnClickListener = listenerInfoClz.getDeclaredField("mOnClickListener");
10             mOnClickListener.setAccessible(true);
11             View.OnClickListener originOnClickListener = (View.OnClickListener) mOnClickListener.get(listenerInfo);
12             // 用自定义的 OnClickListener 替换原始的 OnClickListener
13             View.OnClickListener hookedOnClickListener = new HookedOnClickListener(originOnClickListener);
14             mOnClickListener.set(listenerInfo, hookedOnClickListener);
15         } catch (Exception e) {
16             log.warn("hook clickListener failed!", e);
17         }
18     }
19 
20     class HookedOnClickListener implements View.OnClickListener {
21         private View.OnClickListener origin;
22 
23         HookedOnClickListener(View.OnClickListener origin) {
24             this.origin = origin;
25         }
26 
27         @Override
28         public void onClick(View v) {
29             Toast.makeText(MainActivity.this, "hook click", Toast.LENGTH_SHORT).show();
30             log.info("Before click, do what you want to to.");
31             if (origin != null) {
32                 origin.onClick(v);
33             }
34             log.info("After click, do what you want to to.");
35         }
36     }

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

1 Button btnSend = (Button) findViewById(R.id.btn_send);
2         btnSend.setOnClickListener(new View.OnClickListener() {
3             @Override
4             public void onClick(View v) {
5                 log.info("onClick");
6             }
7         });
8         hookOnClickListener(btnSend);

4. 使用 Hook 拦截应用内的通知

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

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

 1 public void notify(String tag, int id, Notification notification)
 2     {
 3         INotificationManager service = getService();
 4         …… // 省略部分代码
 5         try {
 6             service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id,
 7                     stripped, idOut, UserHandle.myUserId());
 8             if (id != idOut[0]) {
 9                 Log.w(TAG, "notify: id corrupted: sent " + id + ", got back " + idOut[0]);
10             }
11         } catch (RemoteException e) {
12         }
13     }
14     private static INotificationManager sService;
15 
16     /** @hide */
17     static public INotificationManager getService()
18     {
19         if (sService != null) {
20             return sService;
21         }
22         IBinder b = ServiceManager.getService("notification");
23         sService = INotificationManager.Stub.asInterface(b);
24         return sService;
25     }

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

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

 1 private void hookNotificationManager(Context context) {
 2         try {
 3             NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
 4             // 得到系统的 sService
 5             Method getService = NotificationManager.class.getDeclaredMethod("getService");
 6             getService.setAccessible(true);
 7             final Object sService = getService.invoke(notificationManager);
 8 
 9             Class iNotiMngClz = Class.forName("android.app.INotificationManager");
10             // 动态代理 INotificationManager
11             Object proxyNotiMng = Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{iNotiMngClz}, new InvocationHandler() {
12                 
13                 @Override
14                 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
15                     log.debug("invoke(). method:{}", method);
16                     if (args != null && args.length > 0) {
17                         for (Object arg : args) {
18                             log.debug("type:{}, arg:{}", arg != null ? arg.getClass() : null, arg);
19                         }
20                     }
21                     // 操作交由 sService 处理,不拦截通知
22                     // return method.invoke(sService, args);
23                     // 拦截通知,什么也不做
24                     return null;
25                     // 或者是根据通知的 Tag 和 ID 进行筛选
26                 }
27             });
28             // 替换 sService
29             Field sServiceField = NotificationManager.class.getDeclaredField("sService");
30             sServiceField.setAccessible(true);
31             sServiceField.set(notificationManager, proxyNotiMng);
32         } catch (Exception e) {
33             log.warn("Hook NotificationManager failed!", e);
34         }
35     }

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

1     @Override
2     protected void attachBaseContext(Context newBase) {
3         super.attachBaseContext(newBase);
4         hookNotificationManager(newBase);
5     }

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

总结一下:

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

使用代理机制进行API Hook进而达到方法增强是框架的常用手段,比如J2EE框架Spring通过动态代理优雅地实现了AOP编程,极大地提升了Web开发效率;同样,插件框架也广泛使用了代理机制来增强系统API从而达到插件化的目的。本文将带你了解基于动态代理的Hook机制。

阅读本文之前,可以先clone一份 understand-plugin-framework,参考此项目的dynamic-proxy-hook模块。另外,插件框架原理解析系列文章见索引

二、Hook机制之动态代理

代理是什么

为什么需要代理呢?其实这个代理与日常生活中的“代理”,“中介”差不多;比如你想海淘买东西,总不可能亲自飞到国外去购物吧,这时候我们使用第三方海淘服务比如惠惠购物助手等;同样拿购物为例,有时候第三方购物会有折扣比如当初的米折网,这时候我们可以少花点钱;当然有时候这个“代理”比较坑,坑我们的钱,坑我们的货。

从这个例子可以看出来,代理可以实现方法增强,比如常用的_日志_,_缓存_等;也可以实现方法拦截,通过代理方法修改原方法的参数和返回值,从而实现某种不可告人的目的~接下来我们用代码解释一下。

静态代理

静态代理,是最原始的代理方式;假设我们有一个购物的接口,如下:

1 public interface Shopping {
2     Object[] doShopping(long money);
3 }

它有一个原始的实现,我们可以理解为亲自,直接去商店购物:

1 public class ShoppingImpl implements Shopping {
2     @Override
3     public Object[] doShopping(long money) {
4         System.out.println("逛淘宝 ,逛商场,买买买!!");
5         System.out.println(String.format("花了%s块钱", money));
6         return new Object[] { "鞋子", "衣服", "零食" };
7     }
8 }

好了,现在我们自己没时间但是需要买东西,于是我们就找了个代理帮我们买:

 1 public class ProxyShopping implements Shopping {
 2 
 3     Shopping base;
 4 
 5     ProxyShopping(Shopping base) {
 6         this.base = base;
 7     }
 8 
 9     @Override
10     public Object[] doShopping(long money) {
11 
12         // 先黑点钱(修改输入参数)
13         long readCost = (long) (money * 0.5);
14 
15         System.out.println(String.format("花了%s块钱", readCost));
16 
17         // 帮忙买东西
18         Object[] things = base.doShopping(readCost);
19 
20         // 偷梁换柱(修改返回值)
21         if (things != null && things.length > 1) {
22             things[0] = "被掉包的东西!!";
23         }
24 
25         return things;
26     }

很不幸,我们找的这个代理有点坑,坑了我们的钱还坑了我们的货;先忍忍。

动态代理

传统的静态代理模式需要为每一个需要代理的类写一个代理类,如果需要代理的类有几百个那不是要累死?为了更优雅地实现代理模式,JDK提供了动态代理方式,可以简单理解为JVM可以在运行时帮我们动态生成一系列的代理类,这样我们就不需要手写每一个静态的代理类了。依然以购物为例,用动态代理实现如下:

 1 public static void main(String[] args) {
 2     Shopping women = new ShoppingImpl();
 3     // 正常购物
 4     System.out.println(Arrays.toString(women.doShopping(100)));
 5     // 招代理
 6     women = (Shopping) Proxy.newProxyInstance(Shopping.class.getClassLoader(),
 7             women.getClass().getInterfaces(), new ShoppingHandler(women));
 8 
 9     System.out.println(Arrays.toString(women.doShopping(100)));
10 }

动态代理主要处理InvocationHandlerProxy类;完整代码可以见github

代理Hook

我们知道代理有比原始对象更强大的能力,比如飞到国外买东西,比如坑钱坑货;那么很自然,如果我们自己创建代理对象,然后把原始对象替换为我们的代理对象,那么就可以在这个代理对象为所欲为了;修改参数,替换返回值,我们称之为Hook。

下面我们Hook掉startActivity这个方法,使得每次调用这个方法之前输出一条日志;(当然,这个输入日志有点点弱,只是为了展示原理;只要你想,你想可以替换参数,拦截这个startActivity过程,使得调用它导致启动某个别的Activity,指鹿为马!)

首先我们得找到被Hook的对象,我称之为Hook点;什么样的对象比较好Hook呢?自然是容易找到的对象。什么样的对象容易找到?静态变量和单例;在一个进程之内,静态变量和单例变量是相对不容易发生变化的,因此非常容易定位,而普通的对象则要么无法标志,要么容易改变。我们根据这个原则找到所谓的Hook点。

然后我们分析一下startActivity的调用链,找出合适的Hook点。我们知道对于Context.startActivity(Activity.startActivity的调用链与之不同),由于Context的实现实际上是ContextImpl;我们看ConetxtImpl类的startActivity方法:  

 1 @Override
 2 public void startActivity(Intent intent, Bundle options) {
 3     warnIfCallingFromSystemProcess();
 4     if ((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0) {
 5         throw new AndroidRuntimeException(
 6                 "Calling startActivity() from outside of an Activity "
 7                 + " context requires the FLAG_ACTIVITY_NEW_TASK flag."
 8                 + " Is this really what you want?");
 9     }
10     mMainThread.getInstrumentation().execStartActivity(
11         getOuterContext(), mMainThread.getApplicationThread(), null,
12         (Activity)null, intent, -1, options);
13 }

接下来就是想要Hook掉我们的主线程对象,也就是把这个主线程对象里面的mInstrumentation给替换成我们修改过的代理对象;要替换主线程对象里面的字段,首先我们得拿到主线程对象的引用,如何获取呢?ActivityThread类里面有一个静态方法currentActivityThread可以帮助我们拿到这个对象类;但是ActivityThread是一个隐藏类,我们需要用反射去获取,代码如下:这里,实际上使用了ActivityThread类的mInstrumentation成员的execStartActivity方法;注意到,ActivityThread 实际上是主线程,而主线程一个进程只有一个,因此这里是一个良好的Hook点。

1 // 先获取到当前的ActivityThread对象
2 Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
3 Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
4 currentActivityThreadMethod.setAccessible(true);
5 Object currentActivityThread = currentActivityThreadMethod.invoke(null);

 1 public class EvilInstrumentation extends Instrumentation {
 2 
 3     private static final String TAG = "EvilInstrumentation";
 4 
 5     // ActivityThread中原始的对象, 保存起来
 6     Instrumentation mBase;
 7 
 8     public EvilInstrumentation(Instrumentation base) {
 9         mBase = base;
10     }
11 
12     public ActivityResult execStartActivity(
13             Context who, IBinder contextThread, IBinder token, Activity target,
14             Intent intent, int requestCode, Bundle options) {
15 
16         // Hook之前, XXX到此一游!
17         Log.d(TAG, "\n执行了startActivity, 参数如下: \n" + "who = [" + who + "], " +
18                 "\ncontextThread = [" + contextThread + "], \ntoken = [" + token + "], " +
19                 "\ntarget = [" + target + "], \nintent = [" + intent +
20                 "], \nrequestCode = [" + requestCode + "], \noptions = [" + options + "]");
21 
22         // 开始调用原始的方法, 调不调用随你,但是不调用的话, 所有的startActivity都失效了.
23         // 由于这个方法是隐藏的,因此需要使用反射调用;首先找到这个方法
24         try {
25             Method execStartActivity = Instrumentation.class.getDeclaredMethod(
26                     "execStartActivity",
27                     Context.class, IBinder.class, IBinder.class, Activity.class, 
28                     Intent.class, int.class, Bundle.class);
29             execStartActivity.setAccessible(true);
30             return (ActivityResult) execStartActivity.invoke(mBase, who, 
31                     contextThread, token, target, intent, requestCode, options);
32         } catch (Exception e) {
33             // 某该死的rom修改了  需要手动适配
34             throw new RuntimeException("do not support!!! pls adapt it");
35         }
36     }
37 }

拿到这个currentActivityThread之后,我们需要修改它的mInstrumentation这个字段为我们的代理对象,我们先实现这个代理对象,由于JDK动态代理只支持接口,而这个Instrumentation是一个类,没办法,我们只有手动写静态代理类,覆盖掉原始的方法即可。(cglib可以做到基于类的动态代理,这里先不介绍)

Ok,有了代理对象,我们要做的就是偷梁换柱!代码比较简单,采用反射直接修改:

 1 public static void attachContext() throws Exception{
 2     // 先获取到当前的ActivityThread对象
 3     Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
 4     Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
 5     currentActivityThreadMethod.setAccessible(true);
 6     Object currentActivityThread = currentActivityThreadMethod.invoke(null);
 7 
 8     // 拿到原始的 mInstrumentation字段
 9     Field mInstrumentationField = activityThreadClass.getDeclaredField("mInstrumentation");
10     mInstrumentationField.setAccessible(true);
11     Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(currentActivityThread);
12 
13     // 创建代理对象
14     Instrumentation evilInstrumentation = new EvilInstrumentation(mInstrumentation);
15 
16     // 偷梁换柱
17     mInstrumentationField.set(currentActivityThread, evilInstrumentation);
18 }

Android Hook技术 好了,我们启动一个Activity测试一下,结果如下:

可见,Hook确实成功了!这就是使用代理进行Hook的原理——偷梁换柱。整个Hook过程简要总结如下:

  1. 寻找Hook点,原则是静态变量或者单例对象,尽量Hook pulic的对象和方法,非public不保证每个版本都一样,需要适配。
  2. 选择合适的代理方式,如果是接口可以用动态代理;如果是类可以手动写代理也可以使用cglib。
  3. 偷梁换柱——用代理对象替换原始对象

完整代码参照:understand-plugin-framework;里面留有一个作业:我们目前仅Hook了Context类的startActivity方法,但是Activity类却使用了自己的mInstrumentation;你可以尝试Hook掉Activity类的startActivity方法。

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Easter79 Easter79
2年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Stella981 Stella981
2年前
Noark入门之异步事件
引入异步事件主要是为了各模块的解耦,每当完成一个动作时,向系统发布一个事件,由关心的模块自己监听处理,可选择同步处理,异步处理,延迟处理。何时发布事件,当其他模块关心此动作时<br比如获得道具时,任务系统模块要判定完成进度,BI模块需要上报等等都可以监听此事件,已达模块解耦0x00事件源一个实现xyz.noark.core.event
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
为什么mysql不推荐使用雪花ID作为主键
作者:毛辰飞背景在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这