如何用eBPF追踪特定用户/进程的网络活动?网络安全分析师实战指南
如何用eBPF追踪特定用户/进程的网络活动?网络安全分析师实战指南
为什么选择eBPF?
准备工作
编写eBPF程序:追踪DNS查询
1. 定义数据结构
2. 编写eBPF程序
3. 编译eBPF程序
4. 编写用户态程序
5. 编译用户态程序
6. 运行程序
扩展:追踪HTTP请求
总结与展望
风险提示
如何用eBPF追踪特定用户/进程的网络活动?网络安全分析师实战指南
各位网络安全分析师们,大家好!今天,咱们来聊聊如何利用eBPF(extended Berkeley Packet Filter)这一强大的内核技术,来追踪特定用户或进程的网络活动。在威胁分析、故障排除等场景下,了解特定用户或进程的网络行为至关重要。传统的网络监控工具可能难以满足这种精细化的需求,而eBPF则为我们提供了更灵活、更高效的解决方案。
为什么选择eBPF?
在深入代码之前,先简单说说为什么选择eBPF。相比传统的抓包工具(如tcpdump、Wireshark)或内核模块,eBPF有以下几个优势:
- 高性能:eBPF程序运行在内核态,直接与内核交互,避免了用户态/内核态的频繁切换,显著降低了性能开销。
- 安全性:eBPF程序在加载到内核之前,会经过严格的验证器(verifier)检查,确保程序的安全性,防止恶意代码破坏系统。
- 灵活性:eBPF允许我们自定义监控逻辑,可以根据实际需求,灵活地选择需要追踪的网络事件和数据。
- 可编程性:可以使用多种编程语言(如C、Go)编写eBPF程序,并通过LLVM等工具编译成字节码,加载到内核执行。
准备工作
在开始编写eBPF程序之前,需要确保系统满足以下条件:
- Linux内核版本:建议使用4.14及以上版本的内核,以获得更好的eBPF支持。
- bpf工具链:安装
bpftool
等工具,用于加载、管理和调试eBPF程序。 - libbpf:安装
libbpf
库,用于在用户态程序中与eBPF程序交互。 - bcc:可以选择安装
bcc
(BPF Compiler Collection),它提供了一些常用的eBPF工具和示例,可以作为学习和开发的参考。
这里以Ubuntu系统为例,演示如何安装这些工具:
sudo apt update sudo apt install -y linux-headers-$(uname -r) clang llvm libelf-dev zlib1g-dev bpfcc-tools libbpf-dev
编写eBPF程序:追踪DNS查询
现在,咱们来编写一个简单的eBPF程序,用于追踪特定用户或进程的DNS查询。这个程序将hook kprobe:dns_v4_initiate_query
和 kretprobe:dns_v4_initiate_query
内核函数,分别在DNS查询发起前和完成后,记录相关信息。
1. 定义数据结构
首先,定义一个数据结构,用于存储需要记录的DNS查询信息:
// dns_tracker.h #ifndef DNS_TRACKER_H #define DNS_TRACKER_H #include <linux/types.h> // 定义最大域名长度 #define MAX_DOMAIN_LENGTH 256 // 定义DNS查询事件结构体 struct dns_event { u32 pid; // 进程ID u32 uid; // 用户ID u64 timestamp; // 时间戳 char domain[MAX_DOMAIN_LENGTH]; // 域名 u8 type; // 查询类型 (A, AAAA, etc.) u16 port; // 源端口 u8 family; // 地址族 (IPv4, IPv6) u8 protocol; // 传输协议 (TCP, UDP) u32 saddr; // IPv4 源地址 u32 daddr; // IPv4 目的地址 unsigned __int128 daddr_v6; // IPv6 目的地址 u8 rcode; // DNS 响应码 bool is_ipv6; // 是否是 IPv6 }; #endif
这个结构体包含了进程ID、用户ID、时间戳、域名等关键信息。为了方便用户态程序读取数据,我们需要将这个结构体定义为BPF可访问的格式。
2. 编写eBPF程序
接下来,编写eBPF程序,实现DNS查询的追踪逻辑:
// dns_tracker.c #include <linux/sched.h> #include <linux/socket.h> #include <linux/in.h> #include <linux/un.h> #include <linux/inet.h> #include <linux/string.h> #include <net/sock.h> #include <net/inet_sock.h> #include <linux/kthread.h> #include <linux/delay.h> #include <linux/version.h> #include <uapi/linux/bpf.h> #include <linux/bpf.h> #include "dns_tracker.h" // 定义BPF映射,用于存储DNS查询事件 BPF_PERF_OUTPUT(dns_events); // 定义BPF映射,用于存储用户配置 BPF_HASH(configs, u32, u32); // kprobe:dns_v4_initiate_query 探针函数 int kprobe__dns_v4_initiate_query(struct pt_regs *ctx, struct socket *sock, struct msghdr *msg, const char *hostname, size_t len, u16 port, struct net *net, u32 timeout, struct workqueue_struct *wq, struct delayed_work *dwork) { // 获取当前进程的PID和UID u32 pid = bpf_get_current_pid_tgid(); u32 uid = bpf_get_current_uid_gid(); // 获取配置 u32 *config = configs.lookup(&uid); if (config && *config == 0) { return 0; // 如果配置为0,则不追踪 } // 创建DNS事件 struct dns_event event = {}; event.pid = pid; event.uid = uid; event.timestamp = bpf_ktime_get_ns(); event.port = port; // 复制域名 bpf_probe_read_str(event.domain, sizeof(event.domain), hostname); // 获取协议和地址族 struct sock *sk = sock->sk; event.family = sk->__sk_common.skc_family; if (event.family == AF_INET) { event.is_ipv6 = false; event.protocol = sk->sk_protocol; event.saddr = inet_sk(sk)->inet_saddr; event.daddr = inet_sk(sk)->inet_daddr; } else if (event.family == AF_INET6) { event.is_ipv6 = true; event.protocol = sk->sk_protocol; // TODO: 获取 IPv6 地址 // bpf_probe_read(&event.daddr_v6, sizeof(event.daddr_v6), &sk->__sk_common.skc_v6_daddr.in6_u); } // 提交事件到 perf buffer dns_events.perf_submit(ctx, &event, sizeof(event)); return 0; } // kretprobe:dns_v4_initiate_query 探针函数 int kretprobe__dns_v4_initiate_query(struct pt_regs *ctx) { // 这里可以获取返回值,例如 DNS 响应码 // int ret = PT_REGS_RC(ctx); return 0; }
这段代码主要做了以下几件事:
- 定义BPF映射:
BPF_PERF_OUTPUT
定义了一个名为dns_events
的perf buffer,用于将DNS查询事件发送到用户态程序。BPF_HASH
定义了一个名为configs
的hash map,用于存储用户配置,决定是否追踪特定用户的DNS查询。 - kprobe探针:
kprobe__dns_v4_initiate_query
函数在dns_v4_initiate_query
函数执行前被调用。它获取当前进程的PID、UID、时间戳、域名等信息,并将这些信息存储到dns_event
结构体中,然后通过dns_events.perf_submit
将事件发送到perf buffer。 - kretprobe探针:
kretprobe__dns_v4_initiate_query
函数在dns_v4_initiate_query
函数执行后被调用。可以在这里获取返回值,例如DNS响应码,但目前的代码中还没有实现。
3. 编译eBPF程序
将上面的代码保存为dns_tracker.c
,然后使用clang编译成eBPF字节码:
clang -target bpf -O2 -Wall -Wno-address-of-packed-member -c dns_tracker.c -o dns_tracker.o
4. 编写用户态程序
接下来,编写一个用户态程序,用于加载eBPF程序、读取perf buffer中的数据,并将其打印到控制台:
// main.c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <signal.h> #include <errno.h> #include <bpf/libbpf.h> #include <bpf/bpf.h> #include "dns_tracker.h" // 定义eBPF程序的路径 #define BPF_OBJECT_PATH "dns_tracker.o" static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args) { return vfprintf(stderr, format, args); } static volatile bool exiting = false; static void sig_handler(int sig) { exiting = true; } int main(int argc, char **argv) { struct bpf_object *obj = NULL; int dns_events_map_fd = -1; int configs_map_fd = -1; int err = 0; /* Set up libbpf errors and debug info callbacks */ libbpf_set_print(libbpf_print_fn); /* Clean handling of Ctrl-C */ signal(SIGINT, sig_handler); signal(SIGTERM, sig_handler); /* Load the BPF object */ obj = bpf_object__open_file(BPF_OBJECT_PATH, NULL); if (!obj) { fprintf(stderr, "ERROR: opening BPF object file failed\n"); return 1; } /* Load and verify BPF program */ err = bpf_object__load(obj); if (err) { fprintf(stderr, "ERROR: loading BPF object file failed: %d\n", err); goto cleanup; } /* Get file descriptor for the maps */ dns_events_map_fd = bpf_object__find_map_fd_by_name(obj, "dns_events"); if (dns_events_map_fd < 0) { fprintf(stderr, "ERROR: finding dns_events map failed\n"); goto cleanup; } configs_map_fd = bpf_object__find_map_fd_by_name(obj, "configs"); if (configs_map_fd < 0) { fprintf(stderr, "ERROR: finding configs map failed\n"); goto cleanup; } // 配置需要追踪的用户UID (例如,追踪UID为1000的用户) u32 uid_to_track = 1000; u32 track = 1; // 1 表示追踪,0 表示不追踪 err = bpf_map_update_elem(configs_map_fd, &uid_to_track, &track, BPF_ANY); if (err) { fprintf(stderr, "ERROR: updating configs map failed: %d\n", err); goto cleanup; } /* Attach BPF programs to tracepoints */ err = bpf_object__attach(obj); if (err) { fprintf(stderr, "ERROR: attaching BPF object file failed: %d\n", err); goto cleanup; } /* Read events from perf buffer */ struct perf_buffer *pb = perf_buffer__new(dns_events_map_fd, 8, NULL, NULL, NULL); if (!pb) { fprintf(stderr, "ERROR: creating perf buffer failed: %d\n", errno); goto cleanup; } struct dns_event event; while (!exiting) { err = perf_buffer__poll(pb, 100); // Poll for 100ms if (err < 0 && err != -EINTR) { fprintf(stderr, "ERROR: polling perf buffer failed: %d\n", err); goto cleanup; } // 读取perf buffer中的数据并打印 while (perf_buffer__read(pb, &event, sizeof(event)) > 0) { printf("PID: %d, UID: %d, Timestamp: %llu, Domain: %s, Port: %d, Family: %s, Protocol: %s, Saddr: %u, Daddr: %u\n", event.pid, event.uid, event.timestamp, event.domain, event.port, event.family == AF_INET ? "IPv4" : "IPv6", event.protocol == IPPROTO_TCP ? "TCP" : "UDP", event.saddr, event.daddr); } } cleanup: perf_buffer__free(pb); bpf_object__close(obj); return err != 0; }
这段代码的主要流程如下:
- 加载eBPF程序:使用
bpf_object__open_file
和bpf_object__load
函数加载编译好的eBPF程序。 - 获取BPF映射的FD:使用
bpf_object__find_map_fd_by_name
函数获取dns_events
和configs
这两个BPF映射的文件描述符(FD)。 - 配置追踪用户:向
configs
映射中写入配置,指定需要追踪的用户UID。在这个例子中,我们追踪UID为1000的用户的DNS查询。 - 挂载eBPF程序:使用
bpf_object__attach
函数将eBPF程序挂载到内核。 - 读取perf buffer:创建一个perf buffer,并循环读取其中的数据。每当有新的DNS查询事件发生时,eBPF程序会将事件发送到perf buffer,用户态程序就可以读取并打印这些事件。
- 清理资源:在程序退出时,释放perf buffer和eBPF对象。
5. 编译用户态程序
将上面的代码保存为main.c
,然后使用gcc编译:
gcc main.c dns_tracker.o -o dns_tracker -lbpf -lelf
6. 运行程序
确保你有足够的权限(例如,使用sudo),然后运行编译好的程序:
sudo ./dns_tracker
现在,你可以尝试使用UID为1000的用户发起一些DNS查询,例如,使用ping
命令:
su - user1000 ping www.example.com
你应该可以在dns_tracker
程序的输出中看到相应的DNS查询事件。
扩展:追踪HTTP请求
除了DNS查询,我们还可以使用类似的方法追踪HTTP请求。这里提供一个思路:
- hook内核函数:可以选择hook
tcp_connect
、tcp_sendmsg
等内核函数,分别在TCP连接建立和数据发送时记录相关信息。 - 解析HTTP头部:在
tcp_sendmsg
探针函数中,可以尝试解析HTTP头部,提取URL、Host等信息。这需要一些字符串处理技巧,可以使用eBPF提供的bpf_probe_read_str
等函数。 - 考虑TLS加密:如果HTTP请求使用了TLS加密,那么直接在内核中解析HTTP头部会比较困难。可以考虑使用USDT(User Statically Defined Tracing)探针,hook用户态的TLS库函数,例如OpenSSL的
SSL_write
函数,来获取解密后的HTTP数据。
总结与展望
eBPF为网络安全分析提供了强大的工具。通过编写eBPF程序,我们可以精细地追踪特定用户或进程的网络活动,从而更好地进行威胁分析、故障排除等工作。当然,eBPF的学习曲线相对陡峭,需要掌握一定的内核知识和编程技巧。希望这篇文章能帮助你入门eBPF,开启你的网络安全分析之旅。
未来,eBPF在网络安全领域的应用前景非常广阔。例如,可以使用eBPF实现:
- DDoS攻击检测与防御:通过监控网络流量,及时发现并阻止DDoS攻击。
- 入侵检测:通过分析网络行为,发现潜在的入侵行为。
- 漏洞利用检测:通过监控系统调用,发现对漏洞的利用。
- 恶意软件分析:通过监控恶意软件的网络行为,分析其传播和攻击方式。
希望更多的安全工程师能够加入到eBPF的开发和应用中,共同构建更安全的网络环境。
风险提示
请注意,使用eBPF进行网络监控可能会涉及到用户隐私问题。在实际应用中,请务必遵守相关法律法规,并获得用户的明确授权。同时,要确保eBPF程序的安全性,防止恶意代码破坏系统。
最后,感谢大家的阅读!希望这篇文章对你有所帮助。如果你有任何问题或建议,欢迎在评论区留言。