【A9】初探eBPF

文摘   科技   2023-06-19 10:50   广东  

“A9 Team 甲方攻防团队,成员来自某证券、微步、青藤、长亭、安全狗等公司。成员能力涉及安全运营、威胁情报、攻防对抗、渗透测试、数据安全、安全产品开发等领域,持续分享安全运营和攻防的思考和实践。”


01

简介



eBPF(extended Berkeley Packet Filter)是一种在内核中运行用户定义程序的技术。它用于安全高效地扩展内核的功能,而无需更改内核源代码或加载内核模块。它起源于`BPF`(Berkeley Packet Filter),一个为网络数据包捕获提供高性能的过滤机制。`eBPF`通过扩展`BPF`的功能,使其能够在内核中执行更多类型的程序,提供了更强大的性能监控和安全功能。



02

eBPF程序工作原理

我们编写好的`eBPF`程序在执行的时候首先会在用户态通过`clang`/`LLVM`编译器编译成字节码,然后加载到内核空间,进入内核态后会经过验证器进行一系列的验证(进程权限,安全性),接着经过`JIT`编译器编译成机器码指令集加载到指定事件的触发勾子(系统调用,网络事件)上等待触发。


用户空间和内核空间的映射通过哈希表(键值对)共享数据并保持状态。它们是通过带有`BPF_MAP_CREATE`参数的`bpf_cmd`系统调用来创建的,和Linux世界中的其他东西一样,它们是通过文件描述符来寻址。

我们编写好的`eBPF`程序在执行的时候首先会在用户态通过`clang`/`LLVM`编译器编译成字节码,然后加载到内核空间,进入内核态后会经过验证器进行一系列的验证(进程权限,安全性),接着经过`JIT`编译器编译成机器码指令集加载到指定事件的触发勾子(系统调用,网络事件)上等待触发。


用户空间和内核空间的映射通过哈希表(键值对)共享数据并保持状态。它们是通过带有`BPF_MAP_CREATE`参数的`bpf_cmd`系统调用来创建的,和Linux世界中的其他东西一样,它们是通过文件描述符来寻址。



03

如何编写eBPF程序?
在多数情况下不会直接使用`eBPF`,而是通过像`Cilium`、`bcc`或`bpftrace`这样的项目间接使用它们,这些项目在`eBPF`之上提供了抽象层,并且不需要直接编写程序,而是提供了指定基于意图的定义的能力,然后开发者可以根据需求自己实现这些定义。如果不存在更高级别的抽象,则需要直接编写程序。Linux内核期望以字节码形式加载`eBPF`程序。虽然当然可以直接编写字节码,但更常见的开发实践是利用编译器套件如`LLVM`将伪`C`代码编译为`eBPF`字节码。


举个栗子,用python的`bcc`库实现一个文件生命周期监控的脚本:



from __future__ import print_functionfrom bcc import BPFimport argparsefrom time import strftime

# 参数相关examples = """examples: ./filelife # trace lifecycle of file(create->remove) ./filelife -p 181 # only trace PID 181"""parser = argparse.ArgumentParser( description="Trace lifecycle of file", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=examples)parser.add_argument("-p", "--pid", help="trace this PID only")parser.add_argument("--ebpf", action="store_true", help=argparse.SUPPRESS)args = parser.parse_args()debug = 0

# 定义BPF程序bpf_text = """#include <uapi/linux/ptrace.h>#include <linux/fs.h>#include <linux/sched.h>

struct data_t { u32 pid; u64 delta; char comm[TASK_COMM_LEN]; char fname[DNAME_INLINE_LEN];};

BPF_HASH(birth, struct dentry *);BPF_PERF_OUTPUT(events);

static int probe_dentry(struct pt_regs *ctx, struct dentry *dentry){ u32 pid = bpf_get_current_pid_tgid() >> 32; FILTER

u64 ts = bpf_ktime_get_ns(); birth.update(&dentry, &ts);

return 0;}

// trace file creation timeTRACE_CREATE_FUNC{ return probe_dentry(ctx, dentry);};

// trace file security_inode_create timeint trace_security_inode_create(struct pt_regs *ctx, struct inode *dir, struct dentry *dentry){ return probe_dentry(ctx, dentry);};

// trace file open timeint trace_open(struct pt_regs *ctx, struct path *path, struct file *file){ struct dentry *dentry = path->dentry;

if (!(file->f_mode & FMODE_CREATED)) { return 0; }

return probe_dentry(ctx, dentry);};

// trace file deletion and output detailsTRACE_UNLINK_FUNC{ struct data_t data = {}; u32 pid = bpf_get_current_pid_tgid() >> 32;

FILTER

u64 *tsp, delta; tsp = birth.lookup(&dentry); if (tsp == 0) { return 0; // missed create }

delta = (bpf_ktime_get_ns() - *tsp) / 1000000; birth.delete(&dentry);

struct qstr d_name = dentry->d_name; if (d_name.len == 0) return 0;

if (bpf_get_current_comm(&data.comm, sizeof(data.comm)) == 0) { data.pid = pid; data.delta = delta; bpf_probe_read_kernel(&data.fname, sizeof(data.fname), d_name.name); }

events.perf_submit(ctx, &data, sizeof(data));

return 0;}"""

trace_create_text_old="""int trace_create(struct pt_regs *ctx, struct inode *dir, struct dentry *dentry)"""trace_create_text_new="""int trace_create(struct pt_regs *ctx, struct user_namespace *mnt_userns, struct inode *dir, struct dentry *dentry)"""

trace_unlink_text_old="""int trace_unlink(struct pt_regs *ctx, struct inode *dir, struct dentry *dentry)"""trace_unlink_text_new="""int trace_unlink(struct pt_regs *ctx, struct user_namespace *mnt_userns, struct inode *dir, struct dentry *dentry)"""

if args.pid: bpf_text = bpf_text.replace('FILTER', 'if (pid != %s) { return 0; }' % args.pid)else: bpf_text = bpf_text.replace('FILTER', '')if debug or args.ebpf: print(bpf_text) if args.ebpf: exit()

if BPF.kernel_struct_has_field(b'renamedata', b'old_mnt_userns') == 1: bpf_text = bpf_text.replace('TRACE_CREATE_FUNC', trace_create_text_new) bpf_text = bpf_text.replace('TRACE_UNLINK_FUNC', trace_unlink_text_new)else: bpf_text = bpf_text.replace('TRACE_CREATE_FUNC', trace_create_text_old) bpf_text = bpf_text.replace('TRACE_UNLINK_FUNC', trace_unlink_text_old)

# 实例化BPF对象b = BPF(text=bpf_text)b.attach_kprobe(event="vfs_create", fn_name="trace_create")# 版本比较新的内核版本不会掉用 fire vfs_create,二是调用 vfs_open instead:b.attach_kprobe(event="vfs_open", fn_name="trace_open")

if BPF.get_kprobe_functions(b"security_inode_create"): b.attach_kprobe(event="security_inode_create", fn_name="trace_security_inode_create")b.attach_kprobe(event="vfs_unlink", fn_name="trace_unlink")

# 打印结果头信息print("%-8s %-7s %-16s %-7s %s" % ("TIME", "PID", "COMM", "AGE(s)", "FILE"))

# 进程事件def print_event(data): event = b["events"].event(data) print("%-8s %-7d %-16s %-7.2f %s" % (strftime("%H:%M:%S"), event.pid, event.comm.decode('utf-8', 'replace'), float(event.delta) / 1000, event.fname.decode('utf-8', 'replace')))



if __name__ == "__main__": b["events"].open_perf_buffer(print_event) while True: try: b.perf_buffer_poll() except KeyboardInterrupt: exit()
输出结果:


脚本捕获了在`Linux`内核构建期间创建的短期文件。`PID`表示最后删除文件的进程ID,`COMM`是它的进程名。`AGE(s)`列显示文件存活的时间,以秒为单位。


上面示例代码是`eBPF`的一个编程的基本方法,它是在Python里向内核的某些事件挂载一段 “C语言” 的方式就是`eBPF`的编程方式。这样的代码非常难写,但是好在官方工具库里提供了丰富的样例,拿来改吧改吧或者直接使用都是没问题的。


04

写在最后
BCC(BPF Compiler Collection)是一套开源的工具集,我们可以根据业务需求的场景实现各种各样系统级的性能分析和监控。它为我们窥探高深的Linux内核提供了一个方便之门。


[参考]


1. https://www.infoq.com/articles/gentle-linux-ebpf-introduction/

2. https://ebpf.io/what-is-ebpf/

3. https://coolshell.cn/articles/22320.html









A9 Team
A9 Team 甲方攻防团队,成员来自某证券、微步、青藤、长亭、安全狗等公司。成员能力涉及安全运营、威胁情报、攻防对抗、渗透测试、数据安全、安全产品开发等领域,持续分享安全运营和攻防的思考和实践,期望和朋友们共同进步,守望相助,合作共赢。
 最新文章