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: BripCreateBrokeredEvent at 0x1C0006AB0

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 (0x1C0006B910x1C0006BB1) 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:

  1. Reach the export: A component calls BriCreateBrokeredEvent (0x1C0006990) or BriCreateBrokeredEventEx (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.
  2. Pass the null checks: a2 (GUID), a4 (SID), a8, and a9 must all be non-null.
  3. Pass SID validation: RtlValidSid(a4) must return true.
  4. Pass one-time init: RtlRunOnceExecuteOnce(&BrokerInternalClientInitOnce, ...) must succeed.
  5. Once these hold, NtDuplicateObject at 0x1C0006BB1 runs unconditionally and the resulting handle is passed to NdrClientCall3 at 0x1C0006C91. 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 | SYNCHRONIZE rights; 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), r9 points to the TargetHandle output, and [rsp+0x20] holds 0x101000 (DesiredAccess). After the call, the output slot holds the new handle value.
  • At +0x6c91 (NdrClientCall3): [rsp+0x20] holds rbx, the duplicated handle value being marshaled to the broker.
  • In the patched binary neither breakpoint is reachable: the NtDuplicateObject call is gone and NdrClientCall3 (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 the NtDuplicateObject call at 0x1C0006BB1, removed the NtClose cleanup at 0x1C0006CE5, dropped the handle argument from the NdrClientCall3 call, and shrank the stack frame from 0x108 to 0xE8 bytes. The patched RPC call always uses pProxyInfo (the unpatched build selected between off_1C000B5A0 and pProxyInfo via UuidEqual).
  • BriDeleteEventImpl (0x1C00070680x1C0006FEC, 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 from BripCreateBrokeredEvent in both builds. What changed is the RPC selection: the unpatched build always calls NdrClientCall3 with proc off_1C000B5A0 (nProcNum = 1, or pProxyInfo when the UUID does not match g_SmsRouterBrokerId); the patched build calls off_1C000B428 (nProcNum = 1) when the UUID matches and pProxyInfo (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 (0x1C00078940x1C0007824) & BripCreateEventContext (0x1C0006D480x1C0006CCC) (Sim 0.0447 & 0.1342 | Cosmetic): Byte-for-byte identical code relocated to new addresses (their instruction streams match exactly). BripMapRpcStatus is an RPC-status-to-NTSTATUS lookup routine. BripCreateEventContext allocates 0x38 bytes with tag 'EvtA' (0x41747645) via ExAllocatePoolWithTag, 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 NtDuplicateObject call, the handle being marshaled into NdrClientCall3, the NtClose cleanup, 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