cea.sys — Unnecessary caller process-handle exposure over broker RPC (CWE-668) fixed
KB5078752
1. Overview
- Unpatched Binary:
cea_unpatched.sys - Patched Binary:
cea_patched.sys - Overall Similarity Score: 0.9861
- Diff Statistics: 151 matched functions, 4 changed functions, 147 identical functions, and 0 unmatched functions in either direction. Of the 4 changed functions, only two differ in their instruction stream (
BripCreateBrokeredEvent,BriDeleteEventImpl); the other two (BripMapRpcStatus,BripCreateEventContext) are byte-for-byte the same code relocated to new addresses. - Verdict: The patch removes an unnecessary duplication of a handle to the current (caller's) process and stops transmitting that handle across a kernel-mode RPC call to the broker service. This is a least-privilege / attack-surface-reduction hardening at the kernel-to-broker boundary. It is not a memory-corruption bug and no attacker-reachable exploit primitive was demonstrated.
2. Vulnerability Summary
- Severity: Low
- Vulnerability Class: Exposure of a process handle across a trust boundary / least-privilege violation (CWE-668, CWE-269)
- Affected Function:
BripCreateBrokeredEventat0x1C0006AB0
Root Cause:
When handling a brokered-event creation request, the unpatched driver calls NtDuplicateObject((HANDLE)-1, (HANDLE)-1, (HANDLE)-1, &TargetHandle, 0x101000, 0, 0). All three process/handle arguments are the current-process pseudo-handle (-1), so this duplicates the current process into a real handle in the current process's handle table with access mask 0x101000 (SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION). That handle value is then placed into the argument list of NdrClientCall3 (a client-side kernel RPC call) and sent to the broker service, after which the driver closes the handle with NtClose.
The patch deletes the NtDuplicateObject call, its NtClose cleanup, and the handle argument to NdrClientCall3; the RPC call now carries only the remaining event parameters (the broker type id a1, the GUID, the SID, and the output buffers).
Impact assessment (why Low):
- There is no memory corruption; this is purely a change in what capability is handed over an interface.
- The recipient is the broker service, which is the privileged party in this design. A handle to the caller's own process carrying only PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE gives a privileged broker nothing it could not already obtain about a less-privileged caller, so no privilege boundary is crossed in the escalation direction.
- The entry points (BriCreateBrokeredEvent / BriCreateBrokeredEventEx) are exported kernel routines that this driver services as an RPC client; the binary contains no IOCTL/device dispatch reaching them, so a direct unprivileged user-mode trigger is not established from these binaries.
The honest characterization is therefore removal of an unnecessary process-handle exposure (least-privilege hardening), not a delivered fix for a demonstrable, reachable exploit.
Entry Point & Internal Call Chain:
1. A caller reaches the exported BriCreateBrokeredEvent (0x1C0006990) or BriCreateBrokeredEventEx (0x1C00069E0).
2. BriCreateBrokeredEventEx calls the internal BripCreateBrokeredEvent (0x1C0006AB0) at 0x1C0006A79.
3. In the unpatched build, BripCreateBrokeredEvent calls NtDuplicateObject (0x1C0006BB1), producing a real handle to the current process.
4. BripCreateBrokeredEvent passes that handle as an argument to NdrClientCall3 (0x1C0006C91), transmitting it to the broker.
3. Decompilation Diff
The relevant portion of the real decompilation of BripCreateBrokeredEvent (0x1C0006AB0).
// --- UNPATCHED BripCreateBrokeredEvent @ 0x1C0006AB0 ---
v14 = RtlRunOnceExecuteOnce(&BrokerInternalClientInitOnce, BrpInitOnceRunOnceCallback, nullptr, nullptr);
if ( v14 >= 0 )
{
// Duplicates a real handle to the current process (access 0x101000)
v14 = NtDuplicateObject((HANDLE)-1, (HANDLE)-1, (HANDLE)-1, &TargetHandle, 0x101000u, 0, 0);
if ( v14 >= 0 )
{
*a8 = 0;
LODWORD(v15.Pointer) = BripCreateRpcBindingHandle(a2);
if ( LODWORD(v15.Pointer) != 0 )
goto LABEL_9;
v32 = a4;
v31 = RtlLengthSid(a4);
v17 = TargetHandle; // duplicated process handle
v18 = UuidEqual(a2, (UUID *)&g_SmsRouterBrokerId, &Status);
v19 = (MIDL_STUBLESS_PROXY_INFO *)&off_1C000B5A0;
if ( v18 == 0 )
v19 = (MIDL_STUBLESS_PROXY_INFO *)&pProxyInfo;
// Handle (v17) passed to the broker over RPC:
v15.Pointer = NdrClientCall3(v19, 0, nullptr, Binding, v17, /*a1*/v23, a3, &v31, ...).Pointer;
...
}
}
...
if ( TargetHandle != nullptr )
NtClose(TargetHandle);
// --- PATCHED BripCreateBrokeredEvent @ 0x1C0006AB0 ---
v14 = RtlRunOnceExecuteOnce(&BrokerInternalClientInitOnce, BrpInitOnceRunOnceCallback, nullptr, nullptr);
if ( v14 >= 0 )
{
// NtDuplicateObject removed entirely; no TargetHandle local remains.
*a8 = 0;
LODWORD(v15.Pointer) = BripCreateRpcBindingHandle(a2);
if ( LODWORD(v15.Pointer) != 0
|| (v27 = a4,
v26 = RtlLengthSid(a4),
v19 = a1,
// RPC call carries no process handle; a1 now sits in that slot:
v15.Pointer = NdrClientCall3((MIDL_STUBLESS_PROXY_INFO *)&pProxyInfo, 0, nullptr,
Binding, v19, a3, &v26, ...).Pointer,
LODWORD(v15.Pointer) != 0) )
...
}
// No NtClose: no handle is created.
4. Assembly Analysis
The .asm disassembly is authoritative. The following are the exact instructions from BripCreateBrokeredEvent (0x1C0006AB0) in the unpatched build.
Unpatched — NtDuplicateObject setup and call
00000001C0006B91 and dword ptr [rsp+140h+var_110], 0 ; Options = 0
00000001C0006B96 lea r9, [rbp+40h+TargetHandle] ; TargetHandle (output)
00000001C0006B9A and dword ptr [rsp+140h+var_118], 0 ; HandleAttributes = 0
00000001C0006B9F or rcx, 0FFFFFFFFFFFFFFFFh ; SourceProcessHandle = -1 (current process)
00000001C0006BA3 mov r8, rcx ; TargetProcessHandle = -1
00000001C0006BA6 mov [rsp+140h+DesiredAccess], 101000h ; DesiredAccess = SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION
00000001C0006BAE mov rdx, rcx ; SourceHandle = -1
00000001C0006BB1 call cs:__imp_NtDuplicateObject ; ntoskrnl
Unpatched — handle marshaled into the RPC call
00000001C0006BFD mov rbx, [rbp+40h+TargetHandle] ; rbx = duplicated process handle
00000001C0006C83 mov qword ptr [rsp+140h+DesiredAccess], rbx ; placed into NdrClientCall3 argument slot
00000001C0006C88 jnz short loc_1C0006C91
00000001C0006C91 call cs:__imp_NdrClientCall3 ; msrpc — handle sent to broker
Unpatched — handle cleanup at exit
00000001C0006CDC mov rcx, [rbp+40h+TargetHandle] ; Handle
00000001C0006CE0 test rcx, rcx
00000001C0006CE3 jz short loc_1C0006CF1
00000001C0006CE5 call cs:__imp_NtClose ; ntoskrnl
Patched Sequence
In the patched build the entire NtDuplicateObject setup (0x1C0006B91–0x1C0006BB1) and the NtClose cleanup are absent. The NdrClientCall3 call moves to 0x1C0006C39, and its argument list no longer contains a handle; the register r12d/a1 (the broker type id) occupies the slot previously used for the handle. The stack frame shrinks from 0x108 to 0xE8 bytes because the handle and related locals are gone.
5. Trigger Conditions
To reach the removed code path in the unpatched build:
- Reach the export: A component calls
BriCreateBrokeredEvent(0x1C0006990) orBriCreateBrokeredEventEx(0x1C00069E0). These are exported kernel-library routines; the binaries do not expose an IOCTL/device path that reaches them, so no unprivileged user-mode trigger is demonstrated here. - Pass the null checks:
a2(GUID),a4(SID),a8, anda9must all be non-null. - Pass SID validation:
RtlValidSid(a4)must return true. - Pass one-time init:
RtlRunOnceExecuteOnce(&BrokerInternalClientInitOnce, ...)must succeed. - Once these hold,
NtDuplicateObjectat0x1C0006BB1runs unconditionally and the resulting handle is passed toNdrClientCall3at0x1C0006C91. No privilege check gates the duplication.
6. Nature of the Change
- This is a logic / least-privilege change, not memory corruption. The unpatched code hands the broker a handle to the caller's own process with
PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZErights; the patch stops creating and sending it. - Access mask
0x101000:PROCESS_QUERY_LIMITED_INFORMATION(0x1000) allows querying basic process metadata;SYNCHRONIZE(0x100000) allows waiting on the process object. - Why the impact is limited: the broker is the privileged party in this design. A handle to a less-privileged caller's process, restricted to query-limited and synchronize rights, does not grant the broker any capability it could not already obtain. No confused-deputy or privilege-escalation primitive is demonstrable from these binaries, and standard memory mitigations are not relevant because there is no memory-corruption path.
- Correctly scoped conclusion: the fix reduces attack surface by not exposing a process handle over the RPC boundary. The exact upstream motivation (protocol simplification versus defense-in-depth) cannot be determined from the binaries alone.
7. Debugger Observation Aid
For an analyst confirming the removed behavior in cea_unpatched.sys with a kernel debugger:
Breakpoints
bp cea_unpatched+0x6bb1 ".printf \"NtDuplicateObject called (caller process handle)\\n\";"
bp cea_unpatched+0x6c91 ".printf \"NdrClientCall3 carrying process handle\\n\";"
What to Inspect
- At
+0x6bb1(NtDuplicateObject):rcx = rdx = r8 = -1(current-process pseudo-handle),r9points to theTargetHandleoutput, and[rsp+0x20]holds0x101000(DesiredAccess). After the call, the output slot holds the new handle value. - At
+0x6c91(NdrClientCall3):[rsp+0x20]holdsrbx, the duplicated handle value being marshaled to the broker. - In the patched binary neither breakpoint is reachable: the
NtDuplicateObjectcall is gone andNdrClientCall3(now at+0x6c39) carries no handle.
Key Offsets
0x1C0006BB1:call cs:__imp_NtDuplicateObject— removed in the patch.0x1C0006BA6:mov [rsp+140h+DesiredAccess], 101000h— DesiredAccess.0x1C0006C83:mov qword ptr [rsp+140h+DesiredAccess], rbx— handle placed into RPC args.0x1C0006C91:call cs:__imp_NdrClientCall3— handle sent to broker.0x1C0006CE5:call cs:__imp_NtClose— handle cleanup, removed in the patch.
8. Changed Functions — Full Triage
BripCreateBrokeredEvent(0x1C0006AB0, Sim 0.6883 | Security-relevant): The changed function. Removed theNtDuplicateObjectcall at0x1C0006BB1, removed theNtClosecleanup at0x1C0006CE5, dropped the handle argument from theNdrClientCall3call, and shrank the stack frame from0x108to0xE8bytes. The patched RPC call always usespProxyInfo(the unpatched build selected betweenoff_1C000B5A0andpProxyInfoviaUuidEqual).BriDeleteEventImpl(0x1C0007068→0x1C0006FEC, Sim 0.4839 | Behavioral): The RPC error-path cleanup routine. It takes the same four register inputs in both builds (a1=UUID,a2=binding,a3=broker type,a4=state pointer) and is called with four arguments fromBripCreateBrokeredEventin both builds. What changed is the RPC selection: the unpatched build always callsNdrClientCall3with procoff_1C000B5A0(nProcNum = 1, orpProxyInfowhen the UUID does not matchg_SmsRouterBrokerId); the patched build callsoff_1C000B428(nProcNum = 1) when the UUID matches andpProxyInfo(nProcNum = 2) otherwise. This is a wire-protocol/proc-number adjustment consistent with the create path no longer carrying a process handle. Not independently security-relevant.BripMapRpcStatus(0x1C0007894→0x1C0007824) &BripCreateEventContext(0x1C0006D48→0x1C0006CCC) (Sim 0.0447 & 0.1342 | Cosmetic): Byte-for-byte identical code relocated to new addresses (their instruction streams match exactly).BripMapRpcStatusis an RPC-status-to-NTSTATUS lookup routine.BripCreateEventContextallocates0x38bytes with tag'EvtA'(0x41747645) viaExAllocatePoolWithTag, computes a CRC32, and inserts into a hash table (RtlInsertEntryHashTable). No logic change.
9. Unmatched Functions
There were 0 added and 0 removed functions. The patch modified existing routines only.
10. Confidence & Caveats
- Confidence Level: High for the mechanics. The disassembly and decompilation of both builds directly show the
NtDuplicateObjectcall, the handle being marshaled intoNdrClientCall3, theNtClosecleanup, and their complete removal in the patched build. - Severity rationale: Rated Low because there is no memory-corruption path, the recipient broker is the privileged party (so no privilege boundary is crossed in the escalation direction), and no unprivileged user-mode entry point reaching this code is present in these binaries. The change is best described as removal of an unnecessary process-handle exposure over the kernel-to-broker RPC boundary.
- Not determinable from the binaries: whether the RPC runtime re-duplicates the handle into the broker's address space (system-handle marshaling) versus transmitting only its numeric value, and the upstream servicing rationale for the change.
DONE