I recently needed to patch a few instructions in a dynamic library I was loading in. Up until now, I’ve been getting by with applying the changes after loading the library, as none of the exported methods had been called yet, but that wasn’t going to work this time.
Long story short, the code I needed to patch was somewhere within the library’s entrypoint. Unfortunately for me, this is always invoked automatically, whether you like it or not, as part of the call to LoadLibrary
. And yeah, it is possible to prevent this with certain LoadLibraryEx
flags, but I did want DllMain
to be called – I just needed a chance to apply my changes first.
Luckily, there are plenty of solutions to choose from. The easiest and most fool-proof would be to simply patch the file in advance, but… let’s just assume that isn’t possible for some reason. Another option would be to manually map the library, essentially implementing LoadLibrary
from scratch, but that’s far too involved for this scenario. What about hooking LoadLibrary
?
Well… that wouldn’t work either. As soon as we call the original LoadLibrary
, it’s already too late. For hooking to work, we’d have to take control somewhere in the middle – ideally when the library file on disk is mapped into the memory of the process.
Fortunately, that turned out to be a lot easier than expected. The internals of the Windows library loader have been thoroughly documented, making it trivial to find extensive write-ups, such as this excellent one from a 2002 issue of MSDN Magazine, and even an entire implementation of it in ReactOS, an open-source re-implementation of Windows.
Using the ReactOS code as a guide, I started by following LoadLibraryExW
into LdrLoadDll
, and eventually LdrpLoadDll
, where I came across a call to NtMapViewOfSection
. As good a place as any to start playing around, as it appeared to be where the target library, using a section handle obtained from LdrpCreateDllSection
, was mapped into the process. It’s hooking time!
To test this out, I’ve created two shared libraries and a host program. Both libraries print some text to the console, with the only noteworthy thing being that library_b.dll
imports a method from library_a.dll
, so we should see the text from Library A appear first, despite only asking for Library B to be loaded. Here’s what that looks like with no hooks in place:
Now let’s pull in SafetyHook and see what happens when we catch calls to NtMapViewOfSection
.
#include <print>
#include <windows.h>
#include <safetyhook.hpp>
using SECTION_INHERIT = int; // undocumented, lol
auto main() -> int
{
auto const NtMapViewOfSection =
GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtMapViewOfSection");
auto static hook = safetyhook::InlineHook {};
hook = safetyhook::create_inline(NtMapViewOfSection, +[] (
_In_ HANDLE SectionHandle,
_In_ HANDLE ProcessHandle,
_Inout_ PVOID *BaseAddress,
_In_ ULONG_PTR ZeroBits,
_In_ SIZE_T CommitSize,
_Inout_opt_ PLARGE_INTEGER SectionOffset,
_Inout_ PSIZE_T ViewSize,
_In_ SECTION_INHERIT InheritDisposition,
_In_ ULONG AllocationType,
_In_ ULONG Win32Protect
) -> NTSTATUS
{
auto const result = hook.call<NTSTATUS>(SectionHandle, ProcessHandle,
BaseAddress, ZeroBits, CommitSize, SectionOffset, ViewSize,
InheritDisposition, AllocationType, Win32Protect);
return result;
});
if (LoadLibraryA("library_b.dll"))
std::println("library_b.dll loaded");
}
Not really sure what to make of most of these arguments, but we can at least confirm with Process Hacker that the handle, 0xD0
, seen in SectionHandle
is indeed the library_b.dll
that we’re trying to load.
I’m eager to see what gets put in BaseAddress
, so let’s step over this and see what happens next.
Looking good! The module is now mapped into the process without any of its code being executed. Mildly interesting sidenote, but library_a.dll
is nowhere to be seen yet. Before working on this I always assumed it would be loaded first, but I guess not.
Now let’s try patching the “1337
” string in library_a.dll
to something else. First things first, we’ll need to take what we have here and turn it into a filename. Lots of ways to do this, but GetMappedFileName
is probably the easiest one to demonstrate.
auto const result = hook.call<NTSTATUS>(SectionHandle, ProcessHandle,
BaseAddress, ZeroBits, CommitSize, SectionOffset, ViewSize,
InheritDisposition, AllocationType, Win32Protect);
auto path = std::string(MAX_PATH, '\0');
auto size = GetMappedFileNameA(ProcessHandle, *BaseAddress, path.data(), MAX_PATH);
if (!size) [[unlikely]]
return result;
path.resize(size);
if (path.ends_with("library_a.dll"))
std::println("* {} => {:p}", path, *BaseAddress);
return result;
\Device\HarddiskVolume5\Projects\NtdllHookSandbox\cmake-build-debug\library_a.dll => 0x7fff5b240000
library_a::library_a()
library_b::library_b()
-> test() = 1337
library_b.dll loaded
Weird looking path, huh? NT paths like that can be converted back to DOS paths (the ones with drive letters) with enough effort, but let’s just stick with this for now – we only need the filename, so a simple ends_with
check is good enough.
To perform the patch, we just need to find the target string, call VirtualProtect
to make it writeable, make our changes, then restore the page permissions back to normal. No need to break out the hex editor here, let’s just search for it in memory.
if (path.ends_with("library_a.dll"))
{
auto region = std::span { static_cast<std::uint8_t*>(*BaseAddress), *ViewSize };
auto target = std::ranges::search(region, "1337").begin();
if (target == region.end())
return result;
auto old = DWORD {};
auto ptr = region.data() + std::distance(region.begin(), target);
VirtualProtect(ptr, 4, PAGE_EXECUTE_READWRITE, &old);
std::copy_n(/* IT'S OVER 9000! */ "9001", 4, ptr);
VirtualProtect(ptr, 4, old, &old);
}
library_a::library_a()
library_b::library_b()
-> test() = 9001
library_b.dll loaded
There’s a lot of fun to be had with these semi-undocumented APIs. Hopefully someone else might find this useful too.
Another thing I should mention before I let you go is how this might not be suitable if you don’t want to resolve symbolic links. With this example code, if you were to call LoadLibrary
on “game.dll
”, but that links to “game-v1.2.dll
”, GetMappedFileName
will follow that and give you the latter. One workaround for this is to also hook NtOpenFile
and NtCreateSection
.
If you’re curious about how compatible this hook is with older Windows, using the MinGW32 profile in MSYS2, I was able to get the same set of binaries compiled and running on everything from Windows XP all the way up to Windows 11.
Note that you’ll have to set WINVER
and _WIN32_WINNT
to the appropriate value (e.g. 0x0501
for XP) so that everything gets imported from the correct modules. Alternatively, you can find everything at runtime with GetProcAddress
.