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通信信息,包括解析从网络接口接收到的数据包,并将其传递给更高级别的组件。
该漏洞主要是由于Windows TCP/IP堆栈在处理选项类型为31(0x1f,DNS搜索表选项)的ICMPv6的路由广播数据包时,处理逻辑存在越界读,导致拒绝服务漏洞。攻击者成功利用该漏洞可使目标主机失去响应,但无法直接进行任意代码执行或权限提取。
• Microsoft Windows 10 1709<br> • Microsoft Windows 10 1803<br> • Microsoft Windows 10 1809<br>• Microsoft Windows 10 1903<br>• Microsoft Windows 10 1909<br>• Microsoft Windows 10 2004<br>• Microsoft Windows Server 2019<br> • Microsoft Windows Server, version 1903 <br>• Microsoft Windows Server, version 1909 <br>• Microsoft Windows Server, version 2004 <br>
微软官方针对该漏洞已发布安全更新补丁,补丁地址:
https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2020-16899
靶机:Windows 10 1809 x64
靶机操作:使用verifier
开启tcpip.sys
的验证
通过各种手段获取目标主机的IPv6地址和MAC地址(具体方法可自行探索,较为简单)
攻击机python3运行poc:
靶机crash:
(限于篇幅问题,此处不对用于DNS配置的IPv6路由广播进行详细介绍,更详细资料可参考RFC8106 )
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配置。
RFC8106标准化了DNSSL Option
,该结构中包含DNS搜索列表(DNSSL),保证与DHCPv6 option保持相同的奇偶校验,并确保具备确定搜索域的必要功能。
RFC8106中定义的在邻居发现中使用的IPv6 DNS配置算法需要用到2种ND options:RDNSS option
和DNSSL option
。与该漏洞相关的是DNSSL Option
,另外一种则与 CVE-2020-16898相关。
DNSSL Option
包含一个或多个DNS后缀,所有的domain name使用相同的Lifetime。如果需要不同的Lifetime值,则需要多个DNSSL Option
结构。
DNSSL Option
总体结构如下:
对于Length
字段,如果option中仅有一个domain name,则为最小值为2。
Domain Names of DNS Search List
字段中的domain name的编码要遵循RFC1035的3.1节中定义的格式:
多个domian name直接相连。
当主机接收到RA消息中的DNS的options时,其处理过程如下:
首先分析dmp文件,查看crash现场:
CallStack直接给出了函数的调用链:
Icmpv6ReceiveDatagrams() -> Ipv6pHandleRouterAdvertisement() ->Ipv6pUpdateDNSSL() -> GetNextSuffixFromOption()
最终是在GetNextSuffixFromOption()
函数中报了内存页错误,导致最终的crash。
Windows IPv6堆栈为DNSSL
中的每个domain name分配一个256字节的buffer。 RFC 1035将域名限制为255个字节,因此domain name长度加上末尾的空字符刚好可以满足buffer的大小要求。 但是,漏洞代码处理该部分数据时,其上限等于DNSSL Option
中的剩余字节,可以超过256个字节。因此,漏洞代码可能会错误地消耗比为buffer分配的字节更多的字节,从而导致越界读。 如果buffer位于一个memory page的末尾,则该OOB读取就会导致BSOD。
分析使用的文件为Windows 10 1809 x64的tcpip.sys
文件,版本为10.0.17763.316
。
经过简单分析可以确认,调用链的顶层函数Icmpv6ReceiveDatagrams()
没有实质性的与漏洞触发密切相关的处理逻辑,故而跳过。
在Ipv6pHandleRouterAdvertisement()
函数中先对传入的RA消息做预处理,然后根据不同类型的option进入不同的处理流程:
首先读取Option结构的数据以及Domain name的长度: 接下来,确认读取的数据后,处理后缀部分: 这里在计算Suffixes的长度时,BytesToRead
会被限制在0x100字节长度范围内,也就是Suffixes的最大长度为256。然后调用GetNextSuffixFromOption()
函数读取Suffix。
在该函数中,在解析完一个DNS记录后,代码逻辑会来到以下位置: 这里的主要作用是跳过Domain Name中的空字符部分,遇到0就跳过,读取下一个数据,直到遇到非0值。但是在进行边界检查时,使用的条件参数BytesToRead_1
是可以进行控制的,从而可以实现绕过,进行越界读。
Ipv6pUpdateDNSSL()
函数下断,然后发送poc后断下,检查CallStack,确认断点触发流程与静态分析中的函数调用链一致: 此时的各寄存器情况如下: 这里重点看下rdx寄存器中的内容(漏洞函数的第2个参数): rdx中存放的是一个_NET_BUFFER
结构,其详细结构如下:
而且在其中找到了触发漏洞的ICMPv6的相关数据。继续向下,来到NdisGetDataBuffer()
函数的第1处调用:NdisGetDataBuffer()
函数的第1个参数为传入的_NET_BUFFER
结构。NdisGetDataBuffer()
函数声明如下:
如果NetBuffer
参数指向的NET_BUFFER
结构中的NET_BUFFER_DATA
部分的DataLength
字段的值小于BytesNeeded
参数的值,那么函数返回NULL。函数执行完成后,返回结果如下: 返回的恰好为DNSSL Option
的地址。
然后调用NetioAdvanceNetBuffer()
函数。执行NetioAdvanceNetBuffer()
函数之前,_NET_BUFFER
的结构如下所示: 在执行完NetioAdvanceNetBuffer()
函数后,结构变为: 此处该函数主要作用是前进8个字节进行数据读取,其整体流程及部分关键参数值如下: 继续向下,通过Length
字段计算BytesToRead
的长度,并对Lifetime
的值进行是否为0xffffffff
的检查: 后续会进行一些上下文初始化、事件记录等操作,然后进入循环,开始处理Option
中的Suffixes
。首先是BytesToRead
的取值范围限定(限制为0x100字节): 然后使用NdisGetDataBuffer()
函数读取Suffixes
: 在读取Suffixes
之前,BytesToRead
的值会被设置为0x100: 但是在读取完Suffixes
后,此时的BytesToRead
的值使用r15
进行重新赋值为0x110: 然后调用GetNextSuffixFromOption()
函数,该函数关键处理逻辑如下: 其参数中有受控参数,可以在上图代码处实现越界读。函数执行前各参数值如下:rcx
为Suffixes
的地址,rdx
为要读取的字节数,r8
为Suffix
的一个buffer,r9
的作用暂时未知,其值为0,推测可能用于读取时的计数:
在进行过memcpy
后,buffer中的情况如下: 此时已完成第一个Domain Name信息的复制。
后续继续执行,来到处理第2个Domain Name逻辑处: 第2次调用GetNextSuffixFromOption()
函数: 剩余流程大致与第1次相同,并利用memcpy
读取Suffix
到buffer中: %20 接下来进入一个循环:r14
中存放的是Suffixes
,其中存放了大量的0,dil
寄存器中为0,第1处cmp
恒成立。而在esi
中存放的是BytesToRead
,该值受控,在正常读取完第2个Domain Name后,该值为0x101。此时会继续处理后续的0字符,理论上应该在256范围内,但是因为BytesToRead
的值设置为大于0,所以会越界读: 在执行到esi = 8
时,r14
中的数据已不可读: 此时再继续执行就会造成crash: 也就是说,因为BytesToRead
的受控,尝试去进行了越界读取,但是读取到了内存页末尾无法再进行正常读取,从而导致crash。
基本条件
触发过程
attacker直接发送特制的ICMPv6路由广播数据包给target:
建立连接后,利用IPv6直接发送攻击数据包即可。
因为该漏洞直接走的IPv6,所以对于一些部署在IP层以上的防火墙方案就无法针对该漏洞进行流量检测,但是具备IP层流量检测的防火墙可以轻松检测恶意流量: 使用大量的0进行填充以触发漏洞。
针对Ipv6pUpdateDNSSL()
函数的补丁对比结果如下:
根据补丁对比结果,微软新增了对BytesToRead
值的校验,在读取完Suffixes
之后,为确保不发生越界读,新增了一次对BytesToRead
的校验,确保小于0x100。而在漏洞分析中,触发漏洞时该值是大于0x100的。
使用安装更新补丁后的Ipv6UpdateDNSSL()
函数进行验证,新增保证BytesToRead
的值最大为0x100的代码:
管理员启动powershell或cmd,输入以下命令检查所有网络IPv6接口的列表以及相应的索引号:
样例输出如下:
确认网络接口的RDNSS功能开启情况:
执行以下命令关闭RDNSS功能(将Idx number替换为要关闭的网络接口的Idx值):
样例输出如下:
此时再次确认接口的RDNSS开启情况,RDNSS功能已被关闭:
针对该漏洞,目前暂未发现漏洞原理侧的无损检测。
流量防御:需要监控IPv6的流量传输,对于Type
为134的路由广播数据包进行检测,确认其Type
为0x1f的DNSSL
的Padding部分是否有大于等于256个0字符。
终端防御:根据补丁对比结果,可以按照微软的补丁思路使用热补丁进行防御,对BytesToRead
的值再加一次校验。
暂时未知。
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;
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;
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
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.
PVOID NdisGetDataBuffer(
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课