[Linux Kernel Pwn] eBPF 入门笔记(一)

本文总阅读量

eBPF介绍

eBPF 是一项革命性的技术,起源于 Linux 内核,它可以在特权上下文中(如操作系统内核)运行沙盒程序。它用于安全有效地扩展内核的功能,而无需通过更改内核源代码或加载内核模块的方式来实现。

历史上由BPF(伯克利包过滤器 Berkeley Packet Filter)发展而来(现在称为cBPF),直接在内核中运行特权代码,给予了Linux内核编程的极大的灵活性。
eBPF需要内核支持,用户可以通过一系列指令编写程序,在程序被加载后需要经过检验,验证通过后会通过JIT (Just-in-Time) 编译成机器码执行。

为了更加便捷,eBPF还支持在eBPF指令中直接调用函数(被称为Helper函数)。eBPF还提供map等数据结构用来与用户态之间传递信息;
在相关漏洞利用中,一般是想方法绕过严格的eBPF检验,通过漏洞混淆检验的过程,使得检验(模拟执行)结果和实际JIT编译运行结果不一致,从而造成安全漏洞,进一步转化为方便利用的类型。

我们见到的沙箱题一般是用seccomp来实现的,而seccomp-bpf便是采用了BPF技术实现过滤系统调用

相关文档1 相关文档2 Linux系统另见/usr/include/linux/bpf.h

bpf设计相关,另见此

eBPF基础

基础知识

本章的讨论环境基于Linux v6.13.7

1
2
3
4
// in  /usr/include/asm/unistd_64.h
...
#define __NR_bpf 321
...

通过系统调用号为321的系统调用bpf可以使用eBPF相关功能。glibc并没有提供了包装函数,需要我们进行raw syscall:

1
2
3
4
5
6
7
8
#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);
};

  • 参数int cmd标识bpf的行为(类型应为enum bpf_cmd)。常用的有:
    • BPF_PROG_LOAD 用于加载并验证eBPF程序,写好了eBPF程序就用此参数上传。返回一个文件描述符(fd)以供使用。
    • BPF_MAP_CREATE 用于创建一个eBPF map,以键值对(key-value)的形式存储数据。返回一个文件描述符(fd)以供使用。
    • BPF_MAP_LOOKUP_ELEMBPF_MAP_UPDATE_ELEMBPF_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 *,为一个包含多种结构体的联合体指针。该类型完整原型太长了,我们只看部分:(翻译并补充了部分注释)
bpf_attr完整版
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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,部分内容为:

bpf_map_type完整版
1
2
3
4
5
6
7
8
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,部分内容为:

bpf_prog_type完整版
1
2
3
4
5
6
7
8
9
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中,我们往往只需要关注部分参数,下边给出一些调用模板:

需要的头文件/宏:

1
2
3
4
5
6
#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相关manual
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// 创建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创建manual
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#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触发模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 通过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)));
}

有关trigger函数原理/该选用哪一个的问题会在后几章详细解释

eBPF指令

在prog创建时需要提供指令struct bpf_insn *insns,也是eBPF程序的核心部分。在eBPF Pwn中,指令的编写在触发/利用漏洞中扮演重要角色。
在编写指令之前,我们需要搞明白eBPF的工作模式

这是bpf指令结构体,看着好像并不复杂,但麻雀虽小五脏俱全

1
2
3
4
5
6
7
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字节)

编码格式:

操作码(opcode) 8bit 寄存器(regs) (4 + 4 =)8bit 偏移(offset)16bit
立即数(imm) 32bit

存在宽编码格式指令,后接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_ALU 32位算术操作
  • BPF_JMP 64位跳转操作
  • BPF_JMP32 32位跳转操作
  • BPF_ALU64 64位算术操作

BPF_LD(X)/BPF_ST(X)操作涉及取址,需同时提供取址大小(size),分别有:

  • BPF_W word (32位)
  • BPF_H half word (16位)
  • BPF_B byte (8位)
  • BPF_DW double word (64位)

这里的word(字)的大小并不是16位而是32位

该操作还有不同的模式,参阅此处

其中BPF_ALU/BPF_ALU64中有14种不同的操作,常用的有:

名字 描述
ADD dst += src
SUB dst -= src
MUL dst *= src
DIV dst = (src != 0) ? (dst / src) : 0
MOD dst = (src != 0) ? (dst % src) : dst
AND dst &= src
OR dst |= src
XOR dst ^= src
NEG dst = -dst
MOV dst = src
MOVSX dst = (s8,s16,s32)src (根据offset确定)

带符号除法/取模操作在对应命令前加上S即可(SDIV/SMOD
完整版参阅此处

BPF_JMP32/BPF_JMP也有14种不同的类型,常用的有:

名字 描述
JA PC += offset/imm (立即跳转)
JEQ PC += offset if dst == src (相等跳转)
JNE PC += offset if dst != src (不等跳转)
JGT PC += offset if dst > src (无符号)
JGE PC += offset if dst >= src (无符号)
JLT PC += offset if dst < src (无符号)
JLE PC += offset if dst <= src (无符号)
JSET PC += offset if dst & src

带符号比较操作在对应命令中间加上S即可(JSGTJSLE等)

比较特殊的指令有:

名字 源寄存器(src_reg) 描述
CALL src_reg = 0x0 通过imm对应的static id调用Helper函数
CALL src_reg = 0x1 PC += imm (作为函数调用)
CALL src_reg = 0x2 通过imm对应的BTF id调用Helper函数
EXIT src_reg = 0x0 从函数/eBPF程序返回

对于目的寄存器和源寄存器,有BPF_REG_0BPF_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等)

以下是一个示例程序:

需要的头文件:

1
2
#include <linux/bpf.h>
#include "./bpf_insn.h" // your path to bpf_insn.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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()
};

对应生成的汇编如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
0xffffffffc0000668:  endbr64
0xffffffffc000066c: nop DWORD PTR [rax+rax*1+0x0]
0xffffffffc0000671: xchg ax,ax
0xffffffffc0000673: push rbp
0xffffffffc0000674: mov rbp,rsp
0xffffffffc0000677: endbr64
0xffffffffc000067b: sub rsp,0x10
0xffffffffc0000682: push r14
0xffffffffc0000684: mov r14,rdi
0xffffffffc0000687: mov edi,0x1
0xffffffffc000068c: mov esi,0x2
0xffffffffc0000691: mov QWORD PTR [rbp-0x10],0x3
0xffffffffc0000699: lfence
0xffffffffc000069c: mov QWORD PTR [rbp-0x8],0x4
0xffffffffc00006a4: lfence
0xffffffffc00006a7: mov rdx,QWORD PTR [rbp-0x8]
0xffffffffc00006ab: mov rcx,QWORD PTR [rbp-0x10]
0xffffffffc00006af: add rdx,rcx
0xffffffffc00006b2: xor eax,eax
0xffffffffc00006b4: pop r14
0xffffffffc00006b6: leave
0xffffffffc00006b7: ret

可以看出该程序仅仅是进行了一些意义不明的寄存器和栈操作,并没有什么实际用处。为了拓展程序功能,同时减少内核上下文切换的开销,ebPF提供了一系列Helper函数,来实现更强大方便的功能。

Helper函数

eBPF中的Helper函数是内核函数,运行在内核上下文,可以允许eBPF程序如同调用函数一样与内核交互
Helper函数功能强大,用途很广。目前一共有211个Helper函数,可以被分为map相关,trace程序相关,print相关,网络相关等类别
在eBPF pwn中,一般只需要关注部分常用/方便漏洞利用的Helper函数即可

调用函数的方法如下所示

1
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_NAME)

传参规则

在调用Helper函数之前,我们先了解一下Helper函数的传参类型规则。以下是一个示例Helper函数原型:

函数原型出处
1
2
3
4
5
6
7
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
部分完整版
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/* 函数参数约束 */
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
部分完整版
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 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
部分完整版
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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
原型出处
1
2
3
4
5
6
7
8
9
10
11
12
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)

arg1 类型见上文
arg2 类型为ARG_PTR_TO_MAP_KEY,事实上用一个指向栈的指针即可

在map中根据key来查找value

返回值为一个指向map value的指针或NULL(0),需要判断空指针操作

  • bpf_map_update_elem
原型出处
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)

更新/插入key-value对,flag参数为BPF_NOEXIST(key不存在时才会更新),BPF_EXIST(仅在key存在时更新)或BPF_ANY(两种情况均可)

和用户态调用参数一致

arg2, arg3类型均可以用指向栈的指针

如果成功返回0,失败则返回一个负的错误号

完整示例程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
#include "./bpf_insn.h"
#include <asm/unistd_64.h>
#include <linux/bpf.h>
#include <linux/bpf_common.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <unistd.h>

#define ptr_to_u64(val) (uint64_t)val
#define bpf(cmd, attr, size) syscall(__NR_bpf, cmd, attr, size)

int map_fd;

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));
}

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));
}

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));
}

#define LOG_BUF_SIZE 0x800

char bpf_log_buf[LOG_BUF_SIZE];

int bpf_prog_load_once() {
const struct bpf_insn insns[] = {
BPF_MOV64_REG(BPF_REG_9, BPF_REG_1),

BPF_LD_MAP_FD(BPF_REG_1, map_fd),

BPF_ST_MEM(BPF_DW, BPF_REG_10, -0x8, 0x0),
BPF_MOV64_REG(BPF_REG_2, BPF_REG_10),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem),
BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1), BPF_EXIT_INSN(),
BPF_LDX_MEM(BPF_DW, BPF_REG_6, BPF_REG_0, 0), BPF_MOV64_IMM(BPF_REG_0, 0),

BPF_ALU64_IMM(BPF_ADD, BPF_REG_6, 0x10), BPF_LD_MAP_FD(BPF_REG_1, map_fd),

BPF_ST_MEM(BPF_DW, BPF_REG_10, -0x8, 0x1),

BPF_MOV64_REG(BPF_REG_2, BPF_REG_10),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),

BPF_STX_MEM(BPF_DW, BPF_REG_10, BPF_REG_6, -0x10),

BPF_MOV64_REG(BPF_REG_3, BPF_REG_10),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_3, -0x10),

BPF_MOV64_IMM(BPF_REG_4, BPF_ANY),

BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_update_elem),
BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, 0, 1), BPF_EXIT_INSN(),

BPF_MOV64_IMM(BPF_REG_0, 0x0), BPF_EXIT_INSN()};

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));
}

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));
}

int main(int argc, char *argv[]) {
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;
}

int key = 0;
uint64_t value = 0xaaaabbbb;
bpf_update_elem(map_fd, &key, &value, BPF_ANY);
// map[0] = 0xaaaabbbb

int prog_fd = bpf_prog_load_once();

// printf("log: \n%s\n", bpf_log_buf);

if (prog_fd < 0) {
puts("Error while loading bpf program");
return 1;
}
uint64_t *payload = malloc(0x20);

int ret = trigger1(prog_fd, payload);
if (ret < 0) {
puts("Error while trigger bpf program");
}

key = 1;
value = 0x0;
bpf_lookup_elem(map_fd, &key, &value);
printf("find map[%d] = %#lx\n", key, value);

return 0;
}

执行结果:

1
2
bash-5.2$ ./test
find map[1] = 0xaaaabbcb