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_data
のarch
の値をロードしています.そして,arch
がx86かどうかを確認しています.
最初にアーキテクチャを確認するのは,アーキテクチャごとにシステムコール番号が異なるなどの理由からです.その後,
seccomp_data
のnr
をロードし,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で設定したフィルタやfork
やexecv
後も引き継がれ,解除できません.
こうしてみると,確かに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
を読んだ後,fstat
,brk
,write
,exit_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_add
のmanページにあります.
なお,libseccompのソースを少し追ってみましたが,どうやら生成したBPFプログラムを確認する方法はソースをいじらない限りないようです.
ここまでのまとめ
- seccompはLinuxにおいてシステムコールの発行を制限するための機能
- Linux3.5から導入されたseccomp mode 2ではシステムコールの細かい制御が可能となったが,バックエンドではBPFを利用している
- libseccompというseccompのフィルタプログラムを書くためのライブラリがある
次回はBPFの内部処理について見ていきたいと思います.