WEBKT

如何用eBPF追踪特定用户/进程的网络活动?网络安全分析师实战指南

48 0 0 0

如何用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_querykretprobe: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_filebpf_object__load函数加载编译好的eBPF程序。
  • 获取BPF映射的FD:使用bpf_object__find_map_fd_by_name函数获取dns_eventsconfigs这两个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_connecttcp_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程序的安全性,防止恶意代码破坏系统。

最后,感谢大家的阅读!希望这篇文章对你有所帮助。如果你有任何问题或建议,欢迎在评论区留言。

安全小黑屋 eBPF网络安全网络监控

评论点评

打赏赞助
sponsor

感谢您的支持让我们更好的前行

分享

QRcode

https://www.webkt.com/article/9446