ETW-TI notes (WinDbg)

Microsoft-Windows-Threat-Intelligence is a kernel ETW provider. Defender (DefenderApiLogger) and EDRs subscribe. Ordinary processes cannot — consumers are PPL-Antimalware.

GUID {F4E1897C-BB5D-5668-F1D8-040F4D8DD344}. Always hashes to bucket 29.

ETW roles: provider fires → session buffers → consumer reads. If no session, IsEnabled=0 and the kernel skips the event (no cost).

.reload /f first if names do not resolve. LiveKD is read-only — eb needs a writable kernel debug session.

Event Trigger
ALLOCVM NtAllocateVirtualMemory with exec
MAPVIEW NtMapViewOfSection into another process
READVM / WRITEVM cross-process memory
PROTECTVM change to exec
QUEUEUSERAPC APC inject
SETTHREADCONTEXT context hijack

architecture events


Find _ETW_GUID_ENTRY

nt!EtwpDebuggerData[3] → silo → GuidHashTable[29] → walk Flink until GUID

chain

_ETW_GUID_ENTRY
+0x000  GuidList          LIST_ENTRY (bucket chain)
+0x028  Guid              16 bytes  ← first DWORD Data1 = 0xF4E1897C
+0x060  ProviderEnableInfo  _TRACE_ENABLE_INFO  (OR of all slots)
+0x080  EnableInfo[8]     one slot per session, each 0x20
+0x188  SiloState         back-pointer to silo

_TRACE_ENABLE_INFO
+0x000  IsEnabled         1=on, 0=off  ← eb this
+0x004  Level
+0x006  LoggerId          USHORT, index into EtwpLoggerContext
+0x008  EnableProperty
+0x010  MatchAnyKeyword
+0x018  MatchAllKeyword

+0x028 / +0x060 / +0x080 are hex. Slot s = entry + 0x80 + s*0x20.


Hash table

Silo root

dp nt!EtwpDebuggerData L4

4th pointer (+0x18, index 3) = _ETW_SILODRIVERSTATE. Save it.

? nt!EtwpDebuggerData - nt

Dump silo

dt nt!_ETW_SILODRIVERSTATE <silo>
Field Off Use
EtwpLoggerContext +0x1C8 session array (names, optional)
EtwpGuidHashTable +0x1D0 64 buckets, embedded (silo+0x1D0)

Bucket size

?? sizeof(nt!_ETW_HASH_BUCKET)

Expect 0x38 (56). Bucket 29 offset = 29 * 0x38 = 0x658.

TI GUID hash (fixed):

(Data1 ^ Data2Data3 ^ Data4[0] ^ Data4[4]) & 0x3F
= (0xF4E1897C ^ 0xBB5D5668 ^ 0xF1 ^ 0x04) & 0x3F
= 0x1D = 29

Bucket 29

dt nt!_ETW_HASH_BUCKET <silo>+0x1D0+0x658

On current builds ListHead is [3]. TI is a trace GUID → use ListHead[0].Flink.

dt nt!_ETW_GUID_ENTRY <ListHead[0].Flink>

If Guid is not TI, dq that Flink and dt the next node. Stop when Flink equals the bucket address (silo+0x1D0+0x658). Empty list: ListHead[0].Flink == bucket address.

(etwti.c reads kread64(bucketHead) = this same Flink, then walks until Data1 == 0xF4E1897C.)

Prefer the Flink walk. Search only if the list is messy:

s -b <silo>+0x1D0 L?0x2000000 7c 89 e1 f4 5d bb 68 56 f1 d8 04 0f 4d 8d d3 44

The hit is the Guid field, not the struct. Subtract 0x28:

dt nt!_ETW_GUID_ENTRY <hit>-0x28

Do not dt the hit address itself — GuidList will look like the GUID bytes. Multiple hits = extra silos; keep the one whose SiloState (+0x188) matches <silo>.

dt nt!_ETW_GUID_ENTRY <entry>

List: is anyone listening?

Aggregate (fast path):

dt nt!_TRACE_ENABLE_INFO <entry>+0x60

IsEnabled=1 → at least one session. Defender often has MatchAnyKeyword = 0xFFFFFFFFFFFFFFFF.

Eight slots (+0x80, step +0x20):

dt nt!_TRACE_ENABLE_INFO <entry>+0x80
dt nt!_TRACE_ENABLE_INFO <entry>+0xa0
… eight times

Note LoggerId where IsEnabled=1. Typical: 2–3 slots.

Session name

EtwpLoggerContext is on the silo (+0x1C8). Also the third QWORD of dp nt!EtwpDebuggerData L4.

dt nt!_ETW_SILODRIVERSTATE <silo> EtwpLoggerContext

LoggerId comes from an enabled slot (IsEnabled=1):

dt nt!_TRACE_ENABLE_INFO <entry>+0x80

Each array slot is 8 bytes. Example: LoggerId 66*8 = 0x30.

dq <EtwpLoggerContext>+30 L1

Left column = address of slot [6]. Right column = _WMI_LOGGER_CONTEXT *. dt the right value:

dt nt!_WMI_LOGGER_CONTEXT <result>

LoggerName is the consumer (session), e.g. DefenderApiLogger. Confirm field offsets with dt on the target.

Do not dt EtwpLoggerContext itself. Slot [0] is often 1 (sentinel) — skip it. Only follow array + LoggerId*8 when IsEnabled=1.


Disable

Zero aggregate:

eb <entry>+0x60 0

Zero one consumer (slot 0 = +0x80):

eb <entry>+0x80 0

etwti.c disable zeros aggregate and all 8 slots.

Kernel then skips TI events. LiveKD cannot write.


Commands

Cmd Does
dp / dq / dd dump pointers / QWORDs / DWORDs
eb edit byte (IsEnabled)
dt dump typed struct
?? sizeof(...) type size, no memory read
s -b byte search (GUID)
poi(addr) read pointer at addr

Offsets (+0x1D0, +0x60, +0x80, …) are build-specific — confirm with dt on the target. Bucket 29 is GUID-fixed.