LinuxのBPF : (2) seccompでの利用

seccompについて

seccomp (Secure Computingの略らしい)は,Linuxにおいてサンドボックスを実現するために プロセスのシステムコールの発行を制限する機能です. seccompを使っている代表的なアプリケーションにはchromeやOpen SSHなどがあります.最近利用が増えてきているようです.

seccompはLinux 2.6.12 (2005)から導入されました. このときseccompは/proc/pid/seccompを1にするとread()write()exit()sigreturn()のみを許可し, それ以外のシステムコールを呼ぶとSIGKILLするというものでした.

その後,もう少し柔軟にシステムコールを制限できるようにしようという議論があり,2012年のLinux3.5からseccomp mode 2という新しい seccompが導入されました.この新しいseccompではシステムコール単位に制限ができる他,あ るシステムコールに関してある特定の引数の場合だけ許可,といったこともできるようになりました.

さて,そんなseccompですが,BPFとどんな関係があるのかというと,実はシステムコールのフィルタリングにBPFを利用しています(!). seccompでの実際のプログラムを見てみましょう.

seccompのプログラム

seccomp(2)にseccompの使い方が書いてありますが, Linux3.7から追加されたseccompシステムコール

seccomp(SECCOMP_SET_MODE_FILTER, 0, &prog)

とするか,prctrlを使って

prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog))

とすることでseccompの設定ができます.ここでprogがBPFのフィルタプログラムです.

以下にgetpid()システムコールだけ許可しないよう設定する例を示します(特に意味はないです).

#include <errno.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <linux/audit.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <linux/unistd.h>
#include <sys/prctl.h>
#include <sys/types.h>
#include <sys/syscall.h>
#include <sys/ptrace.h>

#ifndef seccomp
int seccomp(unsigned int op, unsigned int flags, void *args)
{
    errno = 0;
    return syscall(__NR_seccomp, op, flags, args);
}
#endif

struct sock_filter filter[] = {
    BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
            (offsetof(struct seccomp_data, arch))),
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 1, 0),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
    BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
            (offsetof(struct seccomp_data, nr))),
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_getpid, 0, 1),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
};

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


int main(){
    int err;

    if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
        perror("prctl");
        exit(EXIT_FAILURE);
    }

    //if(prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)){
    if(seccomp(SECCOMP_SET_MODE_FILTER, 0, &prog)){
        perror("seccomp");
    }

    pid_t pid = getpid();
    printf("%d\n", pid);


    return 0;
}

BPFフィルタはパケットフィルタリングと同じように,sock_filter構造体の配列として定義されます. ここで,パケットフィルタリングと異なるのは,パケットフィルタの場合はロード命令でパケットのデータにアクセスできたのに対し, seccompの場合は以下のseccomp_data構造体 のデータにアクセスできるという点です.

struct seccomp_data {
    int nr ; /* System call number */
    __u32 arch ; /* AUDIT_ARCH_ * value */
    __u64 instruction_pointer ; /* CPU IP */
    __u64 args [6]; /* System call arguments */
}

seccompのフィルタリングプログラムについて詳しくみていきましょう.

    BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
            (offsetof(struct seccomp_data, arch))),
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 1, 0),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
    BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
            (offsetof(struct seccomp_data, nr))),
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_getpid, 0, 1),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),

offsetof()命令を使うことで構造体から配列へのオフセットが計算できます.した がって最初の命令はseccomp_dataarchの値をロードしています.そして,archx86かどうかを確認しています. 最初にアーキテクチャを確認するのは,アーキテクチャごとにシステムコール番号が異なるなどの理由からです.その後, seccomp_datanrをロードし,getpidであればSECCOMP_RET_KILLを,そうでなければSECCOMP_RET_ALLOWを返します. このプログラムを実行してみると,bashなら

Bad system call

と表示され,getpidを実行しようとした時点でプログラムが終了します. なお,seccompを使う場合はseccompを適用する前にprctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)としてsetuidなどを無効化する必要があります.またseccompで設定したフィルタやforkexecv後も引き継がれ,解除できません.

こうしてみると,確かにBPFを使ってシステムコールのフィルタリングができていることが分かります.これだけならあまり BPFを使う理由はないように思うかもしれませんが,ある特定の引数だけ許可したいとなると,確かにBPFのプログラムで効率的 に処理できるような気がしてきたりしないでしょうか. また,seccomp_dataにはinstruction_pointerがあり,これと/proc/pid/mapsを組み合わせれば特定の関数からのみ システムコールを許可するといったことも可能になります.

特定のシステムコールのみ許可する

BPFとは少々関係ない話になりますが,seccompの使い方について折角なのでいくつか書いておこうと思います.

先ほどのプログラムではgetpid以外を許可しましたが,実際にはブラックリスト形式 ではなくホワイトリスト形式で必要なシステムコールだけを許可した方がセキュリティ的には安全でしょう.

そこで,先ほどの例でgetpidのみを許可することを考えてみましょう.BPF_JUMPの分岐先を入れ替えればいいだけ,と思うかも しれませんが,以下のフィルタプログラムは期待どおりには動きません.

    BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
            (offsetof(struct seccomp_data, arch))),
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 1, 0),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
    BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
            (offsetof(struct seccomp_data, nr))),
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_getpid, 1, 0),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),

というのもプログラムは実際にはgetpid以外のシステムコールを発行しているからです.実際にプログラムがどんなシステムコール を発行しているかはstraceを使えばわかります.

% strace ./a.out
...
getpid()                                = 15240
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 4), ...}) = 0
brk(NULL)                               = 0x1342000
brk(0x1363000)                          = 0x1363000
write(1, "15240\n", 615240
)                  = 6
exit_group(0)                           = ?

このプログラムではgetpidを読んだ後,fstatbrkwriteexit_groupを使用していることが分かりました. これらのシステムコールを許可するプログラムは例えば以下のようになります.

struct sock_filter filter[] = {
    BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
            (offsetof(struct seccomp_data, arch))),
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 1, 0),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
    BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
            (offsetof(struct seccomp_data, nr))),
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_getpid, 0, 1),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_fstat, 0, 1),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_brk, 0, 1),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_write, 0, 1),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_exit_group, 0, 1),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
};

ここに書いてあるように,いくつかマクロを定義すれば多少ましにフィルタプログラムが定義できます.

#define ARCH_NR AUDIT_ARCH_X86_64
#define syscall_nr (offsetof(struct seccomp_data, nr))
#define arch_nr (offsetof(struct seccomp_data, arch))

#define VALIDATE_ARCHITECTURE \
     BPF_STMT(BPF_LD+BPF_W+BPF_ABS, arch_nr), \
     BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, ARCH_NR, 1, 0), \
     BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_KILL)

 #define EXAMINE_SYSCALL \
     BPF_STMT(BPF_LD+BPF_W+BPF_ABS, syscall_nr)

 #define ALLOW_SYSCALL(name) \
     BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_##name, 0, 1), \
     BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW)

 #define KILL_PROCESS \
    BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_KILL)


struct sock_filter filter[] = {
    VALIDATE_ARCHITECTURE,
    EXAMINE_SYSCALL,
    ALLOW_SYSCALL(getpid),
    ALLOW_SYSCALL(brk),
    ALLOW_SYSCALL(write),
    ALLOW_SYSCALL(fstat),
    ALLOW_SYSCALL(exit_group),
    KILL_PROCESS,
}

libseccomp

マクロを使えば多少ましになりますが,それでもseccompのフィルタプログラムを書くのは面倒かつ間違いやすいです. パケットフィルタリングでtcpdumpが使えたように,seccompのフィルタプログラムを簡単に書くためにlibseccomp というライブラリがあります.libseccompを使うと,上記のフィルタリングプログラムは以下のようにかけます. 自分でフィルタを直接書くよりかはましだと思います.

#include <stdio.h>
#include <fcntl.h>
#include <seccomp.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>

#define BUF_SIZE    256

int main(int argc, char *argv[])
{
    int rc = -1;
    scmp_filter_ctx ctx;
    struct scmp_arg_cmp arg_cmp[] = { SCMP_A0(SCMP_CMP_EQ, 2) };
    int fd;
    unsigned char buf[BUF_SIZE];

    ctx = seccomp_init(SCMP_ACT_KILL);
    if (ctx == NULL) goto out;

    rc = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(getpid), 0);
    if (rc < 0) goto out;
    rc = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(brk), 0);
    if (rc < 0) goto out;
    rc = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
    if (rc < 0) goto out;
    rc = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 1,
            SCMP_CMP(0, SCMP_CMP_EQ, 1));
    if (rc < 0) goto out;
    rc = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(fstat), 0);
    if (rc < 0) goto out;

    rc = seccomp_load(ctx);
    if (rc < 0) goto out;

    pid_t pid = getpid();
    printf("%d\n", pid);

out:
    seccomp_release(ctx);
    return -rc;
}

主な使い方はまずseccomp_init()でコンテキストを初期化したあと,seccomp_rule_addでルールを追加し, 最後にseccomp_laod()で実際に条件にあったフィルタを生成し,設定します.

あるシステムコールを許可したいだけなら

    rc = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(getpid), 0);

と書けばいいですが,引数までチェックしたい場合は,

    rc = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 1,
            SCMP_CMP(0, SCMP_CMP_EQ, 1));

のようにします.ここではwriteシステムコールはfdが1(つまりstdout)のときのみ許可しています.詳細は seccomp_rule_addmanページにあります

なお,libseccompのソースを少し追ってみましたが,どうやら生成したBPFプログラムを確認する方法はソースをいじらない限りないようです.

ここまでのまとめ

  • seccompはLinuxにおいてシステムコールの発行を制限するための機能
  • Linux3.5から導入されたseccomp mode 2ではシステムコールの細かい制御が可能となったが,バックエンドではBPFを利用している
  • libseccompというseccompのフィルタプログラムを書くためのライブラリがある

次回はBPFの内部処理について見ていきたいと思います.

参考文献