Introduction to Threat Intelligence ETW
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
:
EtwTiLogReadWriteVm
called in MiReadVirtualMemory
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
:
EtwProviderEnabled
called with EtwThreadIntProvRegHandle
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
EtwThreatIntProvRegHandle
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:
EtwThreatIntProvRegHandle
initialisationEtwThreatIntProviderGuid
is defined as such:
EtwThreatIntProviderGuid
GUID valueWe can verify that the Microsoft-Windows-Threat-Intelligence provider exists using logman
on the command line:
logman
showing Microsoft-Windows-Threat-Intelligence providerI'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
:
EtwEventEnabled
with different event descriptorsIf 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):
THREATINT_MAPVIEW_LOCAL_KERNEL_CALLER: false
THREATINT_PROTECTVM_LOCAL_KERNEL_CALLER: false
THREATINT_ALLOCVM_LOCAL_KERNEL_CALLER: false
THREATINT_SETTHREADCONTEXT_REMOTE_KERNEL_CALLER: false
THREATINT_QUEUEUSERAPC_REMOTE_KERNEL_CALLER: false
THREATINT_MAPVIEW_REMOTE_KERNEL_CALLER: false
THREATINT_PROTECTVM_REMOTE_KERNEL_CALLER: false
THREATINT_ALLOCVM_REMOTE_KERNEL_CALLER: false
THREATINT_THAW_PROCESS: false
THREATINT_FREEZE_PROCESS: false
THREATINT_RESUME_PROCESS: false
THREATINT_SUSPEND_PROCESS: false
THREATINT_RESUME_THREAD: false
THREATINT_SUSPEND_THREAD: false
THREATINT_WRITEVM_REMOTE: true
THREATINT_READVM_REMOTE: false
THREATINT_WRITEVM_LOCAL: false
THREATINT_READVM_LOCAL: false
THREATINT_MAPVIEW_LOCAL: false
THREATINT_PROTECTVM_LOCAL: false
THREATINT_ALLOCVM_LOCAL: true
THREATINT_SETTHREADCONTEXT_REMOTE: true
THREATINT_QUEUEUSERAPC_REMOTE: true
THREATINT_MAPVIEW_REMOTE: true
THREATINT_PROTECTVM_REMOTE: true
THREATINT_ALLOCVM_REMOTE: true
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
:
EtwWrite
setup and callFollowing the function definition, the data, UserData
is passed in the 5th argument and the number of entries is in the 4th:
NTSTATUS EtwWrite(
REGHANDLE RegHandle,
PCEVENT_DESCRIPTOR EventDescriptor,
LPCGUID ActivityId,
ULONG UserDataCount,
PEVENT_DATA_DESCRIPTOR UserData
);
EtwWrite
function definitionOn a breakpoint in NtWriteVirtualMemory
, we see the following arguments passed into the function:
rcx=0000000000000e7c (ProcessHandle)
rdx=0000020051af0000 (BaseAddress)
r8=000000cf8697e168 (Buffer)
r9=000000000000018c (NumberOfBytesToWrite)
NtWriteVirtualMemory
On a breakpoint before calling EtwWrite
in EtwTiLogReadWriteVm
, the UserData
can be seen like so:
2: kd> dq @rax L@r9*2
ffffd286`70970880 ffffd286`709709d0 00000000`00000004
ffffd286`70970890 ffffd601`2e59b468 00000000`00000004
ffffd286`709708a0 ffffd601`2e59b490 00000000`00000008
ffffd286`709708b0 ffffd286`70970870 00000000`00000008
ffffd286`709708c0 ffffd601`2e59b878 00000000`00000001
ffffd286`709708d0 ffffd601`2e59b879 00000000`00000001
ffffd286`709708e0 ffffd601`2e59b87a 00000000`00000001
ffffd286`709708f0 ffffd601`2cdb16d0 00000000`00000004
ffffd286`70970900 ffffd601`2cdb1680 00000000`00000008
ffffd286`70970910 ffffd601`2e991368 00000000`00000004
ffffd286`70970920 ffffd601`2e991390 00000000`00000008
ffffd286`70970930 ffffd286`70970878 00000000`00000008
ffffd286`70970940 ffffd601`2e991778 00000000`00000001
ffffd286`70970950 ffffd601`2e991779 00000000`00000001
ffffd286`70970960 ffffd601`2e99177a 00000000`00000001
ffffd286`70970970 ffffd286`709709f0 00000000`00000008
ffffd286`70970980 ffffd286`709709f8 00000000`00000008
EtwWrite
EVENT_DATA_DESCRIPTOR
entriesEach entry is an EVENT_DATA_DESCRIPTOR
structure defined as such:
typedef struct _EVENT_DATA_DESCRIPTOR {
ULONGLONG Ptr;
ULONG Size;
union {
ULONG Reserved;
struct {
UCHAR Type;
UCHAR Reserved1;
USHORT Reserved2;
} DUMMYSTRUCTNAME;
} DUMMYUNIONNAME;
} EVENT_DATA_DESCRIPTOR, *PEVENT_DATA_DESCRIPTOR;
EVENT_DATA_DESCRIPTOR
structureThe 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:
2: kd> dq poi(@rax+f0) L1
ffffd286`709709f0 00000200`51af0000
2: kd> dq poi(@rax+100) L1
ffffd286`709709f8 00000000`0000018c
EtwWrite
dataBut 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:
NtWriteVirtualMemory
event dataHere is the entire argument list:
OperationStatus
CallingProcessId
CallingProcessCreateTime
CallingProcessStartKey
CallingProcessSignatureLevel
CallingProcessSectionSignatureLevel
CallingProcessProtection
CallingThreadId
CallingThreadCreateTime
TargetProcesId
TargetProcessCreateTime
TargetProcessStartKey
TargetProcessSignatureLevel
TargetProcessSectionSignatureLevel
TargetProcessProtection
BaseAddress
BytesCopied
NtWriteVirtualMemory
event dataProtection 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:
MiMakeProtectionMask
operates on the requested protection valueThe return value of MiMakeProtectionMask
is set to the r13d
register which is later referenced when deciding if code should branch to EtwTiLogAllocExecVm
:
MiMakeProtectionMask
return value determines if the call should be loggedWhat'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:
MiMakeProtectionMask
on requested protectionThough 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:
MiMakeProtectionMask
on current protectionThe 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