Loading... 学习这个问题的原因是想做 pwnable.tw 的 seccomp-tool 一题,此题的 elf 可以读取、模拟、加载用户输入的 bpf 代码,其中加载使用的是 prctl 系统调用,功能号为 PR_GET_SECCOMP。 暂时还不知道怎么做,原来觉得应该是通过 bpf 来完成利用,~~总不会是 elf 某处写渣了的溢出~~。由于我对 bpf 和 seccomp 不甚了解,所以先花了点时间了解了一下。了解了之后我感觉还真可能是 elf 本身的洞,在 emulate 功能中,也实现了一个类似于 Linux cbpf 解码器和模拟器的虚拟机,这里的代码还是有一点的,还没来的及看,说不定就是这里存在问题。 首先,根据上下文,seccomp 可能指代三种东西 * Linux 内核 seccomp 沙箱机制,运行在该沙箱中的程序只能使用 exit,sigreturn,read 和 write 四种系统调用。 * seccomp-bpf,seccomp 机制的扩展,通过 bpf 的支持可以使用户自定义需要过滤的系统调用。 * seccomp lib,提供了一系列函数,使用该库可以实现类似 seccomp-bpf 的过滤效果,并且使用者不需要了解 bpf 即可使用。 seccomp-tools 此题中使用的就是 seccomp-bpf,所以这里主要说一下 seccomp-bpf。其实看雪的 [[原创]seccomp沙箱机制 & 2019ByteCTF VIP](https://bbs.pediy.com/thread-258146.htm) 这篇文章写的非常详细,看这篇就差不多了。但是我这里按自己的习惯还是半写半抄在这里再弄一遍。 ```cpp /* part of seccomp-tools install function */ if ( bpf_bytes_len ) { if ( (unsigned __int8)check_filter() == 1 ) { prctl(38, 1LL, 0LL, 0LL, 0LL); // PR_SET_NO_NEW_PRIVS bpf_filter_prog.filter = (struct sock_filter *)&bpf_code_arr; *(_QWORD *)&bpf_filter_prog.len = (int)(unsigned __int16)bpf_bytes_len >> 3; if ( prctl(22, 2LL, &bpf_filter_prog) ) // PR_SET_SECCOMP perror("prctl"); else puts("Installed!"); } } ``` ## BPF BPF(伯克利包过滤器,**B**erkeley **P**acket **F**ilter)最初的设计目标是更快地捕获和过滤网络数据包。引用 [wiki](https://zh.wikipedia.org/wiki/BPF) 的介绍 > **伯克利包过滤器** (Berkeley Packet Filter,缩写 BPF),是[类Unix](https://zh.wikipedia.org/wiki/%E7%B1%BBUnix "类Unix")系统上[数据链路层](https://zh.wikipedia.org/wiki/%E6%95%B0%E6%8D%AE%E9%93%BE%E8%B7%AF%E5%B1%82 "数据链路层")的一种原始接口,提供原始链路层[封包](https://zh.wikipedia.org/wiki/%E5%B0%81%E5%8C%85 "封包")的收发。除此之外,如果网卡驱动支持[混杂模式](https://zh.wikipedia.org/wiki/%E6%B7%B7%E6%9D%82%E6%A8%A1%E5%BC%8F "混杂模式"),那么它可以让网卡处于此种模式,这样可以收到[网络](https://zh.wikipedia.org/wiki/%E7%BD%91%E7%BB%9C "网络")上的所有包,不管他们的目的地是不是所在[主机](https://zh.wikipedia.org/wiki/%E4%B8%BB%E6%A9%9F "主机")。 > > 另外,BPF支持过滤数据包——用户态的进程可以提供一个过滤程序来声明它想收到哪些数据包。通过这种过滤可以避免从[操作系统](https://zh.wikipedia.org/wiki/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F)[内核](https://zh.wikipedia.org/wiki/%E5%86%85%E6%A0%B8 "内核")向用户态复制其他对用户态程序无用的数据包,从而极大地提高性能。 > > BPF有时也只表示过滤机制,而不是整个接口。一些系统,比如[Linux](https://zh.wikipedia.org/wiki/Linux "Linux")和Tru64 Unix,提供了数据链路层的原始接口,而不是BPF的接口,但使用了BPF的过滤机制。 > > BSD 内核实现例程如 `bpf_mtap()`和 `bpf_tap()`,以`BPF_MTAP()`和 `BPF_TAP()`等[宏定义](https://zh.wikipedia.org/w/index.php?title=%E5%AE%8F%E5%AE%9A%E4%B9%89&action=edit&redlink=1 "宏定义(页面不存在)")的形式进行包裹由网卡驱动(以及伪驱动pseudo-drivers) 向BPF机制发送进出的封包。 我的理解就是在内核建立一个以寄存器为基础的虚拟机,用户向内核注入虚拟机字节码,通过内核中的 JIT 编译器就可以高效地在内核态执行用户添加的额外代码。是一个相对高效且安全地在用户态执行代码的机制 * **高效**:不需要重启、不需要修改内核,JIT 编译直接在 CPU 上执行。 * **安全**:在内核态运行之前会先通过一个 verifier 检测字节码是否安全,比 module 直接插入用户代码会更加安全。 BPF 刚被实现时其虚拟机只有两个寄存器,后来又有技术人员对它进行了扩展,增加到了 12 个寄存器和一些新的机制,扩展后的 BPF 就被称为 eBPF(extended BPF),原来的 BPF 就被称为 cBPF(classic BPF)。 ### seccomp 中的使用 seccomp-bpf 使用的是 cBPF(当然一些情况下这句话不怎么准确,因为如果内核支持 eBPF 的话,cBPF 会被翻译成 eBPF 执行)。cBPF 有一个 32 位累加寄存器 BPF_A,一个 32 位索引寄存器和 16 * 32 位的内存(也有人把这个当作 32 位寄存器,用 M[] 访问,这样说也是很有道理的,也就是有 16 个映射到内存的 32 位寄存器),当然还有一个不可编程的 PC。每条指令被抽象为了一个结构体 ```cpp /* * Try and keep these values and structures similar to BSD, especially * the BPF code definitions which need to match so you can share filters */ struct sock_filter { /* Filter block */ __u16 code; /* Actual filter code */ __u8 jt; /* Jump true */ __u8 jf; /* Jump false */ __u32 k; /* Generic multiuse field */ }; ``` 整条指令链就被抽象为了 ```cpp struct sock_fprog { /* Required for SO_ATTACH_FILTER. */ unsigned short len; /* Number of filter blocks */ struct sock_filter *filter; }; ``` 为了便于书写,在 filter.h 中封装了 cBPF 结构体宏,宏展开后实际上就是指令结构体。 ```cpp /* * Macros for filter block array initializers. */ #define BPF_STMT(code, k) { (unsigned short)(code), 0, 0, k } #define BPF_JUMP(code, k, jt, jf) { (unsigned short)(code), jt, jf, k } ``` 可用的指令被定义在了 bpf_common.h 中 ```cpp /* Instruction classes */ #define BPF_CLASS(code) ((code) & 0x07) #define BPF_LD 0x00 // load, 赋值到寄存器中 #define BPF_LDX 0x01 #define BPF_ST 0x02 // store, 赋值到内存中 #define BPF_STX 0x03 #define BPF_ALU 0x04 #define BPF_JMP 0x05 #define BPF_RET 0x06 #define BPF_MISC 0x07 /* ld/ldx fields */ #define BPF_SIZE(code) ((code) & 0x18) #define BPF_W 0x00 /* 32-bit */ #define BPF_H 0x08 /* 16-bit */ #define BPF_B 0x10 /* 8-bit */ /* eBPF BPF_DW 0x18 64-bit */ #define BPF_MODE(code) ((code) & 0xe0) #define BPF_IMM 0x00 #define BPF_ABS 0x20 #define BPF_IND 0x40 #define BPF_MEM 0x60 #define BPF_LEN 0x80 #define BPF_MSH 0xa0 /* alu/jmp fields */ #define BPF_OP(code) ((code) & 0xf0) #define BPF_ADD 0x00 #define BPF_SUB 0x10 #define BPF_MUL 0x20 #define BPF_DIV 0x30 #define BPF_OR 0x40 #define BPF_AND 0x50 #define BPF_LSH 0x60 #define BPF_RSH 0x70 #define BPF_NEG 0x80 #define BPF_MOD 0x90 #define BPF_XOR 0xa0 #define BPF_JA 0x00 #define BPF_JEQ 0x10 #define BPF_JGT 0x20 #define BPF_JGE 0x30 #define BPF_JSET 0x40 #define BPF_SRC(code) ((code) & 0x08) #define BPF_K 0x00 #define BPF_X 0x08 ``` 以禁用 execve 为例,使用 bpf 禁用就是这样 ```cpp #include <unistd.h> #include <stdio.h> #include <stdint.h> #include <sys/syscall.h> #include <linux/seccomp.h> #include <linux/filter.h> #include <sys/prctl.h> int main() { struct sock_filter filter[] = { BPF_STMT(BPF_LD + BPF_W + BPF_ABS, 0), BPF_JUMP(BPF_JMP + BPF_JEQ, SYS_execve, 0, 1), /* sys_number == execve */ BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_KILL), /* true */ BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW) /* false */ }; struct sock_fprog bpf_prog; bpf_prog.filter = filter; bpf_prog.len = sizeof(filter) / sizeof(struct sock_filter); prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); prctl(PR_SET_SECCOMP, 2, &bpf_prog); while(1) { /* code */ } return 0; } ``` 这里在设置 seccomp 之前先执行了 `prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);`,这条语句禁止了此进程提权。这么做的原因是使用 prctl 的 PR_SET_SECCOMP 需要 CAP_SYS_ADMIN 这个 capabilities,如果没有这个 capabilities 就需要设置 PR_SET_NO_NEW_PRIVS 为 1,否则对于非 root 用户 seccomp 就会失效。 bpf 的每一条指令中的 `code` 都由 `指令类型 + 指令` 的方法组成。filter.h 提供的宏 BPF_STMT 是普通语句,做运算和内存操作等,BPF_JUMP 就是跳转语句,jf 和 jt 的值就是相应情况下跳过的指令数。 编译后用 seccomp-tools dump 一下 ```shell $ seccomp-tools dump ./seccomp_bpf line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000000 A = sys_number 0001: 0x15 0x00 0x01 0x0000003b if (A != execve) goto 0003 0002: 0x06 0x00 0x00 0x00000000 return KILL 0003: 0x06 0x00 0x00 0x7fff0000 return ALLOW ``` 可以发现成功过滤了。 到这里就搞清楚了如何用 bpf 来进行系统调用过滤了。 ### 宏观地来看实现流程 然后再宏观地看一下 prctl 对 bpf 的处理流程,以 linux-5.12.9 版本的代码为例。 首先是 prctl 系统调用 ```cpp SYSCALL_DEFINE5(prctl, int, option, unsigned long, arg2, unsigned long, arg3, unsigned long, arg4, unsigned long, arg5) ``` 这个系统调用使用了一个巨大的 switch 来处理各种功能选项,我们关心的是这个 case ````cpp case PR_SET_SECCOMP: error = prctl_set_seccomp(arg2, (char __user *)arg3); ```` 此功能由 prctl_set_seccomp 函数实现,函数比较简短,这里全部放一下 ```cpp /** * prctl_set_seccomp: configures current->seccomp.mode * @seccomp_mode: requested mode to use * @filter: optional struct sock_fprog for use with SECCOMP_MODE_FILTER * * Returns 0 on success or -EINVAL on failure. */ long prctl_set_seccomp(unsigned long seccomp_mode, void __user *filter) { unsigned int op; void __user *uargs; switch (seccomp_mode) { case SECCOMP_MODE_STRICT: op = SECCOMP_SET_MODE_STRICT; /* * Setting strict mode through prctl always ignored filter, * so make sure it is always NULL here to pass the internal * check in do_seccomp(). */ uargs = NULL; break; case SECCOMP_MODE_FILTER: op = SECCOMP_SET_MODE_FILTER; uargs = filter; break; default: return -EINVAL; } /* prctl interface doesn't have flags, so they are always zero. */ return do_seccomp(op, 0, uargs); } #define SECCOMP_MODE_STRICT 1 #define SECCOMP_MODE_FILTER 2 ``` seccomp_mode 就是指定设置的模式了,strict mode 就是只允许 exit,sigreturn,read 和 write,filter mode 就是通过 bpf 来自定义过滤了。然后调用 do_seccomp 函数,也比较短 ```cpp /* Common entry point for both prctl and syscall. */ static long do_seccomp(unsigned int op, unsigned int flags, void __user *uargs) { switch (op) { case SECCOMP_SET_MODE_STRICT: if (flags != 0 || uargs != NULL) return -EINVAL; return seccomp_set_mode_strict(); case SECCOMP_SET_MODE_FILTER: return seccomp_set_mode_filter(flags, uargs); case SECCOMP_GET_ACTION_AVAIL: if (flags != 0) return -EINVAL; return seccomp_get_action_avail(uargs); case SECCOMP_GET_NOTIF_SIZES: if (flags != 0) return -EINVAL; return seccomp_get_notif_sizes(uargs); default: return -EINVAL; } } ``` 我们关系的是 SECCOMP_SET_MODE_FILTER 这个 case,可见实际调用的是 seccomp_set_mode_filter 函数,这个函数比较长,这里就不放了,我们主要关心的是流程对用户 bpf 代码的处理,也就是编译的过程,最后进行编译的函数为 bpf_prepare_filter,调用链为 ```cpp seccomp_prepare_user_filter seccomp_prepare_filter bpf_prog_create_from_user bpf_prepare_filter ``` ````cpp static struct bpf_prog *bpf_prepare_filter(struct bpf_prog *fp, bpf_aux_classic_check_t trans) { int err; fp->bpf_func = NULL; fp->jited = 0; err = bpf_check_classic(fp->insns, fp->len); if (err) { __bpf_prog_release(fp); return ERR_PTR(err); } /* There might be additional checks and transformations * needed on classic filters, f.e. in case of seccomp. */ if (trans) { err = trans(fp->insns, fp->len); if (err) { __bpf_prog_release(fp); return ERR_PTR(err); } } /* Probe if we can JIT compile the filter and if so, do * the compilation of the filter. */ bpf_jit_compile(fp); /* JIT compiler couldn't process this filter, so do the * internal BPF translation for the optimized interpreter. */ if (!fp->jited) fp = bpf_migrate_filter(fp); return fp; } ```` 这里会尝试 jit 编译,如果失败就通过 bpf_migrate_filter 函数来进行转换,此函数会两次调用 bpf_convert_filter 函数,两次调用的注释为 ```cpp /* 1st pass: calculate the new program length. */ err = bpf_convert_filter(old_prog, old_len, NULL, &new_len, &seen_ld_abs); .. /* 2nd pass: remap sock_filter insns into bpf_insn insns. */ err = bpf_convert_filter(old_prog, old_len, fp, &new_len, &seen_ld_abs); ``` 可见第一次是计算转换后的程序长度,第二次是正式的代码转换。bpf_convert_filter 的实现挺复杂的,我也没仔细看,这里就不多说了。 通过调试我发现 cBPF 代码是无法 jit 编译的,返回后 fp->jited 的值仍为 0 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/07/287347397.png "></div> 我的理解是,jit 是 eBPF 实现的,所以 cBPF 代码需要被 bpf_migrate_filter 转换。 ### eBPF 简单介绍 这玩意很复杂,不了解。这里为了解题,简单的了解了一下,相比于 cBPF 还是有不少变动的,首先是指令 ```cpp struct bpf_insn { __u8 code; /* opcode */ __u8 dst_reg:4; /* dest register */ __u8 src_reg:4; /* source register */ __s16 off; /* signed offset */ __s32 imm; /* signed immediate constant */ }; ``` 寄存器也被扩充到了 10 个 ```cpp /* Register numbers */ enum { BPF_REG_0 = 0, BPF_REG_1, BPF_REG_2, BPF_REG_3, BPF_REG_4, BPF_REG_5, BPF_REG_6, BPF_REG_7, BPF_REG_8, BPF_REG_9, BPF_REG_10, __MAX_BPF_REG, }; ``` 这些寄存器有约定功能 * R0:内核函数返回值存储再改寄存器中,同时也是 eBPF 程序的返回值寄存器 * R1 - R5:调用内核函数的参数寄存器 * R6 - R9:被调函数负责备份的寄存器 * R10:用于访问栈的只读寄存器 这十个寄存器在实际运行的时候是直接映射到 CPU 寄存器上的,以 x86_64 为例,R0 - R10 就依次映射到 * rax, rdi, rsi, rdx, rcx, r8, rbx, r13, r14, r15, rbp 结合 cdecl 调用约定就很好理解了。 ## 参考 > [Linux内核工程导论——网络:Filter(LSF、BPF、eBPF)](https://blog.csdn.net/ljy1988123/article/details/50444693) > [[原创]seccomp沙箱机制 & 2019ByteCTF VIP](https://bbs.pediy.com/thread-258146.htm) 最后修改:2021 年 07 月 27 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 2 如果觉得我的文章对你有用,那听听上面我喜欢的歌吧