WEBKT

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

196 0 0 0

如何用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网络安全网络监控

评论点评