😿

seccompを利用し、自プロセスから発行されるシステムコールを制限する

2023/02/07に公開約3,900字

想定環境

x64GNU/Linux

この記事ではseccompを利用し自プロセスから発行されるシステムコールを制限する方法を解説します。

seccompとは

seccompはLinuxカーネル2.6.12から導入されました。
seccompを利用することで、自プロセスが発行するシステムコールを制限することができます。
sys_forkやsys_cloneで子プロセスやスレッドを生成したりsys_execveで異なるプログラムを実行したりしてもこの制限は継承されます。[1]
万が一プロセスを乗っ取られたとしても許可されたシステムコール以外を発行することはできなくなります。

動作モード

従来はsys_read、sys_write、sys_exit、sys_sigretunの4つのシステムコールのみを許可し、
他のシステムコールを発行しようとするとSIGKILLが配送されるという仕組みでした。[2]
SIGKILLはシグナルの一種で強制的にプロセスを終了させます。(ただしゾンビプロセスは終了することができない。)
x64ではLinuxカーネル3.5から個別にフィルタができる仕組みが導入されました。
システムコールのフィルタリングにはBPF(Berkeley Packet Filter)が利用されています。

つまり、動作モードとしては前者の厳格なモードと、自分でフィルタを設定するモードの2つがあります。
(もしかしたら他にもモードがあるのかも)

厳格なモードを使用してみる

実際にコードを書いてみましょう。

strict.c
#include <stdio.h>
#include <sys/prctl.h>
#include <linux/seccomp.h>

int main(void) {
    prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT, NULL);
    return 0;
}

prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT, NULL);を実行した以降で厳格なモードは有効になります。

なおprctl関数はsys_prctl(システムコール)のラッパーです。
なので関数を呼ばなくても、sys_prctlを発行することでも実現できます。

自分でフィルタを設定するモードを使用してみる

次に自分でフィルタ設定するモードを使用してみましょう。

filter.c
#include <unistd.h>
#include <stddef.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <linux/filter.h>
#include <linux/seccomp.h>

int main(void) {
    struct sock_filter filter[] = {
        BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof(struct seccomp_data, nr))),
	BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_write, 0, 3),
	BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof(struct seccomp_data, args[0]))),
	BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, STDOUT_FILENO, 0, 1),
	BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
	BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
    };

    struct sock_fprog prog = {
        .len = (unsigned short) (sizeof(filter) / sizeof(struct sock_filter)),
	.filter = filter,
    };

    prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
    prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog);
    
    return 0;
}

このモードでは事前に呼び出し元プロセスののno_new_privsビットを1に設定しておく必要があります。

prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);により、
呼び出し元プロセスのno_new_privsビットを1に設定しています。
一度設定されるとこのビットは解除することができません。

prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog);を実行した以降で
標準出力以外へのsys_writeが制限されます。
ちなみ&progはstruct sock_fprogへのポインタです。
許可されるシステムコールはこのポインタで定義されます。

BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof(struct seccomp_data, nr))),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_write, 0, 3),
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof(struct seccomp_data, args[0]))),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, STDOUT_FILENO, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),

の部分はフィルタです。

フィルタに利用されるパケットのデータはseccomp_data構造体です。

linux/seccomp.h
struct seccomp_data {
    int nr;                        // システムコール番号
    __u32 arch;                    // アーキテクチャ
    __u64 instruction_pointer;     // syscall命令を発行したアドレス
    __u64 args[6]                  // rdiからr9までの6つの引数
};

フィルタは次のように動作します。

1, パケットからnrの位置にある値を32ビットでアキュムレータにロード。
2, アキュムレータの内容を__NR_writeと等しいか比較。
   等しければ3,にジャンプ。等しくなければ6,にジャンプ。
3, args[0]を32ビットでアキュムレータにロード。
4, アキュムレータの内容をSTDOUT_FILENOと等しいか比較。
   等しければ5,にジャンプ。等しくなければ6,にジャンプ。
5, 実行を許可。
6, スレッドを殺す。

seccomp-toolsによるフィルタのダンプ

https://github.com/david942j/seccomp-tools

このツールを用いることで、プログラムが設定しようとしているフィルタを確認することができます。
filter.cをビルドし、確認してみましょう。

gcc filter.c
seccomp-tools dump ./a.out
seccomp-toolsの出力
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000000  A = sys_number
 0001: 0x15 0x00 0x03 0x00000001  if (A != write) goto 0005
 0002: 0x20 0x00 0x00 0x00000010  A = fd # write(fd, buf, count)
 0003: 0x15 0x00 0x01 0x00000001  if (A != 0x1) goto 0005
 0004: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0005: 0x06 0x00 0x00 0x00000000  return KILL

きれいに出力されました。

参考文献

https://book.mynavi.jp/ec/products/detail/id=122750

脚注
  1. 詳解セキュリティコンテスト CTFで学ぶ脆弱性攻略の技術 p475より引用。 ↩︎

  2. 詳解セキュリティコンテスト CTFで学ぶ脆弱性攻略の技術 p475より引用。 ↩︎

Discussion

ログインするとコメントできます