Featured image of post eBPF例程——监听与拦截文件的读写操作

eBPF例程——监听与拦截文件的读写操作

基于BCC开发eBPF程序,实现对文件读写操作的监听,同时根据规则拦截文件读写操作。

查询函数签名

在eBPF编程中需要查询Linux内核函数的参数,以便在插入kprobe或kretprobe时能够正确获取和解析这些参数。一种查询方式是使用Bootlin Elixir Cross
Referencer直接查询Linux源码,例如可以获取do_sys_openat2()函数的函数参数列表如下。

其次要确认do_sys_openat2()函数能否支持被插入kprobe、进而被eBPF追踪。kprobe是通过函数符号名定位地址,在函数入口插入探针指令。/proc/kallsyms正是内核导出的符号表,能查到do_sys_openat2()函数且符号类型是 T(全局函数),说明内核里存在该函数、且编译时没有被优化掉(不是 inline),kprobe理论上可以插入进去。

1
cat /proc/kallsyms | grep do_sys_openat2

随后使用工具确认do_sys_openat2()函数能被插入kprobe。

1
sudo bpftrace -l 'kprobe:do_sys_openat2'

监听文件读写

基于kprobe

eBPF程序:trace.c

定义结构体data_t,其作用就是用来组织和保存数据,方便eBPF程序将采集到的信息统一传递给用户态程序,其中包括:

  1. pid:指示触发事件的进程ID;
  2. ts:记录事件发生的时间戳,单位为纳秒;
  3. comm:指示触发事件的进程名,TASK_COMM_LEN是Linux内核定义的一个宏,表示进程名的最大长度;
  4. fname:打开的文件名称,NAME_MAX表示文件名最大长度的宏,定义了文件名允许的最大字符数。

BPF_PERF_OUTPUT(events)是BCC框架提供的一个宏,用于在内核态和用户态之间创建一个高效的数据传输通道,底层基于Linux内核的Perf Event 机制(Linux内核的性能事件框架)。该宏定义了一个名为events的Perf Buffer对象(内核和用户空间共享的环形缓冲区),eBPF程序通过调用events.perf_submit()将采集到的事件数据写入到该缓冲区,用户态程序进而可以通过接口接收这些数据。

trace_do_sys_openat2()函数是eBPF程序中的回调函数,使用kprobe挂附到内核函数do_sys_openat2()上做追踪。其中的第一个参数ctx是kprobe回调的固定参数,用来保存do_sys_openat2()调用时CPU的寄存器上下文,后面三个参数与内核里被钩住的do_sys_openat2()函数的参数保持一一对应,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
#include <uapi/linux/openat2.h>
#include <linux/sched.h>

// 定义事件数据结构
struct data_t {
    u32 pid;
    u64 ts;
    char comm[TASK_COMM_LEN];
    char fname[NAME_MAX];
};

// 定义事件事件传输通道
BPF_PERF_OUTPUT(events);

// kprobe钩子函数,跟踪do_sys_openat2函数的系统调用
int trace_do_sys_openat2(struct pt_regs *ctx, int dfd, const char __user *filename, struct open_how *how)
{
    // 实例化事件数据结构
    struct data_t data = {};
    
    // bpf_get_current_pid_tgid()函数是eBPF程序中的Helper函数,用于获取当前进程的PID和TGID
    // 函数返回一个64位的整数,其中高32位是线程组ID (TGID)、低32位是进程ID (PID)
    // 因此需要右移32位获得进程ID
    data.pid = bpf_get_current_pid_tgid() >> 32;
    // 获取当前内核时间戳,用来记录事件发生的时间,单位为纳秒
    data.ts = bpf_ktime_get_ns();
    // 获取当前正在运行的进程名字,并写到data.comm里
    bpf_get_current_comm(&data.comm, sizeof(data.comm));
    // 拷贝文件名到data.fname里,该函数能确保字符串以\0结尾,更加安全
    bpf_probe_read_user_str(&data.fname, sizeof(data.fname), filename);

    // 提交事件数据到用户态
    events.perf_submit(ctx, &data, sizeof(data));

    return 0;
}

用户态程序:trace.py

与eBPF程序中BPF_PERF_OUTPUT(events)相对应的用户态程序中的open_perf_buffer()函数,它定义了事件传输通道,该函数需要传入一个回调函数,用于处理接收到的事件数据。进而可以通过perf_buffer_poll()函数从缓冲区轮询接收事件数据。

BCC框架要求回调函数必须是固定的参数签名:cpu表示触发这个事件的CPU编号;data为从内核空间接收到的事件数据;size为该数据的长度,单位为字节数。

 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
#!/usr/bin/env python3
from bcc import BPF
from time import strftime

# 加载BPF程序
b = BPF(src_file="trace.c", cflags=["-Wno-duplicate-decl-specifier"])

# 将BPF程序使用kprobe挂载到该内核函数do_sys_openat2()
# 回调函数为trace_do_sys_openat2()
b.attach_kprobe(event="do_sys_openat2", fn_name="trace_do_sys_openat2")

# 定义回调函数,将接收到的事件进行打印
def print_event(cpu, data, size):
    # 根据struct data_t结构体自动解析接收到的数据
    event = b["events"].event(data)
    print("%s PID=%d COMM=%s FILENAME=%s" % (
        strftime("%H:%M:%S"),
        event.pid,
        event.comm.decode('utf-8', 'replace'),
        event.fname.decode('utf-8', 'replace')
    ))

# 定义事件传输通道
# 由于文件读写量大,需要增加缓冲区大小(page_cnt=32表示32*4KB=128KB每CPU)
b["events"].open_perf_buffer(print_event, page_cnt=32)

# 循环监听事件数据
while True:
    try:
        b.perf_buffer_poll()
    except KeyboardInterrupt:
        exit()

运行eBPF程序

1
sudo python3 trace.py

基于tracepoint

eBPF程序:trace.c

tracepoint是内核里写好的一段静态埋点,syscalls:sys_enter_openat由2部分组成,其中的syscalls为category、sys_enter_openat为name。BCC根据内核tracepoint的category和name,自动拼接成一个C结构体tracepoint__syscalls__sys_enter_openat,eBPF程序中的tracepoint回调函数必须接收这个结构体的指针。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <linux/sched.h>
#include <linux/limits.h>

struct data_t {
    u32 pid;
    char comm[TASK_COMM_LEN];
    char fname[NAME_MAX];
};

BPF_PERF_OUTPUT(events);

int trace_sys_enter_openat(struct tracepoint__syscalls__sys_enter_openat *ctx) {
    struct data_t data = {};
    data.pid = bpf_get_current_pid_tgid() >> 32;
    bpf_get_current_comm(&data.comm, sizeof(data.comm));
    bpf_probe_read_user_str(&data.fname, sizeof(data.fname), (void *)ctx->filename);
    
    events.perf_submit(ctx, &data, sizeof(data));
    return 0;
}

用户态程序:trace.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from bcc import BPF
from time import strftime

def print_event(cpu, data, size):
    event = b["events"].event(data)
    print("%s PID=%d COMM=%s opened file %s" % (
        strftime("%H:%M:%S"),
        event.pid,
        event.comm.decode('utf-8', 'replace'),
        event.fname.decode('utf-8', 'replace')
    ))

b = BPF(src_file="block.c", cflags=["-Wno-duplicate-decl-specifier"])
b.attach_tracepoint(tp="syscalls:sys_enter_openat", fn_name="trace_sys_enter_openat")
b["events"].open_perf_buffer(print_event)

print("Tracing sys_enter_openat tracepoint... Press Ctrl+C to exit.")
while True:
    try:
        b.perf_buffer_poll()
    except KeyboardInterrupt:
        exit()

拦截文件读写

eBPF程序:block.c

bpf_send_signal(SIGKILL)是在eBPF程序里调用的Helpe 函数,用来向当前正在执行的进程发送一个指定的信号(此处是SIGKILL,表示强制终止进程)。函数本质上只是向当前进程设置了一个待处理信号,真正的信号处理要等调度器或内核后续执行到合适点时才发生,因此拦截操作是异步的。

 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
#include <uapi/linux/openat2.h>
#include <linux/sched.h>
#include <linux/signal.h>

BPF_PERF_OUTPUT(events);

struct data_t {
    u32 pid;
    char comm[TASK_COMM_LEN];
    char fname[NAME_MAX];
};

int trace_do_sys_openat2(struct pt_regs *ctx, int dfd, const char __user *filename, struct open_how *how) {
    // 收集进程信息
    struct data_t data = {};
    data.pid = bpf_get_current_pid_tgid() >> 32;
    bpf_get_current_comm(&data.comm, sizeof(data.comm));
    bpf_probe_read_user_str(&data.fname, sizeof(data.fname), filename);

    // 若进程操作的文件名称是block_test.c,则进行拦截
    char target[] = "block_test.c";
    int i;
    // 字符串匹配
    for (i = 0; i < sizeof(target) - 1; i++) {
        if (data.fname[i] != target[i]) {
            // 文件名不匹配,正常放行
            return 0;
        }
    }
    // 文件名长度可能超过匹配字符串的长度,也属于不匹配,正常放行进程
    if (data.fname[i] != '\0') {
        return 0;
    }

    // 发送SIGKILL信号终止进程
    bpf_send_signal(SIGKILL);
    // 事件上报到用户态进程
    events.perf_submit(ctx, &data, sizeof(data));
    return 0;
}

用户态程序:block.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from bcc import BPF
from time import strftime

def print_event(cpu, data, size):
    event = b["events"].event(data)
    print("%s PID=%d COMM=%s tried to open forbidden file %s, killed." % (
        strftime("%H:%M:%S"),
        event.pid,
        event.comm.decode('utf-8', 'replace'),
        event.fname.decode('utf-8', 'replace')
    ))

b = BPF(src_file="block.c", cflags=["-Wno-duplicate-decl-specifier"])
b.attach_kprobe(event="do_sys_openat2", fn_name="trace_do_sys_openat2")
b["events"].open_perf_buffer(print_event)

print("Tracing forbidden file 'block_test.c' opens, will kill offending processes...")

while True:
    try:
        b.perf_buffer_poll()
    except KeyboardInterrupt:
        exit()

运作eBPF程序

新建终端运行eBPF程序,再新建终端操作文件block_test.c,可以观察到文件操作被强行终止。

1
sudo python3 block.c
1
vim block_test.c

eBPF程序:block.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
#include <uapi/linux/openat2.h>
#include <linux/sched.h>
#include <linux/signal.h>
#include <linux/errno.h>   // for -EACCES

BPF_PERF_OUTPUT(events);

struct data_t {
    u32 pid;
    char comm[TASK_COMM_LEN];
    char fname[NAME_MAX];
};

int trace_do_sys_openat2(struct pt_regs *ctx, int dfd, const char __user *filename, struct open_how *how) {
    struct data_t data = {};
    data.pid = bpf_get_current_pid_tgid() >> 32;
    bpf_get_current_comm(&data.comm, sizeof(data.comm));
    bpf_probe_read_user_str(&data.fname, sizeof(data.fname), filename);
    
    const char target[] = "block_test.c";
    int i;

    #pragma unroll
    for (i = 0; i < sizeof(target) - 1; i++) {
        if (data.fname[i] != target[i]) {
            // 文件名不匹配,正常放行
            return 0;
        }
    }

    // 文件名长度大于 target 长度,也算不匹配
    if (data.fname[i] != '\0') {
        return 0;
    }

    // 文件名完全匹配,立即拦截
    bpf_override_return(ctx, -EACCES);   // 同步阻止
    events.perf_submit(ctx, &data, sizeof(data));
    return 0;
}
Licensed under CC BY-NC-SA 4.0
皖ICP备2025083746号-1
公安备案 陕公网安备61019002003315号



使用 Hugo 构建
主题 StackJimmy 设计