DLL sideloading is an attack where an unintended DLL is loaded, resulting in unintended code execution. This attack is possible for any DLL; in this article, we focus on Windows system DLLs.
Sideloading system DLLs
There’s an inherent problem when loading a Windows system DLL like winhttp.dll. For compatibility reasons, the default Windows DLL search begins in the directory where the executable was launched. An attacker need only copy a rogue version of the system DLL to the same directory as the executable. That’s pretty easy even without elevated privileges — an attacker can copy the executable + the rogue DLL to the temp folder, and execute the binary from there.
This vulnerability is very common when loading Windows system DLLs because we usually omit the full path for these modules.
- Compile-time dynamically-linked libraries are resolved by DLL name alone before execution begins. This is not something we can change.
- When loading system DLLs dynamically at runtime, we generally pass just the DLL name and let the loader figure out where the system directory is.
- We generally skip trust checks on system DLLs since a) we don’t have the full path, and b) we rely on system functions like WinVerifyTrust to validate the signature. If the operating system has been compromised already, the trust check won’t really give us much confidence.
Sideloading is especially effective for executables that run with elevated privileges and are embedded signed. For example, many users will quickly agree to elevate privileges if the UAC prompts tells them that their trusted anti-virus vendor wants to make changes.
This is less of an issue for Microsoft executables since they are usually catalog signed. This means that moving them to another location breaks trust. For example, here’s the dialog when we copy mmc.exe out of the system directory and execute it:
This is not a new attack vector, but it’s worth understanding the nuances and mitigations.
MSDN has a good amount of information about this topic but it is confusing and spread out over multiple articles. When not otherwise specified, the details in this article come from the MSDN article entitled Dynamic-Link Library Search Order. Additional details are provided where appropriate.
Searching for DLLs
By default, when a DLL load is done by name alone, the default DLL search path is:
- The directory from which the application loaded.
- The system directory.
- The 16-bit system directory.
- The Windows directory.
- The current directory.
- The directories that are listed in the PATH environment variable.
There are a couple of exceptions:
- If a DLL with the same name is already loaded in the process, the existing DLL is used.
- If the DLL is in the list of Known DLLs, it has already been loaded at boot time and so the existing DLL is used.
The list of known DLLs is taken from HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\KnownDLLs and is then augmented by additional DLLs that are statically linked to the DLLs in the list. For more information, see Larry Osterman’s article entitled What are Known DLLs anyway?.
If you use WinObj to view the Object directory, you can see the full list of Known DLLs on a system (and yes, the list of Known DLLs is an attack vector but it is out of scope for this article). Note that this list varies by operating system. For example, on Windows Vista, crypt32.dll is not in the Known DLLs list.
Note: The mitigations presented in this section are only applicable on Windows Vista SP1+ and require the critical Windows security update specified in KB2533623 on pre-Win8 machines. This security update was released in 2011 and it is quite reasonable to expect a machine patched to this level.
Explicit System DLL Loads
In all places where your code explicitly loads a system DLL via LoadLibrary or LoadLibraryEx, ensure that you use LoadLibraryEx and pass the LOAD_LIBRARY_SEARCH_SYSTEM32 flag. This ensures that only the system directory is searched for that specific DLL.
Note: In our testing, we found that passing the LOAD_LIBRARY_SEARCH_SYSTEM32 flag on unpatched Vista / Win7 systems can cause LoadLibraryEx to fail. If you need to support pre-KB2533623 versions of Vista/Win7, you will have to do a little more work. One good option is to look for the existence of the SetDefaultDllDirectoriesfunction, which was released in the same KB. If the function exists, it is safe to pass the LOAD_LIBRARY_SEARCH_SYSTEM32 flag.
Implicit DLL Loads
Calls to functions like WinVerifyTrust end up loading additional system DLLs and you have no way to control the LoadLibraryEx flags in this case.
To prevent this you can call the SetDefaultDllDirectories function at the start of your executable and pass in the LOAD_LIBRARY_SEARCH_SYSTEM32 flag. After this, the loader will only search the system directory unless lpFileName specifies a full path and/or override flags are passed into LoadLibraryEx.
After making this change, any loads of non-system DLLs must have the full path or non-zero LoadLibraryEx flags. Passing in a full path is a good practice in any case, as is doing a trust check on the DLL before loading it.
Note: Since SetDefaultDllDirectories is not available on unpatched Vista/Win7 systems it is a really good idea to dynamically load it from kernel32.dll.
Compile-Time DLL Imports
Some DLLs linked at compile-time (like kernel32.dll) will be loaded from the Known DLLs list.
However, there are many DLLs that are not on the Known DLLs list. This includes DLLs like winhttp.dll, sfc.dll, version.dll, lz32.dll and more.
In this case, you have two options:
- Switch to manually loading these DLLs and use GetProcAddress to get the imports.
- Mark the library for delay loading at link-time. This defers the DLL load to the first time one of its exports is called. Before the first call into the DLL, ensure that your executable has already called SetDefaultDllDirectories with the LOAD_LIBRARY_SEARCH_SYSTEM32 flag.
The linker option for this is /DELAYLOAD:<dllname>.dll. You can find various pages on MSDN with this information, including this one.
Note: You still link with the <dllname>.lib file and you also need to link with delayimp.lib as it handles the work of loading the DLL at runtime. Ex: for winhttp.dll, the linker options would be:
winhttp.lib delayload.imp /DELAYLOAD:winhttp.dll
Bonus – Using LOAD_WITH_ALTERED_SEARCH_PATH
If someone forces you at gunpoint to support pre-Win8 systems without KB2533623 installed, you can get most of the way there. Keep in mind that there is no way to handle the case where a function like WinVerifyTrust dynamically loads additional DLLs as part of function execution.
- Always pass in the full path to LoadLibraryEx. For system DLLs, you can get the system directory using the GetSystemDirectory function.
- Always pass in the LOAD_WITH_ALTERED_SEARCH_PATH flag to LoadLibraryEx. This flag was introduced in Windows XP and helps with the following case:
- Dynamically load c:\windows\system32\foo.dll using LoadLibraryEx.
- Foo.dll is linked with bar.dll at compile time, and bar is also a system DLL.
Without the LOAD_WITH_ALTERED_SEARCH_PATH, the search for bar.dll still begins in the executable directory. With the LOAD_WITH_ALTERED_SEARCH_PATH flag, the search path begins with the path to foo.dll, i.e. the system directory.
- Load DLLs manually at runtime wherever possible, and use GetProcAddress to get pointers to the required functions. DLLs like kernel32.dll and ntdll.dll will still be loaded when the executable starts, but they are on the Known DLLs list so that is not a problem.