乐趣区

关于android:Android-Hook-机制之简单实战

简介

什么是 Hook

Hook 又叫“钩子”,它能够在事件传送的过程中截获并监控事件的传输,将本身的代码与零碎办法进行融入。这样当这些办法被调用时,也就能够执行咱们本人的代码,这也是面向切面编程的思维(AOP)。

Hook 分类

1. 依据 Android 开发模式,Native 模式(C/C++)和 Java 模式(Java)辨别,在 Android 平台上

  • Java 层级的 Hook;
  • Native 层级的 Hook;

2. 根 Hook 对象与 Hook 后处理事件形式不同,Hook 还分为:

  • 音讯 Hook;
  • API Hook;

3. 针对 Hook 的不同过程上来说,还能够分为:

  • 全局 Hook;
  • 单个过程 Hook;

常见 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 层,这样的兼容性就十分好。原理是这样的,间接结构出新旧办法对应的虚拟机数据结构,而后替换信息写到内存中即可。

Hook 必须把握的常识

  • 反射

如果你对反射还不是很相熟的话,倡议你先温习一下 java 反射的相干常识。有趣味的,能够看一下我的这一篇博客 Java 反射机制详解

  • java 的动静代理

动静代理是指在运行时动静生成代理类,不须要咱们像动态代理那个去手动写一个个的代理类。在 java 中,咱们能够应用 InvocationHandler 实现动静代理,有趣味的,能够查看我的这一篇博客 java 代理模式详解

本文的次要内容是解说单个过程的 Hook,以及怎么 Hook。


Hook 应用实例

Hook 抉择的关键点

  • Hook 的抉择点:尽量动态变量和单例,因为一旦创建对象,它们不容易变动,非常容易定位。
  • Hook 过程:
  • 寻找 Hook 点,准则是尽量动态变量或者单例对象,尽量 Hook public 的对象和办法。
  • 抉择适合的代理形式,如果是接口能够用动静代理。
  • 移花接木——用代理对象替换原始对象。
  • Android 的 API 版本比拟多,办法和类可能不一样,所以要做好 API 的兼容工作。

简略案例一: 应用 Hook 批改 View.OnClickListener 事件

首先,咱们先剖析 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 事件?

大略分为三步:

  • 第一步:获取 ListenerInfo 对象

从 View 的源代码,咱们能够晓得咱们能够通过 getListenerInfo 办法获取,于是,咱们利用反射失去 ListenerInfo 对象

  • 第二步:获取原始的 OnClickListener 事件办法

从下面的剖析,咱们晓得 OnClickListener 事件被保留在 ListenerInfo 外面,同理咱们利用反射获取

  • 第三步:移花接木,用 Hook 代理类 替换原始的 OnClickListener
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}

简略案例二:HooK Notification

发送音讯到告诉栏的外围代码如下:

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,大略须要三步:

  • 第一步:失去 NotificationManager 的 sService
  • 第二步:因为 sService 是接口,所以咱们能够应用动静代理,获取动静代理对象
  • 第三步:移花接木,应用动静代理对象 proxyNotiMng 替换零碎的 sService

于是,咱们能够写出如下的代码

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 应用进阶

Hook ClipboardManager

第一种办法

从下面的 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 的实现

大略须要三个步骤

  • 第一步:失去 ClipboardManager 的 mService
  • 第二步:初始化动静代理对象
  • 第三步:移花接木,应用 proxyNotiMng 替换零碎的 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}

老套路

  • 第一步:通过反射获取剪切板服务的近程 Binder 对象,这里咱们能够通过 ServiceManager getService 办法取得
  • 第二步:创立咱们的动静代理对象,动静代理原来的 Binder 对象
  • 第三步:移花接木,把咱们的动静代理对象设置进去
 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}
退出移动版