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
| cd ..
git clone https://github.com/libbpf/libbpf
cd libbpf/src
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
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";
struct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 256 * 1024); } rb SEC(".maps");
SEC("tracepoint/syscalls/sys_enter_openat") int handle_openat(struct trace_event_raw_sys_enter *ctx) { struct event *e;
e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0); if (!e) { return 0; }
e->pid = bpf_get_current_pid_tgid() >> 32; bpf_get_current_comm(&e->comm, 16);
bpf_probe_read_user_str(&e->filename, sizeof(e->filename), (char *)ctx->args[1]);
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" #include "common.h"
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;
skel = trace_open_bpf__open_and_load(); if (!skel) return 1;
err = trace_open_bpf__attach(skel); if (err) goto cleanup;
rb = ring_buffer__new(bpf_map__fd(skel->maps.rb), handle_event, NULL, NULL); if (!rb) goto cleanup;
printf("正在拦截 openat 系统调用... 按 Ctrl+C 退出\n");
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
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";
struct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 256 * 1024); } 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);
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);
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" #include "common.h"
static int handle_event(void *ctx, void *data, size_t data_sz) { const struct event *e = data; 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;
skel = trace_open_bpf__open_and_load(); if (!skel) return 1;
err = trace_open_bpf__attach(skel); if (err) goto cleanup;
rb = ring_buffer__new(bpf_map__fd(skel->maps.rb), handle_event, NULL, NULL); if (!rb) goto cleanup;
printf("正在拦截 openat 系统调用... 按 Ctrl+C 退出\n");
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]

其实大概就是可以通过特定的偏移,去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()
|
可以看到,成功拦截到了中间的寄存器和内存

那么问题来了,为什么不用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("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;
e->al = ctx->ax & 0xFF; unsigned long long rbp = ctx->bp; unsigned long long target_addr = rbp - 0x115; 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; }
skel = trace_bpf__open_and_load(); if (!skel) { fprintf(stderr, "加载 eBPF 失败\n"); return 1; }
link = bpf_program__attach_uprobe( skel->progs.handle_xor_stream, false, -1, argv[1], 0x158E );
if (!link) { fprintf(stderr, "安装 Uprobe 失败,请检查路径或偏移量\n"); goto cleanup; }
printf("成功在 %s:0x158E 安装钩子...\n", argv[1]);
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; }
|
运行结果

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