Android 免Hook消息监控

前言

在一些情况下,app中经常要做Hook ActivityThread、Choreographer FrameHandler,ViewRootImpl,InputMethodManager中Handler的操作,然而我们往往不可避免的就去hook替换原有的Handler或者Callback,除此之外,还有什么办法呢?

我们本篇通过Looper实现另一种免hook的方式。

为什么要写这篇文章呢,其实是有位同学在《Android 使用 TextView 实现验证码输入框》发现了一个问题,双击TextView会有个选中辅助弹窗指示器,下图"红色雨滴"效果。

最终原因发现android.widget.Editor.InsertionHandleView导致的,但是无法屏蔽他,最后找了个骚操作,拦截其定时隐藏的方法,把定时时间从4秒改为0s,这样弹窗就看不见了,具体解决办法可以看看篇文章。在这个过程中看Looper源码时,发现一些重要的信息平时被忽视了,而这些信息可以减少不必要的hook。

现状

Android发展已经十多年了,回想起几年前做隐私走查的需求,当时我们使用了aspectj 和 hook android.os.ServiceManager 实现,具体原理是把ServiceManager中的IBinder对象给wrap一层Proxy(动态代理BinderProxy) ,然后再替换进ServiceManager,具体可参考这篇文章《Hook ServiceManager实现隐私走查》,通过这种方式可以解决通过反射和binder.transact直接调用的拦截,避免了通过hook方法名拦截不到的问题。当时以为hook 技术已经到了瓶颈,而现实是plt hook 和 native hook如强势来袭,使得hook技术更上一层楼。

不过,hook 本身存在不稳定和难以维护的风险,比方说一些Binder方法的code 会有一些调整,经常失效,总的来,如果紧跟成熟方案,理论上不会有太大的问题。

扯的有点远,我们本篇的主题是免hook消息监控,本篇不会使用反射或者其他hook工具,就能实现对重要组件的监控。

以往的消息监控都是给Handler设置一个Callback,为什么这么做呢,主要原因是很多Handler都被final修饰,更笨无法替换Handler,因此需要使用Callback,因为Callback优先获得执行机会,这就看Handler#dispatchMessaage实现。该的方法实现如下:

java 复制代码
public void dispatchMessage(@NonNull Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

免Hook原理

为什么Looper能实现消息监控呢 ?得益于Looper#setMessageLogging 来实现消息监控,看到这里和性能监控不是一回事么?还有没有继续看的必要呢?

性能监控和消息监控

本篇的主要内容是消息监控而不是性能监控

我们来看看性能监控的核心代码,实际上是匹配日志,显然,这段日志在Android 各个版本中几乎没有变过,因此被用来巧妙的实现性能监控。

java 复制代码
Looper.getMainLooper().setMessageLogging(new Printer() {
    private static final String START = ">>>>> Dispatching";
    private static final String END = "<<<<< Finished";

    @Override
     public void println(String x) {
          if (x.startsWith(START)) {
                //从这里开启一个定时任务来打印方法的堆栈信息
           }
           if (x.startsWith(END)) {
                  //从这里取消定时任务
          }
       }
  });

然而,这就完了么 ?

显然不是的,我们知道,Looper实现消息监控意味着我们能拿到Message中的一些信息。

深入分析日志

实际上,我们对println(String msg)的消息只拿到了部份,就实现了性能监控,如果我们拿整个msg会怎么样?

我们来打印一下结果

java 复制代码
>>>>> Dispatching to Handler (android.app.ActivityThread$H) {3eede03} null: 159
<<<<< Finished to Handler (android.app.ActivityThread$H) {3eede03} null
>>>>> Dispatching to Handler (android.view.ViewRootImpl$ViewRootHandler) {daba180} android.view.ViewRootImpl$7@648f3b9: 0
<<<<< Finished to Handler (android.view.ViewRootImpl$ViewRootHandler) {daba180} android.view.ViewRootImpl$7@648f3b9
>>>>> Dispatching to Handler (android.view.Choreographer$FrameHandler) {16853fe} android.view.Choreographer$FrameDisplayEventReceiver@777575f: 0
<<<<< Finished to Handler (android.view.Choreographer$FrameHandler) {16853fe} 
>>>>> Dispatching to Handler (android.view.ViewRootImpl$ViewRootHandler) {daba180} com.android.internal.policy.PhoneWindow$1@69ed357: 0
<<<<< Finished to Handler (android.view.ViewRootImpl$ViewRootHandler) {daba180} com.android.internal.policy.PhoneWindow$1@69ed357
>>>>> Dispatching to Handler (android.view.ViewRootImpl$ViewRootHandler) {daba180} null: 29
<<<<< Finished to Handler (android.view.ViewRootImpl$ViewRootHandler) {daba180} null
>>>>> Dispatching to Handler (android.view.ViewRootImpl$ViewRootHandler) {daba180} null: 6
<<<<< Finished to Handler (android.view.ViewRootImpl$ViewRootHandler) {daba180} null
>>>>> Dispatching to Handler (android.view.Choreographer$FrameHandler) {16853fe} android.view.Choreographer$FrameDisplayEventReceiver@777575f: 0
<<<<< Finished to Handler (android.view.Choreographer$FrameHandler) {16853fe} android.view.Choreographer$FrameDisplayEventReceiver@777575f
>>>>> Dispatching to Handler (android.view.ViewRootImpl$ViewRootHandler) {daba180} null: 13
<<<<< Finished to Handler (android.view.ViewRootImpl$ViewRootHandler) {daba180} null
>>>>> Dispatching to Handler (android.view.inputmethod.InputMethodManager$H) {9fe7862} null: 4
<<<<< Finished to Handler (android.view.inputmethod.InputMethodManager$H) {9fe7862} null
>>>>> Dispatching to Handler (android.os.Handler) {7d34df3} androidx.emoji2.text.EmojiCompatInitializer$LoadEmojiCompatRunnable@3aa6ab0: 0
<<<<< Finished to Handler (android.os.Handler) {7d34df3} androidx.emoji2.text.EmojiCompatInitializer$LoadEmojiCompatRunnable@3aa6ab0
>>>>> Dispatching to Handler (android.os.Handler) {3f782e5} androidx.emoji2.text.EmojiCompat$ListenerDispatcher@c62b1ba: 0
<<<<< Finished to Handler (android.os.Handler) {3f782e5} androidx.emoji2.text.EmojiCompat$ListenerDispatcher@c62b1ba
>>>>> Dispatching to Handler (android.app.ActivityThread$H) {3eede03} null: 131
<<<<< Finished to Handler (android.app.ActivityThread$H) {3eede03} null

很明显,只要是当前线程的Looper中的消息,一旦执行都能被拦截。

我们平时想hook的目标能被跟踪到

  • android.app.ActivityThread$H
  • android.view.ViewRootImpl$ViewRootHandler
  • android.view.inputmethod.InputMethodManager
  • android.view.Choreographer$FrameHandler
  • android.media.AudioManager.ServiceEventHandlerDelegate$
  • android.content.AsyncQueryHandler.WorkerHandler

当然,还有一些普通的Handler,如果是业务中的还是相当好定位的,但是如果是第三方的,难度还是稍微有些高。

为什么能实现呢,我们还是从消息文本来看。

java 复制代码
logging.println(">>>>> Dispatching to " + msg.target + " "
        + msg.callback + ": " + msg.what);
      //省略一些代码    
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);

实际上,msg 日志中,一些重要的信息被我们遗忘了,就是msg.target、msg.callback以及msg.what,拿到这些消息其实我们已经完全可以避免去Hook这些目标组件的Handler了。

我们只需要解析字符串就能完全避免hook,告别反射获取Handler的行为。

实现代码

我们只需要解析出msg.callback的className,msg.target的className,以及msg.what即可,这里

java 复制代码
class MessageInfo{
  String target; //className
  String callback;  //className
  int what;
}

我们实现两个方法parseBefore和parseAfter,原理就是从字符串中提取上面的信息,当然你还可以使用正则或者其他算法,这里就省掉了,自行实现即可。

注意: 因为Handler是顺序执行的,为了避免内存问题,这里我们要复用对象 MessageInfo holder = new MessageInfo();

接着我们实现免hook消息监听

java 复制代码
Looper.getMainLooper().setMessageLogging(new Printer() {
    private static final String START = ">>>>> Dispatching";
    private static final String END = "<<<<< Finished";
    
    MessageInfo holder = new MessageInfo();

    @Override
     public void println(String x) {
          if (x.startsWith(START)) {
          
               MessageInfo info = parseBefore(x,holder);
               
               if(isActivityThreadH(info)){
                   ActivityThreadH_before(info);
               }else if(isViewRootHandler(info)){
                   ViewRootHandler_before(info);
               }else if(isFrameHandler(info)){
                   FrameHandler_before(info);
               }
           }
           if (x.startsWith(END)) {
                MessageInfo info = parseAfter(x,holder);
               if(isActivityThreadH(info)){
                   ActivityThreadH_after(info);
               }else if(isViewRootHandler(info)){
                   ViewRootHandler_after(info);
               }else if(isFrameHandler(info)){
                   FrameHandler_after(info);
               }
          }
       }
  });

问题和总结

如何获取消息

实际上到这里我们已经可以实现大部分需求了,只要要拿Message,目前来说除了代理looper 循环或者扫描Messagener之外,兼容全版本的方法是没有的。

在Android 10新增了 Looper Observer,通过Looper Observer 可以拿到后置消息,不过,这里我们还是按实际情况来说,获取Message的意义并不大,往往是获取Handler的意义更大一些。

那么如何拿到Handler呢,这里有两种方法:

  • 通过反射
  • Looper Observer 拿到msg.target获取。

这种方式的可靠性

使用字符串识别可靠么 ?

首先,性能监控app也是这么做的。另外,日志都避免了你获取消息本体,显然没有其他风险,android 官方改动的机率应该不大。

总结

本篇比较简短,但如果是监控ActivityThread、Choreographer、ViewRoot、InputMethodManager 的相关Handler消息的需求,不仅省掉了hook等方式,还能更加细致的追踪每个消息的执行。

优缺点

优点

  • 细化性能监控,由此我们的性能监控可以做的更加细化
  • 避免hook,我们对ActivityThread、choreographer等的监控,完全避免了hook

缺点

缺点也比较明显,因为拦截非常依赖msg.target和mgs.callback类名特征,因此,可能不能满足一些情况,但对ViewRootImpl、Choreographer、ActivityThread、InputMethodManager、AudioPortEventHandler、AudioManager等系统组件的Handler仍然是可以的,因为大部分都有特定的类名特征。

相关推荐
Winston Wood17 分钟前
Android Parcelable和Serializable的区别与联系
android·序列化
清风徐来辽22 分钟前
Android 项目模型配置管理
android
帅得不敢出门1 小时前
Gradle命令编译Android Studio工程项目并签名
android·ide·android studio·gradlew
我要洋人死1 小时前
导航栏及下拉菜单的实现
前端·css·css3
科技探秘人1 小时前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人1 小时前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR1 小时前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香1 小时前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
q2498596931 小时前
前端预览word、excel、ppt
前端·word·excel
小华同学ai1 小时前
wflow-web:开源啦 ,高仿钉钉、飞书、企业微信的审批流程设计器,轻松打造属于你的工作流设计器
前端·钉钉·飞书