Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
CVE-2024-38063- — 通过 IPv6 远程利用内核 | Kitploit
工具/GitHubGitHub/adminpentester/cve-2024-38063-
内存取证漏洞分析漏洞利用逆向工程学习与教育二进制利用
GitHubadminpentester/cve-2024-38063-

CVE-2024-38063-

通过 IPv6 远程利用内核

查看仓库
211年前尚未审核

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

CVE-2024-38063-

通过IPv6远程利用内核

CVE-2024-38063 - 通过IPv6远程利用内核 Marcus Hutchins

自从8月13日发布最新的Windows补丁以来,我一直沉浸在tcpip.sys(处理TCP/IP数据包的内核驱动程序)的深入研究之中。一个在Windows内核最易触及部分、CVSS评分高达9.8的漏洞,实在让我无法错过。我以前从未真正研究过IPv6(或负责解析它的驱动程序),所以我知道尝试逆向分析这个漏洞将极具挑战性,但也是一次很好的学习经历。

在很大程度上,tcpip.sys缺乏文档。我找到了几篇关于旧漏洞的利用分析文章:这里、这里和这里,但除此之外几乎别无他物。当我用谷歌英文搜索,排名靠前的搜索结果却是中文写的时候,我立刻意识到自己严重超出了能力范围,接下来会非常艰难,但我们必须学习。尽管谷歌翻译效果平平,但那篇文章提供了关于IPv6分片如何工作的极其详细的见解,给了我一个不错的起点。

后来,在谷歌搜索一些函数名时,我找到了Axel Souchet(又名0verclok)对同一个2021年漏洞的另一篇分析,这篇文章更深入地探讨了tcpip.sys的内部机制,并给了我足够的信息来定义几个未文档化的结构。 史上最简单的补丁分析

通常,即使是逆向分析补丁,找出哪个代码变更对应漏洞,也可能需要数天甚至数周,但这次几乎是瞬间完成的。实际上这太简单了,以至于社交媒体上有多个人告诉我我搞错了,漏洞应该在别处。我是否真的听了他们的话,然后浪费了一整天去逆向错误的驱动程序?我们可能永远不得而知。

整个驱动程序文件中只更改了一处,而事实证明,那确实就是漏洞所在。

补丁安装前后tcpip.sys的bindiff概览。

整个驱动中只有一个函数被修改。通常,我可能要花一整天去检查20多个不同的函数变更,只为找出应该关注哪一个,但这次不用。

补丁前的Ipv6pProcessOptions()。

补丁后的Ipv6pProcessOptions()。

不仅只更改了一个函数,而且只修改了一行代码。

名称极长的Feature_2660322619__private_IsEnabledDeviceUsage_3()函数是微软有时添加的,用于支持部分补丁回滚。该调用检查一个全局标志或注册表设置,如果设置,该函数将返回false,从而执行原始代码而非补丁版本。

微软这样做的原因是安全补丁有时会无意中破坏功能,因此这个设置允许管理员在不解安装整个月度补丁汇总、不严重削弱系统安全性的情况下,撤销单个漏洞的补丁。

考虑到这一点,很明显这个补丁所做的就是将调用IppSendErrorList()替换为IppSendError(),这给我们一个线索:问题出在某类列表上。史上最简单的补丁差异(至少我当时是这么想的)。 漏洞可选,利用必行

逆向分析补丁找到变更的代码只是挑战的一半(或者在这里不到0.1%)。其余过程包括逆向足够多的代码库以理解发生了什么,弄清楚补丁修复的是哪种漏洞,如何构造请求以到达目标代码,以及什么状态下会导致可利用的条件。

第一部分很容易。变更在Ipv6pProcessOptions()中,这告诉我们它与IPv6有关,并且涉及处理选项。所以,快速查阅RFC就能明确IPv6选项是什么以及在哪里可以找到。

来自维基百科的目的选项头布局。

好的,很酷。我们要找的似乎是目的选项头,它紧挨在主IPv6头部之后。让我们使用Python库'scapy'来构造一个测试IPv6数据包。

注意:为缓解使用伪造IP地址的DDoS攻击,Windows限制了构造原始IP数据包的能力。因此,我选择使用Linux来开发我的概念验证。虽然Linux确实允许用户构造和发送原始的第2层和第3层数据包,但这要求Python脚本以root身份运行。

import sys import struct from scapy.all import *

def send_ipv6_option_packet(dest_ip): ethernet_header = Ether() ip_header = IPv6(dst=dest_ip) options_header = IPv6ExtHdrDestOpt() sendp(ethernet_header / ip_header / options_header)

if len(sys.argv) < 2: print('Use: python3 script.py <target_ipv6_address>') exit(-1)

send_ipv6_option_packet(sys.argv[1])

在tcpip!Ipv6pProcessOptions上设置断点,然后运行脚本,很明显只需发送一个带有空选项结构的IPv6数据包即可到达易受攻击的函数。然后我尝试向结构中添加一些无效选项,看看能否到达对IppSendErrorList()的调用。

简要代码审查表明,几乎任何无效的选项格式化都可能触发对IppSendErrorList的调用。因此,我决定使用长度无效(小于65535字节)的巨型数据包选项。

options_header = IPv6ExtHdrDestOpt(options=[Jumbo(jumboplen=0x1337)])

那么,IppSendErrorList()实际上做什么呢?代码非常简单。

整个IppSendErrorList函数。

代码遍历一个链表,并对列表中的每个项调用IppSendError()。再次,星辰齐聚,事情到目前为止都很简单。如果IppSendErrorList只是对列表中的每个项调用IppSendError,并且补丁将对IppSendErrorList的调用替换为IppSendError,那么问题出现在对列表中第一个以外的项调用IppSendError时。

那么,这是什么列表,我们如何创建一个呢? 他在列清单,他在检查……52,567次

从这里开始,事情从显而易见变得异常困难,尽管我认为很大程度上这是由于我仅有的两个可用脑细胞中的一个正忙于对抗一场严重的COVID感染。我花了几天时间理解代码的各个部分,然后睡着了,接着忘记了我已经弄明白了什么。整个过程需要超过一周的时间来逆向tcpip.sys的部分内容,以弄清楚到底发生了什么。但Axel的博客文章非常有帮助。

通过查看Axel逆向的函数和结构,以及它们被传递给哪些其他函数,很明显,传递给Ipv6pProcessOptions()的唯一参数就是文章中定义的同一个packet_t结构。本质上,传递给Ipv6pProcessOptions并由IppSendErrorList遍历的指针,是一个数据包的链表。

所以,我在Ipv6pProcessOptions()上设置了一个断点,并检查了列表。

list->Next 条目为 NULL。

每次我的断点被命中时,列表中只包含一个数据包。我花了比我自己愿意承认的更长的时间来弄清楚为什么以及如何让我的列表变成一个真正的列表。我首先想到的是IPv6分片:IPv6允许发送方将大数据包分割成多个较小的数据包,将它们保存在一个列表中是合理的。

经过大量逆向分析,我确认我的假设是正确的,尽管分片列表与我们这里处理的列表无关。

实际上,我完全是偶然找到答案的。偶尔,列表会填充起来,但原因不明。在绕了很多圈子之后,我意识到当我的内核断点被触发时,它会暂停整个内核,导致网络适配器累积数据包。当内核恢复时,这些数据包会以整洁的列表形式沿着协议栈向下传递给tcpip.sys。这仅发生在我发送数据包时内核处于暂停状态,但数据包在下一次断点命中之前没有被处理。

这种行为可能是一种性能优化,在低吞吐量时,内核逐个处理数据包,但在高流量时,数据包被组织成列表并批量处理。很可能列表是根据协议和源地址等因素进行分离的,以加快处理速度,所以我们的列表应该只包含我们发送的IPv6数据包。 哟,老兄,我听说你喜欢DoS

现在我们知道了在高吞吐量时数据包会被合并成列表,那么最简单的方法就很清楚了。具有讽刺意味的是,我们的DoS概念验证将不得不使用DoS来触发DoS条件。如果我们用IPv6数据包突发泛洪系统,我们应该能够得到一个不错的列表传递给IppSendErrorList()。

起初,无论我发送多少数据包,只有暂停内核才能让列表的条目数 n > 1。但是……因为我们使用的是Python(极其缓慢),并且在虚拟机中(双倍缓慢),我们可能需要调整一些设置。为了对抗我攻击系统上的虚拟机嵌套,我决定简单地将目标虚拟机重新配置为只使用一个CPU核心。

太好了!数据包列表现在是一个包含很多条目的列表!

看来,虚拟机中的虚拟机不是进行DoS的最佳选择,谁知道呢?但我们最终还是让它生效了。现在,我们只需要弄清楚IppSendError()做了什么,以及问题出在哪一部分。 更多的逆向……再次……永远……

经过大量逆向分析,IppSendError的功能变得更加清晰。在通常情况下,它只是通过将net_buffer_list->Status设置为0xC000021B(STATUS_DATA_NOT_ACCEPTED)来禁用数据包。然后,它会向发送方传输一个包含错误数据包信息的ICMP错误。

IppSendError的两个相关部分。

我首先检查的是tcpip.sys中是否有任何函数忽略net_buffer_list->Status的值。这会导致驱动程序处理处于未定义或意外状态的数据包,希望导致可利用的条件。

负责处理数据包的主循环。

由于负责调用所有解析函数的循环被包装在一个错误检查中(这意味着一旦设置了错误代码,我们就无法继续),我认为这并非正确的探索方向。相反,我决定回到IppSendError,看看在设置错误代码之前是否有任何代码路径修改了数据包状态,这可能导致竞态条件。

在更多逆向分析之后,我在IppSendError的底部附近找到了以下代码。

IppSendError中一个将packet_size设置为零的代码路径。

当IppSendErrorList(因此也是IppSendError)以always_send_icmp参数设置为true被调用时,它似乎会尝试向列表中的每个数据包发送ICMP错误。

然后,出于只有上帝知道的原因,它到达了一个代码块,其中packet->packet_size字段被设置为零。

为了将always_send_icmp设置为true,我们只需在选项头处理中通过将'Option Type'值设置为任何大于0x80的数字来引发特定错误。

def build_malicious_option(next_header, header_length, option_type, option_length): dest_options_header = 60

root@kitploit:~
options_header = struct.pack('BBBB', next_header, header_length, option_type, option_length) + b'1337'
return Ether(dst=mac_addr) / IPv6(dst=ip_addr, nh=dest_options_header) / raw(options_header)

packet = build_malicious_option(next_header=59, header_length=0, option_type=0x81, option_length=0) sendp(packet)

但是,将packet_size设置为零不应该会破坏解析器吗?

来自负责处理数据包的主循环的片段。

数据包处理器只是根据packet->next_header的值调用一个VTable函数,该值在预解析期间设置后保持不变。这使得数据包处理能够继续,甚至让我们能够控制执行哪个处理过程。

因为packet->next_header值是从IPv6数据包的'Next Header'字段获取的,我们可以将其设置为任何有效的IPv6头部值,然后循环将调用相应的解析器。这给了我们大量的潜在攻击面。

IPv6数据包格式。

剩下的就是找到IPv6解析器中某个可达的部分,它对packet_size字段做了一些愚蠢的事情。 回到分片

我决定首先查看IPv6分片解析器,因为那里是旧的CVE-2021-24086漏洞所在,所以似乎是寻找更多疯狂代码的好地方。

嗯……差一点,但还是差得远。

我们这里确实有一个漏洞,但不是RCE。

本质上,在大多数CPU上,寄存器是循环的。如果你将寄存器递增超过其最大值,它会绕回到零。同样,如果你将其递减到低于其最小值,它会绕回到最大值。这分别被称为整数溢出和整数下溢。对于有符号整数,这种行为略有不同,但我们这里不处理有符号整数。

第一行,fragment_size = LOWORD(packet->packet_size) - 0x30,包含以下ASM代码:

计算分片大小的ASM代码。

AX是EAX寄存器的低16位。虽然EAX寄存器是32位的,但AX就像它自己的16位寄存器一样工作,因此任何溢出或下溢都局限于AX,不会影响EAX寄存器的其余部分。这非常方便,因为EAX寄存器的下溢会导致40亿的值,这将导致尝试分配4GB内存,很可能失败。

由于packet->packet_size的值为零,这段代码将ax设置为零,然后从中减去0x30。

正常情况下,数据包头是0x30字节,所以packet_size - 0x30是分片数据的大小。

在我们的例子中,packet->packet_size为0,所以即使减去1,也会导致寄存器绕回到最大可能的16位整数值(0xFFFF)。因为我们减去0x30,AX的值会下溢并变为MAX_VALUE - 0x2F,即0xFFD0,也就是65,488。

不幸的是,由于相同的计算既用于内存分配又用于复制数据,我们不会得到缓冲区溢出。我认为RtlCopyMdlToBuffer()也会对源缓冲区进行边界检查,所以我们甚至不会得到越界读取。但是,我们也并非空手而归。

因为ExAllocatePoolWithTagPriority()不会清零已分配的内存,而RtlCopyMdlToBuffer()只复制实际可用的数据量,我们得到了大约65kb的未初始化内核内存。由于内存地址在释放后被回收,该缓冲区很可能填充了先前存储在地址上的任何内容。如果我们能利用分片构造一个回发给我们的数据包,比如ICMP Echo请求,我们或许能泄露随机内核内存,从而实现ASLR绕过。

除此之外,代码还将reassembly->fragment_size设置为下溢的16位整数(65,488),所以现在我们有两个独立的变量,可能用于造成缓冲区溢出。 被击败,但未被打倒

不幸的是(或者说幸运,因为可能为我节省了大量时间),有人抢先了一步。在我找到使用下溢整数之一触发缓冲区溢出的地方之前,@ynwarcs找到了答案并发布了PoC。这解决了我的最后一块拼图。

解决方案(或其中之一)是Ipv6pReassemblyTimeout()。虽然我们不能在初始分片处理中造成溢出,但显然可以在清理期间造成。

IPv6分片会在内存中保留,直到满足以下三个条件之一:

  • 我们的分片错误严重到系统告诉我们必须停止。
  • 我们发送一个'More'字段设置为0的分片,表示这是最后一个分片,系统将开始重组。
  • 我们在超时期限(60秒)内没有发送最后一个分片,系统将丢弃这些分片。

Ipv6pReassemblyTimeout()在条件3下被调用,所以让我们看看如何利用它。

这正是我们需要的!

之前,我们的问题是代码对内存分配和复制操作使用了完全相同的计算。而这段代码则不然。让我们更深入地查看ASM,了解它如何被利用。

负责计算分配大小的汇编代码。

如你在这里所见,计算的第一部分(fragment_list->net_buffer_length + reassembly->packet_length + 8)使用了16位DX寄存器。

如果你还记得之前,我们将reassembly->packet_length下溢为0xFFD0。所以,DX寄存器在加上8个字节后为0xFFD8。如果fragment_list->net_buffer_length大于0x27(39字节),DX将会溢出并重置为零。

fragment_list->net_buffer_length应该约为0x38字节,所以它将导致DX寄存器溢出为8。在添加0x28字节后,我们只会得到48字节的内存分配。

由于后续的memmove()调用仅使用未修改的reassembly->packet_length值作为大小,这将导致将65,488字节从reassembly->payload复制到一个30字节的缓冲区中。一个很好的附加好处是,复制的数据大部分来自分片负载,这些数据由我们控制,可以是任意格式的任意数据,因此我们得到了一个相当不错且可控的基于内核池的缓冲区溢出。

为了有机会触发漏洞,我们需要在调用IppSendErrorList时,有一个或多个分片数据包位于链表中格式错误选项数据包之后。然而,根据我的测试,这似乎并不能保证利用成功。我认为还需要满足其他一些条件。我怀疑(但尚未确认)IppSendError中的同步代码意味着我们还需要赢得一个竞态条件。

下载工具