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
と決まっている(はず)なので、これと比較し異なっていればコンテナプロセスであるとします。
- https://github.com/torvalds/linux/blob/master/include/linux/proc_ns.h#L44
- https://elixir.bootlin.com/linux/v4.9/source/include/linux/nsproxy.h#L30
(なぜ 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 として持ち、改ざんチェックなどにも応用できそうですね。
これは冬休みの課題かな〜。