逆向工程tcpip.sys:死亡数据包机制解析(CVE-2021-24086)
日期:2021年4月15日
作者:Axel “0vercl0k” Souchet
分类:逆向工程
标签:tcpip.sys, CVE-2021-24086, Ipv6pReassembleDatagram, 分片, 递归分片
引言
自从我踏入计算机安全领域以来,真正的远程漏洞一直让我感到惊叹和着迷。所谓真正的远程漏洞,是指那些无需任何用户交互即可远程触发的漏洞,甚至不需要一次点击。因此,我总是密切关注这类漏洞。
2020年10月13日星期二,微软发布了一个针对CVE-2020-16898的补丁,这是一个影响Windows tcpip.sys内核模式驱动程序的漏洞,被称为“坏邻居”。以下是微软的描述:
当Windows TCP/IP堆栈不当处理ICMPv6路由器通告数据包时,存在远程代码执行漏洞。成功利用此漏洞的攻击者可以在目标服务器或客户端上执行代码。要利用此漏洞,攻击者必须向远程Windows计算机发送特制的ICMPv6路由器通告数据包。
此更新通过纠正Windows TCP/IP堆栈处理ICMPv6路由器通告数据包的方式来解决漏洞。
这个漏洞确实引起了我的注意:影响TCP/IP堆栈的远程漏洞似乎已经灭绝,而能够远程触发Windows内核中的内存损坏对攻击者来说非常有趣。令人着迷。
多年没有对比微软的补丁,我觉得这是一个有趣的练习。我知道我不是唯一一个研究它的人,因为这类独角兽漏洞会引起互联网黑客的广泛关注。事实上,我的朋友pi3非常快速地对比了补丁、编写了PoC并写了一篇博客文章,我甚至没有时间开始,哦好吧 :)
这就是为什么当微软博客中提到另一组在tcpip.sys中修复的漏洞时,我觉得这次我可能能够研究它们。再次,我知道我并不是唯一一个竞相编写CVE-2021-24086的第一个公开PoC的人,但不知何故,互联网保持沉默的时间足够长,让我完成了这个任务,这非常令人惊讶 :)
在这篇博客文章中,我将带你从零到BSoD的旅程。从对比补丁、逆向工程tcpip.sys,到努力编写CVE-2021-24086的PoC。如果你来这里是为了代码,没问题,它可以在我的github上找到:0vercl0k/CVE-2021-24086。
目录:
- 引言
- TL;DR
- 侦察
- 2021年对比微软补丁
- 逆向工程tcpip.sys
- 初步步骤
- 高级概述
- 放大视野
- NET_BUFFER & NET_BUFFER_LIST
- 解析IPv6数据包的机制
- IPv6分片的机制
- 理论与实践:Ipv6pReceiveFragment
- 隐藏在显而易见之处
- 制造死亡数据包:追逐幻影
- 制造死亡数据包:信仰之跃
- 结论
- 奖励:CVE-2021-24074
TL;DR
对于想要快速了解的读者,CVE-2021-24086是tcpip!Ipv6pReassembleDatagram中的一个NULL解引用,可以通过发送一系列特制数据包远程触发。问题的发生是因为代码处理网络缓冲区的方式:
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
|
void Ipv6pReassembleDatagram(Packet_t *Packet, Reassembly_t *Reassembly, char OldIrql)
{
// ...
const uint32_t UnfragmentableLength = Reassembly->UnfragmentableLength;
const uint32_t TotalLength = UnfragmentableLength + Reassembly->DataLength;
const uint32_t HeaderAndOptionsLength = UnfragmentableLength + sizeof(ipv6_header_t);
// …
NetBufferList = (_NET_BUFFER_LIST *)NetioAllocateAndReferenceNetBufferAndNetBufferList(
IppReassemblyNetBufferListsComplete,
Reassembly,
0,
0,
0,
0);
if ( !NetBufferList )
{
// ...
goto Bail_0;
}
FirstNetBuffer = NetBufferList->FirstNetBuffer;
if ( NetioRetreatNetBuffer(FirstNetBuffer, uint16_t(HeaderAndOptionsLength), 0) < 0 )
{
// ...
goto Bail_1;
}
Buffer = (ipv6_header_t *)NdisGetDataBuffer(FirstNetBuffer, HeaderAndOptionsLength, 0i64, 1u, 0);
//...
*Buffer = Reassembly->Ipv6;
|
一个新的NetBufferList(缩写为NBL)由NetioAllocateAndReferenceNetBufferAndNetBufferList分配,而NetioRetreatNetBuffer分配一个uint16_t(HeaderAndOptionsLength)字节的内存描述符列表(缩写为MDL)。这个从uint32_t的整数截断很重要。
一旦网络缓冲区被分配,NdisGetDataBuffer被调用来访问新网络缓冲区的连续数据块。但这次,HeaderAndOptionsLength没有被截断,这允许攻击者触发NdisGetDataBuffer中的一个特殊条件使其失败。当uint16_t(HeaderAndOptionsLength) != HeaderAndOptionsLength时,会命中这个条件。当函数失败时,它返回NULL,而Ipv6pReassembleDatagram盲目信任这个指针并进行内存写入,导致机器bugcheck。要实现这一点,你需要欺骗网络堆栈接收一个具有大量头的IPv6分片。以下是bugcheck的样子:
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
|
KDTARGET: Refreshing KD connection
*** Fatal System Error: 0x000000d1
(0x0000000000000000,0x0000000000000002,0x0000000000000001,0xFFFFF8054A5CDEBB)
Break instruction exception - code 80000003 (first chance)
A fatal system error has occurred.
Debugger entered on first try; Bugcheck callbacks have not been invoked.
A fatal system error has occurred.
nt!DbgBreakPointWithStatus:
fffff805`473c46a0 cc int 3
kd> kc
# Call Site
00 nt!DbgBreakPointWithStatus
01 nt!KiBugCheckDebugBreak
02 nt!KeBugCheck2
03 nt!KeBugCheckEx
04 nt!KiBugCheckDispatch
05 nt!KiPageFault
06 tcpip!Ipv6pReassembleDatagram
07 tcpip!Ipv6pReceiveFragment
08 tcpip!Ipv6pReceiveFragmentList
09 tcpip!IppReceiveHeaderBatch
0a tcpip!IppFlcReceivePacketsCore
0b tcpip!IpFlcReceivePackets
0c tcpip!FlpReceiveNonPreValidatedNetBufferListChain
0d tcpip!FlReceiveNetBufferListChainCalloutRoutine
0e nt!KeExpandKernelStackAndCalloutInternal
0f nt!KeExpandKernelStackAndCalloutEx
10 tcpip!FlReceiveNetBufferListChain
11 NDIS!ndisMIndicateNetBufferListsToOpen
12 NDIS!ndisMTopReceiveNetBufferLists
|
对于任何其他想要深入了解的人,让我们开始吧 :)
侦察
尽管Francisco Falcon已经写了一篇很酷的博客文章讨论他在这方面的