前言
在一些情况下,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实现。该的方法实现如下:
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 各个版本中几乎没有变过,因此被用来巧妙的实现性能监控。
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会怎么样?我们来打印一下执行结果
>>>>> 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,如果是业务中的还是相当好定位的,但是如果是第三方的,难度还是稍微有些高。
为什么能实现呢,我们还是从消息文本来看。
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即可,这里我们实现一个类来记录数据。
class MessageInfo{
String target; //className
String callback; //className
int what;
}
然后,我们再实现两个方法parseBefore和parseAfter,原理就是从字符串中提取上面的信息,当然你还可以使用正则或者其他算法,这里就省掉了,自行实现即可。
注意:因为Handler是顺序执行的,为了避免内存问题,这里我们要复用对象 MessageInfo holder = new MessageInfo();
接着我们实现免hook消息监听
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仍然是可以的,因为大部分都有特定的类名特征。