Back to blog

Module Stomping - The Standard Injection For Modern C2 Agents

Introduction


At Combat Theater, we’re always looking for malware techniques that reflect what modern operators are actually using. We spoke with multiple authors of commercial C2 products about process injection and one technique kept coming up again and again… Module stomping.

There are endless ways to inject code into other processes, but module stomping continues to show up in modern C2 tooling and for good reason. It is relatively simple to implement, gives operators some useful control over where their code executes and avoids some of the more obvious memory characteristics associated with traditional shellcode injection.

In this post we’ll break down how the technique works, look at why C2 developers continue to use it and explore the telemetry defenders can use to spot it.

What Is Module Stomping & How Does It Work


Module stomping is a process injection technique where shellcode is written into the address space of a legitimately loaded DLL and then executed from within that module. There are many variations, but most implementations follow the same general stages:

1. Select a Target Process

Module stomping can be performed locally inside its own process or into a remote process. In practice C2 operators will typically opt for a remote process as their main use for this type of shellcode injection is to migrate their agent, while automated initial stagers typically choose from a predefined list of processes commonly found on default Windows machines.

HANDLE processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, targetPid);

2. Load a Sacrificial DLL

The attacker next loads a sacrificial DLL into the target process that will act as a “container”. The two common approaches are:

LoadLibrary / LdrLoadDll

The DLL is loaded via the normal Windows loader giving it the usual loader metadata and module entries. For remote injection a common pattern is to first write the DLL path into the target process then create a thread in that process which calls LoadLibrary() or LdrLoadDll() with the string as its argument.

One complication with using LoadLibrary() directly is that the DLL is fully initialised. This can cause multiple issues:

  • If the overwritten code is called legitimately it can cause instability or crash the process
  • DLLMain is automatically called which can create new threads, open handles and other potentially noisy activities

Because of this some implementations opt for using LoadLibraryEx() with the DONT_RESOLVE_DLL_REFERENCES flag. This maps the image without resolving imports and doesn’t execute DLLMain automatically, giving the attacker a cleaner sacrificial image to overwrite.

LoadLibraryExW(dll_path, NULL, DONT_RESOLVE_DLL_REFERENCES);

The trade-off is that DONT_RESOLVE_DLL_REFERENCES leaves detectable loader artefacts. In the process’s LDR_DATA_TABLE_ENTRY the module’s EntryPoint can be NULL and the image may not be marked as a DLL (ImageDll == FALSE).

NtCreateSection + NtMapViewOfSection

The DLL can also be “manually” mapped as an image section using lower level NT APIs. Unlike the Windows loader method, this does not automatically create the LDR_DATA_TABLE_ENTRY records found in the PEB so the module can appear incomplete.

NtCreateSection(&sectionHandle, SECTION_ALL_ACCESS, NULL, &sectionSize, PAGE_EXECUTE_READWRITE, SEC_IMAGE, fileHandle);

NtMapViewOfSection(sectionHandle, processHandle, &remoteBase, 0, 0, NULL, &viewSize, ViewUnmap, 0, PAGE_EXECUTE_READ);

Both approaches result in a legitimate PE image being loaded inside the process but the telemetry generated by each method can differ.

3. Overwrite Executable Code

Once the DLL has been loaded the attacker needs to find somewhere inside the module to place the shellcode. A common target is the module’s AddressOfEntryPoint because it is easy to locate, already sits inside executable image-backed memory and can also avoid some issues with Control Flow Guard where jumping to an arbitrary address may be blocked or cause the process to crash.

ReadProcessMemory(processHandle, remoteModule, headerBuffer, sizeof(headerBuffer), NULL);

auto dosHeader  = reinterpret_cast<PIMAGE_DOS_HEADER>(headerBuffer);
auto ntHeader   = reinterpret_cast<PIMAGE_NT_HEADERS>(headerBuffer + dosHeader->e_lfanew);
auto entryPoint = reinterpret_cast<LPVOID>(reinterpret_cast<ULONG_PTR>(remoteModule) + ntHeader->OptionalHeader.AddressOfEntryPoint);

The target region is temporarily made writable, the original bytes are replaced with the payload, and the previous protection is then restored.

VirtualProtectEx(processHandle, entryPoint, payloadSize, PAGE_EXECUTE_READWRITE, &oldProtect);

WriteProcessMemory(processHandle, entryPoint, payload, payloadSize, NULL);

VirtualProtectEx(processHandle, entryPoint, payloadSize, oldProtect, &oldProtect);

At this point the module still appears to be a legitimately loaded DLL, but part of its executable code in memory no longer matches the file on disk.

4. Execute the Stomped Region

Finally, execution is redirected to the entry point of the module. For remote injection this usually involves creating a new thread with APIs such as CreateRemoteThread() or NtCreateThreadEx() however other implementations may reuse an existing thread or make use of an obscure execution primitive.

CreateRemoteThread(processHandle, NULL, 0, reinterpret_cast<PTHREAD_START_ROUTINE>(entryPoint), NULL, 0, NULL);

Why Commercial C2 Frameworks Use Module Stomping


Module stomping is attractive to C2 developers because it changes some of the memory characteristics defenders normally associate with process injection. Instead of executing shellcode from a standard private memory region the attacker can perform the execution inside a legitimately loaded DLL’s memory space.

Image-Backed Execution

One of the biggest advantages is that execution appears to originate from a legitimately loaded module rather than an anonymous region of RX memory. From the outside the process now has several characteristics that look perfectly normal:

  • The DLL was loaded through standard and legitimately used Windows APIs
  • The module has a valid entry in the target process’s PEB LDR
  • The execution address falls inside the address range of a mapped image
  • The module is backed by its on-disk DLL

This matters because many injection detections start by looking for threads executing from private or unbacked executable memory. But with module stomping, the thread is executing from an address that belongs to a mapped image instead.

Clean Callstack

Module stomping can also make identifying the suspicious thread much more difficult. The thread start address is another useful side effect. Instead of pointing into an obvious private executable region it lands inside the address range of a legitimate DLL. For a small stager this can be enough to make the thread look less unusual without having to add extra machinery for call stack spoofing, which would otherwise increase the size and code complexity of the loader.

Suspicious thread from a private executable region vs module-backed thread

Left: thread starting in a private executable region. Right: execution backed by a legitimate loaded module.

Operator Module Selection

The sacrificial DLL does not need to be the same every time either. An operator can choose a module that makes sense for the process they are injecting into which can avoid loading the same obscure DLL across multiple processes, causing an opsec risk. Picking something that would plausibly appear in that process makes simple module name based detections nearly impossible.

Take spoolsv.exe as an example:

Suspicious DLL loaded into a known process

Module stomping performed with a rarely seen DLL loaded into spoolsv.exe, easy to identify as unusual.

Legitimate-looking DLL loaded into a known process

Module stomping performed with a more plausible module for the process, much harder to identify.

This makes detections based purely on specific module names nearly impossible and pushes defenders towards looking at the behaviour of the module after it has been loaded which adds a “visibility check” on the telemetry of the security products present on the machine.

Detection Opportunities


Module stomping has become the popular choice among the higher tier C2 implants and stagers for good reason, it’s not an easy technique to detect. That said, here are some suggestions from both a low level perspective and a higher level telemetry point of view:

Low Level Memory & Loader Artefacts

  • In memory vs on-disk differences: Dump and compare the mapped DLL against the file on disk to verify if it has been tampered
  • PEB loader inconsistencies: Modules loaded with LoadLibraryEx() and DONT_RESOLVE_DLL_REFERENCES can leave unusual LDR_DATA_TABLE_ENTRY fields such as EntryPoint being set to NULL and/or ImageDll being set to FALSE
  • Missing loader entries: Images mapped directly with NtCreateSection() / NtMapViewOfSection() exist in memory without a corresponding entry in the normal PEB->Ldr module lists
  • Abnormal stack unwinding: The thread call stack may appear clean but missing the thread initialization frame

Telemetry & Behavioural Detections

  • Unusual module loads: Identify DLLs that are rarely observed inside a particular process. AI assisted analysis could assign a rarity score to process+module combinations and highlight statistically rare pairings.
  • Suspicious memory modification: A newly loaded module followed by a protection change, a write and then another protection change in the executable memory region.
  • Suspicious thread starts: A new thread starting inside a recently loaded and modified module becomes more interesting when correlated with the previous events.
  • Network activity: An outbound connection immediately after execution can help narrow the signal towards an injected C2 agent.

Testing Your Visibility Against Module Stomping


You don’t really know how much visibility you have into module stomping until you run it.

Emulating the technique gives you something concrete to work with: execute it in a test environment, inspect what your EDR records and see which parts of the behaviour are actually available to build detections from.

Combat Theater includes both the LoadLibrary and section mapping variants of module stomping so the same test can be repeated using different implementations and the resulting telemetry can be compared across your security stack.

Combat Theater module stomping technique configuration

The implementation can also be changed at the API level using Combat Theater’s pick’n’mix system. That makes it easy to rerun the same technique with different API or syscall combinations and compare the telemetry each version produces.

Combat Theater function and syscall customization for module stomping

The same tests can then be repeated with ETW patched to identify how your visibility changes when one of the standard Windows telemetry sources is blinded.

Combat Theater module stomping with ETW patched