Executive Summary
Context handles are one of the most common building blocks in Microsoft RPC. Inside rpcrt4 a context handle behaves like an index: it maps to a memory region that usually holds a server-side object, much the way an object reference works elsewhere in Windows. Many RPC servers expose several distinct context-handle types from the same interface set — and that is where trouble starts. If the IDL marks a handle as FC_BINDING_CONTEXT and the interface code never checks the underlying object type, the RPC runtime will happily hand the raw object to a method that was written for a completely different handle type. The result is a classic type confusion reachable from a remote (or low-privileged local) client.
This article follows researcher k0shl from the first observation in ssdpsrv — where a reference-counter handle could be coerced into CloseHandle on an arbitrary process handle — through the runtime detail that explains why some servers are vulnerable and others throw invalid_handle, to a repeatable hunting method using NtObjectManager. The same pattern produced three CVEs: CVE-2025-48815 in ssdpsrv, CVE-2025-53143 in MQQM, and CVE-2025-54104 in mpssvc. The root cause is consistent: insufficient constraints in IDL definitions combined with missing type validation inside RPC interface functions.
A type-confusion pattern in Windows RPC servers
The story begins in early 2025, during a review of the attack surface of Windows HTTP services. While auditing the various HTTP components, k0shl noticed that ssdpsrv hosts not only an HTTP service but also an RPC server. Auditing that RPC server revealed something interesting: it exposes several different context-handle types — for example CONTEXT_HANDLE_TYPE and SYNC_HANDLE_TYPE — each managed by a different RPC interface:
_SSDPOpenRpc
_SSDPCloseRpc
_InitializeSyncHandle
_RemoveSyncHandle
Because a context handle is conceptually just an index into a memory region that stores an object — a mechanism similar to how object references are represented elsewhere in Windows — one question came to mind immediately: what happens if you pass a CONTEXT_HANDLE_TYPE object into an interface that expects a SYNC_HANDLE_TYPE?
CVE-2025-48815
Inside ssdpsrv!_RemoveSyncHandle, the SYNC_HANDLE is a specific handle value produced by the InitializeSyncHandle interface, which creates a semaphore object:
__int64 __fastcall InitializeSyncHandle(_QWORD *a1)
{
[...]
Semaphore = CreateSemaphoreExW(0i64, 0, 0x7FFFFFFF, 0i64, 0, 0x1F0003u);
[...]
*a1 = v4;
return 0i64;
}
_RemoveSyncHandle then simply calls CloseHandle on that value:
BOOL __fastcall RemoveSyncHandle(void **a1)
{
[...]
result = CloseHandle(v1);
*a1 = 0i64;
[...]
}
The problem: if you pass a context handle returned by _SSDPOpenRpc into RemoveSyncHandle, the call still succeeds — even though that context points to a CONTEXT_HANDLE object rather than a sync handle. That is a straightforward type confusion. What makes it dangerous is the object the open call hands back: the handle returned by _SSDPOpenRpc actually points at the global variable g_lSsdpRef, which tracks a reference count:
__int64 __fastcall SSDPOpenRpc(_QWORD *a1)
{
[...]
++g_lSsdpRef;
*a1 = &g_lSsdpRef;
[...]
}
So by calling _SSDPOpenRpc repeatedly, an attacker can drive the value behind the returned context handle — the reference count — to match any small integer handle value, for example 0x1C4 or 0x200. When that value is then passed to _RemoveSyncHandle, ssdpsrv ends up calling CloseHandle on an arbitrary handle accessible in the process context. Closing handles out from under a privileged service is a powerful primitive (handle-confusion / use-after-free style bugs frequently build on exactly this).
FC_SUPPLEMENT and FC_BINDING_CONTEXT
Once the behaviour was clear, the natural next step was to assume that many Windows RPC servers share the setup — multiple context-handle types within the same interface — and to go looking for more. Other RPC servers with comparable patterns turned up quickly, but when they were tested, RPC raised exception 0x6 (invalid_handle). Why does ssdpsrv allow cross-type usage while others reject it? Answering that meant reverse-engineering the RPC runtime.
A useful debugging trick: exceptions inside RPC servers generally do not break into the debugger by default. To catch exception 0x6 at the exact moment it is thrown, set:
sxe 00000006
Breaking at the throw site and walking the call stack led to the function NDRSContextUnmarshall2. Rather than analysing every call in RPCRT4 in detail, k0shl used a shortcut: set a breakpoint on the same function inside ssdpsrv and compare behaviour against the other target server. Surprisingly, the breakpoint never fired for ssdpsrv — meaning the root cause was not inside that function. Moving outward and comparing the surrounding logic surfaced the key difference: in the IDL of ssdpsrv the context-handle descriptor begins with 0x70, while in the other RPC server it begins with 0x75:
.rdata:000000018003AE04 db 70h
.rdata:000000018003AE05 db 0EDh
.rdata:0000000140017850 db 75h
.rdata:0000000140017851 db 70h
That suggested distinct context-handle types. The type definitions came from victorv’s blog (v-v.space/2023/09/06/rpc_readme), which also explains in detail how to identify parameter types in RPC and is well worth reading. In that post, 0x75 is SUPPLEMENT and 0x70 is BIND_CONTEXT:
FC64_BIND_CONTEXT = 112 0x70
FC64_SUPPLEMENT = 117 0x75
With those structure types in mind, the runtime behaviour observed during reversing makes sense: when the incoming context handle is of type SUPPLEMENT, rpcrt4 validates whether the context ID matches during unmarshalling, and throws exception 0x6 if it does not. When the handle is of type BIND_CONTEXT, however, rpcrt4 simply indexes the corresponding object and passes it straight into the RPC interface — no type check at all. That is precisely why ssdpsrv (a BIND_CONTEXT server) accepts the cross-type handle while the SUPPLEMENT servers reject it.
From binding_context to type confusion
From here the hunt becomes simple: find an RPC server that exposes multiple context handles under the same interface and you have a candidate attack surface. To do this at scale, k0shl used James Forshaw’s NtObjectManager, which parses an RPC server’s IDL and clearly distinguishes FC_BINDING_CONTEXT from FC_SUPPLEMENT. The filter is just to look for interfaces containing multiple occurrences of:
"[out] /* FC_BINDING_CONTEXT */"
CVE-2025-53143
Applying that filter to MQQM (the Message Queuing service) produced another instance of the same pattern. Feeding a mismatched BIND_CONTEXT handle through the transaction-commit path crashes inside CTransaction::StartPrepareRequest while it tries to OR a flag into a confused object pointer.
Crash dump:
0:034> r
rax=00000283529a8f50 rbx=00000283529a8f50 rcx=00007ffe440edb60
rdx=0000000000000001 rsi=00007ffe440eba28 rdi=0000000000000001
rip=00007ffe44027373 rsp=000000a4c2c7e9d0 rbp=0000000000000011
r8=7ffffffffffffffc r9=000000a4c1bb1000 r10=0000000000000000
r11=000000a4c2c7e830 r12=0000000000000001 r13=000000000000000e
r14=000000a4c2c7ee90 r15=0000000000000000
iopl=0 nv up ei pl nz na pe nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010202
MQQM!CTransaction::StartPrepareRequest+0x5b:
00007ffe`44027373 834b1420 or dword ptr [rbx+14h],20h ds:00000283`529a8f64=????????
Stack trace:
0:034> k
# Child-SP RetAddr Call Site
00 000000a4`c2c7e9d0 00007ffe`440265e6 MQQM!CTransaction::StartPrepareRequest+0x5b
01 000000a4`c2c7ea10 00007ffe`44037e01 MQQM!CTransaction::InternalCommit+0x62
02 000000a4`c2c7ea40 00007ffe`43ff81ab MQQM!QMDoCommitTransaction+0xcd
03 000000a4`c2c7ea70 00007ffe`43fc5685 MQQM!qmcomm_v1_0_S_QMCommitTransaction+0xb
04 000000a4`c2c7eaa0 00007ffe`68113882 RPCRT4!Ndr64StubWorker+0x862
05 000000a4`c2c7ead0 00007ffe`680c599c RPCRT4!NdrServerCallAll+0x3c
...
CVE-2025-54104
The same filter pointed at mpssvc (the Windows Firewall service). Here the confused object reaches fwbase!FwStringCopy, which dereferences an attacker-influenced pointer (rbx=0000000002000200) while walking what it believes is a string.
Crash dump:
0:013> r
rax=0000000000000000 rbx=0000000002000200 rcx=00007fffe5104ad8
rdx=00000080007fe188 rsi=00000080007fe188 rdi=0000008001ff9b10
rip=00007fffe50c3943 rsp=00000080007fe120 rbp=00000080007fe1f0
r8=00000080007fe088 r9=0000000000000000 r10=0000000000000000
r11=0000000000000246 r12=0000000000000000 r13=0000008001ff9c78
r14=00007fffe5104000 r15=0000008001ff9c60
iopl=0 nv up ei pl zr ac po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010256
fwbase!FwStringCopy+0x53:
00007fff`e50c3943 66833c4300 cmp word ptr [rbx+rax*2],0 ds:00000000`02000200=????
Stack trace:
0:013> k
# Child-SP RetAddr Call Site
00 00000080`007fe120 00007fff`d90fd4cd fwbase!FwStringCopy+0x53
01 00000080`007fe160 00007fff`d90f253b mpssvc!SvrImpl_FWUnregisterProduct+0x4d
02 00000080`007fe1b0 00007fff`e983de83 mpssvc!RPC_FWUnregisterProduct+0x9b
03 00000080`007fe1e0 00007fff`e9841e83 RPCRT4!Invoke+0x73
...
It is worth stressing that this class of bug is not universally present in RPC services. In practice, many interfaces that declare a context handle as BIND_CONTEXT do perform explicit validation — for instance checking the object type stored in the object value before using it. Type confusion only becomes possible when that validation is missing. The takeaway for IDL definitions is therefore:
- If the context-handle type is
SUPPLEMENT,rpcrt4enforces the checks automatically. - If the type is
BIND_CONTEXT, the interface function must explicitly validate the incoming object type to prevent type confusion.
Key Takeaways
- A context handle in Microsoft RPC is effectively an index to a server-side object; passing one handle type where another is expected is a real, reachable type confusion.
- The IDL descriptor byte encodes the handle class:
0x70=FC64_BIND_CONTEXT,0x75=FC64_SUPPLEMENT. SUPPLEMENThandles are validated automatically byrpcrt4during unmarshalling (mismatch → exception0x6,invalid_handle);BIND_CONTEXThandles are indexed and passed through with no runtime type check.- CVE-2025-48815 (
ssdpsrv) abuses_SSDPOpenRpc‘s reference-count handle to make_RemoveSyncHandleclose an arbitrary process handle. - The same pattern reappears in MQQM (CVE-2025-53143) and
mpssvc(CVE-2025-54104), confirming it is a class of bugs, not a one-off. - James Forshaw’s NtObjectManager makes the surface enumerable: filter for interfaces with multiple
[out] /* FC_BINDING_CONTEXT */handles. - Debugging tip:
sxe 00000006breaks at the exact RPC exception throw, which is how the runtime difference was isolated.
Defensive Recommendations
- Validate object type inside every
BIND_CONTEXTinterface. Never trust that the handle you received corresponds to the type your method expects — check a type tag in the object before dereferencing it. - Prefer
SUPPLEMENT-style handles where feasible so the RPC runtime performs context-ID validation for you during unmarshalling. - Tighten IDL constraints. Treat each context-handle type as a distinct, non-interchangeable type; avoid exposing several mutually confusable handle types from one interface without per-method validation.
- Audit your own RPC servers with NtObjectManager, flagging interfaces that expose multiple
FC_BINDING_CONTEXThandles as priority review targets. - Fuzz across handle types, deliberately feeding a handle obtained from one interface into methods of another to surface cross-type acceptance.
- Apply the relevant Microsoft patches for CVE-2025-48815, CVE-2025-53143 and CVE-2025-54104, and review SSDP/UPnP, MSMQ and Windows Firewall RPC exposure on your fleet.
- Reduce attack surface by disabling unneeded services (SSDP Discovery, Message Queuing) and restricting RPC endpoints to required clients.
Conclusion
The journey from a single odd observation in ssdpsrv to three CVEs illustrates how a small gap between IDL declarations and runtime behaviour can become a repeatable bug class. Because rpcrt4 validates SUPPLEMENT handles but trusts BIND_CONTEXT handles, any interface that omits its own type check inherits a type confusion for free. For defenders, the practical signal is concrete and searchable — multiple FC_BINDING_CONTEXT handles in one interface — and the fix is equally concrete: validate the object type before you use it.
Reference
- https://v-v.space/2023/09/06/rpc_readme/
- https://github.com/googleprojectzero/sandbox-attacksurface-analysis-tools/tree/main/NtObjectManager
Original text: “From context_handle to type confusion” by k0shl at Whereisk0Shl.

