我知道进程无法阻止 SIGKILL。
但是有没有一种外部方法可以暂时阻止 SIGKILL 到达(特定)进程? (比如通过防火墙丢弃数据包)。
答案1
kill()
您可以通过调用(或) 系统调用来终止进程tkill()
(内核也可以自行终止进程/任务(如按 Ctrl-C 发送的 SIGINT 或内存不足杀手发送的 SIGKILL)。可能会发送一些信号由于其他系统调用(例如ptrace
))。
当kill()
被调用时,一切都在内核中发生。
只有内核代码位于发送信号的进程和接收信号的进程之间(并可能因此而终止)。
现在仍然有一些内核功能存在障碍,您可以在这里使用:
简单的 Unix 权限。引用
kill(2)
Linux 上的手册页:对于有权发送信号的进程,它必须具有特权(在 Linux 下:在目标进程的用户命名空间中具有 CAP_KILL 功能),或者发送进程的真实或有效用户 ID 必须等于真实或有效用户 ID保存目标进程的设置用户 ID。
Linux 安全模块。 LSM 可以(至少对于 Smack、SELinux 和 apparmor 来说)过滤可能向什么发送信号的内容。
有些过程是免疫杀戮。
init
Linux上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);
}
(您可能需要对tkill()
、 以及受害者的 pgid 执行相同的操作,具体取决于杀手实际发送信号的方式)。
编译为:
gcc -fPIC -shared -o notme.so notme.c -ldl
并运行杀手命令:
LD_PRELOAD=/path/to/notme.so NOTME=12345 killer args...
或者你可以完全隐藏该过程在与系统其余部分不同(但不是其子级)的 pid 命名空间中运行它。
如果这些都不是一个选项,那么我们可以通过上面的列表来阻止信号的传递:
- 以与杀手不同的用户身份运行受害者(假设杀手没有以 身份运行
root
) 使用 LSM。例如,当使用 Smack(
security=smack
使用内核参数启动)时,label
为受害进程设置不同的值就足以让其他进程看不到它,更不用说杀死它了。例如:sudo zsh -c 'echo unkillable > /proc/self/attr/current && exec sleep 1000'
会
sleep
在该unkillable
域中运行(名称可以是任何名称,重点是当前没有定义允许干扰该域的规则),甚至以相同 uid 运行的进程也无法杀死它。root
虽然会。如果您将受害者进程作为新 pid 命名空间的领导者启动,那么它将免受其后代的影响。
~$ sudo unshare -p --fork --mount-proc zsh ~# kill -s KILL "$$" ~#
(还在那儿)。
这里可以使用 SystemTap。但请注意,您需要内核符号(
linux-image-<version>-dbgsym
在 Debian 上)才能使用它,并且您的stap
脚本将挂钩的 SystemTap 或内部内核函数可能会发生变化。所以也许不是最稳定的选择。大师模式也应该谨慎使用(不要尝试做任何太花哨的事情)。使用
stap
,您可以在正在运行的内核的不同点注入代码。例如,您可以挂接到处理kill()
或tkill()
系统调用的内核函数,并告诉它当 pid 是受害者的 pid 时将信号更改为 0(无害)。stap -ge 'probe kernel.function("sys_kill") { if ($pid == 12345) $sig = 0; }'
$sig == 9
(这里对于任何信号,如果只想覆盖SIGKILL也可以检查)。现在,当tkill()
使用或kill()
与进程一起调用时,这不起作用团体受害者的 id,所以我们需要扩展它。这不包括信号由内核本身发送的情况。但是我们也可以查看内核代码,看看是否可以将自己挂在内核检查发送信号权限的地方。
stap -ge 'probe kernel.function("check_kill_permission").return { if (@entry($t->pid) == 12345) $return = -1; }'
当请求的 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
它也适用于一些内核自行发送信号的情况,但不是全部。为此,我们需要深入到内核代码中执行信号传递的最底层函数:(
__send_signal()
至少在当前版本的 Linux 内核中)。一种方法是挂钩到开头调用
prepare_signal()
的函数(如果返回 0,则退出);__send_signal()
stap -ge 'probe kernel.function("prepare_signal").return { if (@entry($p->pid) == 12345) $return = 0; }'
那么只要该
stap
进程存在,pid 12345 就无法被杀死。请注意,内核通常假设 SIGKILL 会起作用,因此上述方法在某些极端情况下并非不可能产生意外的副作用(例如,如果 oom-killer 不断选择不可杀死的受害者,则它会变得无效)。