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 中就一样了,当时复现没做笔记,这里再记录一下。
-
OOBRW
可以用之前所述的漏洞寄存器来 ALU 一个 map 指针,就可以读写位于 map data 附近的内存。
因为什么原因忘了,可能是 map size 限制?不太能用这种越界访问去读写距离太远的数据。
-
Leak KASLR
紧邻在 map data 前的 bpf_map 结构体包含指向 kernel .text 段的指针,可以泄露基地址
-
Arbitrary Read
任意地址读是使用 bpf_map_get_info_by_fd 中的原语 info.btf_id = map->btf->id,通过修改 map->btf 为任意地址,即可读入数据到 info 中,这是可以从用户态访问的数据。
-
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。- 利用任意地址写获得原始的虚表写入 map 中
- 将
map_push_elem
条目(会在BPF_MAP_UPDATE_ELEM
时被调用)换为map_get_next_key
- OOBW map->ops 到假的虚表(map_ptr)
- Set
explmap->spin_lock_off
to 0 to pass some additional checks - Set
explmap->max_entries
to0xffffffff
to pass the check inarray_map_get_next_key
- Set
explmap->map_type
toBPF_MAP_TYPE_STACK
to be able to reachmap_push_elem
-
调用 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; }
-
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。