

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/ProcessTrace → EtwpQueryRealTimeTraceProperties → ControlTrace 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
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.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.

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


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

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.

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.


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 EtwpStopTrace → EtwpStopLoggerInstance. 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.

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");

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.

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.

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.

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.

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.


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 --->

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.

//
// <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.
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.

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:
- Skip
OpenTrace/ProcessTrace. SpeakNtTraceControlfrom ntdll and never run the sechost QUERY. - Hook
EtwpQueryRealTimeTracePropertiesor exportedControlTraceand 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.


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.

The recipe in the original, as a sequence, not as a drop-in implant:
- 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.
- In your admin consumer, hook
ControlTraceQUERY so it returns a hand-builtEVENT_TRACE_PROPERTIESinstead of talking toEtwpQueryTrace. - Call
OpenTraceandProcessTraceas 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.

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]
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,
NtSystemDebugControlandNtQuerySystemInformation(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.”

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

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.
A glossary for both sides of the table
| Term | Kitchen | Operator |
|---|---|---|
| ETW session | One camera and its DVR clip. | WMI_LOGGER_CONTEXT + buffers; max 80. |
| AutoLogger | Cameras the building turns on at sunrise from a filing cabinet. | HKLM\…\WMI\Autologger; kernel-started; no process identity. |
| SecurityTrace | A ‘security camera’ tick on the clipboard. | Flags bit 14 (0x4000) on the logger context. |
| Antimalware-PPL | The embroidered jacket. | Protected Process Light, antimalware signer; required for QUERY. |
| QUERY / ControlTrace | Asking what the camera is pointed at. | EtwpQueryTrace; PPL-gated when the bit is set. |
| STOP | Unplugging the camera in the hallway. | EtwpStopTrace; no PPL check; DACL may still demand SYSTEM. |
| LogBuffersLost union | One form box, two meanings. | EVENT_TRACE_PROPERTIES.LogBuffersLost overlays WMI_LOGGER_INFORMATION.Flags. |
| OpenTrace/ProcessTrace | Sitting down in the gallery. | Real-time consume; sechost QUERYs first. |
| TI provider | The camera that films memory tricks. | Microsoft-Windows-Threat-Intelligence, F4E1897C-BB5D-5668-F1D8-040F4D8DD344. |
| Optional TI bits | Extra microphones Defender can clip on a guest. | NtSetInformationProcess; still PPL to enable; sometimes pre-enabled. |
ATT&CK, CWE, and what not to file
| Thing | Useful mapping | Do not file |
|---|---|---|
| Admin stops DefenderApiLogger | T1562 Impair Defenses / T1562.006 Indicator Blocking | A CVE on Defender; MSRC declined |
| Admin consumes TI via AutoLogger + QUERY hook | T1562; 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 = 0x4000 | Defense evasion: hide a logger from non-PPL QUERY | CWE on StartTrace documentation alone |
| User-mode security check | CWE-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 loggers | Privileges still matter; local admin ≠ always enough | “Any user can stop Defender ETW” |
Detections that do not need the GitHub tree
- Registry: new AutoLogger subkeys, especially names that sort early (leading
0, digits, punctuation), provider GUIDs equal to TI or Kernel-Audit-Api-Calls, orEnableSecurityProviderpresent. - Kernel/dump:
SecurityTrace==1loggers whose consumerEPROCESS.Protection.Levelis 0. - User-mode: non-PPL processes calling
OpenTrace/ProcessTrace/NtTraceControlagainst logger IDs 4–80 after a denied QUERY. - Integrity: sechost!ControlTrace / EtwpQueryRealTimeTraceProperties hooks in processes that are not your ETW tooling (inline patches, Detours, unexpected trampolines).
- Defender: unexpected STOP of DefenderApiLogger / DefenderAuditLogger (event logs, missing sessions after boot).
- 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
- Alex Ionescu — evolution of protected processes
- Microsoft — AutoLogger sessions
- Trail of Bits — ETW internals for research and forensics
- Jonny Johnson — how TI is protected
- Jonny Johnson — impersonation events
- windows-internals.com — syscall TI and KASLR
- preludeorg/ThreatIntelligenceConsumer
- Origin HQ original posting
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.
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 set | Needs PPL? | Needs reboot? | QUERY hidden? | STOP still admin? |
|---|---|---|---|---|
| AutoLogger lists TI or Kernel-Audit-Api-Calls | No (kernel enable) | Yes (or AutoLogger start) | Yes | Yes, plus DACL |
| AutoLogger EnableSecurityProvider | No | Yes | Yes; also joins Security-Auditing | Yes, plus DACL |
| StartTrace LogBuffersLost=0x4000 | No | No | Yes | Yes, plus DACL |
| EnableTraceEx2(TI) on a live session | Yes | No | N/A if enable fails | N/A |
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.
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.
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
- Dump AutoLogger keys. Note provider GUIDs and EnableSecurityProvider. Snapshot before and after installing security software.
- In a kernel debugger on a lab VM, run the original
dxthat filtersSecurityTrace==1. Confirm DefenderApiLogger / DefenderAuditLogger. Add a throwaway AutoLogger, reboot, confirm a third name. - From SYSTEM, QUERY the Defender loggers; expect ACCESS_DENIED. STOP only on a VM you do not care about; expect success if DACL allows.
- From a non-PPL admin process, StartTrace with LogBuffersLost=0x4000. QUERY from another process; expect deny. STOP from the creator; expect success.
- 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.
- On Insider vs retail, compare the optional-TI process grid. Write detections that survive both.
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
NtTraceControlskips the jacket check. - Three enable paths: AutoLogger TI/Kernel-Audit providers; AutoLogger
EnableSecurityProvider;StartTracewithLogBuffersLost=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
- Monitor AutoLogger keys for TI / Kernel-Audit GUIDs,
EnableSecurityProvider, and odd names that sort first. - Alert on STOP of DefenderApiLogger / DefenderAuditLogger and on SecurityTrace sessions whose consumers are not PPL.
- Treat sechost ETW helpers as sensitive: hooking ControlTrace QUERY in a non-PPL process is the consume trick.
- Do not write detections that say “TI ⇒ PPL process.” Split default-on vs optional bits.
- 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.
- Microsoft-facing: McGarr’s ask — evaluate SecurityTrace on the kernel consume/query path, not only in sechost.
- Lab the public POC on a throwaway VM if you need to validate detections. Do not drop it on a gold image.
- 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).


