Feeding EDRs False Telemetry
In my previous blog post, I presented a project composed of two techniques that facilitated us to subvert EDR hooks and spoof arguments trivially. Now I will look to cover some of the details of "argument spoofing" and its potential. If you are interested in following along, I recommend grabbing a copy of the TamperingSyscalls project.
It is common knowledge that EDRs inject a DLL into an application to install hooks into commonly abused (malicious) functions. If you are interested in a complete list of hooks from various vendors, I recommend checking out the EDRs repository by Mr Un1k0d3r.
Looking at one of the functions, 'NtAllocateVirtualMemory', this function is hooked by the majority of userland DLL will often hook this function to facilitate introspection into the arguments passed to it. Thus allowing it to build a picture of what the application may be doing; if it is determined to be malicious, the EDR may terminate the process. In this manner, the EDR will hook many functions that it believes will allow it to prevent malicious applications. Often EDRs will also provide you with the ability to view at a granular level what an application may be doing, such as if it is allocating memory in a remote or local process.
Let us have a look at spoofing some of the arguments for NtAllocateVirtualMemory. We first want to use the gen.py script to generate the necessary structures and functions to perform this technique.
python gen.py NtAllocateVirtualMemory
[+] Wrote: TamperingSyscalls.cpp!
[+] Wrote: TamperingSyscalls.h!
[+] Wrote: main.cpp!
gen.py provides us with the function pNtAllocateVirtualMemory as we pass it the corresponding parameter. The four-register fast-call calling convention for x64 allows the first four parameters of the function call to be given in RCX, RDX, R8, and R9. As TamperingSyscalls will set a hardware breakpoint on the syscall instruction, we can easily modify what these registers point to, thus potentially restoring them to what we want to use while earlier providing false information to what we wish the EDR to see.
In the exception handler, we can restore the arguments that we initially set them to in the pNtAllocateVirtualMemory thunk.
// pNtAllocateVirtualMemory thunk
pNtAllocateVirtualMemoryArgs.ProcessHandle = ProcessHandle;
pNtAllocateVirtualMemoryArgs.BaseAddress = BaseAddress;
pNtAllocateVirtualMemoryArgs.ZeroBits = ZeroBits;
pNtAllocateVirtualMemoryArgs.RegionSize = RegionSize;
We want to pass these arguments but would not like the EDR to see them. Initially, we can hide them by passing NULL for these first four arguments.
fNtAllocateVirtualMemory( NULL, NULL, NULL, NULL, pNtAllocateVirtualMemoryArgs.AllocationType, pNtAllocateVirtualMemoryArgs.Protect );
However, this is a possible means to lead to detection as various EDR vendors may catch on and start looking for NULL parameters (which is trivial to bypass). Now to begin giving the EDR false telemetry, we need to think about what arguments are helpful for an EDR to pick to determine the nature of a call. Continuing from our earlier examples, NtAllocateVirtualMemory takes six parameters, of which I believe the most important is the first, ProcessHandle. This handle specifies which process the allocation should take place. If we would like to allocate memory in a remote process but would not like the EDR to see it, we'd pass the remote process handle to pNtAllocateVirtualMemory, but we would replace the first parameter with (HANDLE)-1, which is just the current process handle. The EDR will see this value and allow it as it is not much suspicious for just allocating memory in our process compared to allocating memory in a remote process.
status = fNtAllocateVirtualMemory( (HANDLE)-1, NULL, NULL, NULL, pNtAllocateVirtualMemoryArgs.AllocationType, pNtAllocateVirtualMemoryArgs.Protect );
The handle we would like to use will later be restored in the exception handler, as we can see in the case switch below.
ExceptionInfo->ContextRecord->R10 = (DWORD_PTR)((NtAllocateVirtualMemoryArgs*)(StateArray[EnumState].arguments))->ProcessHandle;
ExceptionInfo->ContextRecord->Rdx = (DWORD_PTR)((NtAllocateVirtualMemoryArgs*)(StateArray[EnumState].arguments))->BaseAddress;
ExceptionInfo->ContextRecord->R8 = (DWORD_PTR)((NtAllocateVirtualMemoryArgs*)(StateArray[EnumState].arguments))->ZeroBits;
ExceptionInfo->ContextRecord->R9 = (DWORD_PTR)((NtAllocateVirtualMemoryArgs*)(StateArray[EnumState].arguments))->RegionSize;
The reason in this example that it is R10, not RCX, is that at the start of every syscall stub you have mov r10, rcx. We cast everything to (DWORD_PTR) as this is the required size of the values (it is not an issue as we don't loose anything).
The example provided is only the beginning of what is possible. It should allowt tfor a greater malleability in C2 profiles and the ability to emulate different applications and trick EDRs into an entirely different storyline of what may be happening. For example have a look @__mez0__'s tweet, telling the EDR a completely different story.
Post a Comment