CVE-2021-24086 Windows TCP/IP 拒绝服务漏洞复现分析

CVE-2021-24086 复现

参考:

https://blog.quarkslab.com/analysis-of-a-windows-ipv6-fragmentation-vulnerability-cve-2021-24086.html

https://doar-e.github.io/blog/2021/04/15/reverse-engineering-tcpipsys-mechanics-of-a-packet-of-the-death-cve-2021-24086/#the-mechanics-of-parsing-an-ipv6-packet

0x1 复现过程:

0x01 靶机环境

一个win7虚拟机,查看IPV6地址后记录

0x02 攻击机运行poc

同局域网下的一个kali虚拟机,运行poc,进行发包

image-20210817103826864

0x03 攻击成功,靶机蓝屏重启

image-20210818162353841

image-20210817103818174

0x2 漏洞部分分析:

0x20 前置知识
0x201 驱动程序tcpip.sys

Microsoft Windows中的TCP/IP功能在内核级别运行,并由驱动程序tcpip.sys提供。该驱动程序处理所有传入和传出的TCP/IP通信信息,包括解析从网络接口接收到的数据包,以及解释此数据并将其传递给更高级别的组件。

0x202 ipv6分片

https://datatracker.ietf.org/doc/html/rfc8200#section-4.5

分片

为了发送一个太大而无法放入MTU(最大传输单元)的数据包到目的地的路径,源节点将数据包分成片段,并将每个片段作为单独的数据包发送,在接收处重新组装。

分片过程

Fragment 标头

image-20210818135823005

其中每个字段:

1
2
3
4
5
6
Next Header-下一个         8-bit
Reserved-保留字段 8-bit
Fragment Offset-片段偏移量 13-bit:标头后面的数据是在重组数据包中的哪个位置(偏移量)
Res 2-bit
M flag 1 = more fragments; 0 = last fragment
Identification 32 bits

原始数据包中的Per-Fragment Headers是不分片部分,其中含有IPv6 标头、Hop-by-Hop Options标头

剩下的部分是一个Extension & Upper-Layer Headers和分出来的多个fragment数据

然后每个fragment数据和Per-Fragment Headers以及一个fragment header组合成一个新的数据包进行发送

即每个分片数据包由不可分片部分(Per-Fragment Headers)、分片报头(fragment header)和可分片部分的切片(fragment数据)组成

分片流程如下:

image-20210818140303683

重组过程则是将第一个数据包的不分片部分与所有重组的可分片部分拼接成一个重组包,流程如下:

image-20210818180411748

0x21 漏洞原理

(不知为何蓝屏导出来的dmp文件在windbg中没法看到调用链,所以就引用大佬的图了

漏洞的根本原因是Ipv6pReassembleDatagram 中产生的空指针引用

image-20210818165203285

image-20210818165357425

可以看到是tcpip!Ipv6ReassembleDatagram中的NdisGetDataBuffer返回了一个NULL指针导致的崩溃

使用IDA64反编译驱动程序tcpip.sys(win7)

找到tcpip!Ipv6ReassembleDatagram函数(负责重新组装接收到的分片数据包的功能)

使用NdisGetDataBuffer函数,返回了一个连续数据开头的指针Dsta,然后使用memmove进行缓冲区转移,当引用的Dsta指针是NULL的时候,也就是空指针引用,就会引起崩溃

image-20210818165538248

0x22 poc原理
0x221 如何让Dsta指针返回NULL

https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/nblapi/nf-nblapi-ndisgetdatabuffer

函数定义:NDIS_EXPORTED_ROUTINE PVOID NdisGetDataBuffer(

NET_BUFFER *NetBuffer,

ULONG BytesNeeded,

PVOID Storage,

ULONG AlignMultiple,

ULONG AlignOffset );

NdisGetDataBuffer返回指向连续数据开头的指针或返回NULL

如果NetBuffer 参数指向的NET_BUFFER结构中 NET_BUFFER_DATA结构的DataLength成员小于 BytesNeeded参数中的值,则返回值为NULL

如果缓冲区中请求的数据是连续的,则返回值是指向 NDIS 提供的位置的指针。如果数据不连续,NDIS 使用 Storage参数如下:

  • 如果 Storage参数为非NULL,则 NDIS 将数据复制到Storage处的缓冲区 。返回值是传递给Storage参数的指针 。
  • 如果 Storage参数为NULL,则返回值为NULL

https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/nbl/ns-nbl-net_buffer

DataLength:MDL 链中使用的数据空间的长度(以字节为单位)。最大长度为 0xFFFFFFFF 字节。

根据上面两个函数规定,满足Net Buffer Data中DataLength成员小于BytesNeeded参数中的值的条件,就会返回NULL指针

注意:继续分析Ipv6ReassembleDatagram函数,图中的NdisRetreatNetBufferDataStart函数中的Dst参数被uint16_t截断了

image-20210818172828058

根据图中代码,可以理解为Dst参数就是不分片部分的长度+ipv6头的长度

image-20210818180211148

其中a2是Ipv6pReceiveFragment中的v21=v18=Ipv6pFragmentLookup的返回值=v11=i,可以理解为需要重组的数据包的集合

Ipv6pReceiveFragment:

image-20210818175059255

image-20210818175023952

Ipv6pFragmentLookup

image-20210818175125251

https://docs.microsoft.com/zh-cn/windows-hardware/drivers/ddi/nblapi/nf-nblapi-ndisretreatnetbufferdatastart

NdisRetreatNetBufferDataStart函数:Call the NdisRetreatNetBufferDataStart function to access more used data space in the MDL chain of a NET_BUFFER structure.

那么比如Dst为0x11111,通过NdisRetreatNetBufferDataStart函数(可以理解为功能是获取一个MDL 链中可以使用的数据空间)截断后,MDL 链中使用的数据空间的长度就是0x1111(可以理解为一个0x1111的缓冲区),但是NdisGetDataBuffer函数使用了Dst也就是0x11111大小的数据来进行缓冲区转移,那么就会成功达到NetBuffer->DataLength < BytesNeeded 的值的条件,因此返回NULL指针。

0x222 如何发送一个巨大的包

对具有非常大标头的数据包进行分段并使堆栈重新组装已经重新组装的数据包

先创建一个需要重组的数据包,在最后一个包中,设置m=1,即需要分片,并且标识值和第二个数据包相同

1
2
3
4
IPv6ExtHdrFragment(
id = second_pkt_id, m = 1,
nh = 17, offset = 0
)

image-20210819091132061

设置第二个数据包

1
2
3
4
reassembled_pkt_2 = Ether() \
/ IPv6(dst = args.target) \
/ IPv6ExtHdrFragment(id = second_pkt_id, m = 0, offset = 1, nh = 17) \
/ 'doar-e ftw'

image-20210819091341303

接收到第一个数据包后,系统开始重组,解析到最后一个分片时,Ipv6ReassembleDatagram函数运行到这里,这里的v30就是分片标识符m,如果还需要继续分片,就会运行到else中的IppReceiveHeaderBatch函数

image-20210819092445786

在IppReceiveHeaderBatch函数中,系统会再次调用tcpip!Ipv6pReceiveFragmentList函数,并且认为之前的数据包长度是第二个数据包的已经处理过的数据长度,并设置为不分片的数据长度,最终加上ipv6头,导致不分片的数据长度也就是Dst参数超过0xffff,引起崩溃