Android 免Hook消息监控

2024-05-28 12:43:51 浏览数 (2)

前言

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

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

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

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

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

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

代码语言:javascript复制
public void dispatchMessage(@NonNull Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {  //优先于handleMessage执行
                return;
            }
        }
        handleMessage(msg);
    }
}

免Hook原理

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

性能监控和消息监控

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

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

代码语言:javascript复制
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)的消息只拿到了 >>>>> Dispatching 和 <<<<< Finished,就实现Handler性能监控,如果我们拿整个msg会怎么样?我们来打印一下执行结果

代码语言:javascript复制
>>>>> 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中的消息,一旦执行都能被拦截,而且整条消息的中的Handler、callback、what也会暴露出来。我们平时想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,如果是业务中的还是相当好定位的,但是如果是第三方的,难度还是稍微有些高。

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

代码语言:javascript复制
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即可,这里我们实现一个类来记录数据。

代码语言:javascript复制
class MessageInfo{
  String target; //className
  String callback;  //className
  int what;
}

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

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

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

代码语言:javascript复制
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仍然是可以的,因为大部分都有特定的类名特征。

0 人点赞