Building an Anti-DLL Injection Solution in C#
In the area of cyber security, the DLL injection attack is a technique that can execute malicious code in the context of another process. This can be used to alter the behavior of that process and potentially leak sensitive data.
It is important to keep in mind that DLL injection is not a security issue by itself as it is a feature implemented by the operating system. It has viable uses, like the loading of dynamic modules. Therefore it should not be viewed as something that should be eliminated at all costs on all levels.
The cases where DLL injection opens up a potential attack vector are applications containing business logic. It is essential to enforce the integrity of the data generated by these applications because the data is usually further processed by backend services.
A perfect example of a potential DLL injection attack is with computer games. Imagine that the attacker executes malicious code in the context of an online car racing game. The physical interactions of the car object are usually calculated locally on the device. Executing malicious code might allow the attacker to change the physical properties of the car like speed or weight. This would essentially give the attacker superpowers and an unfair advantage against other players.
This article proposes a solution that implements a solution for C# applications. Although the solution is designed for C#, the principles can still be utilized in other programming languages.
How it works
The DLL injection prevention process can be abstracted into 3 steps:
- Hook onto the execution of the LoadLibrary function.
- Add the trust evaluation code.
- Return loaded library memory address if trusted. Otherwise, return zero memory address which will cause a loading failure.
DLL injection is performed by invocation of the LoadLibrary function from the kernel32 module. The LoadLibrary function is used by the OS to load all of the necessary modules into the process space, be it the system modules or our own application modules. Processes can load modules into other processes with the same user privileges.
The proposed solution is based on hooking onto this function and trust evaluation of the loaded module, essentially rejecting the loading of any module that is not trusted.
Trust evaluation
Trust evaluation consists of several layers, each evaluating different information available for the module to be loaded.
Module path heuristic search
It is possible that the system will ask the process to load a module by its relative name like “dnsapi.dll”. The OS will then search for that module on the device. The path of the loaded module is resolved by a black-boxed heuristic search implemented by the system. It is not practical to replicate this heuristic in order to find the path of the loaded module. Instead, one can tap into the black-boxed search by invoking the LoadLibrary function with a flag that specifies, that the module should be loaded only as data and not be loaded into the application domain. This way it’s possible to inspect the module data for the absolute path without executing the DllMain method which could potentially execute malicious code.
Embedded signature
Most legitimate modules have an embedded signature. This is the modern way of module signing. The signature is embedded into the module binary and can be inspected. Obtaining this signature is very difficult and any malicious parties will likely not be able to get one.
Catalog signature
This type of signature is used for the system modules. The signature is not contained in the binary but is listed in a catalog instead.
Is in the Windows directory
Surprisingly, even some system modules miss any kind of signature making the signature check approach impossible. These modules still need to be loaded on demand as they are necessary for the runtime of the process. It is enough to know that these modules are loaded from the system directory, which should not be accessible to a non-administrative user of a managed device.
Function hooking
In order to perform the trust evaluation, it’s essential to replace the implementation of the LoadLibrary function at runtime. This can be achieved by modifying the instructions loaded into the process memory space at the location where this function is stored.
Now this is where it gets a bit weird and where a bit of assembly talk comes in. On Windows 11 x64 the LoadLibrary function has 24 bytes of memory allocated.
[1–5] jmp qword
[6–24] int3
This can vary depending on the OS version. Therefore it is necessary to verify that the hooking mechanism is compatible with the OS before it is executed. This can be done by disassembling the LoadLibrary function and verifying that the instructions are as expected.
If yes then it is safe to proceed with the next step. Replacing these instructions. If no then it is not safe to replace these instructions as it would have unpredictable consequences.
The function hook implementation in this article requires the first 12 bytes to be replaced with these instructions.
[1–2] mov rax ; load an immediate value into the RAX register
[3–10] <address> ; address of the replacement code
[11–12] jmp rax ; jump to the address of the replacement code
The instructions of the LoadLibrary function are replaced with the jmp (jump) instruction that redirects the function execution to the code performing the trust evaluation.
The following code is the implementation of function hooking within a C# application.
// Defines a class for creating function hooks in 64-bit processes, allowing the redirection of function calls.
public class FunctionHookX64 : IDisposable
{
// Specifies the number of bytes required for the hooking mechanism.
public const int RequiredBytesCount = 12;
// Holds the address of the function to be hooked.
private IntPtr targetFunctionAddress;
// Stores the original memory protection setting so it can be restored later.
private Protection originalProtection;
// Buffer to store the original bytes of the target function to allow unhooking.
private byte[] targetFunctionCode = new byte[RequiredBytesCount];
// Buffer to store the bytes of the hook code that will replace the target function.
private byte[] replacementFunctionCode = new byte[RequiredBytesCount];
// Delegate representing the new function to redirect to, ensuring it is not garbage collected.
private Delegate destinationDelegate;
// Constructor for hooking with function pointers.
public FunctionHookX64(IntPtr source, IntPtr destination)
{
// Change the memory protection of the target function.
VirtualProtect(source, RequiredBytesCount, Protection.PAGE_EXECUTE_READWRITE, out originalProtection);
// Copy the current code at the target function address into a buffer.
Marshal.Copy(source, targetFunctionCode, 0, RequiredBytesCount);
// Set up the replacement code that redirects execution to the new function.
replacementFunctionCode[0] = 0x48; // mov rax, address
replacementFunctionCode[1] = 0xB8;
Array.Copy(BitConverter.GetBytes((long)destination), 0, replacementFunctionCode, 2, 8);
replacementFunctionCode[10] = 0xFF; // jmp rax
replacementFunctionCode[11] = 0xE0;
// Store the target function address for use during install/uninstall.
targetFunctionAddress = source;
}
public FunctionHookX64(IntPtr source, Delegate destination) :
this(source, Marshal.GetFunctionPointerForDelegate(destination))
{
// Store the delegate to prevent it from being garbage collected prematurely.
destinationDelegate = destination;
}
// Installs the hook by replacing the target function's code with the hook code.
public void Install()
{
Marshal.Copy(replacementFunctionCode, 0, targetFunctionAddress, RequiredBytesCount);
}
// Uninstalls the hook by restoring the original code of the target function.
public void Uninstall()
{
Marshal.Copy(targetFunctionCode, 0, targetFunctionAddress, RequiredBytesCount);
}
public void Dispose()
{
// Removes the function hook and restores original settings.
Uninstall();
destinationDelegate = null;
// Restore the original protection settings to the memory region.
VirtualProtect(targetFunctionAddress, RequiredBytesCount, originalProtection, out _);
}
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool VirtualProtect(IntPtr lpAddress, uint dwSize, Protection flNewProtect, out Protection lpflOldProtect);
public enum Protection
{
PAGE_NOACCESS = 0x01,
PAGE_READONLY = 0x02,
PAGE_READWRITE = 0x04,
PAGE_WRITECOPY = 0x08,
PAGE_EXECUTE = 0x10,
PAGE_EXECUTE_READ = 0x20,
PAGE_EXECUTE_READWRITE = 0x40,
PAGE_EXECUTE_WRITECOPY = 0x80,
PAGE_GUARD = 0x100,
PAGE_NOCACHE = 0x200,
PAGE_WRITECOMBINE = 0x400
}
}
And the following is a basic example on how to set up validation of loaded modules.
// Import necessary API calls.
[UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Ansi, SetLastError = true)]
private delegate IntPtr LoadLibraryA_Delegate(string lpFileName);
[DllImport("kernel32.dll", CharSet = CharSet.Ansi)]
public static extern IntPtr GetProcAddress(IntPtr InModule, string InProcName);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
public static extern IntPtr GetModuleHandle(string InPath);
public static void Main()
{
// Initialize the function hook as soon as possible to minimize
// the time window when a malicious module can be injected.
IntPtr moduleHandle = GetModuleHandle("kernel32.dll");
IntPtr loadLibraryAHandle = GetProcAddress(moduleHandle, "LoadLibraryA");
FunctionHookX64 loadLibAHook = null;
loadLibAHook = new FunctionHookX64(loadLibraryAHandle, new LoadLibraryA_Delegate((fileName) =>
{
loadLibAHook.Uninstall(); // Uninstall the hook to avoid loader lock
var libraryPointer = LoadLibraryIfTrusted(fileName);
loadLibAHook.Install(); // Reinstall the hook
return libraryPointer; // Return either the library handle or IntPtr.Zero if the library is not trusted
}));
// !!! Make sure to create hooks for all variants of LoadLibrary.
// LoadLibraryA, LoadLibraryW, LoadLibraryExA, LoadLibraryExW.
}
Final thoughts
Addressing DLL injection in your systems is not always necessary. If you are deciding whether you should implement some safety measures then a good rule of thumb is to ask yourself whether your application is generating business logic data stored on your backend. If it is, then you should consider implementing some integrity checks, DLL injection prevention being one of them.
If you decide to implement some DLL injection prevention then you need to be very cautious not to block the loading of any essential system libraries.
The implementation of the LoadLibrary function can vary on different Windows OS versions. One should be careful when rewriting the memory of the process as on some systems the LoadLibrary function might not be exactly 12 bytes. It could happen that one would rewrite memory outside of the LoadLibrary function which would have unpredictable consequences. However, when done correctly, implementing such prevention mechanism will add one more layer of security and ruin the day of any potential hacker.