1. Overview

  • Unpatched Binary: cea_unpatched.sys
  • Patched Binary: cea_patched.sys
  • Overall Similarity Score: 0.9
  • Diff Statistics: 84 matched functions, 84 changed functions, 0 identical functions, 0 unmatched functions in either direction.
  • Verdict: The patch remediates a single genuine security bug: an integer overflow in the exported EaSignalAggregatedEvent function that leads to a large kernel pool buffer overflow. Alongside that fix the patch performs a system-wide modernization in which every ExAllocatePoolWithTag call site (43 of them) is migrated to ExAllocatePool2, and it refactors the Windows Notification Facility (WNF) state-data parser (WnfEventCallback) into a dedicated helper while adding one new, fully validated optional field. The WNF refactor and the pool migration are hardening/behavioral changes, not fixes for a pre-existing exploitable bug.

2. Vulnerability Summary

Finding 1: Integer Overflow Leading to Kernel Pool Overflow

  • Severity: High
  • Vulnerability Class: Integer Overflow / Kernel Pool Buffer Overflow
  • Affected Function: EaSignalAggregatedEvent (Address: 0x1c00034b0)
  • Root Cause: The exported function EaSignalAggregatedEvent calculates the size of a required kernel pool allocation by taking the caller-supplied variable-length data size (Size, the 8th parameter, a size_t) and adding a fixed 0x10 for each of two optional 16-byte fields that are present when their pointer arguments (a5, a6) are non-NULL. The sum is computed into a 32-bit unsigned int (v20 = Size + (a5 ? 0x10 : 0) + (a6 ? 0x10 : 0)) with no overflow check, so only the low 32 bits of Size participate. By supplying a Size whose low 32 bits are near 0xFFFFFFFF (e.g., low 32 bits 0xFFFFFFF0 with both optional fields present), the 32-bit sum 0xFFFFFFF0 + 0x20 wraps to a tiny nonzero allocation size (0x10). The function then calls memmove with (unsigned int)Size (the low 32 bits, up to ~0xFFFFFFF0 ≈ 4 GiB) as the copy length into this undersized buffer. The patch introduces a helper function (EapSignalContentPrepareEventForPublishing (sub_140008B90)) that computes total = base + variable_length and performs an explicit if (total < base) check, returning STATUS_INTEGER_OVERFLOW (0xC0000095) if the arithmetic wraps.
  • Entry Point & Data Flow:
  • A caller invokes the exported kernel API EaSignalAggregatedEvent.
  • The function calculates the buffer size inline in 32-bit arithmetic: v20 = (unsigned int)(Size + (a5 ? 0x10 : 0) + (a6 ? 0x10 : 0)).
  • The wrapped v20 is passed to ExAllocatePoolWithTag, allocating an undersized buffer.
  • memmove copies the caller-supplied Src data using (unsigned int)Size (the low 32 bits, not the wrapped allocation size), overflowing the adjacent pool.

Finding 2: WNF State Parser Refactor (No Pre-existing OOB Read)

  • Severity: Informational (downgraded: no security-relevant regression)
  • Vulnerability Class: Refactor / added-and-validated optional field (not an out-of-bounds read)
  • Affected Function: WnfEventCallback (sub_1C00018C8) (Address: 0x1c00018c8)
  • Assessment: The unpatched parser is NOT missing the bounds checks the packed WNF structure requires. In cea_unpatched.c (WnfEventCallback) the code already enforces: a5 >= 8 (minimum header), a5 >= 16*((bit23!=0)+(bit24!=0)) + 8 (the optional 16-byte fields must fit), ((header>>2)&0xFFF) >= 16*((bit23!=0)+(bit24!=0)) when the count field is non-zero, and a5 >= ((header>>2)&0xFFF) + 8. The only base pointer read of the payload region on this path is the memmove of count bytes from buffer+8 inside EaiResetTriggerPayload, which is covered by the a5 >= count + 8 check. Header bit 0x19 (bit 25, 0x2000000) is never referenced anywhere in the unpatched build, so no field gated by it is ever read. The patch (a) moves this inline parsing/validation into the new helper EapSignalMessageReadFromBuffer (sub_140008A98) and (b) adds support for one NEW optional 8-byte field gated by header bit 25, together with the matching validation count >= (bit24?0x10:0)+(bit23?0x10:0)+(bit25?8:0). There is no attacker-reachable out-of-bounds read in the unpatched build for this path; the change is a refactor plus a new, fully validated field.
  • Entry Point & Data Flow (unchanged callback path):
  • WNF state data is written to a subscribed state name via NtUpdateWnfStateData.
  • The kernel triggers the WNF callback dispatcher EaLibSubscribeWnfStateChangeNotification (sub_1C0011848).
  • The dispatcher calls AggregateEventWnfCallback (sub_1C00116A0), which queries the state data via ExQueryWnfStateData.
  • The raw buffer is passed to WnfEventCallback (sub_1C00018C8), which validates it before forwarding a bounds-checked pointer/count to EaiProcessNotification.

3. Pseudocode Diff

EaSignalAggregatedEvent (Integer Overflow)

// === UNPATCHED (EaSignalAggregatedEvent @ 0x1C00034B0) ===
// v20 is unsigned int: the sum is computed in 32 bits with NO overflow check.
// a5/a6 are pointers to optional 16-byte fields; present when non-NULL.
v20 = Size + (a5 != NULL ? 0x10 : 0) + (a6 != NULL ? 0x10 : 0);
if (v20 != 0) {
    buf = ExAllocatePoolWithTag(PagedPool, v20, 'EtvA');   // wrapped (tiny) size

    // ...optional 16-byte fields copied from a5/a6 into buf...
    // VULNERABILITY: copy length is the low 32 bits of the caller's Size,
    // which can be ~0xFFFFFFF0 while v20 wrapped to 0x10
    memmove(dst, Src, (unsigned int)Size);
}

// === PATCHED (via EapSignalContentPrepareEventForPublishing (sub_140008B90)) ===
base = 8 + (a5 ? 0x10 : 0) + (a6 ? 0x10 : 0) + (a7 ? 8 : 0);
total = base + variable_length;   // variable_length = struct[0x20]

if (total < base)  // OVERFLOW CHECK ADDED -> STATUS_INTEGER_OVERFLOW (0xC0000095)
    return STATUS_INTEGER_OVERFLOW;

buf = ExAllocatePool2(POOL_FLAG_PAGED, total, 'EtvA');
memset(buf, 0, total);
// safe bounded copy of variable_length bytes follows...

WnfEventCallback (sub_1C00018C8) (WNF Parser Refactor)

// === UNPATCHED === (already bounds-checked; a5 = buffer size, a4 = buffer)
header = *a4;
count  = (header >> 2) & 0xFFF;
nfields = ((header & 0x800000) != 0) + ((header & 0x1000000) != 0);  // bit23 + bit24

if (a5 < 8) return error;                       // minimum header
if (nfields != 0) {
    if (a5 < 16*nfields + 8) return error;       // optional 16-byte fields must fit
    if ((header & 0x3FFC) != 0 && count < 16*nfields) return error;
}
if (a5 < count + 8) return error;                // payload region must fit
// forwards (buffer+8 or NULL), count, flags to EaiProcessNotification (bounds-checked)

// === PATCHED (via EapSignalMessageReadFromBuffer (sub_140008A98)) ===
if (a2 < 8) return STATUS_BUFFER_TOO_SMALL;      // *a4 = 16
if (a2 < count + 8) return 0xC000090B;           // *a4 = 32
fieldbytes = (header & 0x1000000 ? 0x10 : 0)
           + (header & 0x800000  ? 0x10 : 0)
           + (header & 0x2000000 ? 8 : 0);        // NEW: bit25 field added here
if (count < fieldbytes) return STATUS_INVALID_BUFFER_SIZE;  // *a4 = 48
// returns bounds-checked {ptr, count, flags} struct

4. Assembly Analysis

EaSignalAggregatedEvent (Unpatched, real instructions)

; EaSignalAggregatedEvent @ 0x1C00034B0
; Size is loaded as a 32-bit dword, so only its low 32 bits participate.
00000001C0003545  mov   r15d, dword ptr [rbp+3Fh+Size]  ; r15 = (32-bit) Size, zero-extended
; ... optional-field presence computed into ecx / ebx as 0 or 0x10 ...
00000001C00035DB  lea   eax, [r15+rcx]                   ; eax = Size + (field1 ? 0x10 : 0)
00000001C00035DF  add   ebx, eax                         ; ebx = + (field2 ? 0x10 : 0)  -- 32-bit, NO OVERFLOW CHECK
00000001C00035E1  jz    short loc_1C000364A              ; skip alloc only if wrapped result == 0
00000001C00035E3  mov   edx, ebx                         ; NumberOfBytes = truncated 32-bit size
00000001C00035E5  mov   ecx, 1                           ; PoolType = PagedPool
00000001C00035EA  mov   r8d, 41747645h                   ; Tag = 'EtvA'
00000001C00035F0  call  cs:__imp_ExAllocatePoolWithTag   ; allocates the wrapped (tiny) buffer
; ... optional 16-byte fields copied via movups ...
00000001C0003642  mov   r8, r15                          ; Size = low 32 bits of caller Size (~0xFFFFFFF0 max)
00000001C0003645  call  memmove                          ; copies far past the tiny allocation

EapSignalContentPrepareEventForPublishing (sub_140008B90) (Patched Overflow Check)

; @ 0x140008B90 - r8/[rsi+20h] = variable_length, edi = base size
0000000140008C0A  lea   eax, [rdi+r8]                    ; total = base + variable_length (32-bit)
0000000140008C0E  cmp   eax, edi                         ; compare total vs base
0000000140008C10  jb    short loc_140008C26              ; if wrapped (total < base) -> overflow
0000000140008C12  mov   edi, eax                         ; else keep total
; ...
0000000140008C26  mov   ebx, 0C0000095h                  ; STATUS_INTEGER_OVERFLOW
0000000140008C2B  jmp   loc_140008CDB                    ; return

WnfEventCallback optional-field bounds check already present (Unpatched)

; WnfEventCallback @ 0x1C00018C8 - the unpatched build already validates the fields it parses
00000001C00019F6  test  edx, edx                         ; edx = (bit23!=0)+(bit24!=0)
00000001C00019F8  jz    short loc_1C0001A33              ; no optional fields -> skip
00000001C00019FA  shl   rdx, 4                           ; rdx = 16 * nfields
00000001C00019FE  lea   rcx, [rdx+8]                     ; rcx = 16*nfields + 8
00000001C0001A02  cmp   r9, rcx                          ; r9 = buffer size a5
00000001C0001A05  jnb   short loc_1C0001A14              ; require a5 >= 16*nfields + 8, else error 0x14
; ... further: cmp r9, count+8 @ 0x1C0001A41 (require a5 >= count + 8) ...

5. Trigger Conditions

Trigger 1: EaSignalAggregatedEvent Pool Overflow

  1. Obtain a handle or execution context capable of invoking the exported kernel API EaSignalAggregatedEvent.
  2. Prepare the arguments to trigger the arithmetic wrap. Pass non-NULL pointers for the two optional 16-byte fields (a5 and a6), adding 0x20 to the allocation size.
  3. Set the Size parameter so its low 32 bits are 0xFFFFFFF0. The 32-bit allocation arithmetic 0xFFFFFFF0 + 0x20 wraps to a tiny nonzero size (0x10), while the copy uses (unsigned int)Size = 0xFFFFFFF0.
  4. Pass a valid pointer for the Src data argument containing controlled payload data.
  5. Observable Effect: The kernel allocates a 0x10 pool block, followed immediately by a memmove of 0xFFFFFFF0 (~4 GiB) bytes. This causes a kernel access violation. Bugcheck 0x19 (BAD_POOL_HEADER) or 0x50 (PAGE_FAULT_IN_NONPAGED_AREA) will occur.

Trigger 2: WNF Parser (no reachable OOB)

No exploitable trigger exists for the unpatched WNF parser. The unpatched WnfEventCallback (sub_1C00018C8) already validates that the buffer holds the optional 16-byte fields it processes (a5 >= 16*nfields + 8) and that it holds the payload region (a5 >= count + 8), and it never references header bit 0x19. A crafted short buffer with flag bits set is rejected by these checks rather than read out of bounds. The patched path adds an additional optional field (header bit 0x19) with its own validation, so the behavior is stricter, not a fix for a reachable read past the buffer.


6. Exploit Primitive & Development Notes

  • Primitive Provided (Overflow): A highly controllable kernel pool overflow via memmove. The data written is dictated by the caller's input buffer, providing a deterministic linear pool overflow relative to the EtvA pool block. The copy length is the low 32 bits of Size (up to ~4 GiB), so in practice the copy runs off the end of valid memory quickly; controlled corruption requires the copy to be arranged to stop within a groomed region.
  • Development Steps for Full Exploit:
  • Heap Grooming: Use predictable pool allocations to place sensitive kernel objects (e.g., process tokens, KTHREAD structures) or known vulnerable structures adjacent to the EtvA allocation.
  • Data Preparation: Craft the Src data payload so that the overflow overwrites function pointers or reference counts in the adjacent object.
  • Privilege Escalation: Overwrite the TOKEN pointer in the target EPROCESS structure with a pointer to System's token, or hijack execution via a modified function pointer.
  • Mitigations:
  • The unpatched binary uses PagedPool (executable). This makes ring-0 code execution trivial if SMEP/SMAP are disabled or bypassed, as shellcode can be placed directly in the overflowed pool block.
  • In the patched binary (and modern Windows generally), ExAllocatePool2 enforces non-executable (NX) memory (POOL_FLAG_PAGED), neutralizing direct shellcode execution and forcing ROP/JOP-based exploit strategies.

7. Debugger PoC Playbook

To build and verify a PoC for these vulnerabilities, use the following WinDbg/KD commands on the unpatched binary.

Target 1: EaSignalAggregatedEvent (Integer Overflow)

  • Breakpoint: bp cea_unpatched!EaSignalAggregatedEvent (Address: 0x1c00034b0).
  • What to inspect at the breakpoint:
  • Inspect the stack to read the arguments based on the calling convention. In a standard Microsoft x64 ABI, arg1 is in RCX, arg2 in RDX, arg3 in R8, arg4 in R9; the remaining parameters (including the two optional-field pointers and the Size value) are on the stack. Verify the low 32 bits of Size are set to 0xFFFFFFF0 and both optional-field pointers are non-NULL.
  • Step over (p) until the 32-bit add ebx, eax instruction (0x1C00035DF) combining the sizes is hit. You will see the destination register (EBX) contain a wrapped small value (e.g., 0x10).
  • Step into the call ExAllocatePoolWithTag (0x1C00035F0). Verify it returns a valid, but tiny (0x10), pointer.
  • Step to the call memmove (0x1C0003645). Verify the size argument register (R8, loaded from r15 at 0x1C0003642) holds 0xFFFFFFF0.
  • Key instructions/offsets: The 32-bit add ebx, eax instruction (0x1C00035DF) computing the truncated size before the ExAllocatePoolWithTag call is the vulnerable point.
  • Trigger setup: Open a handle to the driver/device exposing EaSignalAggregatedEvent and invoke the API mapping to it, passing your crafted parameters.
  • Expected observation: A bugcheck 0x19 (BAD_POOL_HEADER) when memmove corrupts the pool, or an immediate 0x50 (PAGE_FAULT) if the copy runs off the end of a valid memory page.

Target 2: WnfEventCallback (sub_1C00018C8) (parser validation, no OOB)

  • Breakpoints:
  • bp cea_unpatched!WnfEventCallback (Address: 0x1c00018c8)
  • bp cea_unpatched!AggregateEventWnfCallback (sub_1C00116A0) (Address: 0x1c00116a0)
  • What to inspect at the breakpoint:
  • At WnfEventCallback: RCX = context, R9 = pointer to WNF data buffer; the buffer size (a5) arrives on the stack and is loaded into r9d at 0x1C00019B2.
  • Dump the WNF buffer using db R9 L20. Verify your packed header is present and the flags are set.
  • Key instructions/offsets: Observe that the flag-derived optional-field check is already present: shl rdx, 4 (0x1C00019FA) then cmp r9, rcx (0x1C0001A02) enforcing a5 >= 16*nfields + 8, and cmp r9, count+8 at 0x1C0001A41. A short buffer takes the error branch (ecx = 0x14/0x28) rather than reading past the buffer.
  • Trigger setup: Identify the WNF state name passed to the subscription in AggregateEventWnfCallback (sub_1C00116A0). From a user-mode application, call NtUpdateWnfStateData with the discovered state name, providing a short buffer with flag bits set.
  • Expected observation: The parser returns an error status for the short buffer; no out-of-bounds access occurs on this path.

8. Changed Functions — Full Triage

  • WnfEventCallback (sub_1C00018C8) (Sim: 0.594, Type: Refactor): WNF data parser; the already-present inline bounds checking was moved into EapSignalMessageReadFromBuffer (sub_140008A98), and one new optional 8-byte field (header bit 0x19) was added with matching validation. No pre-existing OOB read; not a security fix.
  • EaSignalAggregatedEvent (Sim: 0.853, Type: Security): Exported API; added integer overflow checks during allocation size calculation and migrated to safe memory copying.
  • BripCreateBrokeredEvent (sub_1C0005340) (Sim: 0.745, Type: Behavioral): RPC marshaling for brokered event creation. SID validation via RtlValidSid/RtlLengthSid is present in both versions; the change restructures the NdrClientCall3 invocation and reduces the BripCreateEventContext signature, whose allocation migrated to ExAllocatePool2.
  • EaiDuplicateConditionsVisitNode (sub_1C0003840) (Sim: 0.842, Type: Security): Pool hardening; migrated to ExAllocatePool2.
  • EapQueryAggregatedEventConditionPostVisitNode (sub_1C0007760) (Sim: 0.905, Type: Behavioral): Array growth function; the allocation size is 48 * (count + 5) where count is a 16-bit field (*((unsigned __int16 *)a1 + 4), max 0xFFFF), so the multiply cannot overflow. Only change is the ExAllocatePoolWithTag -> ExAllocatePool2 migration.
  • EapTreeTraverse (sub_1C00062D0) (Sim: 0.944, Type: Behavioral): Stack parsing; the growth allocation is 16 * v7 + 80 computed in 64 bits where v7 is a 16-bit counter (max 0xFFFF), so no overflow is possible. Only change is the ExAllocatePool2 migration.
  • EACreateAggregateEvent (Sim: 0.95, Type: Behavioral): Fixed allocation size pool hardening.
  • BripCreateBindingContext (sub_1C0005680) (Sim: 0.95, Type: Behavioral): Pool API modernization.
  • EaiProcessNotification (sub_1C0001BC4) (Sim: 0.942, Type: Behavioral): Core event condition state logic restructured; improved tracking.
  • EaiEnableAggregateEvent (sub_1C00027BC) (Sim: 0.93, Type: Behavioral): WNF state init function updated to use ExAllocatePool2.
  • BripCleanupEventTable (sub_1C00050B0) (Sim: 0.88, Type: Behavioral): Cleanup/destructor; simplified loop variable tracking.
  • EapFreeSimpleEventParameters (sub_1C00071BC) (Sim: 0.9, Type: Cosmetic): Redundant variable cleanup in a free-loop.

9. Unmatched Functions

  • Added: 0
  • Removed: 0 (Note: While no completely new unmatched functions were added, security logic was injected via modified matched functions calling new subroutines like EapSignalMessageReadFromBuffer (sub_140008A98) and EapSignalContentPrepareEventForPublishing (sub_140008B90) which were likely linked statically or present in previously unused space).

10. Confidence & Caveats

  • Confidence: High for the integer-overflow finding. The 32-bit truncated size arithmetic in the unpatched EaSignalAggregatedEvent and the explicit if (total < base) overflow check returning STATUS_INTEGER_OVERFLOW in the patched EapSignalContentPrepareEventForPublishing are both directly present in the binaries. The WNF parser change does not fix a pre-existing out-of-bounds read: the unpatched build already validates the fields it parses, and the patch adds a new validated field plus a refactor. The pool-API migration is a defense-in-depth hardening measure.
  • Assumptions:
  • The PoC relies on EaSignalAggregatedEvent being reachable from an attacker-controlled execution context. The exact caller (IOCTL, RPC, or in-kernel client) needs to be mapped by the exploit developer.
  • Manual Verification Required: Verify the exact entry point that routes to EaSignalAggregatedEvent with attacker-influenced Size to test Trigger 1.