前言
本文将通过模仿 boopkit 实现一个类似的程序,来学习 BPF 及 XDP 技术。还不了解 boopkit 的读者请阅读下文:
boopkit是基于eBPF技术实现的Linux后门rootkit,利用任意开启的TCP端口服务,远程唤醒后门,生成反弹shell,具备远程命令执行等能力。
CFC4N,公众号:榫卯江湖新型eBPF后门boopkit的原理分析与演示
声明
本人并不熟悉安全领域,本文也不涉及任何黑客技术。所有实验场景都是假设的 :)
原理
boopkit 通过构造异常的网络包来触发特定的内核函数执行,通过附着在这些内核函数上的 BPF 程序将指令写入 BPF map ,用户态的控制程序读取并执行这些指令。
在我们的实现中,将采用 XDP 来捕获我们构造的特定的网络包,同时,将捕获的包丢弃,网络协议栈将不会感知到这个网络包的存在。
我们将实现这样的两个程序,boop 和 boopd:
客户端 boop 对应于 Boopkit Boop,用于构造并发送特殊的网络包,运行于控制端的机器
服务器 boopd 对应于 Boopkit,用于接收并过滤网络包,运行于被入侵的服务器上
在开始之前,我们先回顾一下计算机网络的基础知识:IP 头部和 TCP 头部
(图片来自维基百科截图)
我们要构造的网络包,仅包含这样两个头部,包含以下特殊标记:
IP 头部 Identification 字段设置为 0xeBaF
TCP 头部的 Source port 字段设置为 0xeBaF
TCP 头部的 Window Size 字段设置为 0xCafe
参见文末代码。
实验
在我们的实验中,有一个客户端(curl)和 HTTP 服务器进行着正常通信:
抓包结果和我们想象的一样,有正常的三次握手和四次挥手:
利用 boop 程序发起嗅探:
boop 程序发出了一个 SYN 包后,便退出了,所以收到对端的 SYN/ACK 之后,内核返回了一个 RST:
假设此时我们悄悄入侵了该服务器并运行了 boopd,再使用 boop 发起 SYN 包:
这个时候,在服务器上抓包,是看不到对应的 SYN 包的,因为我们的 boopd 程序识别出了对应的网络包并将其丢弃了。
而正常的 HTTP 请求仍然可以被正常处理。
在 Boopkit 中,这一步使用了 Tracing 的手段实现并且可以用于触发恶意行为。在这里,出于演示目的,我们只是简单地丢弃相应的网络包。
代码
boop 客户端代码(类似于 Boopkit Boop ),构造一个特殊的网络包,发往目的服务器。
⚠️ Boopkit 的代码中,计算 IP 头部校验和有误 :)
struct iphdr {
uint8_t ihl: 4;
uint8_t version: 4;
uint8_t tos;
uint16_t tot_len;
uint16_t id;
uint16_t frag_off;
uint8_t ttl;
uint8_t protocol;
uint16_t check;
uint32_t saddr;
uint32_t daddr;
};
struct tcphdr {
uint16_t source;
uint16_t dest;
uint32_t seq;
uint32_t ack_seq;
uint16_t res1: 4;
uint16_t doff: 4;
uint16_t fin: 1;
uint16_t syn: 1;
uint16_t rst: 1;
uint16_t psh: 1;
uint16_t ack: 1;
uint16_t urg: 1;
uint16_t ece: 1;
uint16_t cwr: 1;
uint16_t window;
uint16_t check;
uint16_t urg_ptr;
};
struct tcp4_pseudohdr {
uint32_t saddr;
uint32_t daddr;
uint8_t pad;
uint8_t protocol;
uint16_t len;
};
static uint16_t checksum(const uint8_t *pkt, int len)
{
uint32_t csum = 0;
int i;
for (i = 0; i < len - 1; i += 2)
csum += *(uint16_t *)&pkt[i];
if (len & 1)
csum += pkt[i];
while (csum > 0xffff)
csum = (csum & 0xffff) + (csum >> 16);
return ~csum;
}
static int build_packet(struct sockaddr_in *saddr, struct sockaddr_in *daddr,
uint8_t **pkt, int *len)
{
uint8_t data[sizeof(struct tcp4_pseudohdr) + sizeof(struct tcphdr)];
struct tcp4_pseudohdr *pseudo_header;
struct tcphdr *tcp_header;
struct iphdr *ip_header;
uint8_t *buf;
*len = sizeof(struct iphdr) + sizeof(struct tcphdr);
buf = calloc(*len, sizeof(uint8_t));
if (!buf) {
perror("calloc");
return -1;
}
ip_header = (struct iphdr *)buf;
tcp_header = (struct tcphdr *)(buf + sizeof(struct iphdr));
ip_header->ihl = 5;
ip_header->version = 4;
ip_header->tos = 0;
ip_header->tot_len = *len;
ip_header->id = 0xeBaF;
ip_header->frag_off = 0;
ip_header->ttl = 0xff;
ip_header->protocol = IPPROTO_TCP;
ip_header->check = 0;
ip_header->saddr = saddr->sin_addr.s_addr;
ip_header->daddr = daddr->sin_addr.s_addr;
tcp_header->source = saddr->sin_port;
tcp_header->dest = daddr->sin_port;
tcp_header->seq = 0;
tcp_header->ack_seq = 0;
tcp_header->res1 = 0;
tcp_header->doff = 5;
tcp_header->fin = 0;
tcp_header->syn = 1;
tcp_header->rst = 0;
tcp_header->psh = 0;
tcp_header->ack = 0;
tcp_header->urg = 0;
tcp_header->ece = 0;
tcp_header->cwr = 0;
tcp_header->window = htons(0xCafe);
tcp_header->check = 0;
tcp_header->urg_ptr = 0;
memset(data, 0, sizeof(data));
pseudo_header = (struct tcp4_pseudohdr *)data;
pseudo_header->saddr = saddr->sin_addr.s_addr;
pseudo_header->daddr = daddr->sin_addr.s_addr;
pseudo_header->pad = 0;
pseudo_header->protocol = IPPROTO_TCP;
pseudo_header->len = htons(sizeof(struct tcphdr));
memcpy(data + sizeof(struct tcp4_pseudohdr), tcp_header, sizeof(struct tcphdr));
tcp_header->check = checksum((void *)data, sizeof(data));
ip_header->check = checksum((void *)ip_header, sizeof(struct iphdr));
*pkt = buf;
return 0;
}
int main()
{
const int one = 1, flags = 0;
int raw_sock_fd, ret = -1, len = 0;
struct sockaddr_in saddr, daddr;
uint8_t *pkt = NULL;
raw_sock_fd = socket(AF_INET, SOCK_RAW, IPPROTO_TCP);
if (raw_sock_fd < 0) {
perror("socket");
return -1;
}
ret = setsockopt(raw_sock_fd, IPPROTO_IP, IP_HDRINCL, &one, sizeof(one));
if (ret < 0) {
perror("setsockopt");
goto cleanup;
}
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(0xeBaF);
ret = inet_pton(AF_INET, getenv("ATTACKER"), &saddr.sin_addr);
if (ret != 1) {
perror("inet_pton");
goto cleanup;
}
memset(&daddr, 0, sizeof(daddr));
daddr.sin_family = AF_INET;
daddr.sin_port = htons(8086);
ret = inet_pton(AF_INET, getenv("VICTIM"), &daddr.sin_addr);
if (ret != 1) {
perror("inet_pton");
goto cleanup;
}
ret = build_packet(&saddr, &daddr, &pkt, &len);
if (ret < 0)
goto cleanup;
ret = sendto(raw_sock_fd, pkt, len, flags,
(struct sockaddr *)&daddr, sizeof(struct sockaddr));
if (ret < 0) {
perror("sendto");
goto cleanup;
}
cleanup:
free(pkt);
close(raw_sock_fd);
return ret <= 0;
}
boopd 服务端代码(类似于 Boopkit ),这是一个 XDP 类型的 BPF 程序,负责解析网卡上收到的网络包。
我们会判断网络包是否为我们特意构造的,如是,则丢弃(返回 XDP_DROP ),因为我们不想让更上层的协议栈感知这个数据包。