一、问题
Ftrace(几乎适配任何主流内核版本) 和 bpftrace(要求内核版本4.1以上)中都有两个现成的脚本execsnoop(.bt)、killsnoop(.bt), 我经常用他们从外部(不去读代码)观察几个关系紧密的进程之间是如何相互配合的,比如用execsnoop追踪一个大的系统(往往有多个进程)是如何逐一启动的,用killsnoop看他们相互之间信号的发送(进程间交互的一种方式)。
killsnoop是从tracepoint角度写的,今天我准备从各个角度重写此功能,包括:
- 用户空间追踪kill executable
- 用户空间追踪libc
- 发信号端内核空间追踪
- 接受信号端内核空间追踪
二、用户空间追踪kill executable
系统管理员一般使用/usr/bin/kill -9 xxx的方式去结束某进程,这样我们只要使用uprobe hook到main函数或其它函数然后把参数打出来即可。
为了简单直接对main下手,而且只考虑kill -9 xxx这种参数。不过出人意料的是,/usr/bin/kill竟然是strip的,没有main符号。
[root]
/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]
0000000000001f70 <textdomain@plt>:
0000000000001fb0 <bindtextdomain@plt>:
2353: e8 58 fc ff ff callq 1fb0 <bindtextdomain@plt>
235f: e8 0c fc ff ff callq 1f70 <textdomain@plt>
[root]
00000000000022f0 <.text>:
22f0: f3 0f 1e fa endbr64
22f4: 41 57 push %r15
22f6: 66 0f ef c0 pxor %xmm0,%xmm0
22fa: 41 56 push %r14
22fc: 41 55 push %r13
22fe: 41 54 push %r12
2300: 55 push %rbp
2301: 89 fd mov %edi,%ebp
2303: bf 06 00 00 00 mov $0x6,%edi
2308: 53 push %rbx
随便从上面选一个offset 0x2301
[root]
Attaching 1 probe...
ERROR: Could not resolve address: /usr/bin/kill:0x2301
遇到不能解析地址的错误,根据文档加上unsafe即可
[root]
[1] 2293884
[root]
[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) -> 2293884, sig:-9
sig:-9 看着不顺的,自己改改吧。当然这种方式不能追踪直接调用kill函数的情况。
三、用户空间追踪libc
即使/usr/bin/kill也是最终调用的libc中的kill系统函数,所以hook libc能撒出更大的网。
首先查下libc在哪?
[root]
linux-vdso.so.1 (0x00007ffdf93fe000)
libc.so.6 => /lib64/libc.so.6 (0x00007f3bb35a2000)
/lib64/ld-linux-x86-64.so.2 (0x00007f3bb3b70000)
[root]
lrwxrwxrwx. 1 root root 12 Mar 11 2021 /lib64/libc.so.6 -> libc-2.28.so
然后查看kill的函数原型,两个参数分别是pid和sig_no,
#include <signal.h>
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]# 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]
tracepoint:syscalls:sys_enter_kill
[root]
tracepoint:syscalls:sys_enter_kill
int __syscall_nr
pid_t pid
int sig
结果如下(下文都略掉了kill -9.。。。) :
[root]# 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_exit
int 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, arg0, kstack(), ustack());}'
pid:2283764(sleep) Got sig:9, ks:
do_group_exit+1
get_signal+344
do_signal+54
exit_to_usermode_loop+137
do_syscall_64+408
entry_SYSCALL_64_after_hwframe+101
, us:
0x7fe1a96f3d68
你可能怀疑bpftrace给出的参数说明命名是exit_code啊,看下面的内核源码(4.18)或上面链接的源码
2450 if (sig_kernel_coredump(signr)) {
2451 if (print_fatal_signals)
2452 print_fatal_signal(ksig->info.si_signo);
2453 proc_coredump_connector(current);
2454
2462 do_coredump(&ksig->info);
2463 }
2464
2465
2468 do_group_exit(ksig->info.si_signo);
2469
此处传给do_group_exit的参数是signo, 恰好作为退出码exit_code使用。
从exit_code可以看出此脚本还能作为检测程序退出码使用,留给读者验证。
通过内核调用栈,能清晰的看到接收端处理SIGKILL的过程。而且也能看出在上层get_signal、do_signal下probe也是可行的。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)