Android Hook 机制之简单实战[通俗易懂]

Android Hook 机制之简单实战[通俗易懂]简介什么是HookHook又叫“钩子”,它可以在事件传送的过程中截获并监控事件的传输,将自身的代码与系统方法进行融入。这样当这些方法被调用时,也就可以执行我们自己的代码,这也是面向切面编程的思想(AOP)。Hook分类1.根据Android开发模式,Native模式(C/C++)和Java模式(Java)区分,在Android平台上Java层级的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 执行的前后加上自定义代码。

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

}


Android Hook 机制之简单实战[通俗易懂]

第二种方法

对 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 启动优化(一) – 有向无环图

Android 启动优化(二) – 拓扑排序的原理以及解题思路

Android 启动优化(三)- AnchorTask 开源了

Android 启动优化(四)- AnchorTask 是怎么实现的

Android 启动优化(五)- AnchorTask 1.0.0 版本正式发布了

Android 启动优化(六)- 深入理解布局优化

这几篇文章从 0 到 1,讲解 DAG 有向无环图是怎么实现的,以及在 Android 启动优化的应用。

推荐理由:现在挺多文章一谈到启动优化,动不动就聊拓扑结构,这篇文章从数据结构到算法、到设计都给大家说清楚了,开源项目也有非常强的借鉴意义。

在这里插入图片描述

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/135743.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

  • PXE网络

    PXE网络PXE原理和概念PXE严格来说并不是一种安装方式,而是一种引导的方式。进行PXE安装的必要条件是要安装的计算机中包含一个PXE支持的网卡(NIC),即网卡中必须要有PXEClient。PXE(Pre-bootExecutionEnvironment)协议使计算机可以通过网络启动。协议分为client和server端,PXEclient在网卡的ROM中,当计算机引导时,BIOS把PXEclient调入内存执行,由PXEclient将放置在远端的文件通过.

  • ntp 校时 linux 带源码

    ntp 校时 linux 带源码最近做个项目,想通过公司上的NTP服务器给板子校时,但是板子里没有ntpdate这个命令,下面是2个解决方法,1,找到ntpdate源代码,重新编译之后,手动运行,这个方法我上网上查了,比较复杂,据说NTP还与SSL有关,编译的时候必须把SSL也包含进去,于是就迟迟没有动工。2,突然有一天看到rtthread里也提供一个ntp的客户端,比较简单,就一个文件,也没几行,于是想着把这个.c文件移植到linux下,但我仔细研究了一下,发现这个文件的原始作者就是在linux下设计的。原始文件更简单.

  • 国外最流行的Bootstrap后台管理模板

    国外最流行的Bootstrap后台管理模板工欲善其事,必先利其器对于从事软件开发的您也一样,有一套熟悉的bootstrap后台ui框架让您的开发速度大幅度提升这是本人经常使用到的一些bootstrap后台框架推荐给大家第一名inspiniabootstrap演示地址http://cn.inspinia.cn效果图http://cn.inspinia.cnhttp://cn.inspinia.cn第二名…

  • 开发发版流程_文件签发流程

    开发发版流程_文件签发流程迭代流程开发人员:周一到周五产品设计:周一到周五测试人员:周六收集需求:周一周二周三周四需求梳理周五用户意见周六第二次需求梳理需求阶段第一次需求梳理会议开发人员和测试人员通过此会议了解下一次迭代

  • phpspreadsheet中文手册_php读取文件内容

    phpspreadsheet中文手册_php读取文件内容由于phpexcel已经不再维护,phpspreadsheet是phpexcel的下一个版本。phpspreadsheet是一个用纯php编写的库,并引入了命名空间,psr规范等。这里简单介绍下phpspreadsheet的导入导出功能。1、安装使用composer安装:composerrequirephpoffice/phpspreadsheetgithub下载:2、excel文件导出/**…

  • 鼠标捕获(setCapture,releaseCapture)的学习

    鼠标捕获(setCapture,releaseCapture)的学习鼠标捕获(setCapture)作用是将鼠标事件捕获到当前文档的指定的对象——对指定的对象设置鼠标捕获。这个对象会为当前应用程序或整个系统接收所有鼠标事件。所谓鼠标捕获,是指对鼠标事件(onmousedown,onmouseup,onmousemove,onclick,ondblclick,onmouseover,onmouseout)进行捕捉,使在容器内的子对象的鼠标事件均…

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号