December 23, 2019

bpf(bcc) を使ってコンテナ内での execve をトレースする

bpf(bcc) の簡単な使い方などについては 前回 書きました。
bpf は falco, sysdig, cilium など、Kubernetes 環境で使われるケースも増え、コンテナに対するトレース活用も多く出てきている印象があります。

ここでは bcc を使ってコンテナ内の execve をトレースする方法をメモしておきます。

Docker などのいわゆる Linux コンテナもホストからすると単なるプロセスにすぎないため、「そのプロセスがコンテナのプロセスであるか」ということを確認できれば、コンテナ限定でトレース可能になります。
「そのプロセスがコンテナのプロセスであるか」は pid namespace を見ればわかりそうです。
ls -al /proc/self/ns したときに表示される数字ですね。これは task->ns_proxy->pid_ns_for_children->ns.inum で参照できます。

ホストプロセスの場合は 0xEFFFFFFCU と決まっている(はず)なので、これと比較し異なっていればコンテナプロセスであるとします。

(なぜ 0xEFFFFFFCU なのかは LKML を見ればわかったりするのかな..? なぜこの値なのかちょっと気になる)

これだけで、コンテナプロセスの判別ができますが、できればコンテナIDもトレースの情報としてほしいです。
/proc/${pid}/cgroup を参照することも考えられますが、execve やその他イベントの度に open するのはスマートじゃありませんね。
そこで UTS namespace を使います。UTS namespace で付与されたホスト名は Docker の場合だと(というより多くの Linux コンテナはそうなると思う)、そのままコンテナIDとして使われます。
long ID ではありませんが、ひとまずこれで「どのコンテナか」は追跡できそうです。
UTS namespace のホスト名も task->ns_proxy->uts_ns->name.hostname で参照できます。

というわけで以下その PoC です。

from bcc import BPF

bpf_text = """
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>
#include <linux/fs.h>
#include <linux/nsproxy.h>
#include <linux/ns_common.h>
#include <linux/utsname.h>
#include <linux/pid_namespace.h>

int trace_ret_execve(struct pt_regs *ctx)
{
    u32 pid;
    struct task_struct *task;
    char comm[TASK_COMM_LEN];
    char nodename[9];

    pid = bpf_get_current_pid_tgid() >> 32;
    bpf_get_current_comm(&comm, sizeof(comm));

    task = (struct task_struct *)bpf_get_current_task();
    struct pid_namespace *pns = (struct pid_namespace *)task->nsproxy->pid_ns_for_children;


    if (pns->ns.inum == 0xEFFFFFFCU) {
        return 0;
    }

    struct uts_namespace *uns = (struct uts_namespace *)task->nsproxy->uts_ns;

    bpf_trace_printk("name: %s\\n", uns->name.nodename);
    bpf_trace_printk("comm: %s, ns: %lld\\n", comm, pns->ns.inum);
    return 0;
}
"""

b = BPF(text=bpf_text)
execve_fnname = b.get_syscall_fnname("execve")
b.attach_kretprobe(event=execve_fnname, fn_name="trace_ret_execve")

while 1:
    try:
        b.trace_print()
    except KeyboardInterrupt:
        exit()

上記を実行すると次のような結果を得られます。

$ docker run --rm -it alpine:latest sh
/ # id
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)
/ # uname -a
Linux 50bff0a84c43 4.9.0-11-amd64 #1 SMP Debian 4.9.189-3+deb9u2 (2019-11-11) x86_64 Linux
/ # exit

$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
50bff0a84c43        alpine:latest       "sh"                22 seconds ago      Up 21 seconds                           hopeful_yonath

$ sudo python get_container_id.py
              sh-18610 [000] d... 67118.158906: : name: 50bff0a84c43
              sh-18610 [000] d... 67118.158916: : comm: sh, ns: 4026532128
              id-18659 [000] d... 67119.191543: : name: 50bff0a84c43
              id-18659 [000] d... 67119.191567: : comm: id, ns: 4026532128
           uname-18660 [000] d... 67121.588264: : name: 50bff0a84c43
           uname-18660 [000] d... 67121.588362: : comm: uname, ns: 4026532128

こんな感じでコンテナID とそのコンテナ内での execve をトレースできるようになりました。
ここからの発展として、コンテナ内で実行されるプロセスや開かれるファイルをホワイトリスト化し、falco などのルールに落とし込むようなことが可能となりそうです。
他にも、open 先のファイルハッシュ値を DB として持ち、改ざんチェックなどにも応用できそうですね。
これは冬休みの課題かな〜。

このエントリーをはてなブックマークに追加

© Kouhei Morita 2018