Excuse Me, That’s My DLL (A Guide to DLL Hijacking Shenanigans)

Over the past few years, I’ve seen dozens of infection chains where adversaries relied on DLL hijacking as a core part of their toolkit. They did it because, until recently, these techniques easily bypassed most EDRs.

Thankfully, defenses have improved. I’ve noticed EDRs are now much better at validating the paths where binaries run from, which is great for catching sideloading attempts. Still, DLL hijacking remains a powerful technique for code execution, persistence, and privilege escalation (yep). Because it’s still so relevant, I decided to dust off my old notes from when I first started learning and experimenting with these methods.

How DLLs are loaded

Let’s start with the basics: DLLs. At their core, they’re just Windows components containing code snippets that various programs can call upon. Think of them like instruction manuals. Instead of rewriting the same code over and over, programs simply “borrow” these instructions to perform specific tasks. 

Usually, when a program needs a DLL it follows a specific search order to locate that DLL (unless the load instruction hardcodes the direct path). This search order is pre-defined with the following order:

  • Known DLLs – Pre-verified system DLL cache. Held in HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs
  • The application’s directory. 
  • C:\Windows\System32 
  • C:\Windows\System
  • C:\Windows 
  • Current Working Directory
  • PATH variables directory

Note: This assumes that safe DLL Search is enabled, which nowadays is almost always the case.

Search Order Hijacking

With that search order in mind, you can probably spot the problem. Let’s look at an example: say Important.exe needs to load Important.dll, which is located in C:\Windows\System32.

Under normal conditions, the program checks the list of Known DLLs, looks in its own application directory, and eventually lands in C:\Windows\System32 to find the real file.

However, if a user possesses write permissions on the Application Directory let’s say C:\ProgramData\Important\ – the user can place an arbitrary DLL named Important.dll in that folder to make the program load that DLL instead:

These are the fundamentals about DLL Hijacking – tricking a legitimate program to load an adversary controlled DLL, by abusing the way DLLs are searched on Windows. This way, a program with perfect reputation (often system programs) can be abused to execute adversarial code.

Finding Suitable Candidates for Hijacking

The best tool to find suitable DLLs for hijacking is Procmon by Sysinternals. You want to spin it up and look for DLL File operations with result “NAME NOT FOUND”:

Tip: For programs that run on startup, you can configure Procmon to perform a capture during the computer boot (Options > Enable Boot Logging)

As an example, here are all DLLs that the “Autenticação GOV” attempts to load from the folder where it is installed:

So, ideally, any of these DLLs that are not found in the program folder can be a candidate for Hijacking. There is but one problem: Permissions!

Generally, in well-written (and secure) programs, the program files folder is write-protected for Users, only being writable by Administrators. We will come back to permissions later, for now, lets assume we can write in that folder an attempt to hijack this (which is not the case).

By analyzing the imports that the program performs, we can see that several functions are imported from the DLL VCRUNTIME140.dll. This DLL is one of the many that the program attempts to load from its own folder. So, theoretically, if we craft a malicious DLL and place it in the program folder, we can cause the program to execute arbitrary code.

Crafting a DLL

The code below showcases a proof-of-concept DLL that, when loaded by a process, spawns a MessageBox:

#include "pch.h"
#include <windows.h>

DWORD WINAPI SideloadPayload(LPVOID lpParam) {
    MessageBoxA(NULL,"Test!","Test!\n",MB_OK | MB_ICONINFORMATION | MB_TOPMOST);
    return 0;
}
BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH: {
        HANDLE hThread = CreateThread(NULL, 0, SideloadPayload, NULL, 0, NULL);
        if (hThread) {
            CloseHandle(hThread);
        }
    }
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

By compiling this, placing it in the program’s folder, and executing the program, we should observe a MessageBox showcasing the "Test!" message.

Well, not so fast. When the target application boots, it calls LoadLibrary to map the DLL into memory. Right after, it uses GetProcAddress to find its expected entry points and features. Because our current test DLL has no exports, the program attempts to call a null or missing pointer, causing an immediate crash.

Therefore, we need to declare at least some of the exports that the target application expects to use. The easiest and neatest way to do this is via a technique called DLL Proxying (or DLL Forwarding). The concept is quite simple:

We declare all the exports of the original function and forward all the requests for that exports to the original DLL. This means that our DLL is acting as a proxy of the original one, all the functions will still work, and we get to execute our own code:

So, we can start by getting a list of all the exports of the original vcruntime140.dll. We can do this by using dumpbin.exe:

After we that, we can simply declare all the exports on our proxy DLL and point them to the original DLL:

After that, we can observe that the DLL is actually loaded, and our code (still placed under the DLL_PROCESS_ATTACH event gets executed):

It is important to note that, in this specific instance, this does not constitute a vulnerability. The program’s folder is properly secured and is not writeable by regular users, meaning an attacker without administrative privileges cannot place the malicious DLL there in the first place.

Just out of curiosity, if we re-analyze the proxy DLL, we can see that all the exports are actually forwards.

Hijacking for Privilege Escalation

DLL Hijacking for privilege escalation relies on the exact same principles, but targets applications operating at a higher privilege level (such as SYSTEM or Administrator) than the attacker. For this to be exploitable, the target application must attempt to load a DLL from a directory that is user-writable. This scenario typically arises from insecure folder permissions (weak ACLs), allowing an unprivileged attacker to plant a malicious payload.

One such example is CVE-2022-32223, on which node.exe (running as an high integrity process) attempted to load a non existing DLL (providers.dll) from the current user directory:

So by compiling a simple DLL, that spawns a process (like calc.exe), and placing it in the user directory – in this case C:\Users\armando\Desktop\providers.dll, we can force the elevated node.exe process to load our payload. Consequently, node.exe will spawn calc.exe, which successfully inherits the parent’s high-integrity security context.

DLL Side Loading

Finally, another flavor of DLL Hijacking—and perhaps the most commonly observed in my experience—is DLL Side-Loading. Here, the concept is a little different: attackers deliver a malicious DLL alongside a legitimate executable. Because the legitimate program follows the standard Windows DLL Search Order, it will look inside its own folder first, unwittingly loading the attacker’s malicious DLL.

The example below uses a legit executable, signed by Microsoft and part of Windows – net.exe and a malicious DLL IPHLPAPI.dll. When net.exe is executed it will load IPHLPAPI.dll:

By doing this, attackers leverage the capability to hide their own payloads within the processes that appear to belong to legit and signed binaries. On a side note, in recent observations, I have noticed that adversaries seem to have steered away from using Microsoft/Windows binaries, as they are more closely monitored by EDR solutions.

Detection Strategies

Detecting DLL Hijacking attempts is genuinely hard. While EDRs have significantly improved their built-in capabilities over the last few years (especially for abuse targeting Windows default system binaries), I have found over a few engagements that there is still a lot of ground to cover. I have tried a couple of approaches, and none of them proved to be a silver bullet. Nevertheless, here are a few approaches I followed while developing signatures to detect potential hijacking and side-loading:

  • Unsigned DLLs loaded by Signed Binaries: This is highly prone to false positives, especially if you include third-party software.
  • Known System DLLs Loaded Outside of System32: EDRs tend to do this already, but if you can build a baseline of known DLLs, it can cover miles for you. Be sure to check out the HijackLibs project.
  • Rare/First-Time DLL Loads (Least Frequency Analysis): This can be combined with the first approach to reduce the noise ratio (e.g., tracking a first-time unsigned DLL loaded by a signed process).

Beyond these approaches, and depending on the loaded payload, the target application will usually display abnormal behavioral patterns (such as unexpected network connections, anomalous file operations, or unusual process activity), which all serve as critical indicators of compromise.