gop*_*opy 2 linux process-management syscalls sigkill
我知道进程无法阻止 SIGKILL。
但是有没有一种外部方法可以暂时阻止 SIGKILL 到达(特定)进程?(类似于防火墙丢弃数据包)。
您可以通过调用kill()(或tkill()) 系统调用来终止进程(内核也可以自行终止进程/任务(如 Ctrl-C 时发送的 SIGINT 或内存不足杀手发送的 SIGKILL)。可能会发送一些信号作为其他系统调用的结果,如ptrace)。
当kill()被调用时,这一切都发生在内核中。
只有内核代码介于发送信号的进程和接收信号的进程之间(并可能因此而终止)。
现在仍然有一些内核功能阻碍,您可以在这里使用:
简单的 Unix 权限。引用kill(2)Linux 上的手册页:
一个进程要有发送信号的权限,它要么是有特权的(在Linux下:在目标进程的用户命名空间中具有CAP_KILL能力),要么发送进程的真实或有效用户ID必须等于真实或保存的目标进程的 set-user-ID。
Linux 安全模块。LSM 可以(至少对 Smack、SELinux 和 apparmor 可以)过滤可能向什么发送信号的内容。
一些进程对杀戮免疫。initLinux上id为1( )的进程就是这种情况。其他子命名空间的根进程也不受其命名空间中其他进程发送的信号的影响。内核任务也不受信号影响。
然后还有类似于 SystemTap 使用的内核检测机制,它允许您影响内核的行为,在这里可以用来劫持信号传递。
但在到达那里之前,也许首先要尝试的是停止发送 SIGKILL 信号的任何事情。
如果是为了防止一个关键进程(例如一个用于支持根文件系统的进程)在关机时被杀死,大多数 init 系统将有一种方法来防止给定的进程受到killall5当时发生的或等效的影响。见/run/sendsigs.omit.d在一些版本的Debian,或者killmode中systemd的实例。
杀手进程,无论它是什么,都必须有一种方法来确定要杀死哪个进程。如果它基于存储在文件中的受害者的 pid(如/run/victim.pid),您可以更改该文件,如果它基于进程名称(/proc/pid/task/tid/comm),那也是可更改的(例如通过附加调试器并调用prctl(PR_SET_NAME)),对于arg 列表(/proc/pid/cmdline由 显示ps -f)。
如果杀手进程是动态链接的,您可以在其中注入代码,kill()用拒绝为给定 pid 执行此操作的包装函数替换系统调用:
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
#include <errno.h>
int kill(pid_t pid, int sig)
{
static pid_t pid_to_safeguard = 0;
static int (*orig_kill)(pid_t, int) = 0;
if (!orig_kill)
orig_kill = (int (*)(pid_t, int)) dlsym (RTLD_NEXT, "kill");
if (!orig_kill) abort();
if (!pid_to_safeguard) {
char *env = getenv("NOTME");
if (env) pid_to_safeguard = atol(env);
}
if (pid_to_safeguard && pid == pid_to_safeguard) {
errno = EPERM;
return -1;
}
return orig_kill(pid, sig);
}
Run Code Online (Sandbox Code Playgroud)
(您可能需要对tkill(), 和受害者的 pgid执行相同的操作,具体取决于杀手实际发送信号的方式)。
编译为:
gcc -fPIC -shared -o notme.so notme.c -ldl
Run Code Online (Sandbox Code Playgroud)
并运行杀手命令:
LD_PRELOAD=/path/to/notme.so NOTME=12345 killer args...
Run Code Online (Sandbox Code Playgroud)
或者,您可以通过在与系统其余部分不同(但不是其子代)的 pid 命名空间中运行它来完全隐藏该进程。
如果这些都不是一个选项,那么我们可以通过上面的列表来防止信号被传递:
root)使用 LSM。例如,当使用 Smack(以security=smack内核参数引导)时,label为受害进程设置一个不同的值就足以让其他进程无法看到它,更不用说杀死它了。例如:
sudo zsh -c 'echo unkillable > /proc/self/attr/current && exec sleep 1000'
Run Code Online (Sandbox Code Playgroud)
将sleep在该unkillable域中运行(名称可以是任何内容,关键是目前没有定义的规则允许以任何方式干扰该域),甚至以相同 uid 运行的进程也无法杀死它。root会虽然。
如果您将受害者进程作为新 pid 命名空间的领导者启动,那么它将不受其后代的影响。
~$ sudo unshare -p --fork --mount-proc zsh
~# kill -s KILL "$$"
~#
Run Code Online (Sandbox Code Playgroud)
(还在那儿)。
SystemTap 可以在这里使用。但是请注意,您需要内核符号(linux-image-<version>-dbgsym在 Debian 上)才能使用它,并且 SystemTap 或您的stap脚本将挂接到的内部内核函数可能会发生变化。所以可能不是最稳定的选择。Guru 模式也应该小心使用(不要尝试做任何太花哨的事情)。
使用stap,您可以在正在运行的内核中的不同点注入代码。例如,您可以挂钩处理kill()ortkill()系统调用的内核函数,并告诉它当 pid 是您的受害者时将信号更改为 0(无害)。
stap -ge 'probe kernel.function("sys_kill") { if ($pid == 12345) $sig = 0; }'
Run Code Online (Sandbox Code Playgroud)
(这里对于任何信号,您也可以检查$sig == 9是否只想覆盖 SIGKILL)。现在,这在tkill()使用或使用受害者kill()的进程组ID调用时不起作用,因此我们需要扩展它。这不包括信号由内核本身发送的情况。
但是我们也可以查看内核代码,看看我们是否可以在内核检查发送信号的权限的地方钩住自己。
stap -ge 'probe kernel.function("check_kill_permission").return {
if (@entry($t->pid) == 12345) $return = -1; }'
Run Code Online (Sandbox Code Playgroud)
当请求的 pid 是我们目标的 pid 时,我们返回-1( -EPERM) ,这也有让杀手知道它kill()失败的好处(这里12345作为一个例子)。
~$ sleep 1000 &
[1] 8508
~$ sudo stap -ge 'probe kernel.function("check_kill_permission").return {
if (@entry($t->pid) == '"$!"') $return = -1; }' &
[2] 8510
~$ kill -s KILL 8508
kill: kill 8508 failed: operation not permitted
Run Code Online (Sandbox Code Playgroud)
它也适用于内核自行发送信号的某些情况,但不是全部。为此,我们需要深入到在内核代码中进行信号传递的最底层函数:(__send_signal()至少在当前版本的 Linux 内核中)。
一种方法是挂钩在开始时调用的prepare_signal()函数__send_signal()(如果返回 0 则退出);
stap -ge 'probe kernel.function("prepare_signal").return {
if (@entry($p->pid) == 12345) $return = 0; }'
Run Code Online (Sandbox Code Playgroud)
然后,只要该stap进程存在,该 pid 12345 将是不可杀死的。
请注意,内核通常假设 SIGKILL 可以工作,因此在某些极端情况下,上述内容可能会产生意想不到的副作用(例如,如果 oom-killer 继续选择无法杀死的受害者,则它会变得无效)。