Introduction to Threat Intelligence ETW
A quick look into ETW capabilities against malicious API calls.
Recently, the ETW functionality of Windows Defender was reintroduced to my attention after some discussion of existing methods of detecting malicious API calls and kernel callbacks (e.g.
ObRegisterCallbacks). I've briefly heard of the ability for Defender to detect malicious APC injection which was researched here in a blog post by Souhail Hammou on Examining the user-mode APC injection sensor introduced in Windows 10 build 1809 which mentions the
EtwTiLogQueueApcThread code. However, I just discovered that there was more than just APC injection. A recent blog post by B4rtik on Evading WinDefender ATP credential-theft: kernel version talks about attacking the ETW within the kernel by inline patching
nt!EtwTiLogReadWriteVm to bypass detection of LSASS reads with
NtReadVirtualMemory. It made me more curious as to how ETW worked so I had a look...
Note: The software versions at the time of writing are:
- Microsoft Windows 10 Enterprise Evaluation Version 10.0.18363 Build 18363z
- ntoskrnl.exe Version 10.0.18362.592
- Windows Defender Antimalware Client Version: 4.18.1911.3
- Windows Defender Engine Version: 1.1.16700.3
- Windows Defender Antivirus Version: 1.309.527.0
- Windows Defender Antispyware Version: 1.309.527.0
Uncovering Threat Intelligence ETW Capabilities
Following B4rtik, I looked into
MiReadVirtualMemory (which is just wrapped by
NtReadVirtualMemory). As described, it eventually makes a call to
Judging by the name, this is probably called by
NtWriteVirtualMemory as well. If we take a look inside, there's a function call to
EtwProviderEnabled which takes in the argument
So this handle, I assume, is associated with "threat intelligence" events. If we cross-reference this handle, we can see that it is used in multiple other locations, namely:
It's quite obvious from these function names that the threat intelligence provider seems to log event data on very commonly-used malicious API such as
ResumeThread which are the bread and butter of process hollowing.
There is also a reference to
EtwpInitialize which is where the handle is initialised:
EtwThreatIntProviderGuid is defined as such:
We can verify that the Microsoft-Windows-Threat-Intelligence provider exists using
logman on the command line:
I'm assuming that, theoretically, all of the usermode API derived from the cross-references of the
EtwThreatIntProvRegHandle handle can be detected in real time by defensive tools subscribed to the event notifications.
There are different types of descriptors for each type of event "capability". If we take a quick look at the code after the call to
EtwTiLogReadWriteVm, we can see references to symbols like
If we cross-reference one of these, we'll find the entire list of descriptors:
EtwEventEnabled function determines if a certain event is enabled for logging on the associated provider handle. Brief analysis of the function, with the
EtwThreatIntProvRegHandle static, shows that one of the key contributors of which event descriptor is logged relies on the bitmask of both the handle and event descriptor's
_EVENT_DESCRIPTOR.Keyword value. If these two values
tested together is not 0, the event will be logged.
The handle's value is a consistent
0x0000000`1c085445 value (across reboots) and the event descriptor's
Keyword is detailed in the Threat Intelligence array shown above. If we
& the handle's value and each of the event descriptor's bitmask values, we can see which are logged and which aren't (if I got this right):
Here, local and remote refer to either its own (local) process or another (remote) process. We can see that local memory allocation and all but one of the remote operations are set to logged. There is a discrepancy here between this data and B4rtik's post. If remote virtual memory reads are not enabled here then how does Defender detect LSASS reads? Perhaps because B4rtik's Defender is ATP which I, unfortunately, do not have at the time of writing this. If this is true, then maybe the handle's
0x0000000`1c085445 value may be different as well.
Writing Event Data
Since this system does not receive event data on any virtual memory reads, let's look at the case of writes. If the
EtwEventEnabled function returns
TRUE, it will proceed to write the data using
Following the function definition, the data,
UserData is passed in the 5th argument and the number of entries is in the 4th:
On a breakpoint in
NtWriteVirtualMemory, we see the following arguments passed into the function:
On a breakpoint before calling
UserData can be seen like so:
Each entry is an
EVENT_DATA_DESCRIPTOR structure defined as such:
Ptr points to the data and
Size describes the size of the
Ptr data in bytes. But what kind of data is logged? If we peek into some of these values, we can make out that the last two values correspond to the base address and the number of bytes written:
But what are the other 15 arguments? Luckily, the data is already out there. I gathered this information in ETW Explorer written by Pavel Yosifovich. If we explore the Microsoft-Windows-Threat-Intelligence provider and select the appropriate event descriptor, we can see all of the arguments:
Here is the entire argument list:
If we reverse engineer another capability,
NtAllocateVirtualMemory, we can see that there is another requirement besides being a local or remote operation. The call to
MiMakeProtectionMask identifies the requested protection type:
The return value of
MiMakeProtectionMask is set to the
r13d register which is later referenced when deciding if code should branch to
What's interesting is that
MiMakeProtectionMask will return a value such that it will log the call if the requested protection includes execution permissions. I guess judging from the
EtwTiLogAllocExecVm, it could be assumed that this the sole purpose.
This also occurs in the
NtProtectVirtualMemory call. It first has a call to
MiMakeProtectionMask with the requested protection:
Though this is used to check if the protection type is valid, it may also return a value similar to that of
NtAllocateVirtualMemory's. The second call to
MiMakeProtectionMask is used to check the current protection:
The return value of this is combined with the value derived from the new protection. So if either the new or the current protection has execute permissions, the operation will be logged.
The Threat Intelligence ETW provides an interesting insight into how Microsoft may improve detection of malicious threats in conjunction with other kernel callbacks. Some things to note: being event-based makes this a retroactive system and some data is not recorded, for example, in
NtWriteVirtualMemory, the data being written is not captured. Though I guess that the data may already exist in the given target address so it might not matter.
Having analysed which operations may and may not be logged, perhaps creating bypasses against defensive tools that utilise Threat Intelligence ETW may be more reliable. For example, local allocation without execute permissions will not be logged in addition to local protection logging being disabled, it is possible to allocate
RW malicious code before reprotecting it with execute permissions. This would, theoretically, bypass any Threat Intelligence ETW captures.
Despite this technology being introduced, there is always the risk of false positives. Throughout the process of debugging, I've encountered an abundant amount of remote virtual memory writes just from the operating system itself. It's also known that .NET processes use
RWX page permissions for JIT (which can also be abused for local injection of malicious code).
TL;DR: Don't touch other processes and allocate non-execute memory within your own process before reprotecting with execute permission.
Souhail Hammou - Examining the user-mode APC injection sensor introduced in Windows 10 build 1809
B4rtik - Evading WinDefender ATP credential-theft: kernel version