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