Skip to content

Tutorial about how to track down dll load/unload events

License

Notifications You must be signed in to change notification settings

galtza/tutorial-dll-tracker

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ℹ️ Medium level

Tutorial: tracking down dll events

This is a tutorial about how to track down Load and Unload dll events and associated data.

Interface

Even though this tutorial is only about Windows implementation we will hide the implementation details via an interface API using namespaces.

So, let's define the requirements:

  • Track load/unload events

  • Provide the user with a way to receive call-backs

  • Give information like the name/path of the loaded/unloaded dll, the base address and its size

The user interface should look like this:

namespace qcstudio::dll_tracker {

    using namespace std;

    /*
        Callback params...

        bool:      true if load false if unload
        wstring:   full path of the dll
        wstring:   name of the dll
        uintptr_t: base address of the dll
        size_t:    size of the dll
    */

    using callback_t = function<void(bool, const wstring&, const wstring&, uintptr_t, size_t)>;

    // start / stop

    bool start(callback_t&& _callback);
    void stop();

}  // namespace qcstudio::dll_tracker

Let's talk about implementation details for Windows.

Windows

Two functions allow us to get registered to dll events: LdrRegisterDllNotification and LdrUnregisterDllNotification. Both can be found inside ntdll.dll.

⚠️ However, you will NOT find any Windows header file containing the definition of these functions according to the Remarks!

Instead, we have to use the well known mechanism of loading the dll with LoadLibraryA Windows function and then get the function address with GetProcAddress.

But before, we need to write down all the data structures involved as well as the function signatures. This is what we will be using in our Windows implementation straightaway (notice that I use using rather than typedef as after 25+ years programming I still get confused).

When it comes to load and unload events these data structures are used to inform the call-back:

using LDR_DLL_LOADED_NOTIFICATION_DATA = struct {
    ULONG            Flags;         // Reserved.
    PCUNICODE_STRING FullDllName;   // The full path name of the DLL module.
    PCUNICODE_STRING BaseDllName;   // The base file name of the DLL module.
    PVOID            DllBase;       // A pointer to the base address for the DLL in memory.
    ULONG            SizeOfImage;   // The size of the DLL image, in bytes.
};
using PLDR_DLL_LOADED_NOTIFICATION_DATA = LDR_DLL_LOADED_NOTIFICATION_DATA*;

using LDR_DLL_UNLOADED_NOTIFICATION_DATA = struct {
    ULONG            Flags;         // Reserved.
    PCUNICODE_STRING FullDllName;   // The full path name of the DLL module.
    PCUNICODE_STRING BaseDllName;   // The base file name of the DLL module.
    PVOID            DllBase;       // A pointer to the base address for the DLL in memory.
    ULONG            SizeOfImage;   // The size of the DLL image, in bytes.
};
using PLDR_DLL_UNLOADED_NOTIFICATION_DATA = LDR_DLL_UNLOADED_NOTIFICATION_DATA*;

As you can see both have exactly the same fields ¯\(ツ)

The actual generic data received by any call-back is this:

using LDR_DLL_NOTIFICATION_DATA = union {
    LDR_DLL_LOADED_NOTIFICATION_DATA   Loaded;
    LDR_DLL_UNLOADED_NOTIFICATION_DATA Unloaded;
};
using PLDR_DLL_NOTIFICATION_DATA = LDR_DLL_NOTIFICATION_DATA*;
using PCLDR_DLL_NOTIFICATION_DATA = const LDR_DLL_NOTIFICATION_DATA*;

Then, we have the notification function definition and associated values:

using LDR_DLL_NOTIFICATION_FUNCTION = VOID NTAPI (
  _In_     ULONG                       NotificationReason,
  _In_     PCLDR_DLL_NOTIFICATION_DATA NotificationData,
  _In_opt_ PVOID                       Context
);
using PLDR_DLL_NOTIFICATION_FUNCTION = LDR_DLL_NOTIFICATION_FUNCTION*;

#define LDR_DLL_NOTIFICATION_REASON_LOADED 1
#define LDR_DLL_NOTIFICATION_REASON_UNLOADED 2

After all this, now, we can define the actual function signatures for register, un-register and the call-back:

using LdrRegisterDllNotification = NTSTATUS (NTAPI*)(
  _In_     ULONG                          Flags,
  _In_     PLDR_DLL_NOTIFICATION_FUNCTION NotificationFunction,
  _In_opt_ PVOID                          Context,
  _Out_    PVOID                          *Cookie
);

using LdrUnregisterDllNotification = NTSTATUS (NTAPI*) (
  _In_ PVOID Cookie
);

using LdrDllNotification = VOID (CALLBACK*)(
  _In_     ULONG                       NotificationReason,
  _In_     PCLDR_DLL_NOTIFICATION_DATA NotificationData,
  _In_opt_ PVOID                       Context
);

ℹ️ Check out the beginning of the file tutorialdll-tracker-windows.cpp in this repo

As we mentioned before, when it comes to actually retrieving the library and the functions, we use LoadLibraryA and GetProcAddress.

auto ntdll = LoadLibraryA("ntdll.dll");
auto reg   = (LdrRegisterDllNotification)GetProcAddress(ntdll, "LdrRegisterDllNotification");
auto unreg = (LdrUnregisterDllNotification)GetProcAddress(ntdll, "LdrUnregisterDllNotification");

Notice that for this example we did not handle errors for simplicity reasons.

We are going to implement this interface inside a namespace as two global functions. Before we start coding we need to define our data. What we need in Windows is the following:

  • A cookie which is basically a handle that is provided for future calls (It is just a void*)
  • The pointers to the functions (the reg and unreg variables from the previous example)
  • The user-passed call-back

It looks like this:

auto cookie   = (void*)nullptr;
auto callback = callback_t{};
auto reg      = LdrRegisterDllNotification{nullptr};
auto unreg    = LdrUnregisterDllNotification{nullptr};

As we want a generic interface we need to have two call-backs. One for the Windows one that will receive all the Windows data structures and another one provided by the user with no platform-dependent data structures.

The internal one looks like this:

auto internal_callback = (LdrDllNotification)[](ULONG _reason, PCLDR_DLL_NOTIFICATION_DATA _notification_data, PVOID _ctx) {
    if (_notification_data && callback) {
        switch (_reason) {
            case LDR_DLL_NOTIFICATION_REASON_LOADED: {
                callback(
                    true,
                    wstring(_notification_data->Loaded.FullDllName->Buffer),
                    wstring(_notification_data->Loaded.BaseDllName->Buffer),
                    (uintptr_t)_notification_data->Loaded.DllBase,
                    _notification_data->Loaded.SizeOfImage);
                break;
            }
            case LDR_DLL_NOTIFICATION_REASON_UNLOADED: {
                callback(
                    true,
                    wstring(_notification_data->Unloaded.FullDllName->Buffer),
                    wstring(_notification_data->Unloaded.BaseDllName->Buffer),
                    (uintptr_t)_notification_data->Unloaded.DllBase,
                    _notification_data->Unloaded.SizeOfImage);
                break;
            }
        }
    }
};

Regarding the start function:

bool start(callback_t&& _callback) {
    if (cookie) {
        stop();
    }

    auto ntdll = LoadLibraryA("ntdll.dll");
    if (!ntdll) {
        return false;
    }

    reg   = (LdrRegisterDllNotification)GetProcAddress(ntdll, "LdrRegisterDllNotification");
    unreg = (LdrUnregisterDllNotification)GetProcAddress(ntdll, "LdrUnregisterDllNotification");
    if (!reg || !unreg) {
        return false;
    }

    if (reg(0, internal_callback, nullptr, &cookie) != STATUS_SUCCESS) {
        return false;
    }

    callback = move(_callback);

    return true;
}

Here we do check errors

In this order, we:

  • Check if we already started. If we just call stop and proceed
  • We load the ntdll.dll dynamic library where the Windows functions live.
  • We register our internal call-back with the registry function
  • Store the user call-back

After calling this, we are ready to receive any dll event.

Similarly, stop code is like this:

void stop() {
    if (cookie && unreg) {
        unreg(cookie);
    }
    cookie = nullptr;
}

This one is obvious.

Example of usage

In the repo you will find a premake5 script with a workspace (VS solution) with three projects:

  • foo that is a dynamic library
  • bar that is a dynamic library too
  • launcher that is the main

The main function has a local lambda as the call-back:

const auto cb = [](bool _load, const wstring& _path, const wstring& _name, uintptr_t _base_addr, size_t _size) {
    if (_load) {
        wcout << L"Loading ";
    } else {
        wcout << L"Unload ";
    }

    wcout << "\"" << _name.c_str() << "\" at \"" << _path.c_str() << "\" with";
    wcout << "base addr 0x" << hex << _base_addr << " and size " << dec << _size;
    wcout << endl;
};

Then...

if (qcstudio::dll_tracker::start(cb)) {
    auto foo_module = LoadLibrary(L"foo.dll");
    auto bar_module = LoadLibrary(L"bar.dll");

    if (foo_module) {
        if (auto foo_function = (void (*)())GetProcAddress(foo_module, "foo")) {
            foo_function();
        }
    }
    
    if (bar_module) {
        if (auto bar_function = (void (*)())GetProcAddress(bar_module, "bar")) {
            bar_function();
        }
    }

    if (foo_module) {
        FreeLibrary(foo_module);
    }
    
    if (bar_module) {
        FreeLibrary(bar_module);
    }

    qcstudio::dll_tracker::stop();
}

...it just

  • Starts tracking
  • Loads the two dynamic libraries
  • Calls two functions from the libraries
  • Unloads the dynamic libraries
  • Stop tracking

A possible output of this example is

Loading "foo.dll" at "tutorial-dll-tracker\.out\x64\Debug\foo.dll" with base addr 0x7ff8320a0000 and size 159744
Loading "bar.dll" at "tutorial-dll-tracker\.out\x64\Debug\bar.dll" with base addr 0x7ff82f8f0000 and size 159744
This is foo
This is bar
Unload "foo.dll" at "tutorial-dll-tracker\.out\x64\Debug\foo.dll" with base addr 0x7ff8320a0000 and size 159744
Unload "bar.dll" at "tutorial-dll-tracker\.out\x64\Debug\bar.dll" with base addr 0x7ff82f8f0000 and size 159744

Easy peasy lemon squeezy!

Other platforms

As we mentioned earlier, we will not cover other platforms. Nevertheless, if you want to go further and investigate how to make this mini-library really multi-platform and as a starting point check out the following tables that show us the correspondence of functions :

How to load/unload dynamic libraries and query for specific functions inside:

Windows Linux
LoadLibrary Dlopen
GetProcAddress Dlsys
FreeLibrary Dlclose

About

Tutorial about how to track down dll load/unload events

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published