在进行Android恶意APP检测时,需要进行自动化的行为分析,一般至少包括行为采集和行为分析两个模块。其中,行为分析有基于规则、基于机器学习、基于深度学习甚至基于大模型的方案,各有各的优缺点,不是本文关注的重点,本文主要关注Android APP的动态行为采集。在做Android APP逆向分析时经常需要通过hook系统调用观察APP的行为,也需要一个动态行为追踪工具。
btrace(https://github.com/null-luo/btrace)就是一个开源的针对Android APP的动态行为采集/追踪工具。目标是通用、可靠、简单。如果类比到Linux tracing systems的话,我们的工具也可以分成三部分:data sources我们的方案是kprobe/binder_transaction;way to extract data我们采用eBPF;frontends我们使用Golang。
接下来分别介绍这三个部分的方案。
binder是Android IPC的核心机制,Android APP在访问系统服务的时候,实际上就是在进行跨进程通信,因此,监控binder就可以获取到APP调用系统服务的行为。
之前已有帖子介绍了在kernel层进行监控和拦截的优势:https://bbs.kanxue.com/thread-279725.htm,这里就不再重复说明了,我们重点看一下在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来拿到数据的方案。
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往后移到内存拷贝之后,并且把内核空间的数据地址输出,那就完美了,可惜!):
eBPF的核心程序一般是使用C语言编写,clang进行编译后,需要将其加载到内核中。目前有多个项目对eBPF的编写调试运行的流程进行了封装和优化,比如bcc、libbpf等,我们选择的是cilium/ebpf。
它封装了BPF系统调用,与内核提供的libbpf类似,区别在于这个库是Go语言的,更加方便进行用户态程序的开发,而且外部依赖少,与此同时其还提供了bpf2go工具,可用来将eBPF程序编译成Go语言中的一部分,使得交付更加方便。也就是说很容易将项目编译为一个独立可运行的ELF文件。
我们的开发环境是Ubuntu arm64的虚拟机(主机是Mac):
cilium/ebpf使用起来非常方便,整个框架分为三个部分:
我们在内核态程序里将需要的数据放到ringbuf里传递给用户态:
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课