..

d3ctf-d3bpf

怕耽误毕业已经几个月没打比赛了,但总是不想学习,脑壳也动不起来,高压之下反而没有学习和思考的能力了。却发现有意思的是,想到做题就感觉能立马动身起来,虽然也没有游戏那么诱人。罢了,还是想偶尔看一些比赛的题目,预防老年痴呆。

d3bpf-v1

漏洞

diff 文件很直白,当 64b RSH 右移的位数大于等于 64 的时候,原本验证器会把 reg 标记为 unknown。可能是因为不同架构对这种情况有不同的处理,如果右移的位数超过 63 之后会清零寄存器,那修改的文件就没有问题,但是对于 amd64 RSH 会只看低 6 位而无视高位,意味着 RSH 64 其实等同于 RSH 0。这样就可以获得一个验证器认为是 0 但实际运行时不为 0 的寄存器,在题目环境 v5.11.0 中可以结合另一个整数下溢漏洞来很简单绕过 ALU sanitation。或者是通过多次将指针寄存器与 exp 寄存器使用 ALU 运算来绕过 sanitation。

diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 37581919e..8e98d4af5 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -6455,11 +6455,11 @@ static int adjust_scalar_min_max_vals(struct bpf_verifier_env *env,
                        scalar_min_max_lsh(dst_reg, &src_reg);
                break;
        case BPF_RSH:
-               if (umax_val >= insn_bitness) {
-                       /* Shifts greater than 31 or 63 are undefined.
-                        * This includes shifts by a negative number.
-                        */
-                       mark_reg_unknown(env, regs, insn->dst_reg);
+               if (umin_val >= insn_bitness) {
+                       if (alu32)
+                               __mark_reg32_known(dst_reg, 0);
+                       else
+                               __mark_reg_known_zero(dst_reg);
                        break;
                }
                if (alu32)

利用

到这里利用方式其实和 CVE-2020-8835 中就一样了,当时复现没做笔记,这里再记录一下。

  1. OOBRW

    可以用之前所述的漏洞寄存器来 ALU 一个 map 指针,就可以读写位于 map data 附近的内存。

    因为什么原因忘了,可能是 map size 限制?不太能用这种越界访问去读写距离太远的数据。

  2. Leak KASLR

    紧邻在 map data 前的 bpf_map 结构体包含指向 kernel .text 段的指针,可以泄露基地址

  3. Arbitrary Read

    任意地址读是使用 bpf_map_get_info_by_fd 中的原语 info.btf_id = map->btf->id,通过修改 map->btf 为任意地址,即可读入数据到 info 中,这是可以从用户态访问的数据。

  4. Arbitrary Write

    目前的一些利用方式任意地址写需要首先获得 map ptr 的数值,一般通过 init_pid_ns 出发找到进程的 task_struct(可重用 find_task_by_pid_ns 中的代码),再从 task_struct->files->file_array[map_fd]->private_data 中读取 map_ptr 的数值。

    偏移不同编译参数会不一样,可以去内核源码中找一些引用偏移的函数,再去反汇编里查

    之后任意地址写也是可以套 CVE-2020-8835 的模板,原语是 map_get_next_key 中的 *next = index + 1。

    1. 利用任意地址写获得原始的虚表写入 map 中
    2. map_push_elem 条目(会在 BPF_MAP_UPDATE_ELEM 时被调用)换为 map_get_next_key
    3. OOBW map->ops 到假的虚表(map_ptr)
    4. Set explmap->spin_lock_off to 0 to pass some additional checks
    5. Set explmap->max_entries to 0xffffffff to pass the check in array_map_get_next_key
    6. Set explmap->map_type to BPF_MAP_TYPE_STACK to be able to reach map_push_elem
    7. 调用 update_map_element 实现任意地址写,注意参数的对应关系

       update_map_element(pCtx->oob_map_fd, 0, vals, addr);
              
       int update_map_element(int map_fd, uint64_t key, void* value, uint64_t flags)
       {
           int ret = -1;
              
           union bpf_attr attr =
           {
               .map_fd = map_fd,
               .key    = (uint64_t)&key,
               .value  = (uint64_t)value,
               .flags  = flags,
           };
              
           ret = bpf(BPF_MAP_UPDATE_ELEM, &attr);
              
           return ret;
       }
      
  5. LPE

    刚才泄露了 task_struct,直接找到 cred 把 gid,uid 覆盖为 0 即可

d3bpf-v2

漏洞

diff 文件同 v1 相似,只是内核版本提升到了 v5.16.12,对于 v5.11.17+ 的内核版本,ALU sanitation 有更改,考察点是 CVE-2022-23222 中新的绕过 sanitation 的方法。

利用

首先 bpf_prog 在运行的时候,FP 是真的指向栈上而不是一块堆内存数据区,所以另一种更暴力的利用方法就是栈溢出。

#define PROG_NAME(stack_size) __bpf_prog_run##stack_size
#define DEFINE_BPF_PROG_RUN(stack_size) \
static unsigned int PROG_NAME(stack_size)(const void *ctx, const struct bpf_insn *insn) \
{ \
	u64 stack[stack_size / sizeof(u64)]; \
	u64 regs[MAX_BPF_EXT_REG]; \
\
	FP = (u64) (unsigned long) &stack[ARRAY_SIZE(stack)]; \
	ARG1 = (u64) (unsigned long) ctx; \
	return ___bpf_prog_run(regs, insn); \
}

bpf_skb_load_bytes 这个 helper 函数恰好可以实现从 sk_buff 里拷贝数据到栈上的目的,并且不会触发 ALU sanitation,用假的 len 欺骗静态验证器即可。

可以看到 __bpf_prog_run##stack_size 中 BPF_REG_1 初始化即为 ctx,在调用 helper 前不用单独赋值

static u64 bpf_skb_load_bytes(u64 r1, u64 r2, u64 r3, u64 r4, u64 r5)
{
	const struct sk_buff *skb = (const struct sk_buff *)(unsigned long) r1;
	int offset = (int) r2;
	void *to = (void *)(unsigned long) r3;
	unsigned int len = (unsigned int) r4;
	void *ptr;

	if (unlikely((u32) offset > 0xffff || len > MAX_BPF_STACK))
		return -EFAULT;

	ptr = skb_header_pointer(skb, offset, len, to);
	if (unlikely(!ptr))
		return -EFAULT;
	if (ptr != to)
		memcpy(to, ptr, len);

	return 0;
}

bpf 程序崩溃内核不会崩溃,这道题用 panic 来 leak base。