CVE-2020-16898 "Bad Neighbor" 分析
2020年10月14日,微软修复了一个紧急漏洞:Windows TCP/IP 远程代码执行漏洞,漏洞编号为 CVE-2020-16898。
从14 号公开的信息可以得知,这是一个与 ipv6 协议有关,漏洞类型为栈溢出的漏洞。
漏洞寻找
通过公开信息
A remote code execution vulnerability exists when the Windows TCP/IP stack improperly handles ICMPv6 Router Advertisement packets that use Option Type 25 (Recursive DNS Server Option) and a length field value that is even. In this Option, the length is counted in increments of 8 bytes, so an RDNSS option with a length of 3 should have a total length of 24 bytes. The option itself consists of five fields: Type, Length, Reserved, Lifetime, and Addresses of IPv6 Recursive DNS Servers. The first four fields always total 8 bytes, but the last field can contain a variable number of IPv6 addresses, which are 16 bytes each. As a result, the length field should always be an odd value of at least 3, per RFC 8106:
以及补丁的 diff 我们大致定位了漏洞的位置
漏洞发生的原因应该在 Ipv6pUpdateRDNSS
函数中
Router Advertisement
(RA for short) 协议
通过 rfc8106 我们可以知道协议报文如下
1 | 0 1 2 3 |
我们着重知道以下几个
- Type(1个字节):RDNSS选项类型的类型为 25(0x19)
- Length(1个字节):如果该选项中包含一个 IPv6 地址,则长度取最小值3 。每增加一个 RDNSS 地址,长度就会增加2。接收器使用“长度”字段来确定选项中IPv6地址的数量
- Addresses of IPv6 Recursive DNS Servers(可变长度,由“Length”字段确定):一个或多个递归DNS服务器的 128 位 IPv6 地址 。地址个数为(Length - 1)/ 2
协议中规定:
1
2
3
4
5
6
7
8 o The validity of DNS options is checked with the Length field;
that is, the value of the Length field in the RDNSS option is
greater than or equal to the minimum value (3) and satisfies the
requirement that (Length - 1) % 2 == 0. The value of the Length
field in the DNSSL option is greater than or equal to the minimum
value (2). Also, the validity of the RDNSS option is checked with
the "Addresses of IPv6 Recursive DNS Servers" field; that is, the
addresses should be unicast addresses.
即 Length 长度字段要满足 (Length - 1) % 2 == 0
则 length 字段必为奇数,且是大于等于3 的奇数
假设此时 length 长度为 3, 则地址个数为 (3 - 1) / 2 == 1 ,我们知道一个地址长度为 16 字节。IPv6 Recursive DNS Servers 地址前的字段占 8 字节,每个 IPv6 Recursive DNS Servers 地址长度为 16 个字节,所以正常的 RDNSS 选项总长度应满足 16x+8(x>=1),将其除以 8 就是 2x+1(x>=1) ,也就是 Length 字段应该满足的条件。由于 IPv6 RDNSS 地址为 16 个字节,所以 RDNSS 选项总长度会以 16 字节递增,一个最小的长度为 24(8+16)
如果 Length 是偶数
通过学习协议,我们知道通常下, length 的值应为大于等 3 的奇数,但是如果当传入的 length 为偶数 2 ,那么会发生什么事情?
按照协议理解,此时 (2-1)/2 == 0 ,则会判断此 packet 没有地址,则理应会把 RDNSS 选项的最后 8 个字节错误的认为第下一个个选项的前8个字节。
例如假设我们设置 length 长度为 4 -> rdnss.len = len(rdnss.dns) * 2
1 | def poc_last_8_bytes(target_addr): |
我们选取上面的一段汇编做一个简单的注释
1 | fffff801`26bca5c8 e8a39de5ff call tcpip!NetioAdvanceNetBuffer (fffff801`26a24370) |
即这断代码在做计算 地址数 (4 - 1) / 2 == 1
. 因而会将 NET_BUFFER 前进 24 个字节(3*8)
1 | 0: kd> u rip |
当我们让程序走到下一个取下一个选项的时候,发现,此时的选项的前8个字节可被伪造
如何造成栈溢出的?
我们知道此时可以伪造前 8个字节,那么根据 type 可以走不同的程序流
根据交叉引用,以及文档此时的函数有 三种 type,分别为
3: break ,似乎是正常消息
24:Route Information Option
25:RDNSS Option (https://tools.ietf.org/html/rfc4191#section-2.3)
其中 25 是我们触发漏洞的地方, 那么可利用的似乎只有 24 了
当 type 为 24 的时候,会调用 NdisGetDataBuffer
该函数,我们发现此函数的 v221 值在栈上, Elen为可控的长度 * 8
https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/ndis/nf-ndis-ndisgetdatabuffer
搜索微软文档我们发现该函数原型如下:
1 |
|
其第一个参数 NetBuffer 为一个指向 NET_BUFFER 结构的指针;第二个参数 BytesNeeded 为请求数据的长度;第三个参数 Storage 为指向缓冲区的指针,如果调用者不提供缓冲区,则为 NULL。如果此值非 NULL 且请求的数据不连续,则 NDIS 会将请求的数据复制到 Storage 指向的缓冲区。
NdisGetDataBuffer 函数返回指向连续数据的指针,或者NULL。如果缓冲区中请求的数据是连续的,则返回值是指向 NDIS 提供的位置的指针。如果数据不连续,则根据 Storage参数来判断:
- 如果 Storage 参数为非 NULL,即指定缓冲区指针,则 NDIS 将数据复制到Storage 指向的缓冲区中,返回值为 Storage参数指针 。
- 如果 Storage 参数为 NULL,则返回值为 NULL。
所以我们要通过此函数触发缓冲区溢出,则需要构造一个非连续的的数据包,这个问题的解决方案是构造一个 “碎片化” 的数据。对 IPv6 数据进行分段则可以。
如果我们发送带有畸形的RDNSS选项的Router Advertisement数据包时,将其分割成若干个IPv6碎片,那么重新组合的数据包数据就会以非连续的方式存储在NET_BUFFER中。这样一来,对NdisGetDataBuffer的调用就会从我们的数据包中复制任意数量的字节到堆栈中的固定大小的缓冲区中,导致基于堆栈的缓冲区溢出,使得我们可以用任意的值覆盖tcpip!Ipv6pHandleRouterAdvertisement的返回地址。
另外这里要有程序有一个检查,它允许路由信息选项的最大实际大小(option.Length * 3)为0x18。
即在一个循环遍历所有headers,做一些基本的验证
如图,Ipv6pHandleRouterAdvertisement 函数中会检查 Route Information 选项中的 Length 是否大于 3 ,如果大于 3 就会进入错误流程,然后忽略这个包的。
在攻击的过程中
Route Information 选项的前 8 个字节被嵌到了第一个 Recursive DNS Server 选项的末尾。由于在 Case 0x19 的检查流程中,只判断了 Length 是否小于 3 ,而没有判断该字段是否是偶数值,可导致在对数据包选项进行检查的时候将第一个 Recursive DNS Server 选项长度误当成 0x20,因此检查是通过的。而在真正处理的过程中,又将其长度解析为 0x18
对于type为0x18会进入下面的流程处理,调用NdisGetDataBuffer函数,其中第二个参数为长度的实际字节大小,等于length8,所以此时传入的actual_length_bytes = 0x22 8 = 0x110:
而Storage_1 为栈上的数组变量,将0x110个字节赋值过去,就会造成栈上的溢出,实际的崩溃是溢出覆盖了stack cookie,触发tcpip!_security_check_cookie,造成蓝屏(BSOD):
最后贴一个蓝屏: