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选项格式:
|
|
Length字段描述:
- 8位无符号整数
- 选项长度(包括Type和Length字段)以8字节为单位
- 如果选项中包含一个IPv6地址,最小值为3
- 每增加一个RDNSS地址,长度增加2
问题1 – IPv6链接本地地址要求
我们发现漏洞只能在源地址为链接本地IPv6地址时被利用。这个要求限制了潜在目标!
代码中的验证逻辑检查地址是否以0xFE开头(fe80::/10链接本地前缀)。如果源地址不是链接本地地址,解析过程会在到达漏洞代码之前终止。
触发漏洞
根据检测逻辑的分析,当Length字段(选项中的第二个字节)为偶数时会出现问题。
解析逻辑执行以下计算:
|
|
这个逻辑对奇数和偶数产生相同的结果:
- (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 – 包大小验证
在调试过程中发现,在以下检查中失败:
|
|
其中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
代码关键部分:
|
|
这个PoC演示了如何通过精心构造的ICMPv6包利用CVE-2020-16898漏洞,最终导致目标系统崩溃。