摘要

Linux 内核的 XFRM 框架负责 IPsec 变换,其中 ESP 的 BEET(Bound End-to-End Tunnel)模式在解密 IPsec 包后,会重构内部 IP 头。然而,在某些内核路径中,用于构造新 IP 头的 skb 区域未被完全初始化为零,导致内核堆中残留的指针或数据碎片被嵌入数据包并返回给远端用户。攻击者可从互联网发送特制的空载荷 ESP 包,在解密后的响应中泄露堆上的内核基址、堆地址甚至密钥材料,从而彻底击溃 KASLR 并为后续内核利用铺路。

BEET 模式

IPsec ESP 隧道

IPsec ESP 支持两种经典模式:传输模式仅加密传输层以上内容,保留原始 IP 头;隧道模式则加密整个原始 IP 数据包,并添加新的外部 IP 头。BEET 模式是两者的混合:它使用外部 IP 头进行路由,但内部 IP 头在解密后被重新计算,并以最小开销嵌入外部 IP 头之后。这种模式常用于移动 IPv6 和端到端加密场景。

其核心特性对比可以参考以下表:

维度
传输模式
隧道模式
BEET 模式
IP 标头数量
1 个(原始 IP)
2 个(新 IP + 旧 IP)
1 个(映射后的新 IP)
数据额外开销 (Overhead)
极小(无额外 IP 头)
较大(多出 20/40 字节新 IP 头)
极小(等同于传输模式)
主要应用场景
主机到主机(Host-to-Host)
网关到网关(Site-to-Site VPN)
端到端移动性、NAT 优化
隐藏内网 IP
否(暴露原始 IP 标头)
是(加密原始 IP 标头)
是(通过地址绑定隐藏真实端点)

在 Linux 内核中,BEET 的处理由 net/xfrm/xfrm_input.c 和协议相关的 ESP 处理函数完成。当收到一个 BEET 模式的 ESP 包时,解密后内核需要构造一个新的内部 IP 头,其源和目标地址来自 SA(安全关联)的模板,而非原始数据包。这一重建过程对 skb 的操作非常精细,容易出现未初始化内存问题。

处理路径

[IP层/传输层接收] 
   └── xfrm4_rcv() 或 xfrm4_rcv_encap()
        └── xfrm4_rcv_spi()
             └── xfrm_input()                         <-- XFRM 通用输入入口循环
                  │
                  ├── [解密与校验]
                  │    └── x->type->input(x, skb)     <-- 指向 esp_input()
                  │         └── esp_input()           <-- 完成密码学解密与认证
                  │
                  ├── [更新状态与元数据]
                  │    └── XFRM_MODE_SKB_CB(skb)->protocol = nexthdr
                  │
                  └── [模式处理]
                       └── x->inner_mode.input(x, skb) <-- 针对 BEET 模式的回调
                            └── xfrm4_beet_input()    <-- 剥离外层 IP 恢复 BEET 报头

调用链大致如下:

  1. esp_input 解密 ESP 载荷,将解密后的数据放入 skb 的线性缓冲区或片段中。
  2. 检查模式,若为 BEET,调用 xfrm4_beet_input(IPv4)或 xfrm6_beet_input(IPv6)。
  3. xfrm4_beet_input 负责在解密后的数据之前插入一个新的内部 IP 头。它首先移动 skb->data 指针,然后调用 __skb_push 增加头部空间。新 IP 头的字段(如版本、IHL、长度、ID、标志、TTL、协议等)根据 SA 的模板和当前状态填充。
  4. 关键点:部分字段(如 IP 选项或某些未使用的区域)可能未被显式赋值,而 skb 的这些位置先前可能包含内核堆中的旧数据。

未初始化内存的注入漏洞

漏洞根源

在 xfrm4_beet_input 中,构造新的 IPv4 头大致如下(简化自内核源码):

// net/ipv4/xfrm4_input.c
staticintxfrm4_beet_input(struct xfrm_state *x, struct sk_buff *skb){
structiphdr *iph;
int hdrlen = sizeof(*iph);

// 移动 skb->data 向前,为内部 IP 头腾出空间
    __skb_push(skb, hdrlen);
    skb_reset_network_header(skb);
    iph = ip_hdr(skb);

// 填充基本字段
    iph->version = 4;
    iph->ihl = 5;
    iph->tos = 0;
    iph->tot_len = htons(skb->len);
    iph->id = 0;
    iph->frag_off = 0;
    iph->ttl = 255;
    iph->protocol = x->id.proto; // 传输层协议,如 TCP/UDP
    iph->saddr = x->props.saddr.a4;
    iph->daddr = x->id.daddr.a4;

// 校验和计算(在填充完成后)
    ip_send_check(iph);

// 以下字段未初始化:options 区域(如果 IHL > 5),但 BEET 通常不携带选项。
// 然而,tot_len 覆盖了整个 skb,如果 skb->data 尾部有未初始化的区域,
// 在构造过程中可能被包含。
return0;
}

问题并不完全在 IP 头字段,而是 __skb_push 增加的空间和 skb 尾部可能存在的未初始化数据。当 ESP 载荷为空或极小时,解密后的 skb 主要由内核分配的缓冲区构成,这些缓冲区在分配时可能未被 memset 为 0。在将 skb 传递给上层协议处理(如 TCP/UDP)时,这些未初始化数据会作为有效载荷的一部分被返回给用户。如果远程攻击者能触发 IPsec 隧道并发送特制的包,即可从响应中获得内核内存的碎片。

影响范围与触发条件

  • 内核版本:受影响版本包括 Linux 3.x 到 5.10 的某些发行版内核,具体取决于对 BEET 模式的支持和 skb 分配器的行为。较新内核已通过补丁强制清零新分配的头部空间。
  • 攻击者位置:攻击者需要能够向目标发送封装了 ESP 的 IP 包,即需要与目标建立或欺骗 IPsec 隧道。在某些配置(如默认的预共享密钥模式)下,攻击者可能通过 UDP 封装或直接 IPsec 流注入。
  • 泄露数据类型skb 的未初始化部分往往包含先前释放的堆块中的数据,如内核函数指针、文件系统缓存、网络栈控制结构等。关键影响是 KASLR 绕过和堆地址泄露。

利用 Scapy 触发内存泄露

以下 Python 代码使用 Scapy 构造一个简单的 ESP BEET 包,发送给目标 IPsec 网关,并观察返回包中是否包含额外的未初始化数据。

环境假设

  • 受害主机 (10.0.0.1) 与攻击者 (10.0.0.2) 之间已配置 IPsec BEET 隧道,使用预共享密钥或空加密以简化测试。
  • 目标内核未修复 skb 未初始化问题。

构造 ESP BEET 空载荷包

#!/usr/bin/env python3
# esp_beet_leak.py
from scapy.all import *

# 目标IP和SA参数(根据实际配置修改)
dst_ip = "10.0.0.1"
spi = 0x12345678
seq = 1

# 构造空的内部数据(最小长度)
inner_data = b"\x00" * 1# 极短载荷,触发未初始化内存填充

# 创建 ESP 包(简化,实际需加密和认证)
esp = ESP(spi=spi, seq=seq, data=inner_data)

# 假设使用隧道模式的外部IP头
outer_ip = IP(src="10.0.0.2", dst=dst_ip, proto=50)  # ESP
packet = outer_ip / esp

# 发送并等待响应(ICMP或许可行?实际BEET会处理上层协议,这里直接发送)
send(packet, iface="eth0")
print("Sent ESP BEET packet with minimal payload.")

说明:由于 IPsec 涉及加密和认证,Scapy 原生 ESP 模块不支持硬件加密。在实际攻击中,可使用已建立的 IPsec 隧道(通过内核 SA),或利用 Scapy 的 IPsec 模块。另一种方式是修改内核模块,在本地建立 BEET 隧道并发送空载荷,然后抓包分析响应的长度和内容。更可靠的 PoC 需要在内核中插入打印或利用 eBPF 监控 skb 内容。

检测泄露的方法

在目标主机上,可以使用 SystemTap 或 eBPF 脚本探测 xfrm4_beet_input 返回的 skb 长度是否与预期不符。或者,通过攻击者侧观察响应包的长度是否大于预期。例如,一个 BEET 转换后的 TCP 包期望的 IP 头 + TCP 头 + 最小载荷,但实际响应中载荷部分可能包含额外的几十个字节,这即为泄露的内存。

# 使用 bpftrace 跟踪 xfrm4_beet_input 的 skb 变化
bpftrace -e 'kprobe:xfrm4_beet_input { printf("skb len: %d\n", ((struct sk_buff *)arg1)->len); }'

检测与防御

内核修复

提交 xfrm: fix uinitialized memory read in xfrm4_beet_input 的内核补丁通过以下方式修复:

  • 在构造新的内部 IP 头前,使用 skb_push 后立即用 memset 将新分配的头部空间清零。
  • 确保 tot_len 计算时仅覆盖实际有效负载,而非 skb 的整个缓冲区。

防御措施

  • 升级内核:确保运行 Linux 5.15+ 或已 backport 补丁的长期支持内核。
  • 禁用 BEET 模式:如果业务不需要 BEET,可通过 ip xfrm policy 禁止 BEET 模式的 SA。
  • 强制加密:即使在测试环境,也应使用强加密和认证,增加攻击难度。但该漏洞本身在解密后触发,加密无法阻止。
  • 运行时监控:监控 ESP 包的长度异常,或利用网络入侵检测系统(如 Suricata)检测异常大小的 IPsec 隧道包。

结语

XFRM 框架的复杂性为 Linux 内核网络栈埋下了细小的裂痕,BEET 模式中未初始化的内存注入便是其中之一。攻击者仅需发送一个几乎空白的 ESP 包,便能从内核堆中钓取宝贵的地址信息,将 KASLR 的随机化壁垒击得粉碎。这类漏洞提醒我们:安全敏感路径上的每一次内存分配,都必须以零初始化作为默认的安全卫生习惯。在内核社区的快速响应下,这些缝隙正被逐一封堵,但对于未打补丁的旧系统而言,这条隐藏隧道依然是窥探内核隐私的绝佳通道。