スマートフォン解析 top

TOP > タイガーチームセキュリティレポート > Berkeley Packet Filterの基礎と応用 - Part 4

タイガーチームセキュリティレポート

Berkeley Packet Filterの基礎と応用 - Part 4

今回でBPFに関するコラムは最後です。BPFを応用してシステムコールをフィルタするという話です。


-今回のテーマ-

Linux Kernel 3.5以降で実装されたfilterモードのseccomp(seccomp-bpf)について紹介します。 以下では、実例からseccomp-bpfの使い方を把握した上で、Linux Kernel上でのseccomp-bpfの実装を見ていくことにします。


-seccomp-bpfの実例-

seccompをfilterモードで利用するためには、strictモードの場合と同様にprctl()システムコールを呼び出します。prctl()のプロトタイプを再掲します。

SYNOPSIS
       #include <sys/prctl.h>

       int prctl(int option, unsigned long arg2, unsigned long arg3,
                 unsigned long arg4, unsigned long arg5);

filterモードの場合、optionにはPR_SET_SECCOMP、arg2にはSECCOMP_MODE_FILTER、適用したいフィルタをarg3に指定します。

Linux Kernelのsource treeにあるいくつかのを見てみましょう。

dropper.cはコマンドライン引数で指定した1つのシステムコールのみを許可した状態で別のプログラムを起動するユーティリティです。 dropperから起動されたプログラムはシステムコールが制限された状態(sandbox)で安全に実行されます。

以下はdropper.cが使用するフィルタの抜粋です。

    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, arch, 0, 3),
        BPF_STMT(BPF_LD+BPF_W+BPF_ABS,
             (offsetof(struct seccomp_data, nr))),
        BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, nr, 0, 1),
        BPF_STMT(BPF_RET+BPF_K,
             SECCOMP_RET_ERRNO|(error & SECCOMP_RET_DATA)),
        BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW),
    };
dropper.c

BPF_LD+BPF_W+BPF_ABSはソケットフィルタの場合はソケットバッファにコピーしたパケットのデータストリームからデータをロードするために使用されるBPF virtual machineのopcodeになりますが seccomp-bpfの場合はseccomp_data構造体からデータをロードするために使用されています。

このフィルタではコマンドライン引数で指定したアーキテクチャarchとシステムコール番号nrが同じ名前のseccomp_data構造体のフィールドの値と一致している場合のみ、システムコールは許可されます。

以下はseccomp_data構造体の定義です。フィルタがシステムコールを処理するために必要となる呼び出したシステムコールの情報が格納されています。 この構造体のフィールドにはarchやnrのほかにシステムコールの引数を保持するargs[6]などがあります。

 31 /**
 32  * struct seccomp_data - the format the BPF program executes over.
 33  * @nr: the system call number
 34  * @arch: indicates system call convention as an AUDIT_ARCH_* value
 35  *        as defined in <linux/audit.h>.
 36  * @instruction_pointer: at the time of the system call.
 37  * @args: up to 6 system call arguments always stored as 64-bit values
 38  *        regardless of the architecture.
 39  */
 40 struct seccomp_data {
 41         int nr;
 42         __u32 arch;
 43         __u64 instruction_pointer;
 44         __u64 args[6];
 45 };
linux/seccomp.c

ソケットフィルタがパケットのデータストリームへの参照を持つのと同様に、seccomp-bpfのフィルタでは暗黙的にseccomp_data構造体への参照を持つものとしてフィルタを記述します。 この点については後で補足しますが、ここではそういうルールだと思ってください。

もうひとつ例を見てみます。bpf-direct.cは標準入力から読み込んだ(最大4096バイトの)データを標準出力にエコーするプログラムです。 以下はbpf-direct.cが使用するフィルタの抜粋です。

    struct sock_filter filter[] = {
        /* Grab the system call number */
        BPF_STMT(BPF_LD+BPF_W+BPF_ABS, syscall_nr),
        /* Jump table for the allowed syscalls */
        BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_rt_sigreturn, 0, 1),
        BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW),
#ifdef __NR_sigreturn
        BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_sigreturn, 0, 1),
        BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW),
#endif
        BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_exit_group, 0, 1),
        BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW),
        BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_exit, 0, 1),
        BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW),
        BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_read, 1, 0),
        BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_write, 3, 2),

        /* Check that read is only using stdin. */
        BPF_STMT(BPF_LD+BPF_W+BPF_ABS, syscall_arg(0)),
        BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, STDIN_FILENO, 4, 0),
        BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_KILL),

        /* Check that write is only using stdout */
        BPF_STMT(BPF_LD+BPF_W+BPF_ABS, syscall_arg(0)),
        BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, STDOUT_FILENO, 1, 0),
        /* Trap attempts to write to stderr */
        BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, STDERR_FILENO, 1, 2),

        BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW),
        BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_TRAP),
        BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_KILL),
    };
bpf-direct.c

上の例は最初より少し長いですが、処理内容は呼び出したシステムコールの番号をいくつかのシステムコール番号と比較して許可/拒否の判断をしているだけです。 ただし、read()とwrite()の場合は追加の処理として標準入力からの読み込みまたは標準出力への書き込みのみ許可しています。

標準エラー出力への書き込みの場合はいったんシステムコールがトラップされて、Kernelが呼び出し元プロセスにSIGSYSシグナルを送ります。 このプログラムではSIGSYSシグナルに対するシグナルハンドラ(emulate())を定義しており、これが標準エラー出力への書き込みを標準出力にリダイレクトしています。

static void emulator(int nr, siginfo_t *info, void *void_context)
{
    ucontext_t *ctx = (ucontext_t *)(void_context);
    int syscall;
    char *buf;
    ssize_t bytes;
    size_t len;
    if (info->si_code != SYS_SECCOMP)
        return;
    if (!ctx)
        return;
    syscall = ctx->uc_mcontext.gregs[REG_SYSCALL];
    buf = (char *) ctx->uc_mcontext.gregs[REG_ARG1];
    len = (size_t) ctx->uc_mcontext.gregs[REG_ARG2];

    if (syscall != __NR_write)
        return;
    if (ctx->uc_mcontext.gregs[REG_ARG0] != STDERR_FILENO)
        return;
    /* Redirect stderr messages to stdout. Doesn't handle EINTR, etc */
    ctx->uc_mcontext.gregs[REG_RESULT] = -1;
    if (write(STDOUT_FILENO, "[ERR] ", 6) > 0) {
        bytes = write(STDOUT_FILENO, buf, len);
        ctx->uc_mcontext.gregs[REG_RESULT] = bytes;
    }
    return;
}
bpf-direct.c

source treeには他にも例があるので興味があれば読んでみると面白いかもしれません。

このようなフィルタをインストールした上でプログラムを実行することで、プログラムにバファオーバーフロー等の脆弱性が見つかった場合でも悪用は非常に困難になります。


-seccomp-bpfの実装-

Debian WheezyのLinux Kernel 3.11のソースからfilterモードの実装を確認してみましょう。

まずは、フィルタを登録する部分の処理から確認します。

user spaceのプログラムがprctl()システムコールのarg3で指定したフィルタはprctl_set_seccomp()から呼び出されるseccomp_attach_user_filter()でkernel spaceにコピーされます。(500行目)

469 /**
470  * prctl_set_seccomp: configures current->seccomp.mode
471  * @seccomp_mode: requested mode to use
472  * @filter: optional struct sock_fprog for use with SECCOMP_MODE_FILTER
473  *
474  * This function may be called repeatedly with a @seccomp_mode of
475  * SECCOMP_MODE_FILTER to install additional filters.  Every filter
476  * successfully installed will be evaluated (in reverse order) for each system
477  * call the task makes.
478  *
479  * Once current->seccomp.mode is non-zero, it may not be changed.
480  *
481  * Returns 0 on success or -EINVAL on failure.
482  */
483 long prctl_set_seccomp(unsigned long seccomp_mode, char __user *filter)
484 {
485         long ret = -EINVAL;
486
487         if (current->seccomp.mode &&
488             current->seccomp.mode != seccomp_mode)
489                 goto out;
490
491         switch (seccomp_mode) {
492         case SECCOMP_MODE_STRICT:
493                 ret = 0;
494 #ifdef TIF_NOTSC
495                 disable_TSC();
496 #endif
497                 break;
498 #ifdef CONFIG_SECCOMP_FILTER
499         case SECCOMP_MODE_FILTER:
500                 ret = seccomp_attach_user_filter(filter);
501                 if (ret)
502                         goto out;
503                 break;
504 #endif
505         default:
506                 goto out;
507         }
508
509         current->seccomp.mode = seccomp_mode;
510         set_thread_flag(TIF_SECCOMP);
511 out:
512         return ret;
513 }
linux/seccomp.c

seccomp_attach_user_filter()はkernel spaceにコピーしたフィルタをseccomp_attach_filter()に渡します。 seccomp_attach_filter()はseccomp_check_filter()でフィルタの命令列を検査した上で、呼び出し元プロセスのtask_struct構造体にフィルタをセットします。(282, 283行目)

プロセスごとのフィルタは一方向リストで管理されており、セットしたフィルタはリストの先頭に追加されます。古いフィルタはリスト上に保持されます。

229 static long seccomp_attach_filter(struct sock_fprog *fprog)
230 {
231         struct seccomp_filter *filter;
232         unsigned long fp_size = fprog->len * sizeof(struct sock_filter);
233         unsigned long total_insns = fprog->len;
234         long ret;
235
236         if (fprog->len == 0 || fprog->len > BPF_MAXINSNS)
237                 return -EINVAL;
238
239         for (filter = current->seccomp.filter; filter; filter = filter->prev)
240                 total_insns += filter->len + 4;  /* include a 4 instr penalty */
241         if (total_insns > MAX_INSNS_PER_PATH)
242                 return -ENOMEM;
243
244         /*
245          * Installing a seccomp filter requires that the task have
246          * CAP_SYS_ADMIN in its namespace or be running with no_new_privs.
247          * This avoids scenarios where unprivileged tasks can affect the
248          * behavior of privileged children.
249          */
250         if (!current->no_new_privs &&
251             security_capable_noaudit(current_cred(), current_user_ns(),
252                                      CAP_SYS_ADMIN) != 0)
253                 return -EACCES;
254
255         /* Allocate a new seccomp_filter */
256         filter = kzalloc(sizeof(struct seccomp_filter) + fp_size,
257                          GFP_KERNEL|__GFP_NOWARN);
258         if (!filter)
259                 return -ENOMEM;
260         atomic_set(&filter->usage, 1);
261         filter->len = fprog->len;
262
263         /* Copy the instructions from fprog. */
264         ret = -EFAULT;
265         if (copy_from_user(filter->insns, fprog->filter, fp_size))
266                 goto fail;
267
268         /* Check and rewrite the fprog via the skb checker */
269         ret = sk_chk_filter(filter->insns, filter->len);
270         if (ret)
271                 goto fail;
272
273         /* Check and rewrite the fprog for seccomp use */
274         ret = seccomp_check_filter(filter->insns, filter->len);
275         if (ret)
276                 goto fail;
277
278         /*
279          * If there is an existing filter, make it the prev and don't drop its
280          * task reference.
281          */
282         filter->prev = current->seccomp.filter;
283         current->seccomp.filter = filter;
284         return 0;
285 fail:
286         kfree(filter);
287         return ret;
288 }
linux/seccomp.c

seccomp_check_filter()の処理内容を見てみると、命令列の一部をseccomp-bpf向けに書き換えていることがわかります。 ここではBPF_S_LD_W_ABSがBPF_S_ANC_SECCOMP_LD_Wに書き換えられていることに注目します。

BPF_S_ANC_SECCOMP_LD_WはソケットフィルタのBPFでは使用されていないseccomp-bpfに特有のopcodeです。 BPF_S_LD_W_ABSは上の2つの例で使われていたBPF_LD+BPF_W+BPF_ABSに相当するマクロです。

126 static int seccomp_check_filter(struct sock_filter *filter, unsigned int flen)
127 {
128         int pc;
129         for (pc = 0; pc < flen; pc++) {
130                 struct sock_filter *ftest = &filter[pc];
131                 u16 code = ftest->code;
132                 u32 k = ftest->k;
133
134                 switch (code) {
135                 case BPF_S_LD_W_ABS:
136                         ftest->code = BPF_S_ANC_SECCOMP_LD_W;
137                         /* 32-bit aligned and not out of bounds. */
138                         if (k >= sizeof(struct seccomp_data) || k & 3)
139                                 return -EINVAL;
140                         continue;
141                 case BPF_S_LD_W_LEN:
142                         ftest->code = BPF_S_LD_IMM;
143                         ftest->k = sizeof(struct seccomp_data);
144                         continue;
145                 case BPF_S_LDX_W_LEN:
146                         ftest->code = BPF_S_LDX_IMM;
147                         ftest->k = sizeof(struct seccomp_data);
148                         continue;
149                 /* Explicitly include allowed calls. */
150                 case BPF_S_RET_K:
151                 case BPF_S_RET_A:
linux/seccomp.c

以上が、prctl()によるフィルタ登録処理の概要です。つぎにこのフィルタをKernelがどのように実行するのか、確認していきます。

strictモードの場合と同様に、システムコールが呼び出されるとsecure_computing()でシステムコールが許可されているかどうかのチェックが行われます。 filterモードの場合はここでシステムコール番号を引数としてseccomp_run_filters()が呼び出されることになります。(402行目)

377 int __secure_computing(int this_syscall)
378 {
379         int mode = current->seccomp.mode;
380         int exit_sig = 0;
381         int *syscall;
382         u32 ret;
383
384         switch (mode) {
385         case SECCOMP_MODE_STRICT:
386                 syscall = mode1_syscalls;
387 #ifdef CONFIG_COMPAT
388                 if (is_compat_task())
389                         syscall = mode1_syscalls_32;
390 #endif
391                 do {
392                         if (*syscall == this_syscall)
393                                 return 0;
394                 } while (*++syscall);
395                 exit_sig = SIGKILL;
396                 ret = SECCOMP_RET_KILL;
397                 break;
398 #ifdef CONFIG_SECCOMP_FILTER
399         case SECCOMP_MODE_FILTER: {
400                 int data;
401                 struct pt_regs *regs = task_pt_regs(current);
402                 ret = seccomp_run_filters(this_syscall);
403                 data = ret & SECCOMP_RET_DATA;
404                 ret &= SECCOMP_RET_ACTION;
405                 switch (ret) {
406                 case SECCOMP_RET_ERRNO:
407                         /* Set the low-order 16-bits as a errno. */
408                         syscall_set_return_value(current, regs,
409                                                  -data, 0);
410                         goto skip;
linux/seccomp.c

seccomp_run_filters()はフィルタのリストから取り出した命令列を順にsk_run_filter()で実行していき、そのうちの最も小さい返り値がseccomp_run_filters()の返り値になります。(215行目以降)

filterモードの場合はこの返り値でシステムコールの許可/拒否が判定されます。

202 static u32 seccomp_run_filters(int syscall)
203 {
204         struct seccomp_filter *f;
205         u32 ret = SECCOMP_RET_ALLOW;
206
207         /* Ensure unexpected behavior doesn't result in failing open. */
208         if (WARN_ON(current->seccomp.filter == NULL))
209                 return SECCOMP_RET_KILL;
210
211         /*
212          * All filters in the list are evaluated and the lowest BPF return
213          * value always takes priority (ignoring the DATA).
214          */
215         for (f = current->seccomp.filter; f; f = f->prev) {
216                 u32 cur_ret = sk_run_filter(NULL, f->insns);
217                 if ((cur_ret & SECCOMP_RET_ACTION) < (ret & SECCOMP_RET_ACTION))
218                         ret = cur_ret;
219         }
220         return ret;
221 }
linux/seccomp.c

sk_run_filter()はすでに前々回のコラムで紹介したのと同じ、フィルタをinterpreterとして実行する関数です。 実は、ソケットフィルタのinterpreterもseccomp-bpfのinterpreterも同じ関数で共通化されて実装されているんですね。へー。:-)

sk_run_filter()の処理のうち、seccomp-bpfに特有の処理だけを見てみましょう。

以下は、sk_run_filter()内のswitch文の中でopcodeの値がBPF_S_ANC_SECCOMP_LD_Wの場合を処理する部分です。

先ほど触れたように、user space上で指定したBPF_LD+BPF_W+BPF_ABSというopcodeはseccomp_check_filter()の中でBPF_S_ANC_SECCOMP_LD_Wに書き換えられるため、この部分で解釈されることになります。

389 #ifdef CONFIG_SECCOMP_FILTER
390                 case BPF_S_ANC_SECCOMP_LD_W:
391                         A = seccomp_bpf_load(fentry->k);
392                         continue;
393 #endif
net/core/filter.c

seccomp_bpf_load()は引数の値に応じて呼び出し元プロセスのseccomp_data構造体からoffsetで指定したデータをロードします。

 82 /* Helper for bpf_load below. */
 83 #define BPF_DATA(_name) offsetof(struct seccomp_data, _name)
 84 /**
 85  * bpf_load: checks and returns a pointer to the requested offset
 86  * @off: offset into struct seccomp_data to load from
 87  *
 88  * Returns the requested 32-bits of data.
 89  * seccomp_check_filter() should assure that @off is 32-bit aligned
 90  * and not out of bounds.  Failure to do so is a BUG.
 91  */
 92 u32 seccomp_bpf_load(int off)
 93 {
 94         struct pt_regs *regs = task_pt_regs(current);
 95         if (off == BPF_DATA(nr))
 96                 return syscall_get_nr(current, regs);
 97         if (off == BPF_DATA(arch))
 98                 return syscall_get_arch(current, regs);
 99         if (off >= BPF_DATA(args[0]) && off < BPF_DATA(args[6])) {
100                 unsigned long value;
101                 int arg = (off - BPF_DATA(args[0])) / sizeof(u64);
102                 int index = !!(off % sizeof(u64));
103                 syscall_get_arguments(current, regs, arg, 1, &value);
104                 return get_u32(value, index);
105         }
106         if (off == BPF_DATA(instruction_pointer))
107                 return get_u32(KSTK_EIP(current), 0);
108         if (off == BPF_DATA(instruction_pointer) + sizeof(u32))
109                 return get_u32(KSTK_EIP(current), 1);
110         /* seccomp_check_filter should make this impossible. */
111         BUG();
112 }
linux/seccomp.c

このようにBPF_LD+BPF_W+BPF_ABSはseccomp_data構造体からデータをロードする命令として解釈されるので user spaceでフィルタを記述する場合には暗黙的にseccomp_data構造体への参照をもつものとしてフィルタを記述することができます。

以上がフィルタの実行部分の処理です。

BPF_S_ANC_SECCOMP_LD_Wを使うことで、seccomp-bpfに特有の処理を切り離してそれ以外の汎用的な部分はソケットフィルタの処理とうまく共通化しているようですね。:-)


-まとめ-

今回はBPFを使ってシステムコールをフィルタするLinux Kernelの仕組みに迫ってみました。

seccomp-bpfのフィルタはinterpreterとして実行されるためパフォーマンス的には多少不利な要素となりますが、柔軟にアプリケーション独自のsandboxを構築することができるため応用の可能性は広がります。 将来的にseccomp-bpfにもJIT compilerが実装されることによりパフォーマンスは改善されることが期待できます。


タイガーチームメンバー 塚本 泰三