1. 什么是 Hook
Hook 英文翻译过来就是「钩子」的意思,那我们在什么时候使用这个「钩子」呢?在 Android 操作系统中系统维护着自己的一套事件分发机制。应用程序,包括应用触发事件和后台逻辑处理,也是根据事件流程一步步地向下执行。而「钩子」的意思,就是在事件传送到终点前截获并监控事件的传输,像个钩子钩上事件一样,并且能够在钩上事件时,处理一些自己特定的事件。
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 等等。
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 之上的。
总结一下:
- Hook 的选择点:静态变量和单例,因为一旦创建对象,它们不容易变化,非常容易定位。
- Hook 过程:
- 寻找 Hook 点,原则是静态变量或者单例对象,尽量 Hook public 的对象和方法。
- 选择合适的代理方式,如果是接口可以用动态代理。
- 偷梁换柱——用代理对象替换原始对象。
- Android 的 API 版本比较多,方法和类可能不一样,所以要做好 API 的兼容工作。
参考文章:
- 动态注入技术
- Android插件化原理解析——Hook机制之动态代理
使用代理机制进行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 }
动态代理主要处理InvocationHandler
和Proxy
类;完整代码可以见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 }
好了,我们启动一个Activity测试一下,结果如下:
可见,Hook确实成功了!这就是使用代理进行Hook的原理——偷梁换柱。整个Hook过程简要总结如下:
- 寻找Hook点,原则是静态变量或者单例对象,尽量Hook pulic的对象和方法,非public不保证每个版本都一样,需要适配。
- 选择合适的代理方式,如果是接口可以用动态代理;如果是类可以手动写代理也可以使用cglib。
- 偷梁换柱——用代理对象替换原始对象
完整代码参照:understand-plugin-framework;里面留有一个作业:我们目前仅Hook了Context
类的startActivity
方法,但是Activity
类却使用了自己的mInstrumentation
;你可以尝试Hook掉Activity类的startActivity
方法。