Android Hook 机制之简单实战

作者: 程序员徐公 | 来源:发表于2018-09-05 19:54 被阅读56次

    简介

    什么是 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 执行的前后加上自定义代码。

    1. Cydia Substrate

    Cydia Substrate 框架为苹果用户提供了越狱相关的服务框架,当然也推出了 Android 版 。Cydia Substrate 是一个代码修改平台,它可以修改任何进程的代码。不管是用 Java 还是 C/C++(native代码)编写的,而 Xposed 只支持 Hook app_process 中的 Java 函数。

    1. 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 。

    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 事件?

    大概分为三步:

    • 第一步:获取 ListenerInfo 对象

    从 View 的源代码,我们可以知道我们可以通过 getListenerInfo 方法获取,于是,我们利用反射得到 ListenerInfo 对象

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

    从上面的分析,我们知道 OnClickListener 事件被保存在 ListenerInfo 里面,同理我们利用反射获取

    • 第三步:偷梁换柱,用 Hook代理类 替换原始的 OnClickListener
    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);
    }
    
    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”

    mBtn1 = (Button) findViewById(R.id.btn_1);
    mBtn1.setOnClickListener(this);
    try {
        HookHelper.hookOnClickListener(mBtn1);
    } catch (Exception e) {
        e.printStackTrace();
    }
    

    简单案例二: HooK Notification

    发送消息到通知栏的核心代码如下:

    NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
    notificationManager.notify(id, builder.build());
    

    跟踪 notify 方法发现最终会调用到 notifyAsUser 方法

    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 方法即可

    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

    于是,我们可以写出如下的代码

    
    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 的一个点。

    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

    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
    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);
    
    }
    
    
    
    image

    第二种方法

    对 Android 源码有基本了解的人都知道,Android 中的各种 Manager 都是通过 ServiceManager 获取的。因此,我们可以通过 ServiceManager hook 所有系统 Manager,ClipboardManager 当然也不例外。

    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对象
    • 第三步:偷梁换柱,把我们的动态代理对象设置进去
    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);
    
    }
    
    
    
    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 Hook 机制之简单实战

    Android Hook Activity 的几种姿势

    最后的最后

    卖一下广告,欢迎大家关注我的微信公众号,扫一扫下方二维码或搜索微信号 stormjun,即可关注。 目前专注于 Android 开发,主要分享 Android开发相关知识和一些相关的优秀文章,包括个人总结,职场经验等。

    image

    相关文章

      网友评论

      本文标题:Android Hook 机制之简单实战

      本文链接:https://www.haomeiwen.com/subject/rpjzwftx.html