威胁简报
恶意软件 / 漏洞攻击
在TrendAI Research Services的一份漏洞报告中,TrendAI Research团队的Richard Chen和Lucas Miller详细介绍了Windows Internet Key Exchange (IKE)服务中一个最近已修复的双重释放漏洞。该漏洞最初由微软的WARP & MORSE团队发现。成功利用该漏洞可能导致IKEEXT服务崩溃,甚至可能执行任意代码。以下是他们关于CVE-2026-33824的报告节选,并做了少量修改。
Windows Internet Key Exchange (IKEv2) 服务中报告了一个双重释放漏洞。该漏洞是由于处理密钥片段时出现错误造成的。
未经身份验证的远程攻击者可以通过向目标服务器发送精心构造的数据包来利用此漏洞。成功利用此漏洞可能导致 IKEEXT 服务崩溃,甚至可能执行任意代码。
漏洞
微软Windows操作系统包含服务器和桌面组件,并配有易于使用的图形用户界面。所有当前受支持的Windows版本都包含Internet密钥交换协议扩展,以支持虚拟专用网络(VPN)功能。
Windows 的 VPN 功能可加密主机之间的通信。ISAKMP 是 IPsec 主机用于建立安全关联的协商协议。它使用Internet 密钥交换 (IKE) 协议来协商加密通信的密钥。IKE 有两个版本:IKEv1 和 IKEv2。IKE 版本 1 (IKEv1) 和版本 2 (IKEv2) 消息的一般格式如下:
Field Length (Bytes) Description
-----------------------------------------------------------------------
IKE SA Initiator's SPI 8 Initiator's random ID (IKE Security Association)
IKE SA Responder's SPI 8 Responder's random ID (IKE Security Association)
Next Payload 1 Type of payload immediately after the header
Version 1 IKE version in use
Exchange Type 1 Type of exchange being used (e.g., IKE SA INIT \x22)
retransmission
Flags 1 Specifies options for the message
Message ID 4 Used to control the retransmission
Length 4 Length of IKE header + payload in Bytes (n+28)
Payloads n Payloads
有效载荷的类型由前一个有效载荷的“下一个有效载荷”标头或标头中的“下一个有效载荷”字段(在第一个有效载荷的情况下)决定。
Field Length (Bytes) Description
-----------------------------------------------------------------------------
Next Payload 1 Identifies what the next payload type is in the packet.
Critical+Reserved 1 Can be left zero.
Payload Length 2 Length of the current payload (m+4)
Payload Specific Data m Data specific to the payload type
IKEv2 支持RFC 7383中定义的消息分片。当 IKEv2 消息超过路径 MTU 时,它们可能会被拆分为多个加密分片有效载荷。本报告重点关注加密分片 (SKF) 有效载荷(类型 0x35)。SKF 有效载荷格式定义如下:
Field Length (Bytes) Description
-----------------------------------------------------------------------------
Next Payload 1 Identifies what the next payload type is in the packet.
Critical+Reserved 1 Can be left zero.
Payload Length 2 Length of the current payload (n+8)
Fragment Number 2 Position of this fragment
Total Fragments 2 Total number of fragments
Initialization Vector var IV for the encrypted fragment
Encrypted Data var Encrypted fragment payload data
Integrity Check Value var ICV (integrity checksum)
当 IKEv2 实现接收到数据片段时,它会将每个片段插入到一个有序列表中,并在接收到所有片段后重新组装这些片段。在 Windows 实现中,该函数IkeReinjectReassembledPacket()负责执行此重新组装操作。
Windows IKE 扩展库 (ikeext.dll) 中存在双重释放漏洞。该漏洞是由于在 IKEv2 片段重组过程中对堆分配的 blob 指针的所有权处理不当造成的。在 IKE_SA_INIT 交换期间,安全域供应商 ID 有效负载会导致IkeHandleSecurityRealmVendorId()分配一个 blob 并将其存储在偏移量为 0x208 的 MMSA(主模式安全关联)结构中。当片段化的 IKE_AUTH 消息被完全重组时,IkeReinjectReassembledPacket偏移量为 0x178 到 0x21F 的 MMSA 字段(包括偏移量为 0x208 的 blob 指针)会被复制到一个本地栈结构中。然后,该结构会被传递给另一个函数IkeQueueRecvRequest,该函数会将其浅复制到一个堆分配的工作项中。虽然IkeQueueRecvRequest对结构体中偏移量 0x10 处的重组缓冲区进行了深拷贝,但偏移量 0xC8 处的 Security Realm blob 指针仍然是浅拷贝,与 MMSA+0x208 处的原始数据存在别名。
当线程池处理排队的工作项时,IkeDestroyPacketContext会检查偏移量为 0xC8 的 blob 指针,并调用 WfpMemFree() 函数释放它(第一次释放)。此时,MMSA 结构仍然持有指向偏移量为 0x208 的同一分配的原始指针。随后,当 MMSA 被清理时IkeCleanupMMNegotiation,SA 引用计数会通过 free() 函数递减,最终触发IkeFreeMMSA函数释放 MMSA 偏移量为 0x208 的 blob 指针——该分配此前已被 free() 函数释放IkeDestroyPacketContext(第二次释放)。
远程未经身份验证的攻击者可以通过向目标服务器发送精心构造的 IKE_SA_INIT 消息,后跟两个或多个包含无效 IKE_AUTH 消息的加密片段 (SKF) 有效载荷来利用此漏洞。片段重组路径将浅拷贝 blob 指针,随后的 MMSA 清理操作将触发双重释放。成功利用此漏洞可能导致在 IKEEXT 服务 (SYSTEM) 的安全上下文中执行任意代码。
源代码详解
以下代码片段取自 IKEEXT.DLL 文件版本 10.0.20348.2849,并使用 IDA Pro 版本 8.3 进行反编译。TrendAI 添加的注释已突出显示。
__int64 IkeReinjectReassembledPacket{
void *pFragList, __int64 pMMSA, __int64 *pFragContext, __int64 pMMSACtx}
{
IKE_RECV_CONTEXT recvCtx;
memset(&recvCtx, 0, 0xF0);
dwReassembledSize = 0;
status = WfpMemAlloc(pFragList->dwTotalSize);
if ( !status ) {
// Copy of fragment context fields (0xA8 bytes)
recvCtx.sourceAddr = pFragContext[0]; // +0x00: source address
[ ...address and metadata fields copied via SSE moves... ]
recvCtx.destAddr = pFragContext[3]; // +0x30: dest address
[ ...continued... ]
recvCtx.pRealmBlobData_VULN = pFragContext[9]; // +0x90: SHALLOW COPY of blob ptr
recvCtx.pMMSACtxData = pFragContext[20]; // +0xA0: MMSA context
// Reassemble fragments into a single contiguous buffer
pCurEntry = pFragList->pHead;
pReassembledBuf = recvCtx.pReassembledBuf;
while ( pCurEntry != pFragList ) {
status = WfpUINT32Add(dwReassembledSize, pCurEntry->dwDataSize, &tmp);
if ( status )
goto cleanup;
memcpy(pReassembledBuf + dwReassembledSize, pCurEntry->pData,
pCurEntry->dwDataSize);
dwReassembledSize += pCurEntry->dwDataSize;
pCurEntry = pCurEntry->pFlink;
}
[ ...IKEv2 header fixup omitted for readability... ]
// Re-queue reassembled packet for IKEv2 processing
status = IkeQueueRecvRequest(&recvCtx, 1);
}
cleanup:
WfpMemFree(&recvCtx.pReassembledBuf);
// FREE #1 PATH: ClearFragList iterates the fragment list
ClearFragList(pFragList);
if ( status )
WfpReportError(status, "IkeReinjectReassembledPacket");
return status;
}
__int64 IkeQueueRecvRequest(__int64 pRecvCtx, int a2)
{
status = WfpMemAlloc(0xF0); // Allocate heap work item
if ( status )
goto error;
pWorkItem = pWorkItemAlloc;
// Shallow copy of entire IKE_RECV_CONTEXT (0xF0 bytes) into heap
// work item.
*(OWORD *)pWorkItem = *(OWORD *)pRecvCtx; // +0x00
*(OWORD *)(pWorkItem + 1) = *(OWORD *)(pRecvCtx + 1); // +0x10
[ ...14 more 16-byte copies... ]
*(OWORD *)(pWorkItem + 14) = *(OWORD *)(pRecvCtx + 14); // +0xE0
// Deep-copy the reassembly buffer (offset +0x10)
status = WfpMemAlloc(*(DWORD *)(pRecvCtx + 24));
[ ...memcpy of reassembly buffer... ]
// Queue for thread pool processing
IkeQueueWorkItem(IkeHandleRecvRequest, pWorkItem);
[ ... ]
}
__int64 ClearFragList(void *pFragList)
{
pEntry = *(IKE_FRAG_ENTRY **)pFragList;
while ( pEntry != pFragList ) {
pNext = pEntry->pFlink;
FreeFragEntry(pEntry); // Free each fragment entry and its data
pEntry = pNext;
}
pFragList->pTail = pFragList; // Reset list to empty
pFragList->pHead = pFragList;
pFragList->dwCount = 0;
return 0;
}
void FreeFragEntry(IKE_FRAG_ENTRY *pFragEntry)
{
if ( pFragEntry ) {
WfpMemFree(&pFragEntry->pData); // FREE #1: frees blob data at entry+0x20
WfpMemFree(pFragEntry); // Free the entry structure itself
}
}
void IkeCleanupMMNegotiation(LPCRITICAL_SECTION pMMSA, ...)
{
[ ...truncated for readability... ]
pFilterToSA = *(pMMSA + 1136);
if ( pFilterToSA ) {
*(pMMSA + 1136) = 0;
RtlRemoveEntryHashTable(&gIkeExtGlobals[62].LockCount, pFilterToSA, 0);
WfpMemFree(&pFilterToSA);
}
[ ...truncated for readability... ]
IkeMoveMMNToZombieList(pMMSA);
}
__int64 IkeFreeMMSA(__int64 pMMSA)
{
TraceLogHelper("IkeFreeMMSA", 1);
// FREE #2: Free Security Realm blob at MMSA offset +0x208
if ( *(pMMSA + 520) ) {
*(DWORD *)(pMMSA + 512) = 0;
WfpMemFree(pMMSA + 520);
}
// Free FilterToSA entry at MMSA+0x470
pFilterToSA = *(pMMSA + 1136);
if ( pFilterToSA ) {
*(pMMSA + 1136) = 0;
RtlRemoveEntryHashTable(&gIkeExtGlobals[62].LockCount, pFilterToSA, 0);
WfpMemFree(&pFilterToSA);
}
[ ...truncated for readability... ]
}
检测指导
为了检测利用此漏洞的攻击,检测设备必须监控和解析 UDP 端口 500 和 4500 上的流量。IKE 通用格式、有效载荷字段和加密分片 (SKF) 有效载荷格式如上所示。
检测设备应监控所有入站 IKE 流量。检测需要关联同一 IKE 会话中的两个数据包:一个是携带 Microsoft 安全域供应商 ID 的 IKE_SA_INIT 请求,另一个是分片的 IKE_AUTH 请求。单独来看,这两个数据包都不是恶意的;必须从同一来源按顺序观察到它们。
在UDP有效载荷的第17个字节偏移处,设备应检查三字节序列20 22 08,该序列对应于IKEv2版本标识符(0x20)、IKE_SA_INIT交换类型(0x22)和发起方标志(0x08)。然后,设备应扫描数据包的剩余部分,查找16字节序列68 6a 8c bd fe 63 4b 40 51 46 fb 2b af 33 e9 e8,该序列是Microsoft安全域供应商ID。如果满足这两个条件,设备应遵循以下指南。
对于来自同一源的后续数据包,设备应检查UDP有效载荷偏移量16至23处的字节。在偏移量16处,该四字节序列35 20 23 08标识加密分片有效载荷(SKF,类型0x35)、IKEv2版本(0x20)、IKE_AUTH交换类型(0x23)和发起方标志(0x08)。如果找到该序列,检测设备应检查偏移量20处并查找该四字节序列00 00 00 01。如果找到,则应将该流量视为恶意流量;很可能正在进行利用此漏洞的攻击。
注释
- 所有多字节值均应视为大端字节序。
- 检测端口 4500 上的流量时,IKE 数据包前面会添加一个 4 字节的非 ESP 标记(
\x00\x00\x00\x00),将所有 IKE 头部内容偏移量移 4 个字节。
结论
微软已在 2026 年 4 月的版本更新中修复了此漏洞。他们指出,在补丁测试和部署期间,有两种缓解措施可以防止漏洞被利用:
- 阻止不使用 IKE 的系统通过 UDP 端口 500 和 4500 进行入站流量。
- 对于需要 IKE 的系统,配置防火墙规则,仅允许来自已知对等地址的入站流量通过 UDP 端口 500 和 4500。
一旦应用安全补丁,这些缓解措施可能会被移除。彻底修复此漏洞的唯一方法是应用供应商提供的更新。
云栈社区提醒,远程代码执行漏洞的检测与防御需结合网络层防护,尤其是对 UDP端口 的严格过滤。