sysctl 参数防篡改 - 基于 ebpf 的实现 [二]

语言: CN / TW / HK

sysctl 参数防篡改 - 基于 ebpf 的实现 [一]

bpftool 工具

上文提到的这个工具,叫做 "bpftool",由于 5.12 内核的 "tools/testing/selftests/bpf" 的目录中已经包含 bpftool 的代码,所以编译完 testing 中的示例程序后,会同步编译出 bpftool 的可执行文件。如果没有的话,可以进入内核源码的 "tools/bpf/bpftool/" 目录执行 "make" 命令,或者通过 dnf/yum 等包管理器安装现成的 bpftool 软件包。

bpftool 的功能很多,比如通过 "bpftool feature probe",可以查看所支持的 ebpf helper 列表。而本文将主要探寻其在 load 和 attach 一个 ebpf 程序中的作用。

还是以上文那个简化后的 "test_sysctl_prog.c" 文件为例(这里命名为 "sysctl_demo.c" ),但改为单独编译的方式(毕竟咱不需要运行那么多测试用例,也不需要用户态的 loader 代码):

clang -O2 -Wall -target bpf -I ../tools/include -c sysctl_demo.c -o sysctl_demo.o

- Load 操作

然后 load 生成的 "sysctl_demo.o" ,并 pin"/sys/fs/bpf/sysctl_test" 这个文件路径("/sys/fs/bpf" 是一个 bpf 类型的文件系统,没有的话用 "mount -t bpf none /sys/fs/bpf/" 挂载下)。设置这样一种文件系统以及关联一个 ebpf 对象到其上的一个文件,主要是为了方便用户态访问作为内核资源的 ebpf maps/programs。

bpftool prog load sysctl_demo.o /sys/fs/bpf/sysctl_test

接下来就可以用 "prog show" 命令看到这个 ebpf prog 了:

使用 "llvm-objdump --arch-name=bpf -S sysctl_demo.o" 命令反汇编看下,嗯,确实是 40 个 bytes。

- Attach 操作

根据前面获取的 ID 信息,针对 "/sys/fs/cgroup" 这个路径,以 "override" 的方式 attach 类型为 "cgroup/sysctl" 的 ebpf prog:

bpftool cgroup attach /sys/fs/cgroup sysctl id 94 override

如果也使用 "strace -e bpf" 跟一下这条命令的执行,会发现在 bpf 系统调用这个点, 传入的参数和上文自行编写的 loader 代码是一样的,两者可以说是殊途同归。成功后,可通过 "cgroup tree" 命令列出所有的 attached program。

- 检验效果

原先的 demo 示例实在太简单了点,咱们这里在 attach 后的处理函数里获取一下 sysctl 参数的名字,再加上一些打印信息:

char name[64] = {0};

if (ctx->write) {
    bpf_sysctl_get_name(ctx, name, sizeof(name), 0);
    bpf_printk("write %s denied\n", name);
    return 0;
}

"bpf_printk" 的打印会输出到 trace log 里,所以单独开一个 terminal A,执行 "bpftool prog tracelog" (或者 "cat /sys/kernel/debug/tracing/trace_pipe")命令,然后再另一个 terminal B 执行一个 sysctl 参数的写入命令(比如 "net/ipv4/ip_forward"),那么在 terminal B 会得到以下提示:

同时在 terminal A 也会有如下输出:

<...>-1240284 [001] d... 524556.246469: bpf_trace_printk: write net/ipv4/ip_forward denied

如果使能了内核参数 "kernel.bpf_stats_enabled"(由于 stat 存在性能开销,默认没有开),那么用 "bpf prog show/list" 还可以看到 ebpf 程序的执行时间和执行次数,比如:

run_time_ns 6261 run_cnt 2

暂时不用的 ebpf 程序可以 detach 掉,彻底不用的话就 unlink 移除:

bpftool cgroup detach /sys/fs/cgroup sysctl id 94

unlink /sys/fs/bpf/sysctl_test

Cgroup 版本

以上基于 bpftool 的这些操作都是在采用cgroup V2 的系统上完成的,如果 cgroup 是 V1 版本,那么在 attach 这一步是不会成功的(strace 可看到返回 "-EBADF" 的错误)。根据对代码调用路径的分析,是在 "css_tryget_online_from_dir" 函数这里失败的。

cgroup_bpf_prog_attach() 
    --> cgroup_get_from_fd() 
        --> cgroup_get_from_file() 
            --> css_tryget_online_from_dir()

再来看下这个函数的具体实现,得到 "-EBADF" 的错误码有以下几种原因:

if ((s_type != &cgroup_fs_type && s_type != &cgroup2_fs_type) ||
    !kn || kernfs_type(kn) != KERNFS_DIR)
    return ERR_PTR(-EBADF);

在使用 cgroup V1 和 V2 的机器上分别执行 "findmnt" 命令,你会发现针对 V2, "/sys/fs/cgroup" 就是 "cgroup2" 类型的文件系统的挂载点,而针对 V1,该路径挂载的文件系统类型是 "tmpfs",只有子目录 controller 才是 "cgroup" 类型的文件系统。所以在上述代码判断时, "s_type" 这里通不过。

那你说我用 V1 的时候,在 attach 时把路径改为 controller 的呢,比如 /sys/fs/cgroup/systemd" ?咳咳,这一步判断是可以过,但下一步 "cgroup_on_dfl" 函数这里,还是通不过。

好像上文使用的 loader 代码编译后可以在默认 V1 的机器上运行?那是因为它其实也是创建了一个新的 namespace,并在这个 ns 上挂载了 "cgroup2" 的文件系统,然后在这个 V2 的文件系统上操作的。

小结

相比自行编写用户态的 loader 程序,使用 bpftool 更加简单方便,可以快速验证内核态 ebpf prog 的功能,但灵活性和可控性稍差了些。不管是哪种方式,在 5.12 内核上编译好的 ebpf prog,放到 5.4 内核的机器上都可以直接运行,这是传统的内核驱动代码所不及的。

不过尺有所短,寸有所长,相比起内核驱动可以实现的功能,ebpf 程序目前受到的限制还比较多(比如 sysctl 这里就需要不低于 5.2 版本的内核和 cgroup V2 的支持),好在其正在被快速地开发着,相信以后会更加地好用(比如更少的编码限制),功能也更加地丰富(比如更多的 ebpf helper)。

参考:

原创文章,转载请注明出处。