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. PsCreateThreadNotifyRoutine
, and 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 EtwTiLogReadWriteVm
:
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 EtwThreatIntProvRegHandle
:
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:
EtwTiLogInsertQueueUserApc
EtwTiLogAllocExecVm
EtwTiLogProtectExecVm
EtwTiLogReadWriteVm
EtwTiLogDeviceObjectLoadUnload
EtwTiLogSetContextThread
EtwTiLogMapExecView
EtwTiLogDriverObjectLoad
EtwTiLogDriverObjectUnLoad
EtwTiLogSuspendResumeProcess
EtwTiLogSuspendResumeThread
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 VirtualAlloc
, WriteProcessMemory
, SetThreadContext
and 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.
Event Descriptors
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 EtwProviderEnabled
in EtwTiLogReadWriteVm
, we can see references to symbols like THREATINT_WRITEVM_REMOTE
:
If we cross-reference one of these, we'll find the entire list of descriptors:
The 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 test
ed 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 EtwWrite
:
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 EtwWrite
in EtwTiLogReadWriteVm
, the UserData
can be seen like so:
Each entry is an EVENT_DATA_DESCRIPTOR
structure defined as such:
The 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:
Protection Mask
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 EtwTiLogAllocExecVm
:
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.
Conclusion
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.
References
Souhail Hammou - Examining the user-mode APC injection sensor introduced in Windows 10 build 1809
B4rtik - Evading WinDefender ATP credential-theft: kernel version