一、简介:
在进行Android恶意APP检测时,需要进行自动化的行为分析,一般至少包括行为采集和行为分析两个模块。其中,行为分析有基于规则、基于机器学习、基于深度学习甚至基于大模型的方案,各有各的优缺点,不是本文关注的重点,本文主要关注Android APP的动态行为采集。在做Android APP逆向分析时经常需要通过hook系统调用观察APP的行为,也需要一个动态行为追踪工具。
btrace(GitHub - null-luo/btrace: btrace:binder_transaction+eBPF+Golang实现通用的Android APP动态行为追踪工具)就是一个开源的针对Android APP的动态行为采集/追踪工具。目标是通用、可靠、简单。如果类比到Linux tracing systems的话,我们的工具也可以分成三部分:data sources我们的方案是kprobe/binder_transaction;way to extract data我们采用eBPF;frontends我们使用Golang。
接下来分别介绍这三个部分的方案。
二、data sources:kprobe/binder_transaction
binder是Android IPC的核心机制,Android APP在访问系统服务的时候,实际上就是在进行跨进程通信,因此,监控binder就可以获取到APP调用系统服务的行为。
这里就不再重复说明了,我们重点看一下在kernel层的哪个函数做监控比较好。我们的目标是要获取:APP的包名、调用服务
首先想到的是内核已经定义的tracepoint:
可惜大部分tracepoint都没有带上binder核心数据的指针,也就是没有办法获取到目标服务名和函数参数: 只有binder_ioctl这个tracepoint里面的arg指向的是struct binder_write_read:
但问题是struct binder_write_read相当的原始,解析起来比较复杂:
这是因为binder_ioctl是链路上kernel层的第一个函数,传进来的数据还没有经过处理。那么,我们能不能找一找binder_ioctl后面的函数,尽可能让系统对数据进行解析和处理之后我们直接拿到想要的字段呢?
我们把binder_ioctl->binder_ioctl_write_read->binder_thread_write->binder_transaction这条调用链分析了一下,发现binder_transaction是一个比较合适的点,在它之前的函数已经对用户层传入的数据进行了很多解析和过滤,这里拿到的数据是struct binder_transaction_data,相对比较简单了:
其实,仔细看binder_transaction函数的代码可以发现,本来通过binder_debug和trace_binder_transaction这两个地方直接拿到数据是最方便的,可惜的是binder_debug没有输出code(调用函数的编号),trace_binder_transaction又没有输出调用服务名和参数的数据指针。导致没有办法直接使用这两个点。
尤其是trace_binder_transaction,如果往后一点放到内存拷贝(user->kernel)完成之后,再将数据指针输出的话就非常完美了。
所以,最后我们还是回到对binder_transaction这个内核函数进行监控,解析参数struct binder_transaction_data来拿到数据的方案。
三、way to extract data:eBPF
eBPF是一个运行在Linux内核里面的虚拟机组件,它可以在无需改变内核代码或者加载内核模块的情况下,安全而又高效地拓展内核的功能。是一种非侵入性的内核函数hook方法。
并且,Google 为了解决 Android 碎片化提出了GKI(通用内核镜像),要求Android 12以上版本的设备出厂必须使用GKI内核,而且GKI内核的编译选项把eBPF相关的功能都是打开的。
所以eBPF特别适合用于对Android设备中Linux内核函数的监控。
binder_transaction函数总共5个参数,我们可以根据第4个参数来过滤掉回应的transaction,只关注请求的transaction:
我们的目标是要获取:APP的包名、调用服务名、调用函数名、调用参数这几个字段:
-
APP的包名可以通过当前UID来获取(因为binder_transaction函数是在client的进程内);
-
调用函数名可以通过binder_transaction_data->code来获取;
-
调用服务名和调用参数可以通过binder_transaction_data->data.ptr.buffer来获取;
其中要注意的是,binder_transaction_data->data.ptr.buffer指向的数据目前还在用户空间,还没有完成向内核空间的拷贝,所以需要使用bpf_probe_read_user函数。(这就是我上节说的如果把trace_binder_transaction往后移到内存拷贝之后,并且把内核空间的数据地址输出,那就完美了,可惜!):
四、frontend:Golang
eBPF的核心程序一般是使用C语言编写,clang进行编译后,需要将其加载到内核中。目前有多个项目对eBPF的编写调试运行的流程进行了封装和优化,比如bcc、libbpf等,我们选择的是cilium/ebpf。
它封装了BPF系统调用,与内核提供的libbpf类似,区别在于这个库是Go语言的,更加方便进行用户态程序的开发,而且外部依赖少,与此同时其还提供了bpf2go工具,可用来将eBPF程序编译成Go语言中的一部分,使得交付更加方便。也就是说很容易将项目编译为一个独立可运行的ELF文件。
我们的开发环境是Ubuntu arm64的虚拟机(主机是Mac):
cilium/ebpf使用起来非常方便,整个框架分为三个部分:
- 运行在内核态用C写eBPF代码,llvm编译为eBPF字节码;
- 用户态使用Golang编写,cilium/ebpf纯go类库,做eBPF字节码的内核加载,kprobe HOOK对应函数;
- 用户态使用Golang做事件读取、解码、处理。
我们在内核态程序里将需要的数据放到ringbuf里传递给用户态:
用户态程序收到数据后做处理:
1、APP的包名
知道UID后执行命令"pm list packages -U"去查一下就能知道:
2、调用函数名、调用服务名、调用参数
Android进程间通信基于Proxy与Stub的设计模式,AIDL是Android接口定义语言,在写完AIDL文件后,编译器自动生成一个同名的.java文件,里面包含Stub和Proxy两个类,Stub类是服务端抽象层的体现。Proxy的接口供客户端程序调用,然后它内部会把信息包装好,通过binder传递给Stub,而后者通过对应的接口作用于服务端系统,从而完成了“远程调用”。
先来看看Proxy的代码,红色对应的就是要调用函数的编号,也就是binder_transaction_data->code。蓝色就是要调用的服务的接口名,绿色部分则是要调用函数的参数,可以看出来这两部分被打包到一个Parcel里面去,对应的就是binder_transaction_data->data.ptr。最后通过transact函数将以上三部分内容往binder传递。
writeInterfaceToken函数在写入接口名之前,还写了12字节(4+4+4)的其他数据:
我们在解析的时候先跳过头部12字节,接下来的4字节代表接口名字符串的长度,接着的数据即是接口名字符串:
最后,看一下binder_transaction_data->code如何转换成函数名,仔细分析了binder流程代码,函数名在编译.aidl文件的时候就已经转换成code了,之后一直传递的都是code,直到服务端的onTransact函数里才根据code去选择函数:
所以在整个binder数据传输的过程中都找不到合适的hook点,后来偶然发现.aidl文件自动生成的Stub类里面有getTransactionName、getDefaultTransactionName这么两个函数可以根据函数编码获取到函数名,那么我们就可以使用反射来获取函数名
顺手在Android代码里搜索了一下这两个函数,发现有一个类已经将对getDefaultTransactionName的调用包装好了:
但是在golang里面不太好调用Android的API,所以换了一个思路,写了一个Android APP,利用反射把系统服务下所有的transactionCode和methodName的映射关系记录下来,输出给btrace在运行时候查询:
有几个注意点:
- 系统所有的服务名可以通过service list获取:
- 需要打开策略开关才能访问hide的API:adb shell settings put global hidden_api_policy 1。
-
每一个服务内transaction函数一般是从1开始逐个编号的,而且每个函数对应一个field,所以我们获取服务类的field数目就知道此类最多有多少个transaction函数需要我们尝试去获取名字:
最后效果(参数的解析暂不支持):
五、总结:
我们使用binder_transaction+eBPF+Golang来实现一个针对Android APP的动态行为追踪工具,目标是通用、可靠、简单。
-
通用:基于binder底层内核函数,可以监控到所有API调用,覆盖系统版本广泛;
-
可靠:基于eBPF,对内核无侵入,并且有验证器的验证,安全可靠;
-
简单:基于Golang作为frontend,灵活高效,逻辑简单清晰,外部依赖少,单一ELF可独立运行。