使用Go语言编写了一个基于eBPF的Linux僵尸内核探测工具,这个探测工具可以实现根据恶意域名溯源到主机上发起恶意请求的用户和线程等信息。
eBPF是一种可以在 Linux 内核中运行用户编写的程序,不需要修改内核代码的技术,eBPF的流程主要为:编译 eBPF 源代码,eBPF 用户态程序加载eBPF字节码加载到内核,内核验证并运行 eBPF 程序,监控主机上运行的进程和触发eBPF程序执行。
通过将用于监控的BPF代码attach到BPF的hook点,可以实现探测到主机的DNS、TCP,UDP和线程等的行为
eBPF介绍
eBPF(extended Berkeley Packet Filter)是一种可以在 Linux 内核中运行用户编写的程序,而不需要修改内核代码或加载内核模块的技术,简单说,eBPF 让 Linux 内核变得可编程化了。
eBPF 是一个用 RISC 指令集设计的 VM,他可以通过运行 BPF 程序来跟踪内核函数、内存等。
eBPF 程序直接在 Linux 内核中运行。这使他们可以轻松地跟踪操作系统子系统的几乎任何方面,包括 CPU 调度程序、网络、系统调用等。您可以通过 eBPF 程序查看和跟踪操作系统中发生的几乎所有事情。这使其成为在基于 Linux 的部署中设置全局和根深蒂固的监控的可行竞争者。
使用BPF系统调用BPF程序流程图:
工作流程:
编译 eBPF 源代码(把C代码编译成eBPF字节码,本项目通过 go-bindata 库将 bpf 字节码文件内嵌到go文件中)
eBPF 用户态程序加载eBPF字节码加载到内核(决定哪些内核区域(代码、内存)可以被这个程序访问)
内核验证并运行 eBPF 程序(代码在 kernel/bpf/verifier.c)
监控主机上运行的进程(通过 kprobe、tracepoint 来把这些代码插入到对应的位置)
触发eBPF程序执行(触发程序后 bpf 程序代码会把数据写到他们自己的 ringbuffers 或者 key-value maps ,用户态读取这些 ringbuffers 或者 maps 来获取想要的数据。)
基于eBPF的Linux僵尸主机行为监测工具
具备DNS、TCP,UDP,Thread的Linux内核级僵尸主机监控工具
项目结果:
输出:请求DNS解析的域名、解析到的IP、用户ID、进程ID、执行的命令等
eBPF DNS监控原理
1. 在eBPF中创建三个map,分别为start,currres和events
struct addrinfo
{
int ai_flags; /* Input flags. */
int ai_family; /* Protocol family for socket. */
int ai_socktype; /* Socket type. */
int ai_protocol; /* Protocol for socket. */
u32 ai_addrlen; /* Length of socket address. */ // CHANGED from socklen_t
struct sockaddr *ai_addr; /* Socket address for socket. */
char *ai_canonname; /* Canonical name for service location. */
struct addrinfo *ai_next; /* Pointer to next in list. */
};
struct val_t {
u32 pid;
char host[80];
} __attribute__((packed));
struct data_t {
u32 pid;
u32 uid;
u32 af;
u32 ip4addr;
__int128 ip6addr;
char host[80];
} __attribute__((packed));
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, u32);
__type(value, struct val_t);
__uint(max_entries, 1024);
} start SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, u32);
__type(value, struct addrinfo **);
__uint(max_entries, 1024);
} currres SEC(".maps");
struct
{
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
} events SEC(".maps");
2. hook uprobe/getaddrinfo
SEC("uprobe/getaddrinfo")
int getaddrinfo_entry(struct pt_regs *ctx) {
if (!(ctx)->di)
return 0;
struct val_t val = {};
bpf_probe_read(&val.host, sizeof(val.host), (void *)PT_REGS_PARM1(ctx));
u64 pid_tgid = bpf_get_current_pid_tgid();
u32 pid = pid_tgid >> 32;
val.pid = pid;
struct addrinfo **res = (struct addrinfo **)(ctx)->cx;
// 更新这两个map
bpf_map_update_elem(&start, &pid, &val, BPF_ANY);
bpf_map_update_elem(&currres, &pid, &res, BPF_ANY);
return 0;
}
3. hook uretprobe/getaddrinfo
SEC("uretprobe/getaddrinfo")
int getaddrinfo_return(struct pt_regs *ctx) {
struct val_t *valp;
u64 pid_tgid = bpf_get_current_pid_tgid();
u32 pid = pid_tgid >> 32;
valp = bpf_map_lookup_elem(&start, &pid);
if (valp == 0) {
return 0; // missed start
}
struct addrinfo ***res;
res = bpf_map_lookup_elem(&currres, &pid);
if (!res || !(*res)) {
return 0; // missed entry
}
u32 uid = bpf_get_current_uid_gid();
struct addrinfo **resx;
bpf_probe_read(&resx, sizeof(resx), (struct addrinfo **)res);
struct addrinfo *resxx;
bpf_probe_read(&resxx, sizeof(resxx), (struct addrinfo **)resx);
for (int i = 0; i < 9; i++) // Limit max entries that are considered
{
struct data_t data = {};
bpf_probe_read(&data.host, sizeof(data.host), (void *)valp->host);
bpf_probe_read(&data.af, sizeof(data.af), &resxx->ai_family);
if (data.af == AF_INET) {
struct sockaddr_in *daddr;
bpf_probe_read(&daddr, sizeof(daddr), &resxx->ai_addr);
bpf_probe_read(&data.ip4addr, sizeof(data.ip4addr), &daddr->sin_addr.s_addr);
} else if (data.af == AF_INET6) {
struct sockaddr_in6 *daddr6;
bpf_probe_read(&daddr6, sizeof(daddr6), &resxx->ai_addr);
bpf_probe_read(&data.ip6addr, sizeof(data.ip6addr), &daddr6->sin6_addr.in6_u.u6_addr32);
}
data.pid = valp->pid;
data.uid = uid;
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &data, sizeof(data));
break;
}
bpf_map_delete_elem(&start, &pid);
bpf_map_delete_elem(&currres, &pid);
return 0;
}
评论区