Introduction
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:
```cpp #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;       (funcPtr)();     }         // 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:
```python from flask import Flask, render_template, request import subprocess app = Flask(__name__) PLUGIN_DIR = 'Plugins' @app.route('/') 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', Â Â Â Â Â Â Â Â request.form['engine'], Â Â Â Â Â Â Â Â request.form['method'], Â Â Â Â Â Â Â Â request.form['id'], Â Â Â Â Â Â Â Â request.form['title'], Â Â Â Â Â Â Â Â request.form['content'], Â Â Â Â Â Â Â Â request.form['operation'], Â Â Â Â Â Â Â Â request.form['mode'] Â Â Â Â ] Â Â Â Â output = subprocess.run(cmd, stdout=subprocess.PIPE) Â Â Â Â returnVals = output.stdout.decode("utf-8").split('rn') Â Â Â Â if len(returnVals) < 2: Â Â Â Â Â Â returnVals = ['ERROR', 'ERROR'] Â Â Â Â hashes=[{'clear': returnVals[0], 'hashed': returnVals[1]}] Â Â return render_template('hash.html', hashes=hashes) app.run(host='0.0.0.0', port=80) ```
The Worker Executable:
```cpp
#include <windows.h>
#include <iostream>
typedef HRESULT(WINAPI* ENGINEPROC)(DWORD, LPCTSTR, LPCTSTR, DWORD, DWORD);
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:
```cpp
#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:
dllmain.cpp:
```cpp
#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.open(outfileName);
  outfile << infoMessage;
  outfile.close();
}
BOOL APIENTRY DllMain( HMODULE hModule,
           DWORD ul_reason_for_call,
           LPVOID lpReserved
          )
{
  switch (ul_reason_for_call)
  {
  case DLL_PROCESS_ATTACH:
  case DLL_THREAD_ATTACH:
    //attackerMethod();
    break;
  case DLL_THREAD_DETACH:
  case DLL_PROCESS_DETACH:
    break;
  }
  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:
“`cpp
DWORD ProcessData(DWORD, LPCWSTR, LPCWSTR, DWORD, DWORD);
“`
Or, for non-Windows code:
“`cpp
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:
“`cpp
DWORD func(DWORD, LPWSTR);
VOIDÂ func(DWORD, LPCWSTR, LPCWSTR, DWORD);
DWORD func(DWORD, LPCWSTR);
DWORD func(DWORD, LPWSTR, LPWSTR);
VOIDÂ func(DWORD, BYTE);
“`
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:
- It must do something useful for an attacker.
- It must be present in a predictable location in default installations of Windows.
- 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:
“`cpp
HRESULT URLDownloadToFile(
      LPUNKNOWN      pCaller,
      LPCTSTR       szURL,
      LPCTSTR       szFileName,
 _Reserved_ DWORD        dwReserved,
      LPBINDSTATUSCALLBACK lpfnCB
);
“`
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:
“`cpp
DWORD AttackerMethod(DWORD a, LPCWSTR b, LPCWSTR c, DWORD e, DWORD f)
{
  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 22).
Figure 22: Instructing the target machine to download EvilDLL.
After verifying the Apache logs for the download (see figure 23)…
Figure 23: Verifying the apache logs for download.
…we trigger the DLL (see figure 24).
Figure 24: 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 25.
Figure 25: 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.
Shell32.dll
```cpp
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
Kernel32.dll
```cpp
UINT WinExec(
 [in] LPCSTR lpCmdLine,
 [in] UINT  uCmdShow
);
```
CreateProcess – Creates a new process
Kernel32.dll
```cpp
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
Kernel32.dll
```cpp
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
Kernel32.dll
```cpp
BOOL DeleteFileA(
 [in] LPCSTR lpFileName
);
```
CreateDirectory – Creates a new directory
Kernel32.dll
```cpp
BOOL CreateDirectoryA(
 [in]      LPCSTR        lpPathName,
 [in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes
);
```
ExitWindowsEx – Logs off the interactive user and shuts down the system
User32.dll
```cpp
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.
Share via: