(CVE-2025-47985) Windows Event Tracing Insufficient Validation Leading to Elevation of Privilege
CVE: CVE-2025-47985
Affected Versions: Windows 11
CVSS3.1: 7.8 (High) — CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
Summary
| Product | Windows Event Tracing (ETW) |
|---|---|
| Vendor | Microsoft |
| Severity | High — a local attacker may exploit this to elevate privileges to SYSTEM |
| Affected Versions | Windows 11 |
| Tested Versions | Windows 11 23H2 (Build 22631.4660) |
| CVE Identifier | CVE-2025-47985 |
| CVE Description | Insufficient validation in Event Tracing for Windows (ETW) in Microsoft Windows 11 may allow an unprivileged attacker to execute code as SYSTEM |
| CWE Classification(s) | CWE-125: Out-of-Bounds Read |
CVSS3.1 Scoring System
Base Score: 7.8 (High)
Vector String: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
| Metric | Value |
|---|---|
| Attack Vector (AV) | Local |
| Attack Complexity (AC) | Low |
| Privileges Required (PR) | Low |
| User Interaction (UI) | None |
| Scope (S) | Unchanged |
| Confidentiality (C) | High |
| Integrity (I) | High |
| Availability (A) | High |
Product Background
Event Tracing for Windows (ETW) is a high-performance kernel-level tracing facility built into Windows. It provides user-mode applications and kernel-mode drivers with a mechanism to raise and log events, and is widely used for diagnostics, performance monitoring, and security tooling. ETW is implemented in ntoskrnl.exe and exposed to userspace through syscalls including NtTraceControl().
Technical Details
Event Tracing for Windows (ETW) provides a mechanism to trace and log events that are raised by user-mode applications and kernel-mode drivers. ETW is implemented in the Windows operating system and provides developers a versatile set of event tracing features.
ETW is implemented in the main ntoskrnl.exe kernel image, and is exposed to usermode via syscalls like NtTraceControl().
To start an ETW trace session, we call the usermode API StartTraceW(). This internally builds a WMI_LOGGER_INFORMATION structure, and passes to NtTraceControl() with an opcode of 0x1, eventually transitioning to the EtwpStartLogger() function in the kernel.
The WMI_LOGGER_INFORMATION structure has the following fields found through reversing:
struct _WMI_LOGGER_INFORMATION
{
WNODE_HEADER Wnode;
ULONG BufferSize;
ULONG MinimumBuffers;
ULONG MaximumBuffers;
ULONG MaximumFileSize;
ULONG LogFileMode;
ULONG FlushTimer;
TRACE_ENABLE_FLAG_EXTENSION EnableFlags;
LONG AgeLimit;
ULONG64 V2Options;
ULONG64 Pad1;
ULONG NumberOfBuffers;
ULONG FreeBuffers;
ULONG EventsLost;
ULONG BuffersWritten;
ULONG LogBuffersLost;
ULONG RealTimeBuffersLost;
HANDLE LoggerThreadId;
UNICODE_STRING LogFileName;
UNICODE_STRING InstanceName;
DWORD ProviderId;
BYTE Pad2[12];
WCHAR InstanceNameBuf[2];
TRACE_ENABLE_FLAG_EXT_HEADER TraceExtHeader;
TRACE_ENABLE_FLAG_EXT_ITEM TraceExtItem[1];
WCHAR LogFileNameBuf[18];
};
For this vulnerability, we are interested in the TRACE_ENABLE_FLAG_EXTENSION field.
struct _TRACE_ENABLE_FLAG_EXTENSION
{
USHORT Offset;
UCHAR Length;
UCHAR Flags;
};
It contains an offset relative to the start of the WMI_LOGGER_INFORMATION object, where the TRACE_ENABLE_FLAG_EXT_HEADER is located.
struct _TRACE_ENABLE_FLAG_EXT_HEADER
{
USHORT LengthDWORDs;
USHORT Items;
};
This header describes the one or more TRACE_ENABLE_FLAG_EXT_ITEM objects that follow it in memory.
struct TRACE_ENABLE_FLAG_EXT_ITEM
{
USHORT ItemLengthDWORDs;
USHORT Type;
};
Use of offsets in the kernel always have to be validated extensively to prevent OOB access. ETW uses the EtwpValidateFlagExtension() to validate this on entrance of the EtwpStartLogger() function. When ETW wants to use the flag extensions later, it uses the EtwpGetFlagExtension() to retrieve them, which assumes that they are already validated and doesn’t perform any checks.
EtwpValidateFlagExtension() ensures that the offset cannot be larger than the size of the structure in memory. However, it does not ensure that the offset does not overlap with other fields of the structure. This is a problem because the WMI_LOGGER_INFORMATION structure is not immutable. ETW modifies it to pass information back to usermode.
An example is the following code in EtwpStartLogger():
BufferSize = LoggerInfo->BufferSize;
if ( BufferSize )
{
if ( BufferSize > 0x4000 )
{
LoggerInfo->BufferSize = 0x4000;
BufferSize = 0x4000;
}
LoggerContext->BufferSize = BufferSize << 10;
}
If LoggerInfo->BufferSize is greater than 0x4000, it will be set to 0x4000.
Engineers that have triaged bugs in the CLFS driver may immediately recognize such bug patterns. If we set TRACE_ENABLE_FLAG_EXTENSION.Offset such that the LoggerInfo->BufferSize field overlaps with one of the lengths of the TRACE_ENABLE_FLAG_EXT_HEADER or the item that follows it, it can be corrupted after validation to hold a larger size than it really is.
In the POC I provide, I crafted the offsets such that the first item will be corrupted.
loggerInfo->Wnode.Flags = 0x20003; // fake TRACE_ENABLE_FLAG_EXT_HEADER
loggerInfo->BufferSize = 0x10001; // fake item
loggerInfo->MinimumBuffers = 0x1; // fake item
loggerInfo->EnableFlags.Length = 0xff;
loggerInfo->EnableFlags.Offset = (ULONG64)(&loggerInfo->Wnode.Flags) - (ULONG64)(loggerInfo);
loggerInfo->EnableFlags.Flags = 0xff;
I forge the fields such that EtwpValidateFlagExtension() will see two items of 1 DWORD size. After the corruption, the first item’s length will be set to 0x4000 DWORDs, which is clearly OOB.
When EtwpGetFlagExtension() tries to look up the second item, it will go OOB and try to retrieve the item from another pool chunk. By manipulating the pool, an attacker can fully control the item returned, leading to further primitives like double fetch or improper validation.
_TRACE_ENABLE_FLAG_EXT_ITEM *__fastcall __spoils<rax,rcx,r8,r9> EtwpGetFlagExtension(
cstmWMI_LOGGER_INFORMATION *WmiLoggerInformation,
UINT16 ExtType)
{
TRACE_ENABLE_FLAG_EXTENSION offs; // eax
USHORT ctr; // r8
TRACE_ENABLE_FLAG_EXT_HEADER *start; // rax
TRACE_ENABLE_FLAG_EXT_ITEM *item; // rcx
offs = WmiLoggerInformation->EnableFlags;
ctr = 0;
if ( *(int *)&offs < 0 )
{
start = (TRACE_ENABLE_FLAG_EXT_HEADER *)((char *)WmiLoggerInformation + offs.Offset);
item = (TRACE_ENABLE_FLAG_EXT_ITEM *)&start[1];
while ( ctr < start->Items )
{
if ( item->Type == ExtType )
return item;
++ctr;
item += item->Offset;
}
}
return 0LL;
}
As this function has quite some cross references, it’s probably good to check if any of these leads to exploitable primitives.

In the POC, EtwpUpdatePerProcessTracing() is chosen as an example.
FlagExtension = EtwpGetFlagExtension(WmiLoggerInformation, 2u);
if ( FlagExtension )
{
flagExtensionData = FlagExtension + 1;
LODWORD(v15) = v4;
v10 = 4 * FlagExtension->Offset - 4;
BYTE4(v15) = 1;
sizeDword = v10 >> 2;
if ( sizeDword )
{
sizeDword_ = sizeDword;
do
{
if ( PsLookupProcessByProcessId((HANDLE)*(_DWORD *)flagExtensionData, &Process) >= 0 )
{
EtwpUpdateProcessTracingCallback(Process, (__int64)&v15);
ObfDereferenceObjectWithTag(Process, 0x746C6644u);
}
++flagExtensionData;
--sizeDword_;
}
while ( sizeDword_ );
}
}
We can see that if an attacker controls the FlagExtension item returned, OOB read can be achieved. This path is also reachable via the EtwpUpdateTrace() function.
It is to note that to trigger EtwpUpdatePerProcessTracing(), we need to be part of the Performance Log Users group. Interactive users are automatically added to this group during the Visual Studio installation process, so I don’t think this is quite a privileged group, and can serve as a POC. Regardless, it’s just an example to demonstrate the issue. The engineering team should further determine if this attack pattern has more devastating impacts.
Credit
Chen Le Qi of STAR Labs SG Pte. Ltd.
Timeline
- 2025-07-08 — CVE-2025-47985 published; patch released via Microsoft Security Response Center