在6月的“补丁星期二”期间,微软发布了CVE-2024-30078的修复程序。该漏洞的严重性被标记为重要,其影响是远程代码执行 (RCE)。
在阅读了微软的公告后,这个漏洞引起了我们的兴趣。未经身份验证的攻击者似乎可以向相邻系统发送恶意数据包,从而可能启用远程代码执行。即使攻击者必须在目标系统附近,通过无线电传输来发送和接收数据包才能利用此漏洞,但通过无线实现的RCE仍然非常吸引人。
我们的分析是在Windows 11,版本23H2上进行的。
二进制差分分析
该漏洞位于nwifi.sys
Wi-Fi 驱动程序中。我们使用BinDiff对比了该驱动程序的两个版本:
-
版本10.0.22621.3527 – 存在漏洞: SHA1 788E6FD6D60F3CD5A6FAC5C14883A4A3EF53A355
-
版本10.0.22621.3733 – 修补后的版本: SHA1 BF5871100143804B77185314BD4DD433AFAC816B
修补程序应用在名为Dot11Translate80211ToEthernetNdisPacket()
的函数中:
在修补后的版本中,添加了以下检查:
调用图
在Dot11Translate80211ToEthernetNdisPacket()
函数的调用图中,我们可以看到该易受攻击的函数可通过接入点模式 (AP) 和站点模式 (STA) 被调用。
请注意: 此处IDA的图表不完全正确,ExtSTAReceivePacket()
函数并未直接调用,而只是被引用。
接入点模式 (AP): 也称为热点模式。在此模式下,Wi-Fi模块充当接入点,类似于路由器的功能。它创建一个无线网络,允许其他设备连接。
站点模式 (STA): STA模式允许设备连接到现有的无线网络。它将设备变为现有无线网络的客户端,从而实现互联网访问或与其他设备的通信。在STA模式下,设备成为网络中的一个节点,促进与其他设备的数据交换和通信。
我们决定专注于STA路径,因为它对我们的使用案例更有趣。
静态分析
nwifi.sys
驱动程序是一个NDIS过滤驱动程序。它位于协议驱动程序和微型端口驱动程序之间的驱动程序堆栈中。其预期用途是监控并在必要时修改从Wi-Fi发送或接收的数据包,并将它们传递给堆栈中的下一个驱动程序。
驱动程序必须使用NdisFRegisterFilterDriver()
例程注册回调。最有趣的回调是AttachHandler
和ReceiveNetBufferListsHandler
。
在此驱动程序中,ReceiveNetBufferListsHandler
回调存在于Pt6Receive()
函数中;该函数接收来自底层驱动程序的数据包。
尽管出于某种原因,IDA未在上述函数图中显示这一点,但我们可以在WinDbg中动态验证。实际的调用栈如下所示:
Pt6Receive()
接收到指向NET_BUFFER_LIST
结构 (NBL) 的指针,该结构由我们的数据包组成。然后它调用ExtSTARecvInitializeMSDUFromNBL()
函数,将NBL结构转换为名为MSDU的结构。
MSDU
结构看起来像以下代码片段:
struct MSDU_struct {
uint64_t info6;
char flags;
uint32_t llc_offset;
uint32_t data_length;
uint32_t field_14;
uint64_t data_begin;
NET_BUFFER_LIST *nbl;
_MDL *first_mdl;
_MDL *last_mdl;
uint32_t offset;
uint32_t nbl_data_length;
};
稍后,MSDU
结构通过ExtSTAReceivePacket()
和ExtSTAReceiveDataPacket()
函数传递给Dot11Translate80211ToEthernetNdisPacket()
函数。
从函数名称可以明显看出,它正在将IEEE 802.11数据包转换为以太网数据包。该函数将MSDU
结构作为其第二个参数传入。该结构包含指向传入_NET_BUFFER_LIST
的第一个_MDL
的指针,该指针包含要转换的数据包。转换通过在下一层前面格式化以太网头来完成。转换在放置原始数据包的同一内存区域内进行。
漏洞
通过一些逆向工程后,补丁变得更加容易理解了。Dot11Translate80211ToEthernetNdisPacket()
函数会读取 LLC 包的头部,以理解包中的下一层。在读取 LLC 头部之前,它会检查包的缓冲区是否有足够的数据。LLC 头部的大小为 8 个字节:
该检查在代码中如下所示:
如果下一层是 VLAN,则应该有额外的 4 个字节的 IEEE 802.1Q 头部,这 4 个字节也必须在包中存在:
在易受攻击的版本中,没有检查这 4 个字节的 IEEE 802.1Q 头部是否存在。然而,在打过补丁的版本中,这一检查已被加入,如下图所示:
在易受攻击的版本中,如果 LLC->Type == 0x8100
且在 LLC 的 8 个字节之后没有数据,则会发生越界读取(情况 #4)。覆盖发生在构建以太网头部的代码部分:
变量 v17
是构建以太网头部的偏移量。以太网头部的偏移量(v17)根据 LCC 和 IEEE 802.1Q 头部中的值计算出来。以太网头部可以放置在四种不同的情况下:
-
情况 1: LCC->type != 0x81 && LCC->type < 0x600
在这种情况下,v17
将等于LCC offset - 0xE
,以太网头部将在 LCC 头部之前构建:
-
情况 2: LCC->type != 0x81 && LCC->type >= 0x600
在这种情况下,v17
等于LCC offset - 0xE + 8
,以太网头部将覆盖 LCC 头部:
-
情况 3: LCC->type == 0x81 && vlanid < 0x600
在这种情况下v17 = LCC offset - 0xE + 4
。以太网头部将覆盖一半的 LCC 头部:
-
情况 4: LCC->type == 0x81 && vlanid >= 0x600
这正是我们需要的!在这种情况下,v17 = LCC offset - 0xE + 4 + 8
,以太网头部的其余部分将写入外部,由_MDL
描述的内存中:
不幸的是,放置包的缓冲区有其他限制,我们无法控制。
IEEE 802.1Q 头部的字段 tpid
会被检查,且 vlanid
字段必须正确。Dot11Translate80211ToEthernetNdisPacket()
函数检查 IEEE 802.1Q 头部,而我们无法控制该字段。此外,也无法保证这些字节是有用的。
动态分析
为了确认我们的分析,我们动态检查了一切,通过 Wi-Fi 向易受攻击的机器发送包。
正如微软公告中所指出的,攻击者不需要在目标系统上进行身份验证。然而,为了确保数据包被传递,我们必须与目标位于同一个 Wi-Fi 网络中,否则底层驱动程序会阻止这些包。
有几种方式可以执行攻击:
-
设置一个与目标使用的配置相同的虚假 AP。在这种情况下,目标将自动连接到我们的 AP; -
连接到与目标相同的 AP:这种方式将此攻击限制为公共网络,或者需要破解/已知 AP 的密钥。除此之外,还有另一个根本原因不走这条路。网络中存在其他设备的流量会使预测目标捕获哪些数据包变得更加困难。例如,如果我们使用喷洒技术,我们无法确定所有发送给目标的包在内存布局中适合攻击的成功。
假AP
由于我们首先需要能够通过Wi-Fi发送原始数据包,为了进行测试,我们使用Python和scapy库搭建了一个假AP。它的速度足够快,适合用于开发和原型设计,允许我们通过Wi-Fi发送原始数据包。
我们的设置包括两个虚拟机:一个Windows 11 23H2和一台Kali Linux机器。两者都连接了AC1200 Wi-Fi 5 USB适配器。
我们的AP需要执行以下基本功能:
-
信标:发送信标包以告知站点我们的存在。信标包必须每102.4毫秒发送一次; -
响应探测包(广播和定向); -
响应认证请求。对于我们的测试,不需要认证,因此我们使用了开放系统认证; -
响应关联请求。
在关联之后,站点使用DHCP、ARP和其他协议请求网络信息。这些数据包作为数据包发送。我们的有效载荷也必须作为数据包发送,以便能够到达目标函数:
可以使用以下代码片段生成有效载荷(数据包):
def send_payload_v1(self, destination, with_vlan):
radiotap = RadioTap()
dot11 = Dot11(type = 2, subtype = 0, addr1 = destination, addr2 = self.ap.mac, addr3 = self.ap.mac, SC = self.ap.next_sc(), FCfield='from-DS')
llc = LLC(dsap=0xaa, ssap=0xaa, ctrl=0x03) / SNAP(OUI=0x000000, code=0x8100)
packet = radiotap / dot11 / llc # here can be any additional bytes if needed
sendp(packet, iface=self.ap.interface, verbose=False)
调试
现在我们已准备好尝试动态攻击,我们需要在易受攻击的函数(Dot11Translate80211ToEthernetNdisPacket()
)上设置断点,启动假AP并连接到它。在STA成功完成所有连接步骤后,断点被触发:
在上图中我们可以看到我们的数据包,它的长度为0x20
字节。IEEE 802.11报头位于偏移量0x00
处,而LLC报头位于偏移量0x18
。
我们还可以看到LCC->type
是0x81
,因此我们应该通过检查IEEE 802.1Q报头的代码,尤其是tpid字段
。即便如此,tpid
值未能通过检查,因此该数据包不会被转换。
tpid
的12个低位必须为0才能转换数据包。
在这段代码中,溢出已经发生(如上图中的第一条指令所示)。
我们可以喷射具有正确值和大小的数据包,然后发送触发漏洞的数据包。这听起来不错,但在调试器中捕捉到真正的攻击数据包非常困难。所以我们模拟了攻击数据包之后字节正确设置的情况。
为了模拟,发送了另一个数据包,并编辑了其后的缓冲区,以创造触发漏洞的完美条件。
从下面的图片可以看出,这使我们绕过了tpid
检查。它还帮助我们通过vlanid
检查的正确路径,将以太网偏移量增加到8:
以下图片展示了易受越界写入攻击的代码。在这里,你可以看到rcx
寄存器被设置为构建以太网报头的指针。最后三条指令构建了以太网报头。从理论上讲,溢出将在最后两条指令时发生:
在触发漏洞之前,内存布局如下所示:
这里,我们可以看到以太网报头将在何处构建。红色下划线部分是溢出将覆盖的字节。
在执行完这三条指令后,内存布局如下所示:
这里可以看到,前2个字节被攻击者控制的源MAC地址的结尾覆盖了。接下来的两个字节保持不变,因为si
寄存器的值相同,并从同一位置读取。
可利用性
首先,为了获得被覆盖的状态,我们需要非常幸运,并且在数据包缓冲区后有正确的字节,这些字节将通过 tpid
和 vlanid
的检查。我们无法控制数据包中的这些字节,这导致了覆盖问题。我们可能可以在发送触发漏洞的数据包之前使用喷射技术,但还有其他限制:
-
为了利用此漏洞,我们需要某种形式的信息泄露漏洞,而目前缺乏这种漏洞。 -
被覆盖的内存也非常关键。它应该是一些有用的东西,比如一些指针。但是此缓冲区并未包含任何有用的东西。如果我们使用 !address
命令检查此缓冲区,将会得到以下结果:
这个内存区域看起来很奇怪(例如,区域大小)。使用 RamMap 检查此内存区域会显示内存的标签:
如果我们尝试使用 !poolused 4
命令检查此内存,它将显示该标签未知。
剩下唯一的事情就是理解这个内存区域是什么,并在其分配时设置一个断点。
这给了我们一个关于内存如何分配的想法;它是在将适配器连接到系统时分配的,并在断开连接时释放。分配的缓冲区大小为 0x7800
。
结论
经过分析,此漏洞的影响似乎远没有微软预期的那么严重。如果在我们的数据包之后紧接着另一个数据包,我们只能覆盖有限数量的字节,这种情况非常不可能发生,而且没有任何有趣的数据可以用来控制执行流程。无论如何,我们愿意被证明是错误的,因为这似乎是一个非常酷的利用向量。
参考资料
-
https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-30078 -
https://github.com/rpp0/scapy-fakeap -
https://www.amazon.com/Python-Scapy-Dot11-Programacion-pentesters/dp/1542748704 -
https://learn.microsoft.com/en-us/windows-hardware/drivers/network/ndis-filter-drivers
原文始发于微信公众号(3072):CVE-2024-30078 Windows WIFI 驱动 RCE漏洞分析