目录

CVE-2020-16898 Bad Neighbor Windows TCP/IP远程代码执行漏洞分析

CVE-2020-16898 “Bad Neighbor " Windows TCP/IP远程代码执行漏洞分析

一、漏洞信息

1. 漏洞简述

  • 漏洞名称:Windows TCP/IP Remote Code Execution Vulnerability
  • 漏洞编号:CVE-2020-16898
  • 漏洞类型:Design Weakness
  • 漏洞影响:Code Execution
  • CVSS评分:9.8
  • 利用难度:Medium
  • 基础权限:不需要

2. 组件概述

TCP/IP是Internet上使用的通信协议。 在Windows的早期版本中,TCP/IP是一个单独的可选组件,可以像其他任何协议一样删除或添加。从Windows XP/Server 2003开始,TCP/IP成为操作系统的核心组件,无法删除。 将TCP/IP作为Windows的核心组件是非常有意义的,因为它的功能在Microsoft Windows Server上对网络操作和Active Directory域环境尤为重要。 整个Active Directory架构基于DNS层次结构,依赖于TCP/IP 传输协议 。

Microsoft Windows中的TCP/IP功能在内核级别运行,并由驱动程序tcpip.sys提供。该驱动程序处理所有传入和传出的TCP/IP通信信息,包括解析从网络接口接收到的数据包,以及解释此数据并将其传递给更高级别的组件。

3. 漏洞利用

该漏洞主要是由于Windows TCP/IP堆栈在处理选项类型为25(0x19,递归DNS服务器选项)且长度字段值为偶数的ICMPv6的路由广播数据包时,处理逻辑存在纰漏,导致存在远程代码执行漏洞。成功利用该漏洞的攻击者可以在目标机器(主机或服务器)上执行任意代码。

4. 漏洞影响

• Microsoft Windows 10 1709
• Microsoft Windows 10 1803
• Microsoft Windows 10 1809
• Microsoft Windows 10 1903
• Microsoft Windows 10 1909
• Microsoft Windows 10 2004
• Microsoft Windows Server 2019
• Microsoft Windows Server, version 1903
• Microsoft Windows Server, version 1909
• Microsoft Windows Server, version 2004

5. 解决方案

微软官方针对该漏洞已发布安全更新补丁,补丁地址:

https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2020-16898

二、漏洞复现

1. 环境搭建

  • 靶机:Windows 10 1809 x64

  • 靶机操作:无需任何操作,可正常与攻击机通信即可

2. 复现过程

  1. 通过各种手段获取目标主机的IPv6地址和MAC地址(具体方法可自行探索,较为简单)

  2. 攻击机python3运行poc:

    https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898poc.png

  3. 靶机crash:

    https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898Crash.png

三、漏洞分析

1. 基本信息

  • 漏洞文件:tcpip.sys
  • 漏洞函数: Ipv6pUpdateRDNSS()函数
  • 漏洞对象:ICMPv6路由广播中的option结构

2. 背景知识

(限于篇幅问题,此处不对用于DNS配置的IPv6路由广播进行详细介绍,更详细资料可参考RFC8106)

1. 基本知识

IPv6 Router Advertisment (RA) options,也称为DNS RA options,允许IPv6的路由器向IPv6的主机广播DNS Recursive Server Address(DNS递归路由器地址)列表和DNS Search List(DNS搜索列表),其主要用途为在IPv6的主机上进行DNS名称解析以及域后缀的处理。

IPv6 Neighbor Discovery(ND,IPv6邻居发现)和IPv6 Stateless Address Autoconfiguratioin(SLAAC,IPv6无状态地址自动配置)提供了使用一个或多个IPv6地址,默认路由器以及一些其他参数配置固定节点或移动节点的方法。

当漫游主机每次连接到另一个网络时,无法进行手动配置。 虽然可以进行静态配置,但是在诸如笔记本电脑之类的通用主机上通常不建议这样操作。 例如,如果主机运行直接连接到全局DNS的自己的递归名称服务器,那么本地定义的名称空间对主机来说就不可用了。 访问DNS是几乎所有主机的基本要求,因此IPv6 SLAAC在没有任何DNS配置支持的情况下,不能在任何实际的网络环境中单独作为替代部署模型。

对于IPv4环境中的DNS服务器来说,这些问题都很容易解决。但是对于IPv6的网络环境,这些问题显得比较棘手。因此,RFC8106定义了一种基于DNS RA选项的机制,以允许IPv6主机执行自动DNS配置。

在通过IPv6 SLAAC自动配置IPv6主机地址并且没有DHCPv6基础结构或一些主机没有DHCPv6客户端的网络环境中,可以使用基于RA的DNS配置作为替代。 但是,对于需要分发其他信息的网络,可能仍然会使用DHCPv6。 在这些网络中,可能不需要基于RA的DNS配置。 基于RA的DNS配置允许IPv6主机获取主机连接到的链接的DNS配置(即DNS递归服务器地址和DNSSL)。 此外,主机会从提供链接配置信息的同一RA消息中学习此DNS配置。

2. 名词解释

  • Recursive DNS Server (RDNSS):递归DNS服务器,提供递归DNS解析服务的服务器,用于将域名转换为IP地址或解析成RFC1034和RFC1035中定义的PTR记录。

  • RDNSS Option:一个用于向IPv6主机传送RDNSS信息的IPv6的RA option【RFC4861】。

  • DNS Search List (DNSSL):Pv6主机在执行DNS查询搜索时使用的DNS后缀域名列表,用于搜索简短的不合格域名。

  • DNSSL Option:一个IPv6 RA选项,用于将DNSSL信息传递到IPv6主机。

3. 详细分析

1. 基础分析

RFC8106标准化了RDNSS option,其中包含RDNSSes的地址。该信息使用现有ND message(例如RA)作为载体。IPv6主机可以通过RA消息配置一个或多个RDNSS的IPv6地址。

1. 邻居发现扩展

RFC8106中定义的在邻居发现中使用的IPv6 DNS配置算法需要用到2种ND options:RDNSS optionDNSSL option。与该漏洞相关的是RDNSS option,另外一种则与 CVE-2020-16899相关。

2. RDNSS Option Structure

RDNSS option总体结构如下:

Offset Size(bytes) Field Descriptioin
0x00 1 Type 8-bit,RDNSS option type identifier,0x19
0x01 1 Length option长度(包括"Type"和"Length"字段),以8个八位位组为单位。
0x02 2 Reserved 保留字段
0x04 4 Lifetime RDNSS地址可用于名称解析的最长时间(以秒为单位)(相对于接收包的时间)。默认情况下,该值至少为3 * MaxRtrAdvInterval,其中MaxRtrAdvInterval是RFC4861中定义的最大RA间隔。 0xffffffff表示无穷大, 零值意味着必须不再使用RDNSS地址。
0x08 16 Address of IPv6 Recursive DNS Servers 一个或多个128位IPv6地址。 地址的数量由Length字段确定:Number of addresses = (Length - 1) / 2。

对于Length字段,如果该选项中仅包含一个IPv6地址,则最小值为3。 每增加一个RDNSS地址,长度就会增加2。接收的主机使用该字段来确定选项中IPv6地址的数量。

3. Procedure in IPv6 Hosts

当主机接收到RA消息中的DNS的options时,其处理过程如下:

  1. 首先检查Lengh字段的合法性:是否大于等于最小值3,以及是否满足(Length - 1) % 2 == 0
  2. 对于RDNSS option,还会检查Address字段是否为一个单播地址;
  3. 如果以上验证通过,则主机应按顺序将选项的值复制到DNS存储库和解析器存储库中。 否则,主机必须丢弃这些选项。
4. Crash分析

首先分析dmp文件,查看crash现场:

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898Crash-1.png

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898Crash-2.png

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898Crash-3.png

并没有发现明显的较为有价值的Call Stack信息,但是发现最终的crash原因的是GS机制的Security Cookie校验失败,也就是说该值被覆盖掉了。那么很有可能是一个溢出。除此之外,只发现了tcpip!Ipv6pHandleRouterAdvertisement+0x1269函数,再往后就直接报gsfailure了。

2. 静态分析

分析使用的文件为Windows 10 1809 x64的tcpip.sys文件,版本为10.0.17763.316

1. 函数调用链

根据crash现场信息,获取到关键函数tcpip!Ipv6pHandleRouterAdvertisement(),首先确认该函数到漏洞函数的前后调用链。

首先查看其交叉引用关系:

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898xref1.png

其上层调用函数为Icmpv6ReceiveDatagrams(),跟进,并查看交叉引用关系:

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898xref2.png

没有再发现显式的函数调用。转而向tcpip!Ipv6pHandleRouterAdvertisement()的下层搜索:

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898xref3.png

发现漏洞函数调用。至此,函数调用链可以简单概括为:

Icmpv6ReceiveDatagrams() -> tcpip!Ipv6pHandleRouterAdvertisement() -> Ipv6pUpdateRDNSS()

2. 漏洞函数分析

经过简单分析可以明确,调用链的顶层函数Icmpv6ReceiveDatagrams()没有发现实质性的与该漏洞相关的处理代码,而在tcpip!Ipv6pHandleRouterAdvertisement() 函数中发现了对漏洞函数Ipv6pUpdateRDNSS()的调用。根据crash分析,最后报了gsfailure,而且关键函数为tcpip!Ipv6pHandleRouterAdvertisement(),在该函数的起始位置确实发现了GS校验:

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898gs.png

那么很有可能是在漏洞函数Ipv6pUpdateRDNSS()中发生了溢出,导致了其调用函数tcpip!Ipv6pHandleRouterAdvertisement()的GS校验失败。

进入漏洞函数Ipv6pUpdateRDNSS()

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898RDNSS-1.PNG

NdisGetDataBuffer()函数声明如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
PVOID NdisGetDataBuffer(
  PNET_BUFFER NetBuffer,	// [in], a pointer to a NetBuffer structure
  ULONG       BytesNeeded,	// [in], the number of contiguous bytes of data requested
  PVOID       Storage,		// [in, optional], a pointer to a buffer, or NULL if no buffer is provided by the caller. The buffer must be greater than or equal in size to the number of bytes specified in BytesNeeded . If this value is non-NULL, and the data requested is not contiguous, NDIS copies the requested data to the area indicated by Storage .
  UINT        AlignMultiple, // [in], the alignment multiple expressed in power of two. For example, 2, 4, 8, 16, and so forth. If AlignMultiple is 1, then there is no alignment requirement.
  UINT        AlignOffset	 // [in], the offset, in bytes, from the alignment multiple.
);

// Return Value
A pointer to the start of the contiguous data or NULL.

如果NetBuffer参数指向的NET_BUFFER结构中的NET_BUFFER_DATA部分的DataLength字段的值小于BytesNeeded参数的值,那么函数返回NULL。NET_BUFFER的结构如下:

 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
typedef struct _NET_BUFFER {
  union {
    struct {
      PNET_BUFFER Next;
      PMDL        CurrentMdl;
      ULONG       CurrentMdlOffset;
      union {
        ULONG  DataLength;
        SIZE_T stDataLength;
      };
      PMDL        MdlChain;
      ULONG       DataOffset;
    };
    SLIST_HEADER      Link;
    NET_BUFFER_HEADER NetBufferHeader;
  };
  USHORT                ChecksumBias;
  USHORT                Reserved;
  NDIS_HANDLE           NdisPoolHandle;
  PVOID                 NdisReserved[2];
  PVOID                 ProtocolReserved[6];
  PVOID                 MiniportReserved[4];
  NDIS_PHYSICAL_ADDRESS DataPhysicalAddress;
  union {
    PNET_BUFFER_SHARED_MEMORY SharedMemoryInfo;
    PSCATTER_GATHER_LIST      ScatterGatherList;
  };
} NET_BUFFER, *PNET_BUFFER;

首先获取到RDNSS option结构,然后读取Length字段来计算Address字段有几个Address值。

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898RDNSS-2.png

确认有多少Address之后,进入循环,对每个Address进行处理。这里还有一个判断,如果不是单播地址,直接忽略:

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898RDNSS-3.png

在上面的处理过程中,存在一个问题:假设Length的长度为4,那么计算结束之后,AddressCount的值应该为1。此时,按照正常逻辑,Ipv6pUpdateRDNSS()函数应该增加32字节(4*8)的缓冲区,但是后续在分配缓冲区时只分配了24字节:sizeof(ND_OPTION_RDNSS) + sizeof(IN6_ADDR) = 8 + 16 = 24,从而导致了缓冲区的溢出。

根据RFC8106的标准,Length字段的值应该满足最小为3的奇数的情况。当提供一个偶数Length值时,Windows TCP/IP堆栈错误地将buffer前进了8个字节。这主要是因为堆栈在内部以16字节为增量进行计数,并且没有使用非RFC兼容长度值的处理代码。这种不匹配导致堆栈将当前选项的最后8个字节解释为第二个选项的开始,最终导致缓冲区溢出和潜在的RCE。

3. 动态分析

首先在pv6pHandleRouterAdvertisement()函数中第一次调用NdisGetDataBuffer()函数 从 _NET_BUFFER 结构中访问数据(连续或不连续),使用 _NET_BUFFER 结构中的 CurrentMdlOffset 字段定位要访问数据起始地址相对于 MDL 指向内存数据的偏移 :

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16899/dbg.png

第2次调用NdisGetDataBuffer()函数获取第1个RDNSS Option

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898dbg-1.png

来到Ipv6pUpdateRDNSS()函数调用处,此时rdx中存放的是_NET_BUFFER

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898dbg-2.png

首先使用NdisGetDataBuffer()函数读取到Option结构:

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898dbg-3.png

然后调用NetioAdvanceNetBuffer()函数,执行完成后_NET_BUFFER的部分数据如下:

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898dbg-4.png

继续向下,开始计算AddressCount,使用的公式为(Length - 1) / 2

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898dbg-6.png

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898dbg-7.png

最终的计算结果为1。

接下来判断LifeTime字段是否设置为最大值0xffffffff

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898dbg-8.png

继续向下,因为AddressCount = 1,所以来到再一次调用NdisGetDataBuffer处:

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898dbg-9.png

此时的参数情况如下:

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898dbg-10.png

之前是读取了8字节数据,而这次是读取了16字节,此时返回的是存放第1个Recursive DNS Server值的地址:

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898dbg-11.png

然后对地址进行判断0xffffe40b85d8afb8中的内容是否为单播地址:

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898dbg-12.png

继续向下,来到Ipv6pCreateRDNSSEntry()函数调用处,其各参数情况如下:

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898dbg-13.png

先后调用ExAllocatePoolWithTag()memset()进行内存分配并初始化:

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898dbg-14.png

再经过部分处理,最后的结果如下:

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898dbg-15.png

后续在进行security cookie的校验时,校验通过,函数返回:

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898dbg-16.png

回到Ipv6pHandleRouterAdvertisement()函数,再次进行数据读取时,偏移变为0x28,指向第2个Recursive DNS Server

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16899/dbg-17.png

此次的处理流程按照Type为0x18进行处理。Type为0x18时的处理逻辑如下:

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898dbg-18.png

直接调用NdisGetDataBuffer()函数,因为数据不连续并且指定了Storage参数(r8)为栈上的地址,所以会向栈上copy大量数据,从而破坏正常栈数据:

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898dbg-19.png

上图中的循环调用memcpy()函数向栈上copy数据,造成栈破坏,最终栈上结果如下:

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898dbg-20.png

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898dbg-21.png

最终 Ipv6pHandleRouterAdvertisement() 函数返回时,cookie 检查不通过,造成crash。

4. 利用思路

1. 利用条件
  1. 基本条件

    • attacker需要获取target的IPv6和MAC地址
  2. 触发过程

    • attacker需要搭配其他内存泄漏或信息泄漏漏洞来实现RCE
    • attacker需要想办法绕过tcpip.sys的GS保护机制
2. 利用过程

attacker直接发送特制的ICMPv6路由广播数据包给target:

1
[ Attacker ] <--------------------> [ Target ]
3. 攻击向量

建立连接后,利用IPv6直接发送攻击数据包即可。

5. 流量分析

因为该漏洞直接走的IPv6,所以对于一些部署在IP层以上的防火墙方案就无法针对该漏洞进行流量检测,但是具备IP层流量检测的防火墙可以轻松检测恶意流量:

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898traffic.png

在流量中可以明显看出,第一个Option结构的Address字段错误识别计算了一个Recursive DNS Server的值:

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898traffic-1.png

第1个Recursive DNS Server的地址为0018-0027,后续的8个字节不应该再进行识别。选中第2个Recursive DNS Server时情况如下:

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898traffic-2.png

第2个Recursive DNS Server的地址为0028-0037。但是该16个字节中的后8个字节很明显为下一个ICMPv6 Option结构的内容:

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898traffic-3.png

四、缓解措施

管理员启动powershell或cmd,输入以下命令检查所有网络IPv6接口的列表以及相应的索引号:

1
netsh int ipv6 sh int

样例输出如下:

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898workground1.png

确认网络接口的RDNSS功能开启情况:

1
netsh int ipv6 sh int Idx number

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898workground2.png

执行以下命令关闭RDNSS功能(将Idx number替换为要关闭的网络接口的Idx值):

1
 netsh int ipv6 set int Idx number rabaseddnsconfig=disable

样例输出如下:

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898workground3.png

此时再次确认接口的RDNSS开启情况,RDNSS功能已被关闭:

https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/CVE-2020-16898workground4.png

五、参考文献

  1. https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2020-16898
  2. https://tools.ietf.org/html/rfc8106
  3. https://www.mcafee.com/blogs/other-blogs/mcafee-labs/cve-2020-16898-bad-neighbor/