Skip to content

SeeFlowerX/stackplz

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ae64b61 · Sep 16, 2023
Jul 31, 2023
Apr 12, 2023
Sep 15, 2023
Sep 16, 2023
Jul 24, 2023
Sep 16, 2023
Sep 16, 2023
Aug 15, 2023
Sep 4, 2023
Nov 9, 2022
Aug 17, 2023
Sep 16, 2023
Jul 27, 2023
Aug 15, 2023
Jul 24, 2023
Aug 15, 2023
Jul 20, 2023

Repository files navigation

stackplz

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

特性:

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

要求:

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

使用

从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

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