CorpusHigh
by ArgusKB published 2026-06-09
cea.sys — integer overflow leading to pool buffer overflow in EaSignalAggregatedEvent (CWE-190) fixed
KB5094128
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
EaSignalAggregatedEventfunction that leads to a large kernel pool buffer overflow. Alongside that fix the patch performs a system-wide modernization in which everyExAllocatePoolWithTagcall site (43 of them) is migrated toExAllocatePool2, 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
EaSignalAggregatedEventcalculates the size of a required kernel pool allocation by taking the caller-supplied variable-length data size (Size, the 8th parameter, asize_t) and adding a fixed0x10for 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-bitunsigned int(v20 = Size + (a5 ? 0x10 : 0) + (a6 ? 0x10 : 0)) with no overflow check, so only the low 32 bits ofSizeparticipate. By supplying aSizewhose low 32 bits are near0xFFFFFFFF(e.g., low 32 bits0xFFFFFFF0with both optional fields present), the 32-bit sum0xFFFFFFF0 + 0x20wraps to a tiny nonzero allocation size (0x10). The function then callsmemmovewith(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 computestotal = base + variable_lengthand performs an explicitif (total < base)check, returningSTATUS_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
v20is passed toExAllocatePoolWithTag, allocating an undersized buffer. memmovecopies the caller-suppliedSrcdata 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, anda5 >= ((header>>2)&0xFFF) + 8. The only base pointer read of the payload region on this path is thememmoveofcountbytes frombuffer+8insideEaiResetTriggerPayload, which is covered by thea5 >= count + 8check. Header bit0x19(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 helperEapSignalMessageReadFromBuffer (sub_140008A98)and (b) adds support for one NEW optional 8-byte field gated by header bit 25, together with the matching validationcount >= (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 viaExQueryWnfStateData. - The raw buffer is passed to
WnfEventCallback (sub_1C00018C8), which validates it before forwarding a bounds-checked pointer/count toEaiProcessNotification.
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
- Obtain a handle or execution context capable of invoking the exported kernel API
EaSignalAggregatedEvent. - Prepare the arguments to trigger the arithmetic wrap. Pass non-NULL pointers for the two optional 16-byte fields (
a5anda6), adding0x20to the allocation size. - Set the
Sizeparameter so its low 32 bits are0xFFFFFFF0. The 32-bit allocation arithmetic0xFFFFFFF0 + 0x20wraps to a tiny nonzero size (0x10), while the copy uses(unsigned int)Size=0xFFFFFFF0. - Pass a valid pointer for the
Srcdata argument containing controlled payload data. - Observable Effect: The kernel allocates a
0x10pool block, followed immediately by amemmoveof0xFFFFFFF0(~4 GiB) bytes. This causes a kernel access violation. Bugcheck0x19(BAD_POOL_HEADER) or0x50(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 theEtvApool block. The copy length is the low 32 bits ofSize(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,
KTHREADstructures) or known vulnerable structures adjacent to theEtvAallocation. - Data Preparation: Craft the
Srcdata payload so that the overflow overwrites function pointers or reference counts in the adjacent object. - Privilege Escalation: Overwrite the
TOKENpointer in the targetEPROCESSstructure with a pointer toSystem'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),
ExAllocatePool2enforces 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,
arg1is inRCX,arg2inRDX,arg3inR8,arg4inR9; the remaining parameters (including the two optional-field pointers and theSizevalue) are on the stack. Verify the low 32 bits ofSizeare set to0xFFFFFFF0and both optional-field pointers are non-NULL. - Step over (
p) until the 32-bitadd ebx, eaxinstruction (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 fromr15at0x1C0003642) holds0xFFFFFFF0. - Key instructions/offsets: The 32-bit
add ebx, eaxinstruction (0x1C00035DF) computing the truncated size before theExAllocatePoolWithTagcall is the vulnerable point. - Trigger setup: Open a handle to the driver/device exposing
EaSignalAggregatedEventand invoke the API mapping to it, passing your crafted parameters. - Expected observation: A bugcheck
0x19(BAD_POOL_HEADER) whenmemmovecorrupts the pool, or an immediate0x50(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 intor9dat0x1C00019B2. - 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) thencmp r9, rcx(0x1C0001A02) enforcinga5 >= 16*nfields + 8, andcmp r9, count+8at0x1C0001A41. 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, callNtUpdateWnfStateDatawith 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 intoEapSignalMessageReadFromBuffer (sub_140008A98), and one new optional 8-byte field (header bit0x19) 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 viaRtlValidSid/RtlLengthSidis present in both versions; the change restructures theNdrClientCall3invocation and reduces theBripCreateEventContextsignature, whose allocation migrated toExAllocatePool2.EaiDuplicateConditionsVisitNode (sub_1C0003840)(Sim: 0.842, Type: Security): Pool hardening; migrated toExAllocatePool2.EapQueryAggregatedEventConditionPostVisitNode (sub_1C0007760)(Sim: 0.905, Type: Behavioral): Array growth function; the allocation size is48 * (count + 5)wherecountis a 16-bit field (*((unsigned __int16 *)a1 + 4), max0xFFFF), so the multiply cannot overflow. Only change is theExAllocatePoolWithTag->ExAllocatePool2migration.EapTreeTraverse (sub_1C00062D0)(Sim: 0.944, Type: Behavioral): Stack parsing; the growth allocation is16 * v7 + 80computed in 64 bits wherev7is a 16-bit counter (max0xFFFF), so no overflow is possible. Only change is theExAllocatePool2migration.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 useExAllocatePool2.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 likeEapSignalMessageReadFromBuffer (sub_140008A98)andEapSignalContentPrepareEventForPublishing (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
EaSignalAggregatedEventand the explicitif (total < base)overflow check returningSTATUS_INTEGER_OVERFLOWin the patchedEapSignalContentPrepareEventForPublishingare 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
EaSignalAggregatedEventbeing 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
EaSignalAggregatedEventwith attacker-influencedSizeto test Trigger 1.