DLL Side-Loading or DLL Proxy loading allows an attacker to abuse a legitimate and typically signed executable for code-execution on a compromised system. Mitre has been keeping a log of this technique since 2017, and it continues to be a popular option by threat actors (For good reasons!)
Proxy loading is very similar to DLL hijacking, however, it does not break the execution flow or functionality of the original program. This can also be used as a method of persistence, on top of hiding malicious activity behind a legitimate application.
To understand how effective DLL Proxy loading can be for an attacker, we first need to take a look at how typical applications today load external functions for third party libraries.
Using the example flow above, the following happens.
- At startup, application (A) needs to fetch data using a third party function called “GetFunkyData()” (C), GetFunkyData() exists in the dynamic link library called “DataFunctions.dll” (B), which resides in the working directory of the application.
- Application (A) loads the library “DataFunctions.dll” by its name in the attempt of executing “GetFunkyData()” (C). Since the function exists inside the library (B), the function is executed, and the application runs normally.
When performing a DLL Proxy loading attack, the flow is a bit different.
- At startup, application (A) needs to fetch data using a third party function called “GetFunkyData()” (D), GetFunkyData() exists in the dynamic link library called “DataFunctions_Original.dll” (B) which resides in the working directory of the application
- Application (A) loads the library “DataFunctions.dll” by its name in the attempt of executing “GetFunkyData()” (C). This DLL is actually a specifically crafted “Proxy” library from the attacker, the Proxy DLL redirects the function calls to the original DLL, “DataFunctions_Original.dll” (B), using an external export/linker reference. The function is found and executed by the application
- At this point, the attacker has hijacked the execution flow (C) and can execute code on behalf of the running process (E) without the knowledge of the user or application.
As PowerShell is becoming harder to get away with when working inside properly monitored environments, we saw the rise of C# tooling. Not long after, TheWover introduced Donut, A seemingly automagical way of generating position independent code (PIC) otherwise known as shellcode that loads .NET Assemblies. This, together with being able to invoke assemblies at runtime and the lack of AMSI until NET 4.8, kicked the door open for the age of C#. SharpDPAPI, SharpHound, SharpSpray, Rubeus, the list goes on! To put it simply, I have never found myself missing a C# based tooling when needing to perform a specific task on an engagement.
To get yourself up to speed on the magic Donut can offer, you can read the initial blog post.
Finding a target executable
When looking for a target executable, these are some key points that you should keep in mind:
- Size, you are generally looking for an executable less than 10 MB(Often less >1 MB).
- A signature, the target should be a digitally signed “legitimate” executable. The more known, the better.
- Loads a low number of DLL(s) unsafely at runtime, the executable flow needs to be hijackable, but we don’t want to drop more than 1-3 DLL(s) onto the target to get things going.
- Matches your current TTP, what are you trying to “sell” to the defense?
My typical approach is to head over to a site like Ninite, download some applications from common vendors, and simply start poking around in the install directory. For this example, let’s try FileZilla. By default, an x64 bit installation of FileZilla ends up in “C:\Program Files\FileZilla FTP Client”. The folder contains several executables as well as DLLs.
A super low-tech and easy way to find out what executables depends on which DLLs is to simply copy one of the executables out to a separate folder and run it. Does it complain? Let’s try fzsftp.exe!Nice! So fzsftp.exe seems to need “libnettle-7.dll” to execute, let’s copy that over from the previous, “C:\Program Files\FileZilla FTP Client” folder and try again! Vola! It runs, nothing happens (no errors), just like we want it. To confirm, we can use a tool like Process Hacker to check what modules were loaded by the application, and further confirm that the library was indeed loaded.
SharpDllProxy – Crafting the proxy payload
The next step is to craft our proxy DLL to redirect the legitimate function calls to the original DLL(s), as well as silently load our shellcode in the background. To make this step allot easier, I’ve created a simple Dotnet core application called “SharpDllProxy”. SharpDllProxy generates the Proxy DLL source code based on the exported functions which it extracts from the original DLL. The source code generated, simply reads a file into memory, then invokes it into a new thread. Assuming the file we feed it is the raw shellcode we would like to deploy.
Usage is pretty self-explanatory.
In this example, SharpDllProxy creates a new output folder called “/output_libnettle-7”.
SharpDllProxy found a total of 441 function calls in the original “libnettle-7.dll” DLL and generated a complete Proxy DLL source code that redirects the function calls to tmp8AA5.dll (Which is a copy of the original “libnettle-7.dll”, just renamed). Now we simply need to compile the source code that was saved to “D:\SharpDllProxy\output_libnettle-7\libnettle-7_pragma.c”.
Open up Visual Studio, click “Create a new project”Select C++ as your language, search for “library” and click the “Dynamic-link Library (DLL)” template.
The name of the solution should match the original DLL name, so “libnettle-7”, then click create. By default, you will be taken into “dllmain.cpp”, simply copy and paste the full content of “D:\SharpDllProxy\output_libnettle-7\libnettle-7_pragma.c” into this file.
The source code generated creates a new thread once the DLL_PROCESS_ATTACH event Is triggered (L485-489). “DoMagic()” then proceeds to read the binary data from local file “shellcode.bin ” (L455-465) into a buffer. The filename “shellcode.bin” was defined when we generated the source code with SharpDllProxy. The buffer is then copied into memory and invoked.
To compile this, select the correct architecture (x64/x86), choose “release” then “Build” -> “Build Solution”.
Depending on where you saved your Visual Studio solution, the compiled DLL can be found in “C:\Users\\source\repos\libnettle-7\x64\Release”.
Copy the DLL into the previous output folder created by SharpDLLProxy, add the targeted executable as well any x64 shellcode as a raw file named “shellcode.bin”. “libnettle-7_pragma.c” can now also be deleted.
For initial testing, I recommend generating some simple shellcode from the Metasploit framework, for example:
msfvenom -a x64 --platform windows -p windows/x64/messagebox TEXT="Proxy Loading worked!" -f raw > shellcode.bin
Run fzsftp.exe and see the profit! Shellcode.bin is read from disk, then executed, as well as not breaking the application functionality.
Loading your favorite C# implant!
Whether it be PoshC2, Covenant, Ninja, or Nuages, it can all be converted into shellcode and loaded into a native process with some Donut magic. For this example, we will be using PoshC2 as our C2 framework. Grab a pre-compiled release of Donut here, then simply feed donut.exe the targeted implant beacon/dropper (Note that the pre-compiled version of Donut is no longer the “latest”, we are merely too lazy to compile ourself!)
Donut generated our shellcode and saved it as loader.bin, rename loader.bin into shellcode.bin, and replace the test-shellcode we created from Metasploit earlier.
Our C# implant is successfully deployed into memory!
Note: The reason “fzsftp.exe” is not displaying its command prompt is due to the PoshC2 C# implant hiding all windows for the running process at launch. This can be modified ofc.
- In my experience, solutions such as Advanced Threat Protection will not flag potential malicious behaviour when originating from a signed executable. This includes conventional injection techniques such as CreateRemoteThread, QueueUserAPC, and NtCreateThreadEx, as well as network traffic.
- In the case above, choose your target process carefully. Some processes have better reasons for CLR usage than others. The same goes for network traffic. Blend in!
- If you have the correct permission on a system, proxy loading can be used as a long term persistence by merely replacing a DLL inside the install directory.
- In any case, consider adding both AMSI and ETW patching to your implant source code. Donut sometimes fails to bypass AMSI and when injecting into an unmanaged-process you can hide first events!
- Rename C# classes, variables, and methods in the implant to something matching the target process. The same goes for the “DoMagic()” DLL functions, as it can be seen from the thread-stack inspection in the picture.