ebpf学习-ebpf简单模块编写

两只羊 Lv3

test

环境搭建

clang: eBPF 必须用 Clang 编译(它支持生成 BPF 字节码)。

libbpf-dev: 现代 eBPF 的核心库。

linux-tools: 包含 bpftool,用于生成内核头文件。

1
2
sudo apt update
sudo apt install -y clang llvm libbpf-dev make gcc linux-tools-$(uname -r) linux-tools-common

当然最好自己手动编译一个,避免链接过程出现问题

1
2
3
4
5
6
7
8
9
10
11
# 1. 回到你的工作目录上一级(假设你在 ebpf-hello 目录)
cd ..

# 2. 克隆源码
git clone https://github.com/libbpf/libbpf

# 3. 进入源码目录进行编译
cd libbpf/src

# 4. 编译(生成静态库 libbpf.a)
make

编译流程

1
2
3
4
5
6
7
8
9
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
clang -g -O2 -target bpf -D__TARGET_ARCH_x86 -c hello.bpf.c -o hello.bpf.o
bpftool gen skeleton hello.bpf.o > hello.skel.h

# 2. 编译主程序 (注意 -I 和 .a 的路径要对)
clang -g -O2 -o hello main.c \
-I../libbpf/src \
../libbpf/src/libbpf.a \
-lelf -lz

通过tracepoint hook openat

接下来的代码测试都在x86的ubuntu中进行。

接下来编写一个简单的读文件,然后输出的程序

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
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main()
{
int fd = open("flag", O_RDONLY);


if (fd == -1) {
perror("open failed");
return 1;
}
char buf[1024];

ssize_t bytesRead = read(fd, buf, sizeof(buf) - 1);

if (bytesRead == -1) {
perror("read failed \n");
close(fd);
return 1;
}

buf[bytesRead] = '\0';

printf("Context: %s\n", buf);
close(fd);

return 0;
}

那么要怎么写一个简单tracepoint挂载呢

首先要定义一个结构体,用于在用户态和ebpf内核代码之间数据传输并保持结构一致

这里我规定了要传输pid,comm(注意这里最大只有16字节,你的命令长于16会被截断)和open的文件名

common.h

1
2
3
4
5
6
7
8
9
10
11
12
#ifndef __COMMON_H
#define __COMMON_H

#define MAX_FILENAME_LEN 256

struct event {
int pid;
char comm[16];
char filename[MAX_FILENAME_LEN];
};

#endif

然后拦截一波sys_enter_openat,这里通常就是open最终会走的系统调用

这里使用的和用户态之间的数据传输方式是ringbuf,好处不用多讲了,注意在使用前,定义Ring Buffer的map,用于缓冲区预留足够空间。

通过bpf_ringbuf_reserve就可以分配出一块执行共享内存,并进行操作,注意从用户态内存拷贝字符串要通过bpf_probe_read_user_str。

最后通过bpf_ringbuf_submit将这段数据进行传输

trace_open.bpf.c

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
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
#include "common.h"

char LICENSE[] SEC("license") = "GPL";

// 定义 Ring Buffer 地图
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024); // 256 KB
} rb SEC(".maps");

SEC("tracepoint/syscalls/sys_enter_openat")
int handle_openat(struct trace_event_raw_sys_enter *ctx) {
struct event *e;

// 1. 在 ringbuf 中预留空间
e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
if (!e) {
return 0;
}

// 2. 获取数据
e->pid = bpf_get_current_pid_tgid() >> 32;
bpf_get_current_comm(&e->comm, 16);


// openat 的第二个参数是 filename 指针 (args[1])
// 使用 bpf_probe_read_user_str 从用户空间拷贝字符串
bpf_probe_read_user_str(&e->filename, sizeof(e->filename), (char *)ctx->args[1]);

// 3. 提交数据到用户态
bpf_ringbuf_submit(e, 0);

return 0;
}

然后是用户态的代码,负责将ebpf模块安装,然后进行ringbuf的通信

通过ring_buffer__new可以绑定回调函数,通过ring_buffer__poll触发回调

数据格式还是严格按照common.h中的结构定义

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
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <bpf/libbpf.h>
#include "trace_open.skel.h" // 假设使用 bpftool 生成的 skeleton
#include "common.h"

// Ringbuf 回调函数
static int handle_event(void *ctx, void *data, size_t data_sz) {
const struct event *e = data;
printf("PID: %d %16s| open file: %s\n", e->pid, e->comm, e->filename);
return 0;
}

int main() {
struct trace_open_bpf *skel;
struct ring_buffer *rb = NULL;
int err;

// 1. 加载并附着 eBPF 程序
skel = trace_open_bpf__open_and_load();
if (!skel) return 1;

err = trace_open_bpf__attach(skel);
if (err) goto cleanup;

// 2. 设置 Ring Buffer 管理器
rb = ring_buffer__new(bpf_map__fd(skel->maps.rb), handle_event, NULL, NULL);
if (!rb) goto cleanup;

printf("正在拦截 openat 系统调用... 按 Ctrl+C 退出\n");

// 3. 循环轮询数据
while (1) {
err = ring_buffer__poll(rb, 100 /* 毫秒超时 */);
if (err == -EINTR) continue;
if (err < 0) break;
}

cleanup:
ring_buffer__free(rb);
trace_open_bpf__destroy(skel);
return 0;
}

最后进行一波编译

1
2
3
4
5
6
7
8
9
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
clang -g -O2 -target bpf -D__TARGET_ARCH_x86 -c trace_open.bpf.c -o trace_open.bpf.o
bpftool gen skeleton trace_open.bpf.o > trace_open.skel.h

# 2. 编译主程序 (注意 -I 和 .a 的路径要对)
clang -g -O2 -o trace_open main.c \
-I../libbpf/src \
../libbpf/src/libbpf.a \
-lelf -lz

编译好后,记得要加sudo,没有root权限是没有办法安装模块的

接下来可以看到,成功拦截了所有open事件,但是没做任何过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
twogoat@twogoat-virtual-machine:~/Desktop/ebpf/x86_64/learn_1$ sudo ./trace_open 
正在拦截 openat 系统调用... 按 Ctrl+C 退出
PID: 6832 | open file: /dev/shm/.org.chromium.Chromium.teIHRN
PID: 6880 | open file: /proc/13168/cmdline
PID: 6880 | open file: /proc/13168/cmdline
PID: 751 | open file: /proc/meminfo
PID: 6832 | open file: /dev/shm/.org.chromium.Chromium.rVOC6M
PID: 6880 | open file: /proc/13168/cmdline
PID: 6832 | open file: /dev/shm/.org.chromium.Chromium.e2GvaM
PID: 6832 | open file: /dev/shm/.org.chromium.Chromium.z1QDPO
PID: 751 | open file: /proc/meminfo
PID: 6880 | open file: /proc/13168/cmdline
PID: 892 | open file: /proc/interrupts
PID: 892 | open file: /proc/stat
PID: 892 | open file: /proc/irq/16/smp_affinity
PID: 892 | open file: /proc/irq/16/smp_affinity
PID: 892 | open file: /proc/irq/1/smp_affinity
PID: 892 | open file: /proc/irq/1/smp_affinity
PID: 892 | open file: /proc/irq/19/smp_affinity
PID: 892 | open file: /proc/irq/19/smp_affinity

多个tracepoint hook

在ebpf模块中,一般用的都是同一个ringbuf管理器去通信,那么问题就来了,那么多系统调用,要对应那么多结构,只通过一个event结构,如何去区分呢,下面就以同时hook read和write为例

在event中,header的第一项为事件类型的enum值,这个很好理解,然后就是pid和comm,也是每一个syscall的共同。

后面问题就是每一个事件的结构体不同,比如open需要读出指针指向字符串文件名,而对于read调用,参数通常是fd,读地址和size

通过定义对应的结构,然后再通过union联合结构体即可实现

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
#ifndef __COMMON_H
#define __COMMON_H

#define MAX_FILENAME_LEN 256

enum event_type {
EVENT_OPEN,
EVENT_READ,
};

struct trace_header {
enum event_type type;
int pid;
char comm[16];
};

struct open_data {
char filename[MAX_FILENAME_LEN];
int flags;
};

struct read_data {
int fd;
unsigned long buf_ptr;
size_t count;
};

struct event {
struct trace_header header; // 这里同步修改
union {
struct open_data open;
struct read_data read;
};
};

#endif

在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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
#include "common.h"

char LICENSE[] SEC("license") = "GPL";

// 定义 Ring Buffer 地图
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024); // 256 KB
} rb SEC(".maps");

SEC("tracepoint/syscalls/sys_enter_openat")
int handle_openat(struct trace_event_raw_sys_enter *ctx) {
struct event *e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
if (!e) return 0;

// 填充公共部分
e->header.type = EVENT_OPEN;
e->header.pid = bpf_get_current_pid_tgid() >> 32;
bpf_get_current_comm(&e->header.comm, 16);

// 填充 Open 特有部分
bpf_probe_read_user_str(&e->open.filename, sizeof(e->open.filename), (char *)ctx->args[1]);
e->open.flags = (int)ctx->args[2];

bpf_ringbuf_submit(e, 0);
return 0;
}

SEC("tracepoint/syscalls/sys_enter_read")
int handle_read(struct trace_event_raw_sys_enter *ctx) {
struct event *e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
if (!e) return 0;

e->header.type = EVENT_READ;
e->header.pid = bpf_get_current_pid_tgid() >> 32;
bpf_get_current_comm(&e->header.comm, 16);

// 填充 Read 特有部分
e->read.fd = (int)ctx->args[0];
e->read.buf_ptr = (unsigned long)ctx->args[1];
e->read.count = (size_t)ctx->args[2];

bpf_ringbuf_submit(e, 0);
return 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
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <bpf/libbpf.h>
#include "trace_open.skel.h" // 假设使用 bpftool 生成的 skeleton
#include "common.h"

// Ringbuf 回调函数
static int handle_event(void *ctx, void *data, size_t data_sz) {
const struct event *e = data;

// 所有的公共信息通过 e->header 获取
printf("[%d] %-16s | ", e->header.pid, e->header.comm);

// 根据类型分发给不同的处理逻辑
switch (e->header.type) {
case EVENT_OPEN:
printf("OPEN: %s (flags: %d)\n", e->open.filename, e->open.flags);
break;
case EVENT_READ:
printf("READ: fd %d, addr 0x%lx, len %zu\n",
e->read.fd, e->read.buf_ptr, e->read.count);
break;
default:
printf("Unknown event\n");
}
return 0;
}

int main() {
struct trace_open_bpf *skel;
struct ring_buffer *rb = NULL;
int err;

// 1. 加载并附着 eBPF 程序
skel = trace_open_bpf__open_and_load();
if (!skel) return 1;

err = trace_open_bpf__attach(skel);
if (err) goto cleanup;

// 2. 设置 Ring Buffer 管理器
rb = ring_buffer__new(bpf_map__fd(skel->maps.rb), handle_event, NULL, NULL);
if (!rb) goto cleanup;

printf("正在拦截 openat 系统调用... 按 Ctrl+C 退出\n");

// 3. 循环轮询数据
while (1) {
err = ring_buffer__poll(rb, 100 /* 毫秒超时 */);
if (err == -EINTR) continue;
if (err < 0) break;
}

cleanup:
ring_buffer__free(rb);
trace_open_bpf__destroy(skel);
return 0;
}

uprobe编写

在此之前,我曾经写过一个极其吐血的调试器项目,其最大的亮点,就是在进程的任意偏移,添加hook脚本

比如说一个rc4加密,我想要直接拿到xor_stream,在0x158e偏移处,分别读取al和byte [rbp - 0x115]

image-20260511101539906

其实大概就是可以通过特定的偏移,去hook截取当前运行时的寄存器和内存等,可以快速生成VM等复杂状态的执行流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from tgdbg import*


def hook_xor_stream(dbg: Debugger):
al = dbg.reg_read(REG_AL)
rbp = dbg.reg_read(REG_RBP)
xor_val = dbg.mem_read_byte(rbp - 0x115)
result = al ^ xor_val
print(f'{al:02x} ^ {xor_val:02x} = {result:02x}')

if __name__ == "__main__":
dbg = Debugger("rc4", "")
dbg.hook_add(dbg.base_addr + 0x158E, hook_xor_stream)
dbg.start_debugger()
dbg.close_debugger()

可以看到,成功拦截到了中间的寄存器和内存

image-20260511102009174

那么问题来了,为什么不用frida去hook,答案是我当然用过,但问题是inlinehook用于函数头还好,但如果是函数中间的偏移,将会极其不稳定,基本上就是hook一次后程序就崩了,大概是是寄存器状态没处理好,最好通过int3异常处理的方式。

那么问题来了,有没有我自己是手搓了,后来发现其实ebpf的uprobe也能完成类似的工作

首先仍然需要定义ringbuff的通信结构

common.h

1
2
3
4
5
6
7
8
9
#ifndef __COMMON_H
#define __COMMON_H

struct rc4_event {
unsigned char al;
unsigned char xor_val;
};

#endif

然后写以一个uprobe的挂载点,这里传入的参数即为regs,可以直接读取,再配合bpf_probe_read_user读取用户态内存,就可以完成上面的工作了

trace.bpf.c

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
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include "common.h"

char LICENSE[] SEC("license") = "GPL";

struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} rb SEC(".maps");

// 使用通用的 SEC 标签
SEC("uprobe")
int handle_xor_stream(struct pt_regs *ctx) {
struct rc4_event *e;

e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
if (!e) return 0;

// 1. 直接读取寄存器
e->al = ctx->ax & 0xFF; // 获取 AL (RAX 的低 8 位)

// 2. 计算并读取用户态内存 [RBP - 0x115]
unsigned long long rbp = ctx->bp;
unsigned long long target_addr = rbp - 0x115;

// 必须使用 probe_read_user,因为这是用户空间地址
bpf_probe_read_user(&e->xor_val, 1, (void *)target_addr);

bpf_ringbuf_submit(e, 0);
return 0;
}

在用户态的安装程序中,通过bpf_program__attach_uprobe函数,进行断点的安装

main.c

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
#include <stdio.h>
#include <stdlib.h>
#include <bpf/libbpf.h>
#include "trace.skel.h"
#include "common.h"

// 回调函数:处理内核传回的数据
static int handle_event(void *ctx, void *data, size_t data_sz) {
const struct rc4_event *e = data;
printf("[RC4 Trace] %02x ^ %02x = %02x\n", e->al, e->xor_val, e->al ^ e->xor_val);
return 0;
}

int main(int argc, char **argv) {
struct trace_bpf *skel;
struct ring_buffer *rb = NULL;
struct bpf_link *link = NULL;

if (argc < 2) {
fprintf(stderr, "用法: %s <二进制路径>\n", argv[0]);
return 1;
}

// 1. 打开并加载 eBPF 程序
skel = trace_bpf__open_and_load();
if (!skel) {
fprintf(stderr, "加载 eBPF 失败\n");
return 1;
}

// 2. 动态安装 Uprobe (核心步骤)
// 对应你 Python 里的 dbg.hook_add(dbg.base_addr + 0x158E, ...)
link = bpf_program__attach_uprobe(
skel->progs.handle_xor_stream, // BPF 程序
false, // false 为 uprobe, true 为 uretprobe
-1, // PID (-1 表示监控所有进程)
argv[1], // 二进制文件路径 (如 "./rc4")
0x158E // 文件偏移量
);

if (!link) {
fprintf(stderr, "安装 Uprobe 失败,请检查路径或偏移量\n");
goto cleanup;
}

printf("成功在 %s:0x158E 安装钩子...\n", argv[1]);

// 3. 设置 Ring Buffer 轮询
rb = ring_buffer__new(bpf_map__fd(skel->maps.rb), handle_event, NULL, NULL);
if (!rb) goto cleanup;

while (1) {
if (ring_buffer__poll(rb, 100) < 0) break;
}

cleanup:
ring_buffer__free(rb);
bpf_link__destroy(link);
trace_bpf__destroy(skel);
return 0;
}

运行结果

666f09fc-c877-4912-bcd2-14395a0510db

可以看到,虽然说功能上是实现了,但仍有那么一点欣慰的是,我脚本的运行方法和接口更加方便吧…

  • 标题: ebpf学习-ebpf简单模块编写
  • 作者: 两只羊
  • 创建于 : 2026-05-09 17:36:29
  • 更新于 : 2026-08-03 11:14:03
  • 链接: https://twogoat.github.io/2026/05/09/ebpf学习-ebpf简单模块编写/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论