kmod-midori / stackplz

基于eBPF的堆栈追踪工具

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

stackplz

stackplz是一款基于eBPF的堆栈追踪工具,仅适用于Android平台(开发板+Docker也支持)

特性:

  • 支持arm64 syscall trace,可以打印参数、调用栈、寄存器
    • 参数结果包括详细的结构体信息,类似于strace
  • 支持对64位用户态动态库进行uprobe hook,可以打印参数、调用栈、寄存器
  • 支持硬件断点功能,可以打印调用栈、寄存器
  • 支持按线程名黑名单、白名单过滤
  • 支持pid和tid的黑名单、白名单过滤
  • 支持追踪fork产生的进程

要求:

  • root权限,系统内核版本5.10+(可在设置中查看或执行uname -r查看)

常有人问什么设备适合,推荐如下(偏贵的设备就不推荐了):

型号 代号 版本 说明 推荐度
Pixel 6 oriole 5.10 官方出品 **
Redmi Note 11T Pro xaga 5.10 略过时 **
Redmi Note 12R sky 5.10 性价比 ***
Redmi Note 12 topaz 5.15 4G海外版 *
Redmi Note 12 Turbo marble 5.10 略贵 *
Redmi Note 13 gold 5.10 新出不贵 **

其他环境方案:

  • arm开发板刷安卓,5.10+内核
  • arm开发板 + Docker + ReDroid,5.10+内核
  • M1/M2 + 安卓官方arm64模拟器,5.10+内核

使用

从Releases或者Github Action下载最新预编译好的二进制文件即可

  1. 推送到手机的/data/local/tmp目录下,添加可执行权限即可
adb push stackplz /data/local/tmp
adb shell
su
chmod +x /data/local/tmp/stackplz
  1. 每次使用新版本时需要释放库文件,请使用下面的命令
cd /data/local/tmp && ./stackplz --prepare
  1. 命令示意

3.1 追踪syscall

./stackplz -n com.starbucks.cn --syscall connect,sendto,recvfrom -o tmp.log --dumphex

3.2 追踪libc的open

注:默认设定的库是/apex/com.android.runtime/lib64/bionic/libc.so,要自定义请使用--lib指定

./stackplz -n com.starbucks.cn --point strstr[str,str] --point open[str,int] -o tmp.log

3.3 通过指定包名,对libnative-lib.so_Z5func1v符号进行hook,并打印堆栈

./stackplz -p 37919 --brk 0xf3a4:x --brk-lib libnative-lib.so --stack

3.4 在命中uprobe hook时发送信号

有时候希望在经过特定点位的时候停止进程,以便于dump内存,那么可以使用--kill来发送信号,示例:

./stackplz -n com.sfx.ebpf --lib libnative-lib.so -w _Z5func1v --stack --kill SIGSTOP
./stackplz -n com.starbucks.cn --syscall exit --kill SIGSTOP --stack

如果要恢复进程运行,可以用下面这样的命令(另起一个shell,root下执行):

kill -SIGCONT 4326

3.5 硬件断点示例如下,支持的断点类型:r,w,rw,x

pid + 绝对地址

./stackplz -p 9613 --brk 0x70ddfd63f0:x --stack

pid + 偏移 + 库文件

./stackplz -p 3102 --brk 0xf3a4:x --brk-lib libnative-lib.so --stack

3.6 以寄存器的值作为大小读取数据、或者指定大小

./stackplz --name com.sfx.ebpf -w write[int,buf:x2,int]
./stackplz --name com.sfx.ebpf -w write[int,buf:32,int]
./stackplz --name com.sfx.ebpf -w write[int,buf:0x10,int]

进阶用法:

libc.so+0xA94E8处下断,读取x1int,读取sp+0x30-0x2cptr

./stackplz --name com.sfx.ebpf -w 0xA94E8[int:x1,ptr:sp+0x30-0x2c]

libc.so+0xA94E8处下断,读取x1int,读取sp+0x30-0x2cbuf,长度为8

./stackplz --name com.sfx.ebpf -w 0xA94E8[int:x1,buf:8:sp+0x30-0x2c]
.text:00000000000A94E4                 LDR             W1, [SP,#0x30+var_2C]
.text:00000000000A94E8                 MOV             W20, W0

按默认顺序读取,以及按指定寄存器读取,下面的示例中两个方式输出结果相反:

./stackplz --name com.sfx.ebpf -w 0xA94E8[int,int]
./stackplz --name com.sfx.ebpf -w 0xA94E8[int:x1,int:x0]

3.8 按分组批量追踪进程

追踪全部APP类型的进程,但是排除一个特定的uid:

./stackplz -n app --no-uid 10084 --point open[str,int] -o tmp.log

同时追踪一个APP和(所有)isolated进程:

./stackplz -n com.starbucks.cn,iso --syscall openat -o tmp.log

可选的进程分组如下:

  • root
  • system
  • shell
  • app
  • iso

3.9 按分组批量追踪syscall

./stackplz -n com.xingin.xhs -s %file,%net --no-syscall openat,recvfrom

可选的syscall分组如下:

  • all
    • trace all syscall
  • %attr
    • setxattr,lsetxattr,fsetxattr
    • getxattr,lgetxattr,fgetxattr
    • listxattr,llistxattr,flistxattr
    • removexattr,lremovexattr,fremovexattr
  • %file
    • openat,openat2,faccessat,faccessat2,mknodat,mkdirat
    • unlinkat,symlinkat,linkat,renameat,renameat2,readlinkat
    • chdir,fchdir,chroot,fchmod,fchmodat,fchownat,fchown
  • %exec
    • execve,execveat
  • %clone
    • clone,clone3
  • %process
    • clone,clone3
    • execve,execveat
    • wait4,waitid
    • exit,exit_group,rt_sigqueueinfo
    • pidfd_send_signal,pidfd_open,pidfd_getfd
  • %net
    • socket,socketpair
    • bind,listen,accept,connect
    • getsockname,getpeername,setsockopt,getsockopt
    • sendto,recvfrom,sendmsg,recvmsg
    • shutdown,recvmmsg,sendmmsg,accept4
  • %signal
    • sigaltstack
    • rt_sigsuspend,rt_sigaction,rt_sigprocmask,rt_sigpending
    • rt_sigtimedwait,rt_sigqueueinfo,rt_sigreturn,rt_tgsigqueueinfo
  • %kill
    • kill,tkill,tgkill
  • %dup
    • dup,dup3
  • %epoll
    • epoll_create1,epoll_ctl,epoll_pwait,epoll_pwait2
  • %stat
    • statfs,fstatfs,newfstatat,fstat,statx

3.10 应用过滤规则

黑白名单:

./stackplz -n com.starbucks.cn -s openat:f0.f1.f2 -f w:/system -f w:/dev -f b:/system/lib64 -o tmp.log

替换规则(下面的测试命令开两个shell执行):

./stackplz -n com.starbucks.cn,iso -s execve,openat:f0 -f r:/system/bin/su:::/system/bin/zz -o tmp_s.log
./stackplz -n com.starbucks.cn,iso -w popen[str.f0.f1] -f r:mount:::mounx -f "r:which su:::which zz" -o tmp_w.log

ebpf中bpf_probe_write_user需要预先指定写入数据大小,本项目暂且覆盖256字节,可能有潜在的问题

替换功能仅做演示,用于展示ebpf操作数据的能力,如果要改为较为灵活的方式,会涉及常量编辑等功能,暂不实现


使用提示:

  • --showtime 输出事件发生的时间
    • 因为日志中的顺序和实际发生顺序不完全一致
    • 如果要精确发生顺序,请使用该选项
  • --showuid 输出触发事件的进程的uid
    • 在大范围追踪的时候建议使用
  • 可以用--name指定包名,用--uid指定进程所属uid,用--pid指定进程
  • 默认hook的库是/apex/com.android.runtime/lib64/bionic/libc.so,可以只提供符号进行hook
  • hook目标加载的库时,默认在对应的库目录搜索,所以可以直接指定库名而不需要完整路径
    • 例如 /data/app/~~t-iSPdaqQLZBOa9bm4keLA==/com.sfx.ebpf-C_ceI-EXetM4Ma7GVPORow==/lib/arm64
  • 如果要hook的库无法被自动检索到,请提供在内存中加载的完整路径
    • 最准确的做法是当程序运行时,查看程序的/proc/{pid}/maps内容,这里的路径是啥就是啥
  • hook动态库请使用--point/-w,可设置多个,语法是{符号/基址偏移}{+符号偏移}{[参数类型,参数类型...]}
    • --point _Z5func1v
    • --point strstr[str,str] --point open[str,int]
    • --point write[int,buf:64]
    • --point 0x9542c[str,str]
    • --point strstr+0x4[str,str]
  • hook syscall需要指定--syscall/-s选项,多个syscall请使用,隔开
    • --syscall openat
  • 特别的,指定为all表示追踪全部syscall
    • --syscall all
  • 特别说明,很多结果是0xffffff9c这样的结果,其实是int,但是目前没有专门转换
  • 注意,本项目中syscall的返回值通常是errno,与libc的函数返回结果不一定一致
  • --dumphex表示将数据打印为hexdump,否则将记录为ascii + hex的形式
  • 输出到日志文件添加-o/--out tmp.log,只输出到日志,不输出到终端再加一个--quiet即可

注意,默认屏蔽下列线程,原因是它们属于渲染相关的线程,会触发大量的syscall调用

如果有需求追踪下列线程,请手动修改源码去除限制,重新编译使用

  • RenderThread
  • FinalizerDaemon
  • RxCachedThreadS
  • mali-cmar-backe
  • mali-utility-wo
  • mali-mem-purge
  • mali-hist-dump
  • mali-event-hand
  • hwuiTask0
  • hwuiTask1
  • NDK MediaCodec_

更多用法,请通过-h/--help查看:

  • /data/local/tmp/stackplz -h

编译

可参考workflow或查看编译文档

Q & A

  1. preload_libs里面的库怎么编译的?

参见:unwinddaemon

  1. perf event ring buffer full, dropped 9 samples

使用-b/-buffer设置每个CPU的缓冲区大小,默认为8M,如果出现数据丢失的情况,请适当增加这个值,直到不再出现数据丢失的情况

命令示意如下:

./stackplz -n com.starbucks.cn -b 32 --syscall all -o tmp.log

一味增大缓冲区大小也可能带来新的问题,比如分配失败,这个时候建议尽可能清理正在运行的进程

failed to create perf ring for CPU 0: can't mmap: cannot allocate memory

  1. 通过符号hook确定调用了但是不输出信息?

某些符号存在多种实现(或者重定位?),这个时候需要指定具体使用的符号或者偏移

例如strchr可能实际使用的是__strchr_aarch64,这个时候应该指定__strchr_aarch64而不是strchr

coral:/data/local/tmp # readelf -s /apex/com.android.runtime/lib64/bionic/libc.so | grep strchr
   868: 00000000000b9f00    32 GNU_IFUNC GLOBAL DEFAULT   14 strchrnul
   869: 00000000000b9ee0    32 GNU_IFUNC GLOBAL DEFAULT   14 strchr
  1349: 000000000007bcf8    68 FUNC    GLOBAL DEFAULT   14 __strchr_chk
   689: 000000000004a8c0   132 FUNC    LOCAL  HIDDEN    14 __strchrnul_aarch64_mte
   692: 000000000004a980   172 FUNC    LOCAL  HIDDEN    14 __strchrnul_aarch64
   695: 000000000004aa40   160 FUNC    LOCAL  HIDDEN    14 __strchr_aarch64_mte
   698: 000000000004ab00   204 FUNC    LOCAL  HIDDEN    14 __strchr_aarch64
  5143: 00000000000b9ee0    32 FUNC    LOCAL  HIDDEN    14 strchr_resolver
  5144: 00000000000b9f00    32 FUNC    LOCAL  HIDDEN    14 strchrnul_resolver
  5550: 00000000000b9ee0    32 GNU_IFUNC GLOBAL DEFAULT   14 strchr
  6253: 000000000007bcf8    68 FUNC    GLOBAL DEFAULT   14 __strchr_chk
  6853: 00000000000b9f00    32 GNU_IFUNC GLOBAL DEFAULT   14 strchrnul

如图,可以看到直接调用了__strchr_aarch64而不是经过strchr再去调用__strchr_aarch64

交流

有关eBPF on Android系列可以加群交流

个人碎碎念太多,有关stackplz文章就不同步到本项目了,请移步博客查看:

之前针对syscall追踪并获取参数单独开了一个项目,整体结构更简单,没有interface,有兴趣请移步estrace

不过目前estrace的全部功能已经在stackplz中实现,不日将存档

Ref

本项目参考了以下项目和文章:

About

基于eBPF的堆栈追踪工具

License:Apache License 2.0


Languages

Language:C 91.2%Language:Go 8.7%Language:Makefile 0.1%Language:Shell 0.0%