Vibe了一个Binder消息跟踪小玩具. 本质上是在大佬的基础上做了一些拓展和完善. 测了一些厂商的App,发现用来做行为检测还算不错,能给逆向分析提供一些"灵感". 于是放出来大家一起玩儿.
null-luo佬的btrace只做了Binder的请求监控,但是我们在实际逆向分析过程中除了看请求也有看响应内容的需求.尤其是在做风控对抗时,不看响应难以判断这个Binder请求是否暴露了什么痕迹.
Binder请求是可以分为oneway(只调用无响应)和twoway(调用完了还有响应内容)的. oneway请求自然是只抓请求就可以,但是twoway请求,除了App发起调用时的一次Binder调用,目标返回响应其实也是一次Binder调用.
举个例子:
App调用PMS的getPackageInfo方法时, App会先把参数通过Binder消息发送给PMS,而后这个发送Binder消息的线程变成同步阻塞状态. 当PMS把响应Binder消息返回过来以后,App的线程才会继续向下执行.
每一个Binder消息都会经过binder_transaction,而这个函数的第4个参数就是reply标志位.如果标志位被置1,就证明当前的消息是一个relpy的消息.
但是只能判断出reply对于我们的需求还远远不够,只有把请求和响应一一对应起来,我们才能知道这一次Binder调用的真实情况.
这里我通过debug_id来将两个请求帧关联起来. debug_id是内核在创建每一笔事务时分配的编号,全局唯一而且稳定,一笔事务从request到reply自始至终都是同一个debug_id.
twoway请求中的reply消息,在到达binder_transaction函数时结构体内已经有了debug_id,直接读就行.
reply是服务端(比如PMS)处理完请求以后发回来的,这个时候服务端线程的thread->transaction_stack上还挂着当初那笔request的binder_transaction对象(内核里管它叫in_reply_to), debug_id就是它的一个属性.
沿用btrace在binder_transaction入口的Hook点,可以直接拿到请求Parcel消息内容,但request的struct binder_transaction结构体要到函数体里才被内核创建出来,所以这里我又加了两个Hook点来关联出来request的Parcel内容和debug_id.
首先是在binder_transaction上挂了一个raw_tracepoint. tracepoint的触发点在函数体里,这时候相关结构体已经建好了,debug_id能读到.
但这又带出一个新问题: 入口kprobe上报的"事件本体"和raw_tracepoint上报的debug_id是分两次发出去的,单次直接发的话会因为ebpf的限制而导致ebpf加载失败. 所以这里又加了第二个Hook点,来关联前后的Parcel消息和debug_id.
单个request的入口kprobe和raw_tracepoint,跑在同一个线程、同一次函数调用里,中间不会切走. 所以在入口处先分配一个cookie写进pending_map[tid],实现Parcel和中间态的映射,等raw_tracepoint触发时,按当前tid把中间态cookie取回来,和debug_id一起上报.
到这里两条路径就都打通了:
两条帧都带上了debug_id以后,最终的App端只要看谁和谁的debug_id相等,就能把一次twoway调用的request和reply重新拼回一对.
btrace受限于纯Golang的原因,只能用离线跑再持久化的方式来解析包名&方法名. 这里我补齐了动态反射的能力,还额外加了App私有的AIDL解析.
Binder消息中自带了调用方和被调用方的UID,有了这个信息就可以得到基础包名信息.而进一步的接口名和方法名就需要进一步对Parcel内容进行解析了.
这里我兼容了Android O / Android P / Android Q+ 三种格式的Binder消息. 同时针对于系统驱动的HIDL做了单独适配,如果还是不能覆盖,则会采用所谓的"启发式"推理,根据数据格式大致推测一个结构. 这个不保准,所以会单独标注出来.
[内核课程]《Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。