简介 什么是 Hook Hook 又叫“钩子” ,它可以在事件传送的过程中截获并监控事件的传输,将自身的代码与系统方法进行融入。
这样当这些方法被调用时,也就可以执行我们自己的代码,这也是面向切面编程的思想(AOP)。
Hook 分类 1.根据Android开发模式,Native模式(C/C++)和Java模式(Java)区分,在Android平台上
Java层级的Hook;
Native层级的Hook;
2.根 Hook 对象与 Hook 后处理事件方式不同,Hook还分为:
3.针对Hook的不同进程上来说,还可以分为:
常见 Hook 框架 在Android开发中,有以下常见的一些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 层,这样的兼容性就非常好。
原理是这样的,直接构造出新旧方法对应的虚拟机数据结构,然后替换信息写到内存中即可。
Hook 必须掌握的知识
如果你对反射还不是很熟悉的话,建议你先复习一下 java 反射的相关知识。有兴趣的,可以看一下我的这一篇博客 Java 反射机制详解
动态代理是指在运行时动态生成代理类,不需要我们像静态代理那个去手动写一个个的代理类。在 java 中,我们可以使用 InvocationHandler 实现动态代理,有兴趣的,可以查看我的这一篇博客 java 代理模式详解
本文的主要内容是讲解单个进程的 Hook,以及怎样 Hook。有兴趣的可以关注我的微信公众号:程序员徐公
Hook 使用实例 Hook 选择的关键点
简单案例一: 使用 Hook 修改 View.OnClickListener 事件 首先,我们先分析 View.setOnClickListener 源码,找出合适的 Hook 点。可以看到 OnClickListener 对象被保存在了一个叫做 ListenerInfo 的内部类里,其中 mListenerInfo 是 View 的成员变量。ListeneInfo 里面保存了 View 的各种监听事件。因此,我们可以想办法 hook ListenerInfo 的 mOnClickListener 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public void setOnClickListener(@Nullable OnClickListener l) { if (!isClickable()) { setClickable(true); } getListenerInfo().mOnClickListener = l; } static class ListenerInfo { --- ListenerInfo getListenerInfo() { if (mListenerInfo != null) { return mListenerInfo; } mListenerInfo = new ListenerInfo(); return mListenerInfo; } --- }
接下来,让我们一起来看一下怎样 Hook View.OnClickListener 事件?
大概分为三步:
从 View 的源代码,我们可以知道我们可以通过 getListenerInfo 方法获取,于是,我们利用反射得到 ListenerInfo 对象
第二步:获取原始的 OnClickListener事件方法
从上面的分析,我们知道 OnClickListener 事件被保存在 ListenerInfo 里面,同理我们利用反射获取
第三步:偷梁换柱,用 Hook代理类 替换原始的 OnClickListener
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static void hookOnClickListener(View view) throws Exception { // 第一步:反射得到 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); // 第三步:用 Hook代理类 替换原始的 OnClickListener View.OnClickListener hookedOnClickListener = new HookedClickListenerProxy(originOnClickListener); mOnClickListener.set(listenerInfo, hookedOnClickListener); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class HookedClickListenerProxy implements View.OnClickListener { private View.OnClickListener origin; public HookedClickListenerProxy(View.OnClickListener origin) { this.origin = origin; } @Override public void onClick(View v) { Toast.makeText(v.getContext(), "Hook Click Listener", Toast.LENGTH_SHORT).show(); if (origin != null) { origin.onClick(v); } } }
执行以下代码,将会看到当我们点击该按钮的时候,会弹出 toast “Hook Click Listener”
1 2 3 4 5 6 7 mBtn1 = (Button) findViewById(R.id.btn_1); mBtn1.setOnClickListener(this); try { HookHelper.hookOnClickListener(mBtn1); } catch (Exception e) { e.printStackTrace(); }
简单案例二: HooK Notification 发送消息到通知栏的核心代码如下:
1 2 NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(id, builder.build());
跟踪 notify 方法发现最终会调用到 notifyAsUser 方法
1 2 3 4 5 public void notify(String tag, int id, Notification notification) { notifyAsUser(tag, id, notification, new UserHandle(UserHandle.myUserId())); }
而在 notifyAsUser 方法中,我们惊喜地发现 service 是一个单例,因此,我们可以想方法 hook 住这个 service,而 notifyAsUser 最终会调用到 service 的 enqueueNotificationWithTag 方法。因此 hook 住 service 的 enqueueNotificationWithTag 方法即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public void notifyAsUser(String tag, int id, Notification notification, UserHandle user) { // INotificationManager service = getService(); String pkg = mContext.getPackageName(); // Fix the notification as best we can. Notification.addFieldsFromContext(mContext, notification); if (notification.sound != null) { notification.sound = notification.sound.getCanonicalUri(); if (StrictMode.vmFileUriExposureEnabled()) { notification.sound.checkFileUriExposed("Notification.sound"); } } fixLegacySmallIcon(notification, pkg); if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1) { if (notification.getSmallIcon() == null) { throw new IllegalArgumentException("Invalid notification (no valid small icon): " + notification); } } if (localLOGV) Log.v(TAG, pkg + ": notify(" + id + ", " + notification + ")"); final Notification copy = Builder.maybeCloneStrippedForDelivery(notification); try { service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id, copy, user.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } private static INotificationManager sService; static public INotificationManager getService() { if (sService != null) { return sService; } IBinder b = ServiceManager.getService("notification"); sService = INotificationManager.Stub.asInterface(b); return sService; }
综上,要 Hook Notification,大概需要三步:
第一步:得到 NotificationManager 的 sService
第二步:因为 sService 是接口,所以我们可以使用动态代理,获取动态代理对象
第三步:偷梁换柱,使用动态代理对象 proxyNotiMng 替换系统的 sService
于是,我们可以写出如下的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public static void hookNotificationManager(final Context context) throws Exception { NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); Method getService = NotificationManager.class.getDeclaredMethod("getService"); getService.setAccessible(true); // 第一步:得到系统的 sService final Object sOriginService = getService.invoke(notificationManager); Class iNotiMngClz = Class.forName("android.app.INotificationManager"); // 第二步:得到我们的动态代理对象 Object proxyNotiMng = Proxy.newProxyInstance(context.getClass().getClassLoader(), new Class[]{iNotiMngClz}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Log.d(TAG, "invoke(). method:" + method); String name = method.getName(); Log.d(TAG, "invoke: name=" + name); if (args != null && args.length > 0) { for (Object arg : args) { Log.d(TAG, "invoke: arg=" + arg); } } Toast.makeText(context.getApplicationContext(), "检测到有人发通知了", Toast.LENGTH_SHORT).show(); // 操作交由 sOriginService 处理,不拦截通知 return method.invoke(sOriginService, args); // 拦截通知,什么也不做 // return null; // 或者是根据通知的 Tag 和 ID 进行筛选 } }); // 第三步:偷梁换柱,使用 proxyNotiMng 替换系统的 sService Field sServiceField = NotificationManager.class.getDeclaredField("sService"); sServiceField.setAccessible(true); sServiceField.set(notificationManager, proxyNotiMng); }
Hook 使用进阶 Hook ClipboardManager 第一种方法 从上面的 hook NotificationManager 例子中,我们可以得知 NotificationManager 中有一个静态变量 sService,这个变量是远端的 service。因此,我们尝试查找 ClipboardManager 中是不是也存在相同的类似静态变量。
查看它的源码发现它存在 mService 变量,该变量是在 ClipboardManager 构造函数中初始化的,而 ClipboardManager 的构造方法用 @hide 标记,表明该方法对调用者不可见。
而我们知道 ClipboardManager,NotificationManager 其实这些都是单例的,即系统只会创建一次。因此我们也可以认为 ClipboardManager 的 mService 是单例的。因此 mService 应该是可以考虑 hook 的一个点。
1 2 3 4 5 6 7 8 9 10 11 public class ClipboardManager extends android.text.ClipboardManager { private final Context mContext; private final IClipboard mService; /** {@hide} */ public ClipboardManager(Context context, Handler handler) throws ServiceNotFoundException { mContext = context; mService = IClipboard.Stub.asInterface( ServiceManager.getServiceOrThrow(Context.CLIPBOARD_SERVICE)); } }
接下来,我们再来一个看一下 ClipboardManager 的相关方法 setPrimaryClip , getPrimaryClip
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public void setPrimaryClip(ClipData clip) { try { if (clip != null) { clip.prepareToLeaveProcess(true); } mService.setPrimaryClip(clip, mContext.getOpPackageName()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Returns the current primary clip on the clipboard. */ public ClipData getPrimaryClip() { try { return mService.getPrimaryClip(mContext.getOpPackageName()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } }
可以发现这些方法最终都会调用到 mService 的相关方法。因此,ClipboardManager 的 mService 确实是一个可以 hook 的一个点。
hook ClipboardManager.mService 的实现
大概需要三个步骤
第一步:得到 ClipboardManager 的 mService
第二步:初始化动态代理对象
第三步:偷梁换柱,使用 proxyNotiMng 替换系统的 mService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 public static void hookClipboardService(final Context context) throws Exception { ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); Field mServiceFiled = ClipboardManager.class.getDeclaredField("mService"); mServiceFiled.setAccessible(true); // 第一步:得到系统的 mService final Object mService = mServiceFiled.get(clipboardManager); // 第二步:初始化动态代理对象 Class aClass = Class.forName("android.content.IClipboard"); Object proxyInstance = Proxy.newProxyInstance(context.getClass().getClassLoader(), new Class[]{aClass}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Log.d(TAG, "invoke(). method:" + method); String name = method.getName(); if (args != null && args.length > 0) { for (Object arg : args) { Log.d(TAG, "invoke: arg=" + arg); } } if ("setPrimaryClip".equals(name)) { Object arg = args[0]; if (arg instanceof ClipData) { ClipData clipData = (ClipData) arg; int itemCount = clipData.getItemCount(); for (int i = 0; i < itemCount; i++) { ClipData.Item item = clipData.getItemAt(i); Log.i(TAG, "invoke: item=" + item); } } Toast.makeText(context, "检测到有人设置粘贴板内容", Toast.LENGTH_SHORT).show(); } else if ("getPrimaryClip".equals(name)) { Toast.makeText(context, "检测到有人要获取粘贴板的内容", Toast.LENGTH_SHORT).show(); } // 操作交由 sOriginService 处理,不拦截通知 return method.invoke(mService, args); } }); // 第三步:偷梁换柱,使用 proxyNotiMng 替换系统的 mService Field sServiceField = ClipboardManager.class.getDeclaredField("mService"); sServiceField.setAccessible(true); sServiceField.set(clipboardManager, proxyInstance); }
第二种方法 对 Android 源码有基本了解的人都知道,Android 中的各种 Manager 都是通过 ServiceManager 获取的。因此,我们可以通过 ServiceManager hook 所有系统 Manager,ClipboardManager 当然也不例外。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public final class ServiceManager { /** * Returns a reference to a service with the given name. * * @param name the name of the service to get * @return a reference to the service, or <code>null</code> if the service doesn't exist */ public static IBinder getService(String name) { try { IBinder service = sCache.get(name); if (service != null) { return service; } else { return getIServiceManager().getService(name); } } catch (RemoteException e) { Log.e(TAG, "error in getService", e); } return null; } }
老套路
第一步:通过反射获取剪切板服务的远程Binder对象,这里我们可以通过 ServiceManager getService 方法获得
第二步:创建我们的动态代理对象,动态代理原来的Binder对象
第三步:偷梁换柱,把我们的动态代理对象设置进去
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public static void hookClipboardService() throws Exception { //通过反射获取剪切板服务的远程Binder对象 Class serviceManager = Class.forName("android.os.ServiceManager"); Method getServiceMethod = serviceManager.getMethod("getService", String.class); IBinder remoteBinder = (IBinder) getServiceMethod.invoke(null, Context.CLIPBOARD_SERVICE); //新建一个我们需要的Binder,动态代理原来的Binder对象 IBinder hookBinder = (IBinder) Proxy.newProxyInstance(serviceManager.getClassLoader(), new Class[]{IBinder.class}, new ClipboardHookRemoteBinderHandler(remoteBinder)); //通过反射获取ServiceManger存储Binder对象的缓存集合,把我们新建的代理Binder放进缓存 Field sCacheField = serviceManager.getDeclaredField("sCache"); sCacheField.setAccessible(true); Map<String, IBinder> sCache = (Map<String, IBinder>) sCacheField.get(null); sCache.put(Context.CLIPBOARD_SERVICE, hookBinder); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class ClipboardHookRemoteBinderHandler implements InvocationHandler { private IBinder remoteBinder; private Class iInterface; private Class stubClass; public ClipboardHookRemoteBinderHandler(IBinder remoteBinder) { this.remoteBinder = remoteBinder; try { this.iInterface = Class.forName("android.content.IClipboard"); this.stubClass = Class.forName("android.content.IClipboard$Stub"); } catch (Exception e) { e.printStackTrace(); } } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Log.d("RemoteBinderHandler", method.getName() + "() is invoked"); if ("queryLocalInterface".equals(method.getName())) { //这里不能拦截具体的服务的方法,因为这是一个远程的Binder,还没有转化为本地Binder对象 //所以先拦截我们所知的queryLocalInterface方法,返回一个本地Binder对象的代理 return Proxy.newProxyInstance(remoteBinder.getClass().getClassLoader(), new Class[]{this.iInterface}, new ClipboardHookLocalBinderHandler(remoteBinder, stubClass)); } return method.invoke(remoteBinder, args); } }
Hook Activity 关于怎样 hook activity,以及怎样启动没有在 AndroidManifet 注册的 activity,可以查看我的这一篇博客。
Android Hook Activity 的几种姿势
源码下载地址 : HookDemo
推荐阅读 Android 启动优化(一) - 有向无环图
Android 启动优化(二) - 拓扑排序的原理以及解题思路
Android 启动优化(三)- AnchorTask 开源了
Android 启动优化(四)- AnchorTask 是怎么实现的
Android 启动优化(五)- AnchorTask 1.0.0 版本正式发布了
Android 启动优化(六)- 深入理解布局优化
这几篇文章从 0 到 1,讲解 DAG 有向无环图是怎么实现的,以及在 Android 启动优化的应用。
推荐理由:现在挺多文章一谈到启动优化,动不动就聊拓扑结构,这篇文章从数据结构到算法、到设计都给大家说清楚了,开源项目也有非常强的借鉴意义。