首页
论坛
课程
招聘
[原创]CVE-2021-24086 Windows TCP/IP拒绝服务漏洞分析
2021-4-10 08:14 6270

[原创]CVE-2021-24086 Windows TCP/IP拒绝服务漏洞分析

2021-4-10 08:14
6270

漏洞简介

Windows IPv6 协议栈存在一处拒绝服务漏洞,未经身份认证的远程攻击者可通过向目标系统发送特制数据包来利用此漏洞,成功利用此漏洞可导致目标系统拒绝服务(蓝屏)。

 

近日,奇安信 CERT 监测到此漏洞 POC 在 Github 上公开发布,经测试,该 POC 有效,以下为复现截图:

 

漏洞复现分析

以下为崩溃现场,可以发现,事故出现在 tcpip!Ipv6pReassembleDatagram 函数中,程序试图向 rax 指向的地址处写入数据,但 rax 中的值为 0,由于内存地址 0 不允许访问,因而会触发异常。

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
0: kd> .trap 0xfffff80572ea1a40
NOTE: The trap frame does not contain all registers.
Some register values may be zeroed or incorrect.
rax=0000000000000000 rbx=0000000000000000 rcx=0000000000000014
rdx=0000000000000000 rsi=0000000000000000 rdi=0000000000000000
rip=fffff805727cc27b rsp=fffff80572ea1bd0 rbp=ffffb88d5bfff4a0
 r8=ffffb88d5be14990  r9=0000000000000001 r10=ffffb88d5eb7d2c0
r11=0000000000000002 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0         nv up ei ng nz na po cy
tcpip!Ipv6pReassembleDatagram+0x14f:
fffff805`727cc27b 0f1100          movups  xmmword ptr [rax],xmm0 ds:00000000`00000000=????????????????????????????????
 
0: kd> kb
  *** Stack trace for last set context - .thread/.cxr resets it
 # RetAddr               : Args to Child                                                           : Call Site
00 fffff805`727cd002     : ffffb88d`5bc72000 00000000`00000000 00000000`00000002 ffffb88d`0000fffa : tcpip!Ipv6pReassembleDatagram+0x14f
01 fffff805`727cd122     : ffffb88d`610850a0 00000000`00000008 ffffb88d`5bc70028 ffffb88d`5b83c460 : tcpip!Ipv6pReceiveFragment+0x83a
02 fffff805`726415e7     : fffff805`00000001 ffffb88d`5d5d0780 ffffb88d`610d9c50 00000000`00000000 : tcpip!Ipv6pReceiveFragmentList+0x42
03 fffff805`726964df     : fffff805`72818000 ffffb88d`5bd19720 ffffb88d`5bc72000 00000000`00000000 : tcpip!IppReceiveHeaderBatch+0x4c7
04 fffff805`7269617c     : ffffb88d`5bcbead0 00000000`00000001 00000000`00000001 00000000`00000000 : tcpip!IppFlcReceivePacketsCore+0x34f
05 fffff805`72662bdf     : ffffb88d`5d5cf030 00000000`00000000 00000000`00000000 fffff805`72ea2014 : tcpip!IpFlcReceivePackets+0xc
06 fffff805`726620b2     : ffffb88d`5bcbd901 ffffb88d`5be9e800 fffff805`72650690 fffff805`72ea22f8 : tcpip!FlpReceiveNonPreValidatedNetBufferListChain+0x25f
07 fffff805`6fad98f8     : fffff805`72ea2390 00000000`00000002 ffffb88d`5b0d8080 fffff805`72ea2318 : tcpip!FlReceiveNetBufferListChainCalloutRoutine+0xd2
08 fffff805`6fad986d     : fffff805`72661fe0 fffff805`72ea2318 ffffb88d`5bae0110 00000000`00000000 : nt!KeExpandKernelStackAndCalloutInternal+0x78
09 fffff805`72650bc1     : ffffb88d`5d5cf030 fffff805`72ea2390 00000000`00000000 00060080`02000000 : nt!KeExpandKernelStackAndCalloutEx+0x1d
0a fffff805`723cb561     : ffffb88d`5d6bd4b0 fffff805`72ea2690 00000000`00000000 00000000`00000000 : tcpip!FlReceiveNetBufferListChain+0x311
0b fffff805`723cb057     : ffffb88d`5be8e7f0 00000000`0000dd01 00000000`00000000 ffffb88d`00000001 : ndis!ndisMIndicateNetBufferListsToOpen+0x141
0c fffff805`723d0ee1     : ffffb88d`5b7681a0 ffffb88d`00000000 ffffb88d`5b7681a0 00000000`00000001 : ndis!ndisMTopReceiveNetBufferLists+0x227
0d fffff805`724017d6     : ffffb88d`5d5cf030 fffff805`72ea27e1 00000000`00000000 fffff805`72ea2810 : ndis!ndisCallReceiveHandler+0x61
0e fffff805`723ce8a4     : 00000000`00003293 00000000`00000801 ffffb88d`5b7681a0 00000000`00000001 : ndis!ndisInvokeNextReceiveHandler+0x206e6
0f fffff805`73c383b3     : 00000000`00000000 00000000`00000801 00000000`00000001 ffffb88d`5d5cf030 : ndis!NdisMIndicateReceiveNetBufferLists+0x104
10 fffff805`73c3947a     : ffffb88d`5d5cf000 ffffb88d`5bb4e000 ffffb88d`5bb4f080 00000000`00000000 : e1i65x64!RECEIVE::RxIndicateNBLs+0x12f
11 fffff805`73c40c14     : ffffb88d`5ba54bd0 ffffb88d`5bb4e000 ffffb88d`5bb4e001 00000000`00000000 : e1i65x64!RECEIVE::RxProcessInterrupts+0x20a
12 fffff805`73c4108f     : ffffb88d`5ba54b70 ffff0001`00000000 ffff0001`00000000 fffff805`73c5b560 : e1i65x64!INTERRUPT::MsgIntDpcTxRxProcessing+0x124
13 fffff805`73c40668     : ffffb88d`5ba54b70 00000000`fffffffe ffffb88d`00000000 00000000`00000000 : e1i65x64!INTERRUPT::MsgIntMessageInterruptDPC+0x1ff
14 fffff805`723cf2fd     : ffffb88d`5b75e1a0 ffffb88d`5b75eb90 ffffb88d`5b75e050 fffff805`72211f53 : e1i65x64!INTERRUPT::MiniportMessageInterruptDPC+0x28
......

使用 IDA 查看 tcpip!Ipv6pReassembleDatagram 函数,查找 rax 被赋值为 0 的线索。如下所示,rax(buffer_ptr)为 NdisGetDataBuffer 函数的返回值,NdisGetDataBuffer 函数可返回数据指针或者 NULL。也就是说 buffer_ptr 有可能被赋值为 0,但后面在向其复制数据时并没有判断其是否是空指针,这将会导致异常。

1
2
3
4
5
6
// tcpip!Ipv6pReassembleDatagram
  buffer_ptr = NdisGetDataBuffer(NetBuffer, BytesNeeded, 0i64, 1i64, 0);
  ……
  *(_OWORD *)buffer_ptr = *(_OWORD *)(v1 + 0x90);
  *(_OWORD *)(buffer_ptr + 0x10) = *(_OWORD *)(v1 + 0xA0);
  *(_QWORD *)(buffer_ptr + 0x20) = *(_QWORD *)(v1 + 0xB0);

在 ndis!NdisGetDataBuffer 函数中,如下所示,如果 net_buffer->DataLength 小于传进来的第二个参数 BytesNeeded 就会返回 0。

1
2
3
4
5
6
7
8
9
// ndis!NdisGetDataBuffer
  #buffer_ptr = NdisGetDataBuffer(NetBuffer, BytesNeeded, 0i64, 1i64, 0);  //函数调用
  if ( !a2 || !current_mdl || net_buffer->DataLength < a2 )
    goto LABEL_25;
  ……
LABEL_25:
    result = 0i64;
  }
  return result;

重新调试,在 tcpip!Ipv6pReassembleDatagram 函数下断点,重点关注 ndis!NdisGetDataBuffer 函数,调试过程如下, net_buffer->DataLength(0x10)确实小于 BytesNeeded(0x10010):

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
0: kd>
ndis!NdisGetDataBuffer+0x33:
fffff805`723ce133 395118          cmp     dword ptr [rcx+18h],edx
0: kd> ?poi(rcx+18)
Evaluate expression: 16 = 00000000`00000010
0: kd> r edx
edx=10010
0: kd> p
ndis!NdisGetDataBuffer+0x36:
fffff805`723ce136 0f82be000000    jb      ndis!NdisGetDataBuffer+0xfa (fffff805`723ce1fa)
0: kd>
ndis!NdisGetDataBuffer+0xfa:
fffff805`723ce1fa 33c0            xor     eax,eax
0: kd>
ndis!NdisGetDataBuffer+0xfc:
fffff805`723ce1fc e971ffffff      jmp     ndis!NdisGetDataBuffer+0x72 (fffff805`723ce172)
0: kd> u fffff805`723ce172
ndis!NdisGetDataBuffer+0x72:
fffff805`723ce172 488b5c2450      mov     rbx,qword ptr [rsp+50h]
fffff805`723ce177 488b6c2458      mov     rbp,qword ptr [rsp+58h]
fffff805`723ce17c 488b742460      mov     rsi,qword ptr [rsp+60h]
fffff805`723ce181 4883c430        add     rsp,30h
fffff805`723ce185 415f            pop     r15
fffff805`723ce187 415e            pop     r14
fffff805`723ce189 5f              pop     rdi
fffff805`723ce18a c3              ret

在调用 ndis!NdisGetDataBuffer 函数之前,还调用了 NetioRetreatNetBuffer 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
__int64 __fastcall NetioRetreatNetBuffer(_NET_BUFFER *netbuffer, __int64 a2, __int64 a3)
{
  ULONG v3; // eax
  __int64 v5; // [rsp+20h] [rbp-8h]
 
  v3 = netbuffer->CurrentMdlOffset;
  if ( v3 < (unsigned int)a2 )
    return NdisRetreatNetBufferDataStart(netbuffer, a2, a3, NetioAllocateMdl, v5);
  netbuffer->DataOffset -= a2;
  netbuffer->DataLength += a2;
  netbuffer->CurrentMdlOffset = v3 - a2;
  return 0i64;
}

在该函数调用之前,netbuffer->CurrentMdlOffset 还没有被设置,因而会调用 NdisRetreatNetBufferDataStart 函数来设置 net_buffer 结构,net_buffer->DataLength 就是在这里被设置为 0x10,在函数调用处可以发现,此处 BytesNeeded 被限制为 unsigned __int16,也就是 2 个字节(可以看成整数溢出),而后面的 ndis!NdisGetDataBuffer 函数调用中使用的是 unsigned int 类型,由于 0x10010 > 0x10,函数返回了0:

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
//NetioRetreatNetBuffer(*(_NET_BUFFER **)(v11 + 8), (unsigned __int16)BytesNeeded, 0i64)
0: kd>
tcpip!Ipv6pReassembleDatagram+0xd9:
fffff805`727cc205 e8dafce8ff      call    tcpip!NetioRetreatNetBuffer (fffff805`7265bee4)
0: kd> dt _net_buffer @rcx
ndis!_NET_BUFFER
   +0x000 Next             : (null)
   +0x008 CurrentMdl       : (null)
   +0x010 CurrentMdlOffset : 0
   +0x018 DataLength       : 0
   +0x018 stDataLength     : 0
   +0x020 MdlChain         : (null)
   +0x028 DataOffset       : 0
   +0x000 Link             : _SLIST_HEADER
   +0x000 NetBufferHeader  : _NET_BUFFER_HEADER
   +0x030 ChecksumBias     : 0
   +0x032 Reserved         : 0
   +0x038 NdisPoolHandle   : 0xffffb88d`5ba47940 Void
   +0x040 NdisReserved     : [2] (null)
   +0x050 ProtocolReserved : [6] (null)
   +0x080 MiniportReserved : [4] (null)
   +0x0a0 DataPhysicalAddress : _LARGE_INTEGER 0x0
   +0x0a8 SharedMemoryInfo : (null)
   +0x0a8 ScatterGatherList : (null)
0: kd> r rcx
rcx=ffffb88d5df85f20
0: kd> t
tcpip!NetioRetreatNetBuffer:
fffff805`7265bee4 4883ec28        sub     rsp,28h
0: kd> pc
tcpip!NetioRetreatNetBuffer+0x19:
fffff805`7265befd e8ce39d7ff      call    ndis!NdisRetreatNetBufferDataStart (fffff805`723cf8d0)
0: kd> p
tcpip!NetioRetreatNetBuffer+0x1e:
fffff805`7265bf02 4883c428        add     rsp,28h
0: kd> dt _net_buffer ffffb88d5df85f20
ndis!_NET_BUFFER
   +0x000 Next             : (null)
   +0x008 CurrentMdl       : 0xffffb88d`5bec4220 _MDL
   +0x010 CurrentMdlOffset : 0x50
   +0x018 DataLength       : 0x10
   +0x018 stDataLength     : 0x10
   +0x020 MdlChain         : 0xffffb88d`5bec4220 _MDL
   +0x028 DataOffset       : 0x50
   +0x000 Link             : _SLIST_HEADER
   +0x000 NetBufferHeader  : _NET_BUFFER_HEADER
   +0x030 ChecksumBias     : 0
   +0x032 Reserved         : 0
   +0x038 NdisPoolHandle   : 0xffffb88d`5ba47940 Void
   +0x040 NdisReserved     : [2] (null)
   +0x050 ProtocolReserved : [6] (null)
   +0x080 MiniportReserved : [4] (null)
   +0x0a0 DataPhysicalAddress : _LARGE_INTEGER 0x0
   +0x0a8 SharedMemoryInfo : (null)
   +0x0a8 ScatterGatherList : (null)

下一步,就是追踪 BytesNeeded ,搞清楚它从何而来。经过静态分析,可知 BytesNeeded 为 v3 加 0x28,而 v3 来自于 a2 偏移 0x88 处。参数 a2 最初由 IppCreateInReassemblySet 生成(Ipv6pReceiveFragment 函数中),当满足一定条件时,才会调用 tcpip!Ipv6pReassembleDatagram 函数进行重组。因而下一步便是追踪 important_D8 结构中偏移 0x88 处的长度数据(为何会那么大,加造成 v3 整数溢出):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// tcpip!Ipv6pReassembleDatagram
  v3 = *(unsigned __int16 *)(a2 + 0x88)
  BytesNeeded = v3 + 0x28;
 
// tcpip!Ipv6pReceiveFragment
  important_D8 = IppCreateInReassemblySet(
                     (PKSPIN_LOCK)(v5 + 0x4F50),
                     *(void **)(v1 + 0x110), 
                     v59, 
                     *((_DWORD *)v11 + 1),
                     (KIRQL)arg_0);
 
  if ( *((unsigned __int16 *)important_D8 + 0x2A) == *((_DWORD *)important_D8 + 0x23) )
      Ipv6pReassembleDatagram(v1, (__int64)important_D8, v23);

逆向分析

总体来说,Ipv6pReceiveFragment 函数是用来处理分片选项的,它将分片数据排列到一起,当最后一个分片到来且校验通过,就会调用 Ipv6pReassembleDatagram 函数进行重组。重组之后需要处理 IPv6 分片后隐藏的其他 option,这还是会调用 IppReceiveHeaderBatch 函数进行分配处理的(后面会介绍)。

 

Ipv6pReceiveFragment 函数
当数据传递进这个函数的时候,就已经处理到 Fragment header for IPv6 这个选项了。该函数从 net_buffer 结构中读取 Fragment header for IPv6 选项数据,并对其进行解析和处理。

 

Fragment header for IPv6 的结构如下(这里是测试用的 ICMP 数据包),1字节的 next header,1字节的保留字段,2字节的offset长度(最低 3 bits不参与,因此长度定是 8 的倍数,最后 1 bit是 more fragments 标志),最后 4 字节为Identification 字段,是整个数据包重组的标志。

 

 

这样就可以得到这样一个结构体。

 

 

好啦,经过调试可以发现 Ipv6pReceiveFragment 函数首先会从 net_buffer 结构中读取 IPv6 分片头数据。

 

 

下面分别看一下,这几个字段都是怎么处理的(这一段可以略过)。首先是 next header,如下所示,当 fragment offset 为 0 (意味着正在处理的是第一个分片包)
且 more fragment flag 为 0 (这意味着这个包是最后一个分片……) 时,会将 next header 字段存放在 v1 偏移 0x2c 处。在一般的场景下,不会走到这部分代码。

 

 

然后再看 offset 字段,除了上面那一处,它的出现几乎都伴随着 netbuffer->DataLength,netbuffer->DataLength 一般指当前 mdl 结构指向数据的剩余长度。然后是 more fragments 标志,可以看下面第 2 条,当检测出 more fragments 标志为 0 时,就会跳转到 LABEL_36,通过 fragment_offset + length_last 计算出分片载荷的原始长度 sum_length,判断 important_D8->field_8C 有没有更新(初始化是 0xFFFFFFFF的),如果没有更新就将其赋值为 sum_length。

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
1.     netbuffer->DataLength + fragment_offset > 0xFFFF
 
2.     if ( !length_last )
  {
    if ( fragment_offset || fragment_header_for_ipv6_p2->offset_more_fragment_flag & 0x100 )
      goto label_error_30;
LABEL_36:
    v30 = important_D8->fragment_sum_length;              
    v31 = fragment_offset + length_last;
    if ( v30 == 0xFFFFFFFF )                    // 判断是不是还没有更新长度
    {
      v32 = (unsigned __int16)important_D8->unknow_length;
      important_D8->fragment_sum_length = v31;           
      if ( v32 > v31 || (unsigned __int16)important_D8->field_56 > v31 )
        goto label_error_30;
    }
    else if ( v31 != v30 )
    {
      goto label_error_30;
    }
    goto LABEL_41;
  }
  if ( !(fragment_header_for_ipv6_p2->offset_more_fragment_flag & 0x100) )
    goto LABEL_36;
 
3.     *(_WORD *)(data_mdl_struct + 0xA) = fragment_offset;//data_mdl_struct 结构体偏移 0xA 处也会刷新 fragment_offset

最后是 identification 字段,其主要用做以下两个函数的参数。通过它来标识一个数据包的所有分片。

1
2
3
4
5
6
7
8
9
10
11
12
D8_structure = (important_D8 *)Ipv6pFragmentLookup(
                                 v4,
                                 fragment_header_for_ipv6_p2->identification,
                                 fragment_buffer,
                                 (KIRQL *)&NewIrql);
 
important_D8 = (important_D8 *)IppCreateInReassemblySet(
                                   (PKSPIN_LOCK)(v5 + 0x4F50),
                                   *(void **)(v1 + 0x110),
                                   arg_18,
                                   fragment_header_for_ipv6_p2->identification,
                                   (KIRQL)NewIrql);

大概流程是调用 NdisGetDataBuffer 函数取出 IPv6 分片头,然后通过 Ipv6pFragmentLookup 函数寻找相同 identification 标记的数据包的 important_D8 结构。如果没有找到的话(处理第一个分片包是这样的),会调用 IppCreateInReassemblySet 函数初始化这个包的 important_D8 结构。然后通过 IppReassemblyFindLocation 函数寻找上一个分片的 data_mdl 结构。然后通过 MmSizeOfMdl、ExAllocatePoolWithTagPriority、MmBuildMdlForNonPagedPool 等函数为当前分片初始化一个 data_mdl 结构,0x10 长度的头,0x40 长度的 MDL 结构,以及后面不定长度的分片载荷数据。这个结构体大概如下:

1
2
3
4
5
6
7
00000000 data_mdl        struc ; (sizeof=0x40, mappedto_378)
00000000 next_data_mdl   dq ?
00000008 fragment_length dw ?
0000000A fragment_offset dw ?
0000000C unknown         dd ?
00000010 field_10        _MDL ?
00000040 data_mdl        ends

如果 IPv6 分片头前面还有其他 options 数据也需要计算出来(当然也是第一个分片包才有的机会),关键代码如下,会从 v1 偏移 0x30 处取出长度,然后减去 0x30 的长度(0x28 的 IPv6 头 + 0x8 的 IPv6 分片头),如果在这之间有未分片的 options 数据,需要申请空间将其存放,并将指针存放在 important_D8->options_data,将其长度存放在 important_D8->options_data_length。

 

 

接着,将分片载荷数据复制到 data_mdl_struct 结构体后面。将当前分片链到 important_D8 偏移 0x60 处的 MDL_data 的链表中,并更新其 MDL_data_last 指针。重复这个流程,直到 important_D8->fragment_sum_length 更新,且 important_D8->frag_accum_length == important_D8->fragment_sum_length 时,进行分片重组。

 

通过栈回溯可知,该函数的调用链为 tcpip!IppReceiveHeaderBatch -> tcpip!Ipv6pReceiveFragmentList -> tcpip!Ipv6pReceiveFragment。在 IppReceiveHeaderBatch 函数中会通过 next_header_index 计算出对应的处理函数,比如 0x2c 就对应了 Ipv6pReceiveFragmentList 函数,0x3c 对应了 Ipv6pReceiveDestinationOptions 函数。Ipv6pReceiveFragmentList 函数会调用 Ipv6pReceiveFragment 函数循环处理一组分片包中的 IPv6 分片头。

1
2
3
4
5
6
7
8
0: kd> k
 # Child-SP          RetAddr               Call Site
00 fffff805`72ea1d48 fffff805`727cd122     tcpip!Ipv6pReceiveFragment
01 fffff805`72ea1d50 fffff805`726415e7     tcpip!Ipv6pReceiveFragmentList+0x42
02 fffff805`72ea1d80 fffff805`726964df     tcpip!IppReceiveHeaderBatch+0x4c7
03 fffff805`72ea1e80 fffff805`7269617c     tcpip!IppFlcReceivePacketsCore+0x34f
04 fffff805`72ea1f90 fffff805`72662bdf     tcpip!IpFlcReceivePackets+0xc
……

在最后一个分片包被处理时,会更新 important_D8->fragment_sum_length,然后经过条件判断进入 tcpip!Ipv6pReassembleDatagram 函数进行重组,实际上是申请一个新的 NET_BUFFER 结构并将重组数据链入其 MDL 链中。也就是前面漏洞复现时的场景。以下为部分相关代码:

1
2
3
4
5
6
7
8
9
10
//Ipv6pReassembleDatagram
buffer_ptr = NdisGetDataBuffer(NetBuffer, BytesNeeded, 0i64, 1i64, 0);
……
  *(_OWORD *)buffer_ptr = *(_OWORD *)&v4->IPv6_header_data;
  *(_OWORD *)(buffer_ptr + 0x10) = *(__int128 *)((char *)&v4->IPv6_src + 8);
  *(_QWORD *)(buffer_ptr + 0x20) = *((_QWORD *)&v4->IPv6_dst + 1);
  memmove((void *)(buffer_ptr + 0x28), (const void *)v4->options_data, (unsigned __int16)v4->options_data_length);
……
  for ( i = v4->MDL_data_first; i; i = *(_QWORD *)i )
    NetioExpandNetBuffer(NetBuffer, i + 0x10, *(unsigned int *)(i + 0x38));

值得注意的是,在这个函数中还是会调用 IppReceiveHeaderBatch 函数进行后续处理,若分片载荷中存在需要解析的 option,该函数会继续对它们进行处理。

1
2
3
4
5
6
7
8
if ( !(*(_BYTE *)(a1_1 + 0xB0) & 8) )
{
  v25 = *((_QWORD *)v15 + 0x18);        
  v26 = v15;
  v27 = v15;                           
  IppReceiveHeaderBatch((_QWORD **)&v26, *(_QWORD *)(v25 + 0x28), (__int64)v4, v6, (__int64)NetBuffer, 0i64);
  return;
}

漏洞分析

以下为部分 POC 截图,值得关注的是,在第一组分片数据的最后加入了一个 Fragment header for IPv6 选项(0x2c),但其 identification 设置为了第二个数据包的 identification。这应该就是漏洞触发的关键,通过在 tcpip!Ipv6pReassembleDatagram 函数再次调用 IppReceiveHeaderBatch 函数进行分片中选项的处理,对结构体中的长度进行累加。并利用 0x2C 对应 Ipv6pReceiveFragmentList 函数的特点再次进行分片头处理,由于 identification 标识不同了,这样使程序将上一组数据包累计的处理长度当作第二组数据包未分片数据的长度。从而在后续流程中引发异常。

 

 

以下为验证吧,追踪 important_D8 结构中偏移 0x88 处的长度数据。分析其上层函数 Ipv6pReceiveFragment 可以发现,当数据包的第一个分片进来的时候,会从第一个参数 v1 偏移 0x30 处取出一个长度,并用它减去 0x30,经过前面的分析可知,该长度为前面读取过的 IPv6 数据长度,包括 IPv6 分片头(也就是没有参与分片的数据长度),减去 0x30 就是前面的所有 option 数据的长度。下面来继续分析为什么会出现异常的长度。

1
2
3
4
5
if ( last_data_mdl_structure == &important_D8->MDL_data_first )
{
  LOBYTE(important_D8->func_index_data) = v54->next_header;
  v47 = *(_WORD *)(v1 + 0x30) - 0x30;
  important_D8->options_data_length = v47;    // 重组数据包的options长度

在 tcpip!Ipv6pReassembleDatagram 函数中会调用 IppCopyPacket 函数复制 Ipv6pReassembleDatagram 函数第一个参数指向的关键结构。然后需要关注该结构偏移 0x30 处长度的变化。

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
0: kd>
tcpip!Ipv6pReassembleDatagram+0x11d:
fffff805`727cc249 e896eaebff      call    tcpip!IppCopyPacket (fffff805`7268ace4)
0: kd> db rdx
ffffb88d`5bc72000  00 00 00 00 00 00 00 00-30 00 bb 5b 8d b8 ff ff  ........0..[....
ffffb88d`5bc72010  00 00 00 00 06 00 00 00-20 58 57 5d 8d b8 ff ff  ........ XW]....
ffffb88d`5bc72020  20 a1 af 5b 8d b8 ff ff-40 00 00 00 2c 00 00 00   ..[....@...,...
ffffb88d`5bc72030  30 00 00 00 00 00 00 00-60 c4 83 5b 8d b8 ff ff  0.......`..[....
ffffb88d`5bc72040  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffb88d`5bc72050  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffb88d`5bc72060  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffb88d`5bc72070  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
0: kd> p
tcpip!Ipv6pReassembleDatagram+0x122:
fffff805`727cc24e 488bd8          mov     rbx,rax
0: kd> db rax
ffffb88d`5eb7d2c0  00 00 00 00 00 00 00 00-30 00 bb 5b 8d b8 ff ff  ........0..[....
ffffb88d`5eb7d2d0  00 00 00 00 06 00 00 00-20 58 57 5d 8d b8 ff ff  ........ XW]....
ffffb88d`5eb7d2e0  20 a1 af 5b 8d b8 ff ff-40 00 00 00 2c 00 00 00   ..[....@...,...
ffffb88d`5eb7d2f0  30 00 00 00 00 00 00 00-60 c4 83 5b 8d b8 ff ff  0.......`..[....
ffffb88d`5eb7d300  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffb88d`5eb7d310  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffb88d`5eb7d320  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffb88d`5eb7d330  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

在这个结构偏移 0x30 处下断点,可以清晰的看出程序对数据的处理。首先将其设置为 0x28,然后逐个 option 处理,处理完成后累加这个长度。对比 POC 可知,需要处理的选项都是 Destination Option,长度为 0x710,如下所示:

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
0: kd> ba r4 ffffb88d`5eb7d2f0 "dd ffffb88d`5eb7d2f0 l1"
0: kd> g
ffffb88d`5eb7d2f0  00000028
tcpip!IppReceiveHeadersHelper+0x189:
fffff805`72641ba9 4c896738        mov     qword ptr [rdi+38h],r12
 
0: kd> g     //Destination Option
ffffb88d`5eb7d2f0  00000028
tcpip!Ipv6pReceiveDestinationOptions+0x46:
fffff805`727cba06 488bcb          mov     rcx,rbx
0: kd> u rip l3
tcpip!Ipv6pReceiveDestinationOptions+0x46:
fffff805`727cba06 488bcb          mov     rcx,rbx
fffff805`727cba09 6689831a010000  mov     word ptr [rbx+11Ah],ax
fffff805`727cba10 e813e5f0ff      call    tcpip!Ipv6pProcessOptions (fffff805`726d9f28)
 
0: kd> g
ffffb88d`5eb7d2f0  00000738
tcpip!Ipv6pProcessOptions+0x189:
fffff805`726da0b1 410fb64500      movzx   eax,byte ptr [r13]
0: kd> ub rip l1
tcpip!Ipv6pProcessOptions+0x185:
fffff805`726da0ad 44017f30        add     dword ptr [rdi+30h],r15d
0: kd> r r15
r15=0000000000000710
0: kd> ?r15+28
Evaluate expression: 1848 = 00000000`00000738
 
0: kd> g     //Destination Option
ffffb88d`5eb7d2f0  00000738
tcpip!Ipv6pReceiveDestinationOptions+0x46:
fffff805`727cba06 488bcb          mov     rcx,rbx
0: kd> g
ffffb88d`5eb7d2f0  00000e48
tcpip!Ipv6pProcessOptions+0x189:
fffff805`726da0b1 410fb64500      movzx   eax,byte ptr [r13]
0: kd> ?r15+738
Evaluate expression: 3656 = 00000000`00000e48
1
2
3
4
5
6
7
8
9
0: kd> k 7
 # Child-SP          RetAddr               Call Site
00 fffff805`72ea1a10 fffff805`727cba15     tcpip!Ipv6pProcessOptions+0x189
01 fffff805`72ea1aa0 fffff805`726415e7     tcpip!Ipv6pReceiveDestinationOptions+0x55
02 fffff805`72ea1ad0 fffff805`727cc512     tcpip!IppReceiveHeaderBatch+0x4c7
03 fffff805`72ea1bd0 fffff805`727cd002     tcpip!Ipv6pReassembleDatagram+0x3e6
04 fffff805`72ea1c70 fffff805`727cd122     tcpip!Ipv6pReceiveFragment+0x83a
05 fffff805`72ea1d50 fffff805`726415e7     tcpip!Ipv6pReceiveFragmentList+0x42
06 fffff805`72ea1d80 fffff805`726964df     tcpip!IppReceiveHeaderBatch+0x4c7

在 tcpip!IppReceiveHeaderBatch 函数中会通过 option_header_number 来计算相应的处理函数,并在处理函数中将下一个 option 的 number 写入结构体偏移 0x2C 处。

 

 

后面的操作就很明显了,程序会继续处理重组后的 option,并将处理过的长度累加,存放在结构体偏移 0x30 处。注意到 POC 中第一组数据的最后一个包,最后一个选项被设置为 IPv6 分片选项,并且将其 identification 设置为下一个包的 identification(0x0c02f90a)。这将会导致 tcpip!IppReceiveHeaderBatch 函数再次调用 tcpip!Ipv6pReceiveFragmentList 函数,并且上下文环境为第一组数据包的信息。

 

 

 

如下所示, 该结构体偏移 0x30 处的长度本是上一组数据包处理过的数据长度,而一个新的 identification 会使程序认为它是新的数据包已经处理过的数据长度。

1
2
3
4
5
6
7
8
9
10
11
12
13
0: kd> g    //伪造的分片包处理
Breakpoint 1 hit
tcpip!Ipv6pReceiveFragmentList:
fffff805`727cd0e0 4885c9          test    rcx,rcx
0: kd> db rcx
ffffb88d`5eb7d2c0  00 00 00 00 00 00 00 00-a0 5d f8 5d 8d b8 ff ff  .........].]....
ffffb88d`5eb7d2d0  00 00 00 00 06 00 00 00-98 42 ec 5b 8d b8 ff ff  .........B.[....
ffffb88d`5eb7d2e0  20 a1 af 5b 8d b8 ff ff-40 00 00 00 2c 00 00 00   ..[....@...,...
ffffb88d`5eb7d2f0  10 00 01 00 00 00 00 00-60 c4 83 5b 8d b8 ff ff  ........`..[....
ffffb88d`5eb7d300  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffb88d`5eb7d310  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffb88d`5eb7d320  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffb88d`5eb7d330  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

在 tcpip!Ipv6pReceiveFragment 函数中会为其设置一个新的 important_D8 结构,并将其 options_data_length 设置为 0x10010+8-0x30,即 0xffe8。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  if ( last_data_mdl_structure == &important_D8->MDL_data_first )
  {
    LOBYTE(important_D8->func_index_data) = v54->next_header;
    v47 = *(_WORD *)(v1 + 0x30) - 0x30;
    important_D8->options_data_length = v47;    // 重组数据包的options长度
 
0: kd> g
tcpip!Ipv6pReceiveFragment+0x6cb:
fffff805`727cce93 0fb74730        movzx   eax,word ptr [rdi+30h]
0: kd> db rbx+80 l10
ffffb88d`61a484a0  00 00 00 00 00 00 00 00-00 00 00 00 ff ff ff ff  ................
 
0: kd> p 3
tcpip!Ipv6pReceiveFragment+0x6cf:
fffff805`727cce97 6683e830        sub     ax,30h
tcpip!Ipv6pReceiveFragment+0x6d3:
fffff805`727cce9b 66898388000000  mov     word ptr [rbx+88h],ax
tcpip!Ipv6pReceiveFragment+0x6da:
fffff805`727ccea2 0f84d3000000    je      tcpip!Ipv6pReceiveFragment+0x7b3 (fffff805`727ccf7b)
 
0: kd> db rbx+80 l10
ffffb88d`61a484a0  00 00 00 00 00 00 00 00-e8 ff 00 00 ff ff ff ff  ................
0: kd> ?0x10010+8-0x30
Evaluate expression: 65512 = 00000000`0000ffe8

因而在 tcpip!Ipv6pReassembleDatagram 函数中,使用该长度加 0x28(IPv6 头长度)会导致一个溢出。而在之前最后一个分片进来,函数对其进行校验时,比较的是最后一个分片包的长度加其偏移是否大于 0xFFFF,以及当前累加长度是否大于最后一个分片中的分片长度加偏移的值,没有去校验未分片的 option 数据长度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//Ipv6pReceiveFragment
  if ( netbuffer->DataLength + fragment_offset > 0xFFFF )
  {
    IppDeleteFromReassemblySet((KSPIN_LOCK *)(v5 + 0x4F50), (__int64)important_D8, v23);
    v50 = _byteswap_ulong(*(_DWORD *)(v1 + 0x30) - 6);
LABEL_28:
    IppSendError(0, (__int64)Ipv6Global, v1, 4, 0, v50, 0);
    goto label_error_57;
  }
……
  if ( !length_last )
  {
    if ( fragment_offset || fragment_header_for_ipv6_p2->offset_more_fragment_flag & 0x100 )
      goto label_error_30;
LABEL_36:
    v30 = important_D8->fragment_sum_length;
    v31 = fragment_offset + length_last;
    if ( v30 == 0xFFFFFFFF )
    {
      v32 = (unsigned __int16)important_D8->fragment_accu_length;
      important_D8->fragment_sum_length = v31; 
      if ( v32 > v31 || (unsigned __int16)important_D8->field_56 > v31 )
        goto label_error_30;
    }

在后续 NetioRetreatNetBuffer 函数中使用了 2 个字节长度来设置 _NET_BUFFER,而在 NdisGetDataBuffer 函数中使用 4 字节长度来请求 _NET_BUFFER,导致该函数校验失败,返回空指针。并且程序在没有判断的情况下向其赋值,导致拒绝服务。

漏洞小结

Windows IPv6 协议栈存在一处拒绝服务漏洞,在进行 IPv6 分片重组时,由于 NetioRetreatNetBuffer 函数和 NdisGetDataBuffer 函数使用的参数类型不匹配,可导致后者返回空指针,在后续复制操作时引发空指针问题。攻击者可通过向一个超大数据包最后分片的末尾插入带有另一个标识的 Fragment header for IPv6 选项,并且继续发送带有该标识数据包的剩余分片,从而在第二组数据包重组的时候触发漏洞,导致目标系统拒绝服务。

 

补丁分析

对比 tcpip.sys 补丁前后,可发现主要修改了三个函数 (用以修补 2 月这几个 TCP/IP 漏洞):Ipv6pReassembleDatagram、Ipv6pReceiveFragment 和 Ipv4pReassembleDatagram。

 

 

在 Ipv6pReassembleDatagram 函数中,增加了一处判断,当重组数据包的 options_data_length 和 fragment_sum_length 之和大于 0xFFFF 会调用 IppDeleteFromReassmblySet 函数删除这个包的信息,然后跳过重组。

 

 

在 NetioRetreatNetBuffer 函数调用处,将第二个参数的长度由原来的 16位 变为 32 位 (DWORD),避免了在后续调用 NdisGetDataBuffer 函数时参数不一致的情况。

 

参考链接

https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-24086

 

https://github.com/0vercl0k/CVE-2021-24086

 

https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/ndis/ns-ndis-_net_buffer_list

 

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

 

https://blog.csdn.net/luguifang2011/article/details/81667826


[公告] 欢迎大家踊跃尝试高研班11月试题,挑战自己的极限!

最后于 2021-4-10 10:12 被蝶澈——编辑 ,原因:
收藏
点赞2
打赏
分享
最新回复 (3)
雪    币: 12863
活跃值: 活跃值 (10697)
能力值: (RANK:710 )
在线值:
发帖
回帖
粉丝
有毒 活跃值 10 2021-4-10 08:45
2
0
图片没上来
雪    币: 4021
活跃值: 活跃值 (2203)
能力值: ( LV9,RANK:170 )
在线值:
发帖
回帖
粉丝
蝶澈—— 活跃值 4 2021-4-10 10:15
3
0
有毒 图片没上来[em_78]
改好啦,谢大佬提醒
雪    币: 2289
活跃值: 活跃值 (3197)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
舒默哦 活跃值 1 2021-5-28 10:05
4
0
游客
登录 | 注册 方可回帖
返回