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仍然是可以的,因为大部分都有特定的类名特征。

相关推荐
学习使我快乐0124 分钟前
JS进阶 3——深入面向对象、原型
开发语言·前端·javascript
bobostudio199525 分钟前
TypeScript 设计模式之【策略模式】
前端·javascript·设计模式·typescript·策略模式
黄尚圈圈1 小时前
Vue 中引入 ECharts 的详细步骤与示例
前端·vue.js·echarts
浮华似水2 小时前
简洁之道 - React Hook Form
前端
服装学院的IT男2 小时前
【Android 13源码分析】Activity生命周期之onCreate,onStart,onResume-2
android
Arms2062 小时前
android 全面屏最底部栏沉浸式
android
服装学院的IT男2 小时前
【Android 源码分析】Activity生命周期之onStop-1
android
正小安4 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
ChinaDragonDreamer5 小时前
Kotlin:2.0.20 的新特性
android·开发语言·kotlin
_.Switch6 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j