web-dev-qa-db-ja.com

sigkillがプロセスに到達するのを防ぐ方法はありますか?

プロセスがSIGKILLを防ぐことはできないことを私は知っています。

しかし、SIGKILLが(特定の)プロセスに到達するのを一時的に防ぐ外部の方法はありますか? (ファイアウォールによるパケットのドロップのようなもの)。

1
gopy

kill()(またはtkill())システムコールを呼び出すことでプロセスを強制終了します(カーネルはプロセス/タスクを単独で強制終了することもできます(Ctrl-Cで送信されたSIGINTやoutから送信されたSIGKILLなど)。 -of-memory killer)ptraceなどの他のシステムコールの結果として、一部のシグナルが送信される場合があります。

kill()が呼び出されると、すべてカーネルで発生します。

信号を送信するプロセスと信号を受信するプロセスの間にカーネルコードのみが存在します(結果として終了する可能性があります)。

現在でも、邪魔になり、ここで使用できる可能性のあるカーネル機能がいくつかあります。

  1. 単純なUnixパーミッション。 Linuxでのkill(2)のマニュアルページの引用:

    プロセスがシグナルを送信する権限を持つには、特権が与えられている(Linuxの場合:ターゲットプロセスのユーザー名前空間にCAP_KILL機能がある)か、送信プロセスの実際のユーザーIDまたは有効なユーザーIDが実際のユーザーIDまたは有効なユーザーIDと等しい必要があります。ターゲットプロセスの保存されたset-user-ID。

  2. Linuxセキュリティモジュール。 LSMは、何が何に信号を送信するかをフィルタリングできます(そして、少なくともSmack、SELinux、およびapparmorに対しては実行できます)。

  3. 一部のプロセスは殺害の影響を受けません。これは、LinuxでのID 1(init)のプロセスの場合です。他の子名前空間のルートプロセスも、その名前空間内の他のプロセスによって送信されるシグナルの影響を受けません。カーネルタスクもシグナルの影響を受けません。

  4. そして、SystemTapで使用されるようなカーネルインストルメンテーションメカニズムがあり、カーネルの動作に影響を与えることができます。ここでは、シグナル配信をハイジャックするために使用できます。

しかし、そこにたどり着く前に、おそらく最初に試みることは、そのSIGKILLシグナルを送信しているものがそれを実行するのを停止することでしょう。

重要なプロセス(たとえば、ルートファイルシステムをサポートするために使用されるプロセス)がシャットダウン時に強制終了されるのを防ぐための場合、ほとんどのinitシステムには、特定のプロセスがkillall5または同等のものの対象となるのを防ぐ方法があります。その後に起こります。 Debianの一部のバージョンでは/run/sendsigs.omit.dを、たとえばkillmodesystemdを参照してください。

キラープロセスは、それが何であれ、どのプロセスを強制終了するかを決定する方法を備えている必要があります。ファイルに保存されている被害者のpid(/run/victim.pidなど)に基づいている場合は、そのファイルを変更できます。プロセス名(/proc/pid/task/tid/comm)に基づいている場合は、ファイルを変更することもできます(たとえばデバッガーでprctl(PR_SET_NAME))を呼び出します。これは、引数リスト(/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名前空間でプロセスを実行する によってプロセスを完全に非表示にすることもできます。

これらのいずれもオプションではない場合は、上記のリストを調べて、シグナルが配信されないようにすることができます。

  1. 犠牲者をキラーとは別のユーザーとして実行します(キラーがrootとして実行されていないと仮定します)
  2. LSMを使用します。たとえば、Smack(カーネルパラメータとしてsecurity=smackを使用して起動)を使用する場合、犠牲プロセスに別のlabelを設定するだけで、他のプロセスがそれを見ることができなくなり、キルすることはできません。 。例えば:

    Sudo zsh -c 'echo unkillable > /proc/self/attr/current && exec sleep 1000'
    

    そのsleepドメインでunkillableを実行し(名前は何でもかまいません。ポイントは、そのドメインに干渉することを許可する現在定義されているルールがないことです)、同じuidとして実行されているプロセスですらそれを殺すことはできません。 rootはしかし

  3. 新しいpid名前空間のリーダーとして被害者プロセスを開始すると、その子孫の影響を受けなくなります。

    ~$ Sudo unshare -p --fork --mount-proc zsh
    ~# kill -s KILL "$$"
    ~#
    

    (まだそこにあります)。

  4. SystemTapはここで使用できます。ただし、カーネルシンボル(Debianではlinux-image-<version>-dbgsym)を使用するにはカーネルシンボルが必要であり、stapスクリプトがフックするSystemTapまたは内部カーネル関数は変更される可能性があることに注意してください。したがって、おそらく最も安定したオプションではありません。達人モードも注意して使用する必要があります(あまり凝ったことをしようとしないでください)。

    stapを使用すると、実行中のカーネルのさまざまなポイントにコードを挿入できます。たとえば、kill()またはtkill()システムコールを処理するカーネル関数にフックして、pidが被害者のものである場合に信号を0(無害)に変更するように指示できます。

    stap -ge 'probe kernel.function("sys_kill") { if ($pid == 12345) $sig = 0; }'
    

    (ここでは、シグナルについて、SIGKILLのみをカバーしたい場合は、$sig == 9を確認することもできます)。 tkill()が使用されている場合、またはkill()が被害者のプロセスgroupIDで呼び出されている場合、これは機能しません。 dそれを拡張する必要があります。これは、信号がカーネル自体によって送信される場合には適用されません。

    ただし、カーネルコードを調べて、カーネルがシグナルを送信する許可を確認する場所に自分自身をフックできるかどうかを確認することもできます。

    stap -ge 'probe kernel.function("check_kill_permission").return {
               if (@entry($t->pid) == 12345) $return = -1; }'
    

    -1-EPERM)を返します。これには、要求されたpidがターゲットのpidである場合にkill()が失敗したことをキラーに知らせるという利点もあります(ここでは12345 as例)。

    ~$ 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カーネル)に移動する必要があります。

    1つの方法は、prepare_signal()が最初に呼び出す__send_signal()関数にフックすることです(0が返されるとベイルアウトします)。

    stap -ge 'probe kernel.function("prepare_signal").return {
      if (@entry($p->pid) == 12345) $return = 0; }'
    

    その場合、そのstapプロセスが存続している限り、そのpid12345は強制終了できません。

    カーネルは一般にSIGKILLが機能すると想定しているため、一部のコーナーケースで上記が予期しない副作用を引き起こす可能性はありません(たとえば、殺せない犠牲者を選び続けるとoom-killerが無効になるなど)。

6