stackplz是一款基于eBPF技术实现的追踪工具,目的是辅助安卓native逆向,仅支持64位进程,主要功能如下:
pref_event
实现的硬件断点功能,在断点处可读取寄存器信息,不会被用户态探测到
CONFIG_HAVE_HW_BREAKPOINT
,5.10+内核默认开启pid/uid/tid/package
等组合进行过滤SIGSTOP
可挂起进程(硬件断点暂未实现发送信号
使用要求:内核 5.10+(开发板+ReDroid也是可以的,当前支持rock5b),该限制主要来自于需要使用bpf_probe_read_user
接口
stackplz最初是受到定制bcc/ebpf在android平台上实现基于dwarf的用户态栈回溯启发,加上自己对eBPF技术略有一点研究,然后决定开发这样一款工具
当时核心目的是寻求一种门槛低、不依赖额外环境、能实际落地的方案开发eBPF程序
受限于当时高版本内核安卓设备的普及程度,只实现了寄存器获取与堆栈回溯,限制是内核要求4.14+
经过一年左右的发展,GKI 2.0设备多了起来,各类eBPF相关的内容不断涌现 ,加上之前自己立了flag
于是最近几个月把之前给stackplz立的flag给陆续落地,加上了许多功能,以期能真正将eBPF技术投入实际的安卓逆向分析中
另外KernelSU近期也发布了新的计划:
什么,无限制的eBPF?那真的是太好了! 所以本文再不出,stackplz就要凉啦(>_<)
再次说明要求与限制:内核 5.10+,仅支持64位进程
请前往Releases或者Actions下载使用
1 2 3 |
|
注意:每次更新版本请手动执行一次./stackplz --prepare
本文演示过程中没有任何APP受到伤害!
写原理太枯燥你们可能就划走了,下次再写,还是先看看实际效果吧~
硬件断点支持四种类型,即r,w,x,rw
,断点长度为4字节
命令中必须提供pid,断点绝对地址
或者断点偏移+动态库名
,下面是两个示例效果:
1 |
|
1 |
|
说明:
--regs
输出寄存器信息--stack
默认获取用户态8192字节的栈上数据用于回溯堆栈--stack-size
可以用于指定要获取的栈上数据大小,必须是8的倍数如果要给内核空间的地址下硬件断点,可以使用pid+绝对地址的方式,pid给定一个存在的进程即可(不过目前没有回溯内核堆栈的功能,所以给内核空间地址下硬件断点聊胜于无
其原理是替换目标位置动态库的指令为brk指令,执行到目标位置的时候会陷入内核,执行eBPF虚拟机中的代码
美中不足的是,这个brk指令是可以被扫描出来的,但优势也是只有这一个brk指令,没有inlinehook那么多特征
如果确实担心被扫描,那么可以采用硬件断点先检查下^_^
基于该能力,stackplz可以在断点位置读取上下文的内容,在逆向时以最小的侵入力度获取想要的内容,尽可能避开检测
下面是具体效果:
1 |
|
1 |
|
1 2 3 |
|
说明:
--getoff
计算LR与PC的偏移情况,方便快速定位调用位置-l/--lib
指定动态库路径,默认为libc.so
/proc/{pid}/maps
中的完整路径-w/--point
意为观测点,配合动态库指定符号或者偏移,可设置多个,上限6个
int,uint,int64,uint64,str,ptr,buf
:
后面是寄存器表明以该寄存器大小为长度读取数据:
后面是十进制或十六进制字符串,则表明以这个数的大小为长度读取数据:
后的第一个内容表示长度,末尾的才是要读取的寄存器-f/--filter
字符串过滤规则,有三种,分别是w/white b/black r/replace
,示例如下
.
作为分隔符:::
作为分隔符openat
的第二个参数,应用规则0和规则1:
原理是将eBPF程序挂载到raw_tracepoint/sys_enter
和raw_tracepoint/sys_exit
,然后根据预设配置解析参数
追踪syscall的时候自然也是能输出堆栈的,不过限于篇幅,下面的例子中就不演示了,加--stack
即可
下面是具体效果:
1 |
|
1 |
|
1 |
|
说明:
-s/--syscall
一般情况,追踪多个syscall要用,
作为分隔符
all %attr %file %exec %clone %process %net %signal %kill %stat
:
作为起始分隔符,使用.
作为多个规则的分隔符
-s openat:f0.f1.f2
下面将演示如何过root检查,虽然对于eBPF来说做这个事情比较鸡肋,但是还是值得演示一下
核心原理就是通过bpf_probe_write_user
方法修改内存数据,这里过root检查就是简单替换下相关的字符串
再次说明:本文演示过程中没有任何APP受到伤害!
这里请一位不愿透露姓名的APP做演示,过程如下:
execve
和openat
两个系统调用--stack
、--mstack
和--getoff
,进行更准确的分析mount
和which su
等命令,而且是走的libc.so,于是再开一个shell执行第三个命令 1 2 3 |
|
使用--mstack
,该选项意为手工解析堆栈,因为某些情况下默认的方案可能无法给出详细的堆栈(全是unknown
在后面两个命令同时使用的情况下,最终能走到APP的启动页面:
不过使用eBPF过这个检查实在是麻烦...所以stackplz的定位是辅助分析,这里仅仅演示一下eBPF修改内存的能力
对了,APP怎么老是退出,想个办法让它不要退出好不好哇(
1 |
|
这样每次调用exit的时候,都会发送SIGSTOP
信号,可以把整个进程挂起
每次挂起后如果要恢复进程运行,那么执行kill -SIGCONT {pid}
这样的命令即可
不过有一个需要注意的问题,那就是发信号的时候,syscall还是会执行,也就是说如果在exit这个系统调用处发信号,进程有可能还是没办法挂起
那么可以配合使用--stack
、--mstack
和--getoff
来判断从用户态的什么地方调用,从上面的结果来看,是libc.so的__exit
那么使用下面的命令进行追踪,这样就可以真正在执行到__exit
的时候挂起,上下文也会保持在__exit
的状态
1 |
|
这是lldb附加上去的效果:
这里的iso
指追踪isolated
进程,-n/--name
选项可以给定进程分组,目前的分组是root system shell app iso
如果单条命令hook无法一次性实现想要的追踪内容,那么多开stackplz即可,以及同一个位置多次下hook都是支持的
具有黑白名单的选项:
白名单 | 黑名单 |
---|---|
-u/--uid | --no-uid |
-p/--pid | --no-pid |
-t/--tid | --no-tid |
--tname | --no-tname |
-s/--syscall | --no-syscall |
在某些情况下,stackplz无法解析到偏移、堆栈,可以设置debug和输出日志到文件,然后根据日志中的各类事件信息进行分析
--debug -o tmp.log
仅将日志输出到文件,不输出到终端,这对于进行大量追踪的时候,有助于减少数据丢失
-q/--quiet
默认情况下,buf类型的数据打印为ASCII + hex
,使用下面的选项将打印为hexdump
--dumphex
追踪一些特别频繁的调用时,可能会出现部分数据丢失的情况,这个时候请适当增加eBPF的数据缓冲区大小
-b/--buffer
默认8M,一般不建议超过32M更多命令,请执行./stackplz --help
查看
Q: 不能修改寄存器吗
A: 是的,eBPF中无法修改寄存器,只能修改给定地址处的内存数据
Q: 运行不了
A: 首先请使用5.10+
内核的设备,如果确实不行请反馈至issue
Q: 技术架构等
A: 语言:Golang + C
A: 三方库:ebpf + ebpfmanager + libbpf
A: 预编译:BTFHubForAndroid + unwinddaemon
A: 其他:ndk clang + Makefile