core-jmp core-jmpdeath of core jump

Windows Internals: Check Your Privilege — The Curious Case of ETW’s SecurityTrace Flag

Connor McGarr on ETW’s undocumented SecurityTrace bit: QUERY wants Antimalware-PPL, STOP does not, and a user-mode ControlTrace hook lets admin consume Threat-Intelligence without a driver. MSRC: not a CVE.

oxfemale September 21, 2026 37 min read 65 reads
Export PDF
Windows Internals: Check Your Privilege — The Curious Case of ETW’s SecurityTrace Flag
Original text: "Windows Internals: Check Your Privilege – The Curious Case of ETW’s SecurityTrace Flag"Connor McGarr, Connor McGarr’s Blog / Origin (by Prelude) (16 January 2026). Also posted at Origin (by Prelude). WinDbg dumps, C snippets, and all twenty figures follow the source. The full consumer stays on GitHub; this draft does not re-host it. Lab VMs only.
SOC desk with kernel-debug glow
A security-trace session is a velvet rope around a camera feed. The rope is checked in user-mode. The camera can still be unplugged from the hallway.

Executive Summary

On 16 January 2026 Connor McGarr published a Windows-internals note, first on the Origin (by Prelude) blog, then on his own site. While building Origin’s Runtime Memory Protection preview, his team’s Antimalware-PPL ETW tooling could stop a trace session that had an undocumented SecurityTrace bit set — without Antimalware-PPL. That flag is supposed to keep such sessions (mostly Defender AutoLoggers) in the PPL club. Querying them as SYSTEM fails. Stopping them as admin, if you know the logger name and pass any extra DACL, succeeds. MSRC later said this is not a vulnerability: the administrative ↔ PPL boundary is not an enforceable security boundary.

The practical result is sharper than “you can stop DefenderApiLogger.” AutoLogger enablement of Microsoft-Windows-Threat-Intelligence (GUID F4E1897C-BB5D-5668-F1D8-040F4D8DD344) is done by the kernel, with no process identity to check. SecurityTrace is the delayed bouncer. Consume-time checks for that bit live in user-mode Sechost.dll (OpenTrace/ProcessTraceEtwpQueryRealTimeTracePropertiesControlTrace QUERY). Hook QUERY, or call NtTraceControl yourself, and an admin process can sit on that session without PPL and without a kernel driver. A public POC is preludeorg/ThreatIntelligenceConsumer. This page keeps every original screenshot and dump, then adds kitchen-table translations and operator notes on what TI you actually get, what you still need PPL for, and what to hunt.

This allowed us to identify a new method to consume events from ETW providers which require Antimalware-PPL, like Microsoft-Windows-Threat-Intelligence, without running as Antimalware-PPL and without relying on a kernel driver.

Connor McGarr, 16 January 2026
Kitchen table: Imagine a nightclub camera room. Only people with a special embroidered jacket (Antimalware-PPL) are allowed to look at the monitors. The jackets are checked at the door of the viewing gallery — that door is in user-mode. The cameras themselves were turned on at boot by the building, not by a jacketed guest. If you already have the master key to the hallway (Administrator), you can unplug a camera (stop the trace) without ever showing the jacket. If you tape a fake “all good” sticker on the gallery clipboard (hook ControlTrace QUERY), you can also sit down and watch. Microsoft’s reply: the hallway key was never promised to be weaker than the jacket. Patch the clipboard check into the brick wall (the kernel) if you care.
For operators: Not a CVE, per MSRC. Still an ops fact: WMI_LOGGER_CONTEXT.Flags.SecurityTrace (bit 14, 0x4000) gates QUERY, not STOP. Three enable paths: AutoLogger provider list includes Kernel-Audit-Api-Calls or Threat-Intelligence; AutoLogger EnableSecurityProvider DWORD; StartTrace with EVENT_TRACE_PROPERTIES.LogBuffersLost = 0x4000 because that field is unioned with WMI_LOGGER_INFORMATION.Flags. Default TI events you can hear without extra NtSetInformationProcess opt-in: exec alloc/map, remote APC, SetThreadContext, driver load/unload, a short syscall list. Impersonation / VM protect / suspend-resume still need PPL to enable per process — unless Defender already opted those processes in, which Insider Canary often has.

How to read this page

  • If ETW is a blur: stay with the green boxes. Sessions are cameras, AutoLogger is “turn the cameras on at sunrise,” PPL is a jacket, QUERY is the clipboard at the door.
  • If you write detections: jump to “What you actually receive from TI,” then Defensive Recommendations. Do not treat the GitHub repo name as the only IOC.
  • If you live in nt!: the dumps and IDA screenshots are in the same order as McGarr’s post. The C snippets are the public ones from the article, not a re-hosted tree.
  • If you ship PPL software: assume admin can stop your SecurityTrace session by name, and can consume it if they hook QUERY in their own process.
Three-column map of AutoLogger, user-mode query check, and kernel enable path
The split that matters: kernel turns providers on; user-mode decides whether you may look; STOP never asked for the jacket.

Introduction

Origin (by Prelude) was digging through ETW for a runtime memory-protection preview. Their internal tooling already ran as Antimalware-PPL. From that height they noticed they could send a stop-trace to a session that had an undocumented “security trace” flag — and that an ordinary administrative process could do the same, no special signing, no PPL. Protected-process folklore says resources owned by PPL should not be toyed with by lesser processes, including Administrator. The flag looked like it reserved session management for Antimalware-PPL, especially AutoLoggers. The stop path disagreed.

Following that disagreement produced two results. First: how to set and manage SecurityTrace without PPL. Second, and the one that matters in a lab: how to consume PPL-gated providers such as Microsoft-Windows-Threat-Intelligence without PPL, without a driver, and without patching the kernel. The public encapsulation is ThreatIntelligenceConsumer on GitHub.

This is a personal-blog republication of the Origin post, not a Microsoft whitepaper. The recommendation at the end is a single sentence: move the SecurityTrace check into the kernel; stop trusting user-mode to be the bouncer.

ETW session management

User-mode APIs start and steer traces. The kernel still owns the objects. The big structure for one session is WMI_LOGGER_CONTEXT: logger name, buffers, LBR/IPT enablement, and a flags word. McGarr’s truncated WinDbg dump, as published:

lkd> dt nt!_WMI_LOGGER_CONTEXT
   +0x000 LoggerId         : Uint4B
   +0x004 BufferSize       : Uint4B
   +0x008 MaximumEventSize : Uint4B
   +0x00c LoggerMode       : Uint4B
   +0x010 AcceptNewEvents  : Int4B
   +0x018 GetCpuClock      : Uint8B
   +0x020 LoggerThread     : Ptr64 _ETHREAD
   +0x028 LoggerStatus     : Int4B
   +0x02c FailureReason    : Uint4B
   +0x030 BufferQueue      : _ETW_BUFFER_QUEUE
   +0x040 OverflowQueue    : _ETW_BUFFER_QUEUE
   +0x050 GlobalList       : _LIST_ENTRY
   +0x060 DebugIdTrackingList : _LIST_ENTRY
   +0x070 DecodeControlList : Ptr64 _ETW_DECODE_CONTROL_ENTRY
   +0x078 DecodeControlCount : Uint4B
   +0x080 BatchedBufferList : Ptr64 _WMI_BUFFER_HEADER
   +0x080 CurrentBuffer    : _EX_FAST_REF
   +0x088 LoggerName       : _UNICODE_STRING
   +0x098 LogFileName      : _UNICODE_STRING
   +0x0a8 LogFilePattern   : _UNICODE_STRING
   +0x0b8 NewLogFileName   : _UNICODE_STRING
<--- Truncated --->

The flags overlay at offset +0x330 is the map for this post. SecurityTrace is bit 14:

+0x330 Flags            : Uint4B
+0x330 Persistent       : Pos 0, 1 Bit
+0x330 AutoLogger       : Pos 1, 1 Bit
+0x330 FsReady          : Pos 2, 1 Bit
+0x330 RealTime         : Pos 3, 1 Bit
+0x330 Wow              : Pos 4, 1 Bit
+0x330 KernelTrace      : Pos 5, 1 Bit
+0x330 NoMoreEnable     : Pos 6, 1 Bit
+0x330 StackTracing     : Pos 7, 1 Bit
+0x330 ErrorLogged      : Pos 8, 1 Bit
+0x330 RealtimeLoggerContextFreed : Pos 9, 1 Bit
+0x330 PebsTracing      : Pos 10, 1 Bit
+0x330 PmcCounters      : Pos 11, 1 Bit
+0x330 PageAlignBuffers : Pos 12, 1 Bit
+0x330 StackLookasideListAllocated : Pos 13, 1 Bit
+0x330 SecurityTrace    : Pos 14, 1 Bit
+0x330 LastBranchTracing : Pos 15, 1 Bit
+0x330 SystemLoggerIndex : Pos 16, 8 Bits
+0x330 StackCaching     : Pos 24, 1 Bit
+0x330 ProviderTracking : Pos 25, 1 Bit
+0x330 ProcessorTrace   : Pos 26, 1 Bit
+0x330 QpcDeltaTracking : Pos 27, 1 Bit
+0x330 MarkerBufferSaved : Pos 28, 1 Bit
+0x330 LargeMdlPages    : Pos 29, 1 Bit
+0x330 ExcludeKernelStack : Pos 30, 1 Bit
Kitchen table: A logger context is the clipboard hanging on one camera: its name, whether it is recording live, whether it started at boot, and a row of ticks. One of those ticks says “this camera is a security camera.” The tick does not, by itself, say who may unplug it.

At most 0x50 (80) loggers exist. A LINQ-style dx over PspHostSiloGlobals->EtwSiloState->EtwpLoggerContext, keeping only SecurityTrace == 1, returned two names on his box:

lkd> dx ((nt!_WMI_LOGGER_CONTEXT*(*)[0x50])(((nt!_ESERVERSILO_GLOBALS*)&nt!PspHostSiloGlobals)->EtwSiloState->EtwpLoggerContext))->Where(l => l != 1).Where(l => l->SecurityTrace == 1).Select(i => i->LoggerName)
((nt!_WMI_LOGGER_CONTEXT*(*)[0x50])(((nt!_ESERVERSILO_GLOBALS*)&nt!PspHostSiloGlobals)->EtwSiloState->EtwpLoggerContext))->Where(l => l != 1).Where(l => l->SecurityTrace == 1).Select(i => i->LoggerName)                
    [5]              : "DefenderApiLogger" [Type: _UNICODE_STRING]
    [6]              : "DefenderAuditLogger" [Type: _UNICODE_STRING]

DefenderApiLogger and DefenderAuditLogger. They are not created by StartTrace in a Defender binary you can grep. They are AutoLoggers: the kernel starts them early from HKLM\SYSTEM\CurrentControlSet\Control\WMI\Autologger. That is why they exist before a user-mode PPL process is around to “own” them.

AutoLogger registry key
AutoLogger sessions live in the WMI Autologger registry hive. Source: original article.
DefenderApiLogger AutoLogger values
DefenderApiLogger: AutoLogger-legal settings and provider GUID subkeys — no visible SecurityTrace DWORD. Source: original article.

The key has the usual AutoLogger knobs and a list of provider GUIDs (the first in the write-up is Microsoft-Windows-Services, 0063715B-EEDA-4007-9429-AD526F62696E), plus optional Filters. Nothing is labeled Flags. The documented EVENT_TRACE_PROPERTIES you pass to StartTrace also has no “security trace” field. So the bit is being set somewhere the SDK brochure does not mention — inside the kernel’s AutoLogger bring-up, and, as we will see, via a union.

For operators: If you inventory SecurityTrace sessions, start with that dx (or walk EtwpLoggerContext in a dump). Do not grep Defender for StartTrace(DefenderApiLogger). Trail of Bits’ ETW-internals post is the companion McGarr links for using WinDbg on this surface.

What SecurityTrace actually gates

Look where the bit is tested. The main read is nt!EtwpQueryTrace, the kernel side of “tell me about this session.” logman and any ControlTrace QUERY end up here.

EtwpQueryTrace in the kernel
EtwpQueryTrace is where SecurityTrace is evaluated on a query. Source: original article.

Queries do not send EVENT_TRACE_PROPERTIES through the trap. They send the undocumented WMI_LOGGER_INFORMATION, which NtTraceControl understands. The Windows Research Kernel definition is stale. Private symbols on combase.dll (COM is hard to debug, so Microsoft ships them) still have the type:

0:000> dt combase!_WMI_LOGGER_INFORMATION
   +0x000 Wnode            : _WNODE_HEADER
   +0x030 BufferSize       : Uint4B
   +0x034 MinimumBuffers   : Uint4B
   +0x038 MaximumBuffers   : Uint4B
   +0x03c MaximumFileSize  : Uint4B
   +0x040 LogFileMode      : Uint4B
   +0x044 FlushTimer       : Uint4B
   +0x048 EnableFlags      : Uint4B
   +0x04c AgeLimit         : Int4B
   +0x04c FlushThreshold   : Int4B
   +0x050 Wow              : Pos 0, 1 Bit
   +0x050 QpcDeltaTracking : Pos 1, 1 Bit
   +0x050 LargeMdlPages    : Pos 2, 1 Bit
   +0x050 ExcludeKernelStack : Pos 3, 1 Bit
   +0x050 V2Options        : Uint8B
   +0x058 LogFileHandle    : Ptr64 Void
   +0x058 LogFileHandle64  : Uint8B
   +0x060 NumberOfBuffers  : Uint4B
   +0x060 InstanceCount    : Uint4B
   +0x064 FreeBuffers      : Uint4B
   +0x064 InstanceId       : Uint4B
   +0x068 EventsLost       : Uint4B
   +0x068 NumberOfProcessors : Uint4B
   +0x06c BuffersWritten   : Uint4B
   +0x070 LogBuffersLost   : Uint4B
   +0x070 Flags            : Uint4B
   +0x074 RealTimeBuffersLost : Uint4B
   +0x078 LoggerThreadId   : Ptr64 Void
   +0x078 LoggerThreadId64 : Uint8B
   +0x080 LogFileName      : _UNICODE_STRING
   +0x080 LogFileName64    : _STRING64
   +0x090 LoggerName       : _UNICODE_STRING
   +0x090 LoggerName64     : _STRING64
   +0x0a0 RealTimeConsumerCount : Uint4B
   +0x0a4 SequenceNumber   : Uint4B
   +0x0a8 LoggerExtension  : Ptr64 Voidf
   +0x0a8 LoggerExtension64 : Uint8B

sechost.dll is the user-mode receptionist: EtwpCopyPropertiesToInfo maps SDK properties into WMI_LOGGER_INFORMATION, the kernel fills it from WMI_LOGGER_CONTEXT, EtwpCopyInfoToProperties maps back. That round-trip is a QUERY.

sechost property translation
sechost.dll translates EVENT_TRACE_PROPERTIES ↔ WMI_LOGGER_INFORMATION around the kernel. Source: original article.

If WMI_LOGGER_CONTEXT.Flags.SecurityTrace is set, that round-trip is refused unless the caller is at least Antimalware-PPL. SYSTEM is not enough.

PPL check before populating logger information
Querying a SecurityTrace session requires Antimalware-PPL. Source: original article.
SYSTEM access denied on query
A SYSTEM process still gets access denied on QUERY. Source: original article.
Kitchen table: Asking “what is on camera 5?” is a QUERY. The clipboard at the gallery door checks for the jacket. The night janitor with a master key (SYSTEM) still does not have the jacket. He cannot read the clipboard. That does not mean he cannot pull the plug in the hallway.

The bit is also consulted on other security-ish paths. It is not consulted as a PPL gate on stop. User-mode stop is sechost!EtwpStopLogger; kernel is EtwpStopTraceEtwpStopLoggerInstance. The kernel does look at SecurityTrace there, but only to update bookkeeping if Microsoft-Windows-Security-Auditing was on that logger — special-cased provider state, not “is the caller PPL?” STOP does not QUERY, so it never trips the gallery clipboard either.

EtwpStopLoggerInstance SecurityTrace handling
Stop looks at SecurityTrace for Security-Auditing bookkeeping, not for a PPL check. Source: original article.

So if you know the session name, and you satisfy any extra DACL (Defender’s loggers want SYSTEM, not just a local admin), you can stop a SecurityTrace session from a non-PPL process. Public snippet:

eventProperties->Wnode.Guid = k_DefenderApiLoggerGuid;
eventProperties->LoggerNameOffset = sizeof(EVENT_TRACE_PROPERTIES);
   
error = ControlTraceW(0,
                      L"DefenderApiLogger",
                      eventProperties,
                      EVENT_TRACE_CONTROL_STOP);
if (error != ERROR_SUCCESS)
{
    goto Exit;
}

wprintf(L"[+] Successfully stopped DefenderApiLogger trace session.\n");
DefenderApiLogger successfully stopped
Stop succeeded without Antimalware-PPL. Source: original article. Source: original article.

EtwCheckSecurityLoggerAccess is the usual paired helper: it is the function that actually asks “are you Antimalware-PPL?” Jonny Johnson’s write-up on the Threat-Intelligence provider (linked in the original) is the extra reading. The same helper is why only PPL can opt a process in to extra TI events (read/write VM, and friends). Those events are off by default even with the right keywords.

EtwCheckSecurityLoggerAccess
EtwCheckSecurityLoggerAccess: the PPL gate for logger access and TI opt-in. Source: original article.
For operators: Operational split: QUERY = PPL. STOP = admin/SYSTEM + name + DACL. ENABLE of TI on a live session via EnableTraceEx2 = PPL (EtwpCheckNotificationAccess). ENABLE of TI on an AutoLogger at boot = kernel, no process, SecurityTrace bit set as a rain-check. CONSUME = supposed to be PPL via the user-mode QUERY inside OpenTrace/ProcessTrace.

Three ways to turn the bit on

AutoLogger path one — privileged providers in the hive

AutoLoggers do not call EtwEnableTrace the way a process does. They go through EtwpEnableAutoLoggerProvider, which walks provider GUID subkeys. If a GUID is Microsoft-Windows-Kernel-Audit-Api-Calls or Microsoft-Windows-Threat-Intelligence, the session’s WMI_LOGGER_CONTEXT gets SecurityTrace.

EtwpEnableAutoLoggerProvider sets SecurityTrace
Privileged AutoLogger providers stamp SecurityTrace on the logger context. Source: original article.

There is no PPL check here because there is no caller. The “process” is System: the kernel. A normal EnableTraceEx2 from user-mode would bounce a non-PPL caller. AutoLogger enablement never takes that syscall. The OS’s answer is: check later, when someone tries to consume. That later check is the bit — and, fatally, it was implemented on the user-mode side of consume.

Filing cabinet of AutoLogger registry folders
AutoLogger: the building turns cameras on from a filing cabinet at sunrise. Nobody in a jacket is in the room yet.

AutoLogger path two — EnableSecurityProvider

An undocumented but valid AutoLogger value, EnableSecurityProvider, is honored in EtwpStartAutoLogger. McGarr’s screenshots call the union SecTraceUnion for clarity; that is his name, not the kernel’s. The Flags overlay matches WMI_LOGGER_CONTEXT.Flags.

EnableSecurityProvider in EtwpStartAutoLogger
EnableSecurityProvider in the AutoLogger key sets SecurityTrace at start. Source: original article.

Side effects: the logger is automatically subscribed to Microsoft-Windows-Security-Auditing, and its logger ID is added to the known-security-logger list on ETW_SILODRIVERSTATE under PspHostSiloGlobals. EtwpSecurityProviderGuidEntry is always that auditing provider, set in EtwpPreInitializeSiloState.

EtwpSecurityProviderGuidEntry
The security-provider GUID entry is always Microsoft-Windows-Security-Auditing. Source: original article.
EtwpSecurityLoggers array
Logger ID 3 is reserved for EventLog-Security; EnableSecurityProvider AutoLoggers join the list. Source: original article.

Logger ID 3 is hardcoded as EventLog-Security. Everyone else with the DWORD joins the array and gets the bit.

Programmatic path — LogBuffersLost = 0x4000

You do not need an AutoLogger at all. WMI_LOGGER_INFORMATION unions LogBuffersLost and Flags at +0x070:

0:000> dt combase!_WMI_LOGGER_INFORMATION
    <--- Truncated --->
   +0x070 LogBuffersLost   : Uint4B
   +0x070 Flags            : Uint4B
   <--- Truncated --->
One mailbox with two overlapping nameplates
Two names, one slot: LogBuffersLost and Flags occupy the same four bytes.

The documented SDK structure only exposes LogBuffersLost. EtwpCopyPropertiesToInfo copies the union blindly. Set LogBuffersLost to 0x4000 (SecurityTrace’s bit) on StartTrace, and the kernel writes it into WMI_LOGGER_CONTEXT.Flags. It must be on start; EVENT_TRACE_CONTROL_UPDATE ignores a later change.

Flags copied from the user-mode union
The unioned Flags dword is imported into the kernel logger context. Source: original article.
//
// <snip>
//
traceProperties->LogBuffersLost = 0x4000;  // Treated as "Flags" if 0x4000 is set in nt!EtwpStartLogger.

error = StartTraceW(TraceHandle,
                    TraceName,
                    traceProperties);
if (error != ERROR_SUCCESS)
{
    wprintf(L"[-] Error in StartTraceW! (Error: 0x%lx)\n", error);
    goto Exit;
}

After that call, a third name appears next to the Defender loggers:

3: kd> dx ((nt!_WMI_LOGGER_CONTEXT*(*)[0x50])(((nt!_ESERVERSILO_GLOBALS*)&nt!PspHostSiloGlobals)->EtwSiloState->EtwpLoggerContext))->Where(l => l != 1).Where(l => l->SecurityTrace == 1).Select(i => i->LoggerName)
((nt!_WMI_LOGGER_CONTEXT*(*)[0x50])(((nt!_ESERVERSILO_GLOBALS*)&nt!PspHostSiloGlobals)->EtwSiloState->EtwpLoggerContext))->Where(l => l != 1).Where(l => l->SecurityTrace == 1).Select(i => i->LoggerName)                
    [5]              : "DefenderApiLogger" [Type: _UNICODE_STRING]
    [6]              : "DefenderAuditLogger" [Type: _UNICODE_STRING]
    [41]             : "MyTrace" [Type: _UNICODE_STRING]

Useful if you want a session other processes cannot QUERY — no AutoLogger key, no PPL. Useless for consume, if you use the documented APIs, because consume QUERYs first.

Kitchen table: You can hang a “security camera” sign on a webcam you bought yourself by writing the magic number in the lost-buffers box on the start form. The form has only one box; the back office treats that box as either “buffers lost” or “flags,” depending on the number. 0x4000 means flags. You still cannot sit in the gallery without passing the clipboard — unless you steal the clipboard.

Why documented consume fails

Real-time consume is OpenTrace then ProcessTrace. Both, in sechost, call private EtwpQueryRealTimeTraceProperties, which issues a QUERY. The bit is already set (it had to be set at start). The caller is not PPL. ERROR_ACCESS_DENIED.

EtwpQueryRealTimeTraceProperties
OpenTrace/ProcessTrace query real-time properties in user-mode — and that QUERY is the PPL gate. Source: original article.

The gate is not inline in the kernel’s consume NtTraceControl path. It is in a DLL inside a process you own. Two public options in the original:

  1. Skip OpenTrace/ProcessTrace. Speak NtTraceControl from ntdll and never run the sechost QUERY.
  2. Hook EtwpQueryRealTimeTraceProperties or exported ControlTrace and lie on QUERY. Microsoft Detours, or a homegrown trampoline. McGarr’s POC used a small hook, not Detours, because of time.

A hook on the private function must return: processor count, HistoricalContext (logger ID / index in EtwpLoggerContext), a 0x1078-byte EVENT_TRACE_PROPERTIES, and ERROR_SUCCESS. Hooking exported ControlTrace is more portable: only success plus output properties. TraceQueryInformation for processor count does not hit EtwpQueryTrace. Trial and error: the properties from the original StartTrace are enough; a fresh kernel copy is not required.

ControlTrace QUERY hook
QUERY detour: return the StartTrace properties and never ask the kernel about SecurityTrace. Source: original article.
Receptionist intercepting a clipboard form
The gallery clipboard is a user-mode function in your process. You already own the receptionist.
For operators: This is not a kernel 0-day. It is “the security check for a kernel object was implemented in a user-mode helper the consumer can patch.” Any admin code that already runs in the consuming process can patch sechost. EDR that assumes TI is PPL-only because EnableTraceEx2 says so is reading the wrong syscall.

Threat-Intelligence without the jacket

On a non-AutoLogger session, enabling TI hits EtwpCheckNotificationAccess and dies without PPL. AutoLogger enablement does not call that function. It only sets the bit.

EtwpCheckNotificationAccess
Non-AutoLogger TI enablement checks the caller. AutoLogger enablement does not. Source: original article.

The recipe in the original, as a sequence, not as a drop-in implant:

  1. Write an AutoLogger registry entry that lists Microsoft-Windows-Threat-Intelligence. Boot (or otherwise let the kernel start AutoLoggers). No PPL check: there is no process.
  2. In your admin consumer, hook ControlTrace QUERY so it returns a hand-built EVENT_TRACE_PROPERTIES instead of talking to EtwpQueryTrace.
  3. Call OpenTrace and ProcessTrace as usual. The QUERY inside them is now a no-op success.

The awkward field is WNODE_HEADER.HistoricalContext — the logger ID. You cannot QUERY it without PPL. AutoLoggers take low IDs, alphabetically, with exceptions. ID 2 is the classic kernel logger, ID 3 is EventLog-Security, so the first free AutoLogger ID is usually 4. The POC brute-forces 4–80 until QUERY returns ERROR_ACCESS_DENIED as a hint, and, for demos, names the session starting with 0 so it sorts first and lands on 4. Other recon methods are left as an exercise; IDs are just numbers.

Needed properties when you did not call StartTrace yourself: WNODE fields (especially HistoricalContext), BufferSize (he used 0x40), LogFileMode = EVENT_TRACE_REAL_TIME_MODE, FlushTimer, MinimumBuffers, LoggerNameOffset — aligned with the AutoLogger key.

Threat-Intelligence GUID on the session
Consumer attached to a TI-enabled session. GUID F4E1897C-BB5D-5668-F1D8-040F4D8DD344. Source: original article.

Walking WMI_LOGGER_CONTEXT.Consumers as ETW_REALTIME_CONSUMER shows the only realtime consumer: the POC process, _PS_PROTECTION.Level = 0 — not PPL.

3: kd> dx ((nt!_WMI_LOGGER_CONTEXT*(*)[0x50])(((nt!_ESERVERSILO_GLOBALS*)&nt!PspHostSiloGlobals)->EtwSiloState->EtwpLoggerContext))->Where(l => l != 1).Where(l => l->SecurityTrace == 1).Select(i => new { Name = i->LoggerName, Consumers = Debugger.Utility.Collections.FromListEntry(i->Consumers, "nt!_ETW_REALTIME_CONSUMER", "Links")})[0n4].Consumers[0]
((nt!_WMI_LOGGER_CONTEXT*(*)[0x50])(((nt!_ESERVERSILO_GLOBALS*)&nt!PspHostSiloGlobals)->EtwSiloState->EtwpLoggerContext))->Where(l => l != 1).Where(l => l->SecurityTrace == 1).Select(i => new { Name = i->LoggerName, Consumers = Debugger.Utility.Collections.FromListEntry(i->Consumers, "nt!_ETW_REALTIME_CONSUMER", "Links")})[0n4].Consumers[0]                 [Type: _ETW_REALTIME_CONSUMER]
    [+0x000] Links            [Type: _LIST_ENTRY]
    [+0x010] ProcessHandle    : 0xffffffff800037b0 [Type: void *]
    [+0x018] ProcessObject    : 0xffffa58900524080 [Type: _EPROCESS *]
    [+0x020] NextNotDelivered : 0x0 [Type: void *]
    [+0x028] RealtimeConnectContext : 0x0 [Type: void *]
    [+0x030] DisconnectEvent  : 0xffffa5890188e2e0 [Type: _KEVENT *]
    [+0x038] DataAvailableEvent : 0xffffa5890188e760 [Type: _KEVENT *]
    [+0x040] UserBufferCount  : 0x202d0255450 : Unable to read memory at Address 0x202d0255450 [Type: unsigned long *]
    [+0x048] UserBufferListHead : 0x202d0255448 [Type: _SINGLE_LIST_ENTRY *]
    [+0x050] BuffersLost      : 0x0 [Type: unsigned long]
    [+0x054] EmptyBuffersCount : 0x0 [Type: unsigned long]
    [+0x058] LoggerId         : 0x4 [Type: unsigned short]
    [+0x05a] Flags            : 0x0 [Type: unsigned char]
    [+0x05a ( 0: 0)] ShutDownRequested : 0x0 [Type: unsigned char]
    [+0x05a ( 1: 1)] NewBuffersLost   : 0x0 [Type: unsigned char]
    [+0x05a ( 2: 2)] Disconnected     : 0x0 [Type: unsigned char]
    [+0x05a ( 3: 3)] Notified         : 0x0 [Type: unsigned char]
    [+0x05a ( 4: 4)] Wow              : 0x0 [Type: unsigned char]
    [+0x060] ReservedBufferSpaceBitMap [Type: _RTL_BITMAP]
    [+0x070] ReservedBufferSpace : 0x202d0360000 : Unable to read memory at Address 0x202d0360000 [Type: unsigned char *]
    [+0x078] ReservedBufferSpaceSize : 0x80000 [Type: unsigned long]
    [+0x07c] UserPagesAllocated : 0x0 [Type: unsigned long]
    [+0x080] UserPagesReused  : 0x3d [Type: unsigned long]
    [+0x088] EventsLostCount  : 0x202d0255368 : Unable to read memory at Address 0x202d0255368 [Type: unsigned long *]
    [+0x090] BuffersLostCount : 0x202d025536c : Unable to read memory at Address 0x202d025536c [Type: unsigned long *]
    [+0x098] SiloState        : 0xffffa588f8631000 [Type: _ETW_SILODRIVERSTATE *]

3: kd> dx ((nt!_EPROCESS*)0xffffa58900524080)->Protection
((nt!_EPROCESS*)0xffffa58900524080)->Protection                 [Type: _PS_PROTECTION]
    [+0x000] Level            : 0x0 [Type: unsigned char]
    [+0x000 ( 2: 0)] Type             : 0x0 [Type: unsigned char]
    [+0x000 ( 3: 3)] Audit            : 0x0 [Type: unsigned char]
    [+0x000 ( 7: 4)] Signer           : 0x0 [Type: unsigned char]
Kitchen table: At sunrise the building turns on a camera labeled Threat-Intelligence and hangs a “jackets only” sign. At 9 a.m. you, with a hallway key, tape “approved” on your own clipboard and sit down. The camera never asked the kernel whether your jacket was real. It asked a sticker you printed.

What you actually receive from TI

The POC cannot flip per-process optional TI bits. Those go through NtSetInformationProcess and still want PPL (see Johnson’s impersonation-event post, linked in the original). Default-on telemetry the method can hear without that call, as listed in the source:

  • Executable memory allocation (user and kernel callers)
  • Executable memory mapping (user and kernel callers)
  • Remote APC (user-mode)
  • Thread context updates (SetThreadContext)
  • Kernel-mode device and driver load/unload
  • A short syscall set — at publication, NtSystemDebugControl and NtQuerySystemInformation (the KASLR-bypass angle is in the windows-internals.com link McGarr cites)

On current Insider Canary, several processes are already opted into the optional set (memory protect, suspend/resume, and others) because Defender, which is PPL, turned those bits on. The same consumer then receives those events “for free.”

Insider optional TI bits
Canary Insider: Defender has already opted some processes into optional TI. Source: original article.

WinDbg one-liner from the original for processes with optional TI logging enabled:

dx -g @$cursession.Processes.Where(p => (p.KernelObject.EnableProcessImpersonationLogging == 1 || p.KernelObject.EnableProcessLocalExecProtectVmLogging == 1) || p.KernelObject.EnableProcessRemoteExecProtectVmLogging == 1 || p.KernelObject.EnableProcessSuspendResumeLogging == 1 || p.KernelObject.EnableReadVmLogging == 1 || p.KernelObject.EnableThreadSuspendResumeLogging == 1 || p.KernelObject.EnableWriteVmLogging == 1).Select(p => new { Name = p->Name, EnableProcessImpersonationLogging = p.KernelObject.EnableProcessImpersonationLogging, EnableProcessLocalExecProtectVmLogging = p.KernelObject.EnableProcessLocalExecProtectVmLogging, EnableProcessRemoteExecProtectVmLogging = p.KernelObject.EnableProcessRemoteExecProtectVmLogging, EnableProcessSuspendResumeLogging = p.KernelObject.EnableProcessSuspendResumeLogging, EnableReadVmLogging  = p.KernelObject.EnableReadVmLogging, EnableThreadSuspendResumeLogging = p.KernelObject.EnableThreadSuspendResumeLogging, EnableWriteVmLogging = p.KernelObject.EnableWriteVmLogging }),d
Processes opted into optional Threat-Intelligence telemetry
Grid of processes with impersonation / VM-protect / suspend / RW VM logging bits. Source: original article.
For operators: If you are hunting this technique: an AutoLogger subkey whose provider list includes the TI GUID, plus a non-PPL process in that session’s consumer list, is the smoking gun. A StartTrace with LogBuffersLost=0x4000 and a subsequent consume from the same PID is the non-AutoLogger cousin (hidden from QUERY, still stoppable, consume via hook). Do not wait for the GitHub binary name. Do not assume “we don’t see impersonation events, so nobody is on TI.”

MSRC’s line, and why it is still worth a ticket

Microsoft’s response, quoted in spirit from the conclusion: no vulnerability, because the administrative ↔ PPL boundary is not enforceable. That is consistent with years of “admin can load a driver / patch user-mode / debug.” It is also why Origin published instead of sitting on a CVE calendar.

The engineering complaint still stands: a check whose purpose is to keep PPL-only telemetry from non-PPL consumers was implemented in sechost, in the consumer. Moving it into the kernel consume path would not create a new security boundary against admin. It would stop this particular “I am admin and I hooked my own QUERY” shortcut, which is exactly the shortcut researchers used to need a driver or a kernel patch for. McGarr’s one-line recommendation is that move.

Kitchen table: The city says a master hallway key is allowed to open the camera room; that was always in the lease. The interesting part is that the camera room’s bouncer was a paper sign on the inside of the door, and the guest could replace the sign. Putting the bouncer in brick does not revoke the hallway key. It stops the paper-sign trick.

A glossary for both sides of the table

TermKitchenOperator
ETW sessionOne camera and its DVR clip.WMI_LOGGER_CONTEXT + buffers; max 80.
AutoLoggerCameras the building turns on at sunrise from a filing cabinet.HKLM\…\WMI\Autologger; kernel-started; no process identity.
SecurityTraceA ‘security camera’ tick on the clipboard.Flags bit 14 (0x4000) on the logger context.
Antimalware-PPLThe embroidered jacket.Protected Process Light, antimalware signer; required for QUERY.
QUERY / ControlTraceAsking what the camera is pointed at.EtwpQueryTrace; PPL-gated when the bit is set.
STOPUnplugging the camera in the hallway.EtwpStopTrace; no PPL check; DACL may still demand SYSTEM.
LogBuffersLost unionOne form box, two meanings.EVENT_TRACE_PROPERTIES.LogBuffersLost overlays WMI_LOGGER_INFORMATION.Flags.
OpenTrace/ProcessTraceSitting down in the gallery.Real-time consume; sechost QUERYs first.
TI providerThe camera that films memory tricks.Microsoft-Windows-Threat-Intelligence, F4E1897C-BB5D-5668-F1D8-040F4D8DD344.
Optional TI bitsExtra microphones Defender can clip on a guest.NtSetInformationProcess; still PPL to enable; sometimes pre-enabled.
Kitchen column is ours; operator column follows McGarr.

ATT&CK, CWE, and what not to file

ThingUseful mappingDo not file
Admin stops DefenderApiLoggerT1562 Impair Defenses / T1562.006 Indicator BlockingA CVE on Defender; MSRC declined
Admin consumes TI via AutoLogger + QUERY hookT1562; T1556 (modify auth/check in own process); collection of T1055-class telemetry“PPL bypass 0-day” as if PPL were a boundary against admin
LogBuffersLost = 0x4000Defense evasion: hide a logger from non-PPL QUERYCWE on StartTrace documentation alone
User-mode security checkCWE-602 Client-Side Enforcement of Server-Side Security (sechost as the ‘server’ of the check)CWE-787; this is not memory corruption
Need SYSTEM DACL on Defender loggersPrivileges still matter; local admin ≠ always enough“Any user can stop Defender ETW”
MSRC: admin ↔ PPL is not enforceable. Hunt it as tradecraft, not as an unpatched CVE.

Detections that do not need the GitHub tree

  1. Registry: new AutoLogger subkeys, especially names that sort early (leading 0, digits, punctuation), provider GUIDs equal to TI or Kernel-Audit-Api-Calls, or EnableSecurityProvider present.
  2. Kernel/dump: SecurityTrace==1 loggers whose consumer EPROCESS.Protection.Level is 0.
  3. User-mode: non-PPL processes calling OpenTrace/ProcessTrace/NtTraceControl against logger IDs 4–80 after a denied QUERY.
  4. Integrity: sechost!ControlTrace / EtwpQueryRealTimeTraceProperties hooks in processes that are not your ETW tooling (inline patches, Detours, unexpected trampolines).
  5. Defender: unexpected STOP of DefenderApiLogger / DefenderAuditLogger (event logs, missing sessions after boot).
  6. StartTrace with LogBuffersLost = 0x4000 from non-PPL processes — a hiding trick even when TI is not involved.

None of that replaces PPL for products that need TI opt-in. It means “TI is PPL-only” is false for the default-on event set if AutoLogger + a user-mode QUERY lie is in play, and it has been public since this post.

What this is not

  • Not a kernel exploit and not a CVE, per MSRC.
  • Not a full ThreatIntelligenceConsumer tree. Snippets above are the ones in the blog. The repo is the rest.
  • Not “any process can read TI.” You still need to write HKLM AutoLogger (admin), or already be able to StartTrace, and you still need to win consume (hook or native API). Sandboxed users are out.
  • Not a replacement for PPL when you need optional per-process TI bits, unless Defender (or another PPL agent) already set them.
  • Not permission to stop Defender telemetry on a production fleet to “see if it works.” Lab VMs, authorized research.

Related reading McGarr already pointed at

PPL, admin, and the boundary Microsoft will not sell you

Protected Process Light exists so that a local administrator cannot, with ordinary APIs, open an antimalware process and empty its brain. Pass-the-hash mitigations in Windows 8.1, Ionescu’s history, and a decade of “PPL-Dump” papers are all about that process-protection bit. ETW SecurityTrace is trying to extend a similar idea to a kernel object that is not a process: a logger context created at boot, sometimes with no PPL process holding a handle yet. That is a different shape of problem. A process can refuse OpenProcess. A logger that the kernel started from a registry key has to refuse ControlTrace. Microsoft’s public position, restated here, is that once you are Administrator you were never promised you could not interfere with PPL-adjacent resources. Drivers, own-process hooks, and debug privileges are the classic illustrations. Origin’s consume path is another illustration, not a contradiction of that policy.

For a SOC that sentence is unsatisfying, because the product pitch for Threat-Intelligence ETW is “only antimalware can hear this.” That pitch is true for EnableTraceEx2 on a live session and for NtSetInformationProcess opt-in. It is not true for AutoLogger-subscribed default-on events if the consumer is willing to lie to sechost. Write your architecture docs with that split, or you will over-promise to a CISO and under-detect a lab technique that has a GitHub link.

Kitchen table: The embroidered jacket stops people from pickpocketing the bouncer. It was never a law that the building owner (Administrator) cannot rewire the cameras. The surprise is not the law. The surprise is that the ‘jackets only’ sign on the gallery was printed on paper and handed to the guest.

Boot-time cameras, alphabetical IDs, and the leading zero

AutoLoggers start in a constrained world. Session IDs 0–1 are not the interesting ones here. ID 2 is the traditional NT kernel logger. ID 3 is reserved for EventLog-Security in EtwpPreInitializeSiloState. Everything in the Autologger hive then takes the next slots, largely in alphabetical order of key name, with a few exceptions McGarr says are easy to compensate for. That is why a POC session named 0Something tends to become logger ID 4: it sorts first, and 4 is the first ID that is actually up for grabs.

HistoricalContext brute-force (QUERY 4 through 80, treat ACCESS_DENIED as “maybe SecurityTrace”) is a POC heuristic, not a proof. Other sessions can deny query for other reasons. A defender who wants to break the demo can put a decoy SecurityTrace logger in front, or name production AutoLoggers so they occupy 4–10. A researcher who wants a robust ID can read the hive, simulate the sort order, and add the known reserved IDs. The original leaves that as an exercise; it is the right exercise.

BufferSize 0x40, real-time mode, FlushTimer, MinimumBuffers, LoggerNameOffset — those are not magic. They have to match what the AutoLogger key already told the kernel, because OpenTrace is trying to attach to an existing object, not create one. Mismatch and you fail for boring reasons that look like the PPL failure. Always distinguish ACCESS_DENIED on QUERY from ERROR_BAD_LENGTH on a malformed property blob.

The 0x4000 trick in slow motion

Bit 14 of a 32-bit flags word is 0x4000. LogBuffersLost is a documented ULONG that means “how many buffers did we drop?” on a QUERY result. On a START request it is not a statistic; it is just another ULONG in a structure that sechost memcpy-adjacent-copies into a union that the kernel treats as Flags. Nobody documented “please set lost buffers to 16384 to hide your session from logman.” It works because two generations of structures share a slot and only one generation is in the SDK.

UPDATE cannot set the bit later because the kernel ignores that field on EVENT_TRACE_CONTROL_UPDATE. So you cannot start a normal session, confirm it works, then flip SecurityTrace to hide. You hide at birth or not at all. That is convenient for hunters: a hidden session was born hidden. Look at StartTrace telemetry (if you have it) for LogBuffersLost=0x4000, or look at the kernel array after the fact with the dx from the original.

How the bit gets setNeeds PPL?Needs reboot?QUERY hidden?STOP still admin?
AutoLogger lists TI or Kernel-Audit-Api-CallsNo (kernel enable)Yes (or AutoLogger start)YesYes, plus DACL
AutoLogger EnableSecurityProviderNoYesYes; also joins Security-AuditingYes, plus DACL
StartTrace LogBuffersLost=0x4000NoNoYesYes, plus DACL
EnableTraceEx2(TI) on a live sessionYesNoN/A if enable failsN/A
Four doors. Three of them do not check the jacket at enable time.

What a red team should not learn the hard way

If you use this on an authorized assessment, OPSEC is the AutoLogger key and the consumer process, not the GitHub exe name. A leading-zero session name is a demo convenience and a noisy IOC. A QUERY hook in sechost is visible to any integrity product that walks in-proc patches. Native NtTraceControl consume is quieter than a trampoline and is the option McGarr listed first and did not ship, for time. Stopping DefenderApiLogger is loud: you just turned off a Microsoft logger. Consuming TI in parallel with Defender is quieter and more useful, and it is the actual point of the paper.

Do not expect impersonation, VM-protect, or suspend events unless you are PPL or you are on a box where Defender already opted the target in. Canary Insider screenshots in the original are a warning that “optional” is a fleet-dependent set. Test on the customer build, not on your research VM’s preview SKU.

For operators: ATT&CK T1562.006 is the closest stop-logger bucket. For consume, prefer a custom hunt: AutoLogger TI GUID + non-PPL consumer, or StartTrace with 0x4000. Do not file “PPL bypass” in a client report as if you crossed a promised boundary. File “admin can subscribe to default-on TI via AutoLogger; sechost QUERY is not a kernel ACL.”

A longer kitchen walk through the whole building

Sunrise: the super prints a list of cameras from a cabinet in the basement (the Autologger hive). Two of them are Defender’s. One of them, if you added a line, is yours, pointed at “memory mischief” (TI). The super is the kernel. No jacket check: the super is the building.

Morning: a guest in a paper ADMIN badge asks the gallery for a printout of who is recording (QUERY / logman). The clipboard says jackets only. The guest is denied. Good.

Still morning: the same guest walks the hallway and unplugs Defender’s camera (STOP). The clipboard was never in the hallway. The camera goes dark. The DACL on that plug might say SYSTEM, so a mere local admin might need to steal the super’s other key first. On many boxes SYSTEM is a short walk from admin.

Afternoon: the guest tapes a green stamp on their own clipboard (hook ControlTrace QUERY), sits in the gallery, and watches the memory-mischief camera. The kernel is still streaming. Defender may still be watching. Now there are two viewers, and one of them is not wearing a jacket. That is the consume result, confirmed in the original by walking ETW_REALTIME_CONSUMER to an EPROCESS with Protection.Level = 0.

Evening: Microsoft legal says the lease always let the building owner sit in the gallery. Origin says please put a lock on the gallery door that is not a stamp the guest can print. Both can be true.

sechost as a client-side bouncer (CWE-602)

CWE-602 is usually taught with JavaScript validation: the browser checks a form, the server forgets to. Here the “server” is the NT kernel and the “client” is sechost.dll loaded into whoever called OpenTrace. The kernel will stream TI to a realtime consumer that attached through NtTraceControl without repeating the PPL test that EtwpQueryTrace would have applied. That is a design smell even if it is not a CVE. Origin’s hook is the educational version of “the client skipped its own validation.” Native ntdll consume is the version that never loaded the validator.

Why would the check live in user-mode? QUERY already had to copy a large property blob out; someone reused that path to also enforce SecurityTrace, which is cheap and keeps a policy next to the documented API surface. The cost is exactly this paper: documented consume is gated, undocumented consume is not, and hooking the documented path is enough because the gate is not on the undocumented path either. A kernel-side check on consumer registration would collapse those three sentences into one denial.

For code reviewers of Windows-adjacent security products: any time you implement an ACL in the same process as the untrusted caller, assume the caller can skip it. PPL is supposed to make that assumption false for other processes attacking you. It does not make it false for you attacking your own imports. Antimalware-PPL on the consumer would still stop a second, non-PPL process from using the documented APIs. It would not stop that second process from using ntdll, which is the first option in McGarr’s list.

Kitchen table: If the bouncer’s only job is to look at a stamp you hold, you can draw the stamp. If the door itself refuses to open without a jacket sewn into the lock, drawing stops working. Today the stamp is sechost. The lock would be nt!Etwp*.

The twenty figures in this draft are the original IDA and WinDbg screenshots, in original order, unscaled. The C and debugger dumps are the original blocks. The GitHub repo is the runnable rest. If you need to see a full consumer, clone it in a VM; do not paste it into a ticket tracker. If you need to explain the issue to someone who will never read dt nt!_WMI_LOGGER_CONTEXT, the velvet-rope story at the top is the same paper.

Eighty loggers is a hard cap. SecurityTrace sessions are a handful on a stock desktop (two Defender names, plus whatever you added). That smallness is helpful: a hunt that lists every SecurityTrace logger and its consumers is a short list, not a SIEM firehose. Make that list part of a weekly dump or a live kernel query if you already collect memory. The original dx one-liners are enough to start.

Defender’s extra lock on the plug

McGarr is careful: stop is not “any admin, any logger.” DefenderApiLogger carries additional security descriptors, so the practical caller for that name is SYSTEM. Local admin to SYSTEM is a well-worn staircase (service configuration, named pipes, token games, scheduled tasks). It is still a step. A hunt that only looks for Administrator-integrity ControlTrace(STOP) will miss the SYSTEM-integrity variant that actually matches the Defender DACL. Look at the calling SID and the logger name together.

The same DACL does not save you from consume-via-AutoLogger of a new session you created. That session’s DACL is yours. The interesting Defender-adjacent case is not stealing Defender’s logger; it is standing up a sibling AutoLogger that also lists the TI GUID, then consuming that sibling. Defender can keep its cameras. You added another.

Johnson, Ionescu, and the rest of the bookshelf

This post sits on a shelf, not in a vacuum. Ionescu’s PPL history is why anyone expects “admin cannot touch that.” Johnson’s Threat-Intelligence series is why anyone knows TI is keyword-plus-opt-in, not a firehose. Trail of Bits’ ETW-internals piece is why WinDbg dx on EtwpLoggerContext is a normal research move. windows-internals.com on syscall TI is why NtQuerySystemInformation showing up in the default-on list is a KASLR conversation, not only an EDR conversation.

If you only read Origin’s consume trick, you will overestimate what you hear (optional bits still gated) and underestimate what you hide (0x4000 StartTrace is a stealth logger even with no TI). Read the four links in the original before you write a detection that keys on one GitHub string.

A lab checklist that is not a weaponized consumer

  1. Dump AutoLogger keys. Note provider GUIDs and EnableSecurityProvider. Snapshot before and after installing security software.
  2. In a kernel debugger on a lab VM, run the original dx that filters SecurityTrace==1. Confirm DefenderApiLogger / DefenderAuditLogger. Add a throwaway AutoLogger, reboot, confirm a third name.
  3. From SYSTEM, QUERY the Defender loggers; expect ACCESS_DENIED. STOP only on a VM you do not care about; expect success if DACL allows.
  4. From a non-PPL admin process, StartTrace with LogBuffersLost=0x4000. QUERY from another process; expect deny. STOP from the creator; expect success.
  5. Do not paste a full ControlTrace hook into production tooling. If you must validate consume, use the public repo in a disposable VM and then delete the VM.
  6. On Insider vs retail, compare the optional-TI process grid. Write detections that survive both.
Kitchen table: This is a fire drill, not a new hobby for the office laptops. Unplug cameras only in the building you rent for the afternoon. The public repo is a spare key in a labeled envelope. Leave it in the envelope until you are inside the rental.

Key Takeaways

  • SecurityTrace (bit 14 / 0x4000) marks ETW sessions that should be PPL-only to QUERY. Defender AutoLoggers are the stock examples.
  • STOP does not require PPL. QUERY does. Consume via documented APIs QUERYs in user-mode, so a hook or NtTraceControl skips the jacket check.
  • Three enable paths: AutoLogger TI/Kernel-Audit providers; AutoLogger EnableSecurityProvider; StartTrace with LogBuffersLost=0x4000.
  • AutoLogger TI enablement is kernel-context: no EtwpCheckNotificationAccess. That is why a later user-mode consume check was doing all the work — and why it fails closed only if you do not patch yourself.
  • Default TI events (exec alloc/map, remote APC, SetThreadContext, driver load, a few syscalls) are reachable without PPL. Optional bits still need PPL unless already opted in.
  • MSRC: not a vuln; admin ↔ PPL is not a boundary. Fix still worth doing: check in the kernel. Public POC on GitHub; not mirrored here.

Defensive Recommendations

  1. Monitor AutoLogger keys for TI / Kernel-Audit GUIDs, EnableSecurityProvider, and odd names that sort first.
  2. Alert on STOP of DefenderApiLogger / DefenderAuditLogger and on SecurityTrace sessions whose consumers are not PPL.
  3. Treat sechost ETW helpers as sensitive: hooking ControlTrace QUERY in a non-PPL process is the consume trick.
  4. Do not write detections that say “TI ⇒ PPL process.” Split default-on vs optional bits.
  5. If you ship an ETW-based product: do not rely on SecurityTrace plus sechost QUERY as your only ACL. Assume admin can see and stop you. Use DACLs, PPL for your own consumer, and kernel-side checks where you can.
  6. Microsoft-facing: McGarr’s ask — evaluate SecurityTrace on the kernel consume/query path, not only in sechost.
  7. Lab the public POC on a throwaway VM if you need to validate detections. Do not drop it on a gold image.
  8. Read Johnson’s TI posts before you decide which events you thought were private.

Conclusion

A bit named SecurityTrace looks like a velvet rope. On QUERY, it is. On STOP, it is a polite nod. On AutoLogger bring-up, it is a rain-check because the kernel itself flipped the cameras on. The consume check that was supposed to cash that rain-check lives in a DLL inside the guest. Origin’s team hooked the clipboard, sat down, and listened to Threat-Intelligence without a jacket, a driver, or a kernel patch. MSRC declined a CVE. The cameras still work. Put the bouncer in brick, watch the AutoLogger hive, and leave the GitHub consumer for a VM you intend to wipe.

Original text: “Windows Internals: Check Your Privilege – The Curious Case of ETW’s SecurityTrace Flag” by Connor McGarr at Connor McGarr’s Blog / Origin (by Prelude).

oxfemale Vulnerability research, reverse engineering, and exploit development.
// Discussion