Ftrace 和 bpftrace中都有两个现成的脚本execsnoop、killsnoop, 我经常用他们从外部(不去读代码)观察几个关系紧密的进程之间是如何相互配合的,比如用execsnoop追踪一个大的系统(往往有多个进程)是如何逐一启动的,用killsnoop看他们相互之间信号的发送(进程间交互的一种方式)。
killsnoop是从tracepoint角度写的,今天我准备从各个角度重写此功能,包括:
系统管理员一般使用/usr/bin/kill -9 xxx的方式去结束某进程,这样我们只要使用uprobe hook到main函数或其它函数然后把参数打出来即可。
为了简单直接对main下手,而且只考虑kill -9 xxx这种参数。不过出人意料的是,/usr/bin/kill竟然是strip的,没有main符号。
[root]# file /usr/bin/kill
/usr/bin/kill: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=2eaa68ee706e53cb99b1f07f6508dae7656c5a61, stripped
所以只好用offset
[root]# objdump -D /usr/bin/kill |grep 00000000000022f0 -A10
00000000000022f0 <.text>:22f0: f3 0f 1e fa endbr6422f4: 41 57 push %r1522f6: 66 0f ef c0 pxor %xmm0,%xmm022fa: 41 56 push %r1422fc: 41 55 push %r1322fe: 41 54 push %r122300: 55 push %rbp2301: 89 fd mov %edi,%ebp2303: bf 06 00 00 00 mov $0x6,%edi2308: 53 push %rbx
随便从上面选一个offset 0x2301
[root]# bpftrace -e 'struct argvarr {long a1; long a2; long a3;} uprobe:/usr/bin/kill:0x2301 {$tar=((struct argvarr*)reg("si"))->a3; $sig=((struct argvarr*)reg("si"))->a2; printf("%-6d(%s) -> %s, sig:%s", pid, comm, str($tar), str($sig));}'
Attaching 1 probe...
ERROR: Could not resolve address: /usr/bin/kill:0x2301
遇到不能解析地址的错误,根据文档加上unsafe即可
[root]# sleep 1000 &
[1] 2293884
[root]# kill -9 2293884
[root]# bpftrace -e 'struct argvarr {long a1; long a2; long a3;} uprobe:/usr/bin/kill:0x2301 {$tar=((struct argvarr*)reg("si"))->a3; $sig=((struct argvarr*)reg("si"))->a2; printf("%-6d(%s) -> %s, sig:%s", pid, comm, str($tar), str($sig));}' --unsafe
Attaching 1 probe...
WARNING: Could not determine instruction boundary for uprobe:/usr/bin/kill:8961 (binary appears stripped). Misaligned probes can lead to tracee crashes!
2293895(kill) -> 2293885, sig:-9
sig:-9 看着不顺的,自己改改吧。当然这种方式不能追踪直接调用kill函数的情况。
即使/usr/bin/kill也是最终调用的libc中的kill系统函数,所以hook libc能撒出更大的网。
首先查下libc在哪?
[root@]# ldd /usr/bin/killlinux-vdso.so.1 (0x00007ffdf93fe000)libc.so.6 => /lib64/libc.so.6 (0x00007f3bb35a2000)/lib64/ld-linux-x86-64.so.2 (0x00007f3bb3b70000)
[root@]# ls -l /lib64/libc.so.6
lrwxrwxrwx. 1 root root 12 Mar 11 2021 /lib64/libc.so.6 -> libc-2.28.so
然后查看kill的函数原型,两个参数分别是pid和sig_no,
#include
int kill(pid_t pid, int sig);
所以,这个大网根据调用约定可以这样编写
bpftrace -e 'uprobe:/lib64/libc-2.28.so:kill {printf("%d(%s) -> %d, sig:%d\n", pid, comm, reg("di"), reg("si"));}'
bpftrace还给我们提供了快速访问参数的方便:argN
bpftrace -e 'uprobe:/lib64/libc-2.28.so:kill {printf("%d(%s) -> %d, sig:%d\n", pid, comm, arg0, arg1);}'
结果如下:
[root@jun perf-tools]# bpftrace -e 'uprobe:/lib64/libc-2.28.so:kill {printf("%d(%s) -> %d, sig:%d\n", pid, comm, arg0, arg1);}'
Attaching 1 probe...
2261306(bash) -> 2319005, sig:9
内核中有个tracepoint - sys_enter_kill,如果读过bpftrace中的样例killsnoop.bt, 就能知道内核中有这么个tracepoint。
如果不知道,也可以按关键词kill盲猜,猜出个大概后查看参数详情。
[root@]# bpftrace -l |grep kill
tracepoint:syscalls:sys_enter_kill
[root@]# bpftrace -lv sys_enter_kill*
tracepoint:syscalls:sys_enter_killint __syscall_nrpid_t pidint sig
结果如下(略掉了kill -9.。。。) :
[root@jun bpftrace]# bpftrace -e 'tracepoint:syscalls:sys_enter_kill {printf("%d(%s) -> %d, sig:%d\n", pid, comm, args->pid, args->sig);}'
Attaching 1 probe...
2258577(bash) -> 2300678, sig:9
以上三种办法都是在发送端截获,信号接收端也是一种办法。Google了下内核在接收端是如何处理SIGKILL的,如有兴趣请参考这儿。
直接对do_group_exit下手,并额外打印了内核调用栈:
[root]# bpftrace -lv do_group_exit*
kfunc:do_group_exitint exit_code
kprobe:do_group_exit
[root]# bpftrace -e 'kprobe:do_group_exit {printf("pid:%-6d(%s) Got sig:%d, ks:%s, us:%s\n", pid, comm, reg("di"), kstack(), ustack());}'pid:2283764(sleep) Got sig:9, ks:do_group_exit+1get_signal+344do_signal+54exit_to_usermode_loop+137do_syscall_64+408entry_SYSCALL_64_after_hwframe+101
, us:0x7fe1a96f3d68
通过内核调用栈,能清晰的看到接收端处理SIGKILL的过程。而且也能看出在上层get_signal、do_signal下probe也是可行的。