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");
}
NtMapViewOfSection hook before calling original method

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.

NtMapViewOfSection hook after calling original method

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.

Ready for a nostalgia trip?

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.

Published on Monday, 23 September 2024 at 10:34 AM.