Вы уничтожаете процесс, вызываяkill()
(илиtkill()
)системный вызов (ядро также может само уничтожать процессы/задачи (например, SIGINT, отправленный при нажатии Ctrl -C, или SIGKILL, отправленный из -из -убийцы памяти ). Некоторые сигналы могут быть отправлены в результате других системных вызовов, таких как ptrace
).
При вызове kill()
все происходит в ядре.
Только код ядра стоит между процессом, отправляющим сигнал, и процессом, получающим его (и, возможно, завершается как следствие ).
Теперь есть еще несколько функций ядра, которые мешают и которые вы можете использовать здесь:
простые разрешения Unix . Цитирование справочной страницы kill(2)
по Linux:
For a process to have permission to send a signal, it must either be privileged (under Linux: have the CAP_KILL capability in the user namespace of the target process), or the real or effective user ID of the sending process must equal the real or saved set-user-ID of the target process.
Модули безопасности Linux . LSM могут (и делают, по крайней мере, для Smack, SELinux и apparmor )фильтровать, что может посылать сигналы чему.
Некоторые процессы невосприимчивы к уничтожению . Это случай процесса с идентификатором 1(init
)в Linux. Корневой процесс других дочерних пространств имен также невосприимчив к сигналам, посылаемым другими процессами в его пространстве имен. Задачи ядра также невосприимчивы к сигналам.
Кроме того, существуют механизмы инструментирования ядра , подобные используемому SystemTap, которые позволяют вам влиять на поведение ядра и здесь могут использоваться для перехвата доставки сигнала.
Но прежде чем добраться до этого, возможно, первое, что нужно попробовать, это остановить то, что отправляет этот сигнал SIGKILL.
Если это необходимо для предотвращения остановки критического процесса (, например, используемого для поддержки корневой файловой системы )при завершении работы, в большинстве систем инициализации будет способ предотвращения того, чтобы заданные процессы подвергались killall5
или что-то подобное, что происходит тогда. См., например, /run/sendsigs.omit.d
в некоторых версиях Debian или killmode
в systemd
.
Убийственный процесс,что бы это ни было, должен быть способ определить, какой процесс убить. Если он основан на идентификаторе жертвы, хранящемся в файле (, таком как /run/victim.pid
), вы можете изменить этот файл, если он основан на имени процесса (/proc/pid/task/tid/comm
), его также можно изменить (, например, подключив отладчик. и вызовите prctl(PR_SET_NAME)
), то же самое для списка аргументов (/proc/pid/cmdline
, показанногоps -f
).
Если процесс-убийца динамически связан, вы можете внедрить в него код, чтобы заменить kill()
системный вызов функцией-оболочкой, которая отказывается делать это для данного pid:
#define _GNU_SOURCE
#include
#include
#include
#include
#include
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
И запустите команду kill как:
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-
в Debian ), чтобы иметь возможность использовать его, а SystemTap или внутренние функции ядра, к которым будет подключаться ваш скрипт stap
, потенциально могут быть изменены. Так что, возможно, не самый стабильный из вариантов. Режим Гуру также следует использовать с осторожностью (и не пытаться делать что-то слишком замысловатое ).
С помощью stap
вы можете внедрять код в разные точки работающего ядра. Например, вы можете подключиться к функции ядра, которая обрабатывает системные вызовы kill()
или tkill()
, и сказать ей изменить сигнал на 0 (безопасный ), когда pid принадлежит вашей жертве.
stap -ge 'probe kernel.function("sys_kill") { if ($pid == 12345) $sig = 0; }'
(здесь для любого сигнала, вы также можете проверить $sig == 9
, если хотите охватить только SIGKILL ). Теперь это не работает, когда используется tkill()
или когда kill()
вызывается с идентификатором процесса группы жертвы, поэтому нам нужно расширить это. Это не распространяется на случаи, когда сигнал отправляется самим ядром.
Но мы также можем посмотреть код ядра и посмотреть, сможем ли мы зацепиться в том месте, где ядро проверяет наличие разрешения на отправку сигнала.
stap -ge 'probe kernel.function("check_kill_permission").return {
if (@entry($t->pid) == 12345) $return = -1; }'
Мы возвращаем -1
(-EPERM
), что также имеет то преимущество, что позволяет убийце узнать, что его kill()
не удалось, когда запрошенный pid является идентификатором нашей цели (здесь 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()
, которую __send_signal()
вызывает в начале (и отключается, если она возвращает 0 );
stap -ge 'probe kernel.function("prepare_signal").return {
if (@entry($p->pid) == 12345) $return = 0; }'
Тогда этот pid 12345 будет неубиваемым, пока этот stap
процесс жив.
Обратите внимание, что ядро обычно предполагает, что SIGKILL сработает, поэтому не исключено, что приведенное выше может иметь неожиданные побочные эффекты в некоторых крайних случаях (, например, убийца oom -становится неэффективным, если он продолжает выбирать вашу неубиваемую жертву ).
Другой способ сделать это — использовать подстановку процесса:
cat file1 <(echo "newtextinbetween") file2 > file3
Редактировать :Если вы не знаете, что echo
добавить для разрыва строки, используйте вместо этого echo -n
.