0x00 引言
本文主要研究Linux下如果防止进程被kill的方法。
系统实现杀死进程一般是通过调用 kill(或tkill、pkill) 来终止进程(内核也可以自行终止进程,如 Ctrl-C 时发送的 SIGINT 或内存不足杀手发送的 SIGKILL。某些信号可能是其他系统调用如 ptrace的结果)。
当 kill 被调用时,这一切都发生在内核中。如果要实现进程防杀,只有使内核代码介于发送信号的进程和接收信号的进程之间。
Linux系统下,要实现系统防杀可以从两个角度[1]考虑,控制接收kill流程与控制被kill进程的状态。
0x01 审计kill命令
使用audit能方便地审计系统的命令执行
1 | # 安装 |
0x02 控制接收kill流程
1 | 1 LD_PRELOAD注入程序 |
1、LD_PRELOAD注入程序
这里主要叙述的是LD_PRELOAD注入程序[2]这个方法。如果杀手进程是链接动态库的,就可以通过修改LD_PRELOAD(是个环境变量,用于动态库的加载)注入拒绝杀死指定PID的代码,并替换 kill 系统调用(您可能需要对 tkill或pkill执行相同的操作,具体取决于杀手实际发送信号的方式)。
1.1、简单使用
首先写一个任意生成十个数字random.cpp文件(注意进程可以用.cpp或.c做后缀):
1 |
|
然后进行编译:
1 | gcc random.cpp -o random |
然后写一个用于注入为动态库的源文件unrandom.c文件(*注意进程不可以用.cpp做后缀,必须用.c,原因未知):
1 | int rand(){ |
然后生成动态库:
1 | gcc -shared -fpic unrandom.c -o unrandom.so |
然后执行如下命令,发现生成随机数均为42,且已经链接到unrandom.so上了
1 | # 方法一 |
1.2、防止递归调用
假如我们要封装一个open函数的动态库,很明显open函数将被递归调用:
1 | int open(const char *pathname, int flags){ |
我们将使用dlfcn.h的dlsym函数来解决递归问题,修改后的注入源码如下:
1 |
|
然后生成动态库(注意使用dlsym需要添加 ‘-ldl’ 编译属性):
1 | gcc -shared -fpic injectopen.c -o injectopen.so -ldl |
新建一个调用open()函数的.cpp源文件:
1 |
|
然后进行编译:
1 | gcc opener.cpp -o opener |
注入与不注入动态库的输出结果如下:
1 | # 注入动态库 |
1.3、进程防杀示例
接下来记录一个完整的防杀示例[3],能够记录下杀手进程终端、杀手进程、被杀进程的详细信息。首先,实现注入进程的源码injectkill.c :
1 |
|
然后生成动态库(注意使用dlsym需要添加 ‘-ldl’ 编译属性):
1 | gcc -shared -fpic injectkill.c -o injectkill.so -ldl |
为了验证kill注入程序有起到作用,需要先写一个被杀的进程killed.cpp:
1 |
|
然后进行编译:
1 | gcc killed.cpp -o killed |
最后来验证一下注入效果:
1 | # 运行被杀进程 |
2、sys_kill劫持
LD_PRELOAD方法本质是劫持用户态的kill,但是仍然有些不足的地方,比如当用户直接调用kill就无法劫持了。如果想验证两者区别,可以用以下方法分析,两者输出是不同的因此不是同一个进程:
1 | # 内建kill |
3、其他方法
如果是为了防止一个关键进程(例如一个用于支持根文件系统的进程)在关机时被杀死,大多数 init 系统将有一种方法来防止给定的进程受到 killall5 或类似功能的影响。参见某些版本的 Debian 中的 /run/sendsigs.omit.d,或者例如 systemd 的 killmode。
要杀死进程,无论如何都必须确定要杀死哪个进程。如果它基于存储在文件中的受害者的 PID(如 /run/victim.pid),那么可以更改该文件,如果它基于进程名称(/proc/pid/task/tid/comm),那么也可以更改(例如通过附加调试器并调用 prctl(PR_SET_NAME)),对于 arg 列表(ps -f 显示的 /proc/pid/cmdline)也是如此。
0x03 控制被kill进程状态
还可以从被kill进程的状态角度出发来实现防杀。
1 | 1 简单的Unix权限 |
1、简单的Unix权限
引用 Linux 上的 kill(2) 手册页:
1 | 一个进程要有发送信号的权限,它要么是有root用户(Linux下:在目标进程的用户命名空间中具有CAP_KILL能力),要么发送进程的真实或有效用户ID必须等于真实或保存的目标进程的 set-user-ID。 |
2、Linux安全模块
1 | LSM 可以(至少对 Smack、SELinux 和 apparmor 可以)过滤可能向什么发送信号的内容。 |
3、一些进程对杀戮免疫
1 | Linux上id为1(init)的进程就是这种情况。其他子命名空间的根进程也不受其命名空间中其他进程发送的信号的影响。内核任务也不受信号影响。 |
4、内核检测机制(未仔细研究)
还有类似于 SystemTap 使用的内核检测机制,它允许您影响内核的行为,在这里可以用来劫持信号传递。
1 | SystemTap 可以在这里使用。 但是请注意,您需要内核符号(Debian 上的 linux-image-<version>-dbgsym)才能使用它,并且 SystemTap 或您的 stap 脚本将挂钩的内部内核函数可能会发生变化。 所以可能不是最稳定的选择。 Guru 模式也应该小心使用(不要尝试做任何太花哨的事情)。 |
0x04 引用文献
[1]https://unix.stackexchange.com/questions/483913/is-there-a-way-to-prevent-sigkill-to-reach-a-process
[2]https://rafalcieslak.wordpress.com/2013/04/02/dynamic-linker-tricks-using-ld_preload-to-cheat-inject-features-and-investigate-programs/
[3]https://www.52coder.net/post/ld-preload