(CVE-2025-49660) Windows Event Tracing Reference Count Overflow Leading to Use-After-Free and Elevation of Privilege

CVE: CVE-2025-49660

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-49660
CVE Description Insufficient checks in Event Tracing for Windows (ETW) in Microsoft Windows 11 may allow an unprivileged attacker to execute code as SYSTEM
CWE Classification(s) CWE-190: Integer Overflow or Wraparound; CWE-416: Use After Free

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().

In ETW, we have Providers that are responsible for emitting trace events. Internally a provider is represented by a single _ETW_GUID_ENTRY object, and one or more _ETW_REG_ENTRY objects. A new _ETW_REG_ENTRY is created for each registration (call to usermode API EventRegister()). The limit for the number of registrations in the usermode API is 0x800, but there are no limitations when calling the underlying NtTraceControl() directly with function code 0xF. All the registrations will be linked in _ETW_GUID_ENTRY.RegListHead.

Traits can be set on a RegEntry object, using usermode API EventSetInformation() or directly using NtTraceControl() with function code 0x1E. This is handled by EtwpSetProviderTraitsCommon(), which inserts the trait as a node into a global rbtree EtwpProviderTraitsUmTree.

Each node has the following structure found by reversing:

struct _ETW_TRAIT_NODE
{
  _RTL_BALANCED_NODE Node;
  DWORD RefCount;
  BYTE X[1] // Actual Trait Data;
};

EtwpSetProviderTraitsCommon() performs a check to see if there is any trait in the tree that is identical to the current trait being inserted:

// traverse tree until find node to store
    while ( 1 )
    {
      res = TraitsCompare(TraitsBufKernel, curNode);
      if ( res <= 0 )
      {
        if ( res >= 0 )
        {
          ++curNode->RefCount;
          FromExisting = 1;
          traitsBufKernel = curNode;
          traits?
          goto BindToRegEntry;
        }

If an identical trait exists, this trait will not be inserted. Instead, the existing trait’s reference will be incremented, and that trait will be linked to our RegEntry.

The problem is that the reference counter is a 32-bit integer, and there are no checks in place to verify whether the reference has overflown, unlike in other reference counting functions like EtwpReferenceGuidEntry(). When the reference overflows and wraps around to 1, it will lead to UAF in EtwpReleaseProviderTraitsReference().

  TraitEntry = (cstmEtwTraitBufKernel *)_InterlockedExchange64((volatile __int64 *)&RegHandle->Traits, 0LL);
  if ( TraitEntry )
  {
    IsKernel = RegHandle->Flags.Flags & 1;
    lock = &EtwpProviderTraitsKmMutex;
    if ( !IsKernel )
      lock = &EtwpProviderTraitsUmMutex;
    ExAcquireFastMutex(lock);
    if ( TraitEntry->RefCount-- == 1 )
    {
      Tree = (unsigned __int64 *)&EtwpProviderTraitsKmTree;
      if ( !IsKernel )
        Tree = (unsigned __int64 *)&EtwpProviderTraitsUmTree;
      RtlRbRemoveNode(Tree, (unsigned __int64)TraitEntry);
    }
    else
    {
      TraitEntry = 0LL;
    }
    ExReleaseFastMutex(lock);
    if ( TraitEntry )
      ExFreePoolWithTag(TraitEntry, 0);
  }
  return 0LL;
}

The reference is incremented everytime we create a new RegEntry object and set a trait on it. The double freeing is triggered when closing the handle to a RegEntry, which makes it a powerful and reliable primitive to exploit. After using WinDbg to manually set the reference to 0xFFFFFFFF, I can confirm that the overflow and UAF is possible.

Since each RegEntry object consumes a handle, we can only increment slightly less than 0x1000000 references per process from my testing. Therefore we will require 256 processes and a substantial amount of RAM to perform the attack. Once the conditions are satisfied though, we can perform the attack in under 30 minutes.

A similar bug was previously reported by Project Zero (issue 42451732).

Credit

Chen Le Qi of STAR Labs SG Pte. Ltd.

Timeline