Finding a valid binary for DLL sideloading is not usually that difficult. The hardest part is everything around it: reviewing imports, checking paths, ruling out KnownDLLs, rebuilding exports, running the binary and repeating the process until we find a stable combination.
With the SideFinder tool we have released at RedTeamer.es, the idea is to simplify this process. It works in a straightforward way: load a PE, bring together in a single view the information we would normally look for using several tools, and turn a list of DLLs into candidates we can use.
In this article, we are going to walk through that process using two examples: analysing and discovering sideloading opportunities in Notepad++, and quickly finding candidates across the more than 150 binaries included in Sysinternals Suite.
What we are really looking for
DLL sideloading consists of making a legitimate application load a DLL under our control. Our code ends up running inside the application's process, which may be signed by Microsoft or another trusted third party.
The technique usually relies on one of these scenarios:
- The application searches for a DLL in an order that lets us place our copy before the legitimate one.
- The binary references a DLL that is not present on the system or in the application package.
- We replace one of the application's own DLLs and preserve its functions through a proxy DLL.
This technique is particularly useful for initial access, lateral movement or persistence. However, the fact that a DLL appears in the import table does not mean we have a working sideload. There are still plenty of questions in between: does it actually load? From which path? Is it a KnownDLL? Which exports does the program need? Does the application keep working afterwards, or does it become unstable?
With this tool, we can answer all these questions and generate our sideloads quickly and easily.
First analysis: Notepad++
We can load the executable by dragging it into SideFinder or by using the Load Binary button. After the initial analysis, the interface is divided into three main areas.
Target Info
This summarises the binary's context: architecture, path, digital signature and relevant protections. There is not much more to it.
Import Table
This is where we find the dependencies declared by the PE:
- Imports contains the conventional dependencies that the loader resolves while loading the process.
- Delay Imports contains libraries whose resolution is postponed until some part of the program needs them.
- Bound Imports shows information, when available, about dependencies that were previously bound against specific versions.
Delay imports are especially interesting because a DLL may not appear during the first few seconds of execution. It may only be loaded when opening a dialogue, checking for updates or using a specific function.
Sideload Candidates
This table contains the DLLs found, ordered using a custom scoring algorithm.
SideFinder ranks the DLLs using contextual scoring. Based on the indicators it receives, each image is scored according to its viability as a DLL Sideloading candidate. The higher the score, the greater the likelihood of successfully loading arbitrary code.
The information used for these calculations, which is also displayed visually to the operator, is as follows:
- Hijackable: indicates whether we can take advantage of the Windows search order to make the executable load a DLL we have placed ourselves. System libraries such as
VERSION.dllorWININET.dllcommonly appear here: the legitimate copy is normally located inC:\Windows\System32, but if the application searches its own directory first, a DLL with the same name placed alongside the executable may be resolved before it. - App DLL: indicates that the DLL belongs to the application itself or is already located in the same directory. Replacing an application-specific library may be less obvious than introducing a DLL named after a Windows component whose signature, path or hash would not match the expected values. The trade-off is that these DLLs usually contain functionality required by the application, so we will probably need to preserve it through a proxy to prevent errors or unexpected crashes.
- Delay: shows that the DLL is not resolved when the process starts along with the rest of the IAT, but later, when the application uses one of its functions. Its loading may be associated with actions such as opening a dialogue, loading a plugin or checking for updates. This delays execution and avoids altering the program's startup, but it also requires us to identify and reproduce the exact flow that triggers the load.
- Missing: tells us that the binary references a DLL that SideFinder could not locate by following the evaluated search order. This is a particularly interesting candidate because placing it in the expected path could allow code execution without replacing an existing file. Even so, a missing reference does not mean the application will request it during normal use; dynamic analysis helps us confirm whether it eventually attempts to load it.
- Documented: indicates that the name or hijacking case has already been documented in HijackLibs. Besides confirming that the DLL has previously been studied for this purpose, it lets us review other binaries, paths and conditions where it has been observed. We use it as context and a starting point, not as a guarantee that it will work with our particular version.
- Exports: shows the total number of functions exported by the DLL. It provides a quick indication of the artefact's potential complexity: the more exports the DLL exposes and the application uses, the more calls we will need to reproduce or forward and the greater the risk of instability. A low number usually makes initial validation easier, although what really matters is which functions the binary consumes and when it invokes them.
Testing a candidate
In our example, the static analysis of Notepad++ returns 19 DLLs. Six are given high priority because they combine a potentially hijackable path with previous documentation; three others remain interesting even though they are not documented. KnownDLLs are ruled out with a score of zero.
Now that we have a list of DLLs that may be suitable for sideloading, the next step is to test whether they work by running a controlled test with the Test Sideload button. SideFinder generates a DLL with the required exports and a harmless MessageBox payload, compiles it and automatically runs the application in a matter of seconds. If a popup appears when the process is created, the candidate is valid.
If the message appears, we already have evidence of execution. Even so, we still need to observe the program's stability. An application that displays the popup and immediately closes is not ideal from an OPSEC perspective, as it may make the user suspicious.
Candidates such as SensApi.dll, VERSION.dll, WININET.dll, UxTheme.dll and dwmapi.dll worked in this test. dbghelp.dll also allowed code execution, but caused error windows while plugins were loading. That distinction matters: the sideload exists, but to use it without breaking the application we would need to preserve the original DLL through a proxy.
What static analysis cannot see
An import table is only a partial snapshot. Applications can also resolve dependencies at runtime, build their names dynamically or load modules only after a user action.
Using the Run Dynamic Analysis button, SideFinder runs the binary during an observation window and records the loaded DLLs. In our example, 69 modules appear and there are no missing dependencies. As a result, the loading surface increases considerably after dynamic analysis.
Correlating this observation with the previous analysis reveals new candidates that were not present in the initial static view.
We can now test new candidates based on application modules, which also turn out to be functional. Once we have reached the point where we know several potentially good options for performing the technique, we could directly generate our proxy DLL to execute shellcode. We will look at that later.
When there are too many binaries
Analysing one executable by hand is reasonable. Doing the same with more than a hundred binaries, as in the case of Sysinternals Suite, is not.
Scan Directory automates the analysis of every binary it finds under a specified directory and recursively scans up to three levels by default. In just a few seconds, it produces a summary that helps us find candidates.
The view lets us quickly identify which binaries contain confirmed or potential candidates. Instead of opening 150 executables one by one, we can jump directly to those with an interesting surface and continue the usual analysis from there.
For the demonstration, we select Bginfo64.exe and test VERSION.dll. The load is once again confirmed through a MessageBox.
Enriching the analysis with HijackLibs
HijackLibs maintains a community-managed database of DLLs used for hijacking. SideFinder can download it and use it to flag previously documented findings.
The Documented label provides information about DLLs that have previously been documented as useful for hijacking with certain binaries. By double-clicking the "YES" label, we can see which binaries and paths are associated with that DLL and use the information as a starting point for validation.
Once the database has been downloaded, we can also search the entire system for applications matching documented paths. This allows us to replicate target environments and identify paths where persistence could be deployed or which could be leveraged for lateral movement.
Generating functional artefacts
Once we have found and tested a DLL that is valid for our sideloading scenario, the next step is to build a DLL that runs our routine without preventing the application from continuing to do what it was designed for.
We can use SideFinder to generate it quickly. With the candidate selected, click Generate Proxy DLL.
The application provides several options for generating the artefact, which we will define below:
Proxy DLL or direct sideload
The choice depends on how much the application needs the legitimate library:
- A Sideload DLL reproduces the exports expected by the executable through minimal or empty functions. It is useful for quickly confirming execution, but the application may lose functionality or eventually fail when it attempts to use one of those functions.
- A Proxy DLL forwards calls to the original DLL, which is kept under a different name. This allows us to run our routine while preserving the program's expected behaviour.
For VERSION.dll with Bginfo64.exe, we choose Proxy DLL. This way, we aim to preserve the original stability and functionality.
When generating the proxy, we must provide the name under which the legitimate library will be stored. If we use notVERSION.dll, the generated DLL takes the name VERSION.dll and forwards the original exports to notVERSION.dll.
Where to obtain the exports
SideFinder can build the list from either the executable's IAT or the original DLL's EAT.
- The IAT reflects the functions imported by the analysed binary. It may be sufficient for a specific test, but it does not guarantee that other modules or plugins will not use additional exports.
- The EAT describes everything actually exported by the original DLL. When we have access to it, this is usually the safest choice for generating a complete proxy.
In this case, we keep EAT as the source. SideFinder collects names and ordinals and generates the stubs or directives required to forward the calls. Even so, matching exports does not remove the need for testing: a DLL may depend on its initialisation, a specific version or state shared with other modules.
Choosing when to run our routine
The most direct option is DllMain: the routine is initialised when the DLL is attached to the process. SideFinder moves the work into a separate context to avoid concentrating heavy logic inside the loader lock.
We can also associate execution with a specific export. This is useful when we want the routine to activate only when the application reaches a particular function rather than immediately at startup. The trade-off is clear: if that export is never invoked, the payload will not execute either.
In general, it is safer to rely on DllMain. Once the artefact is stable, it makes sense to study the program flow and consider a later trigger.
Selecting the payload
The generator provides three starting points:
- MessageBox provides a harmless and visible way to validate that the artefact is loaded.
- Shellcode Runner prepares the in-memory loading and execution of a supplied shellcode fragment.
- Custom provides the proxy structure ready for us to add our own logic.
To move from the popup test to a useful artefact, we choose Shellcode Runner and select the binary file containing our malicious payload.
SideFinder keeps this content outside the DLL to avoid increasing the executable's size or creating high-entropy sections or resources that may trigger indicators. This lets us generate a .dat, .ini or .txt file, or use any other kind of file such as a PNG image. In the latter mode, the shellcode is written at the end of the image bytes. The DLL then locates the content, retrieves it and decrypts it during execution.
For this test, we use a test.dat file and a simple shellcode fragment that starts the calc.exe process.
Execution method
SideFinder includes several methods for transferring execution within the same process. The intention is to take advantage of the context we have already gained through sideloading without unnecessarily adding a second injection into another process.
- Callbacks: use an API that ultimately invokes a function supplied by the program. Their simplicity and constraints depend on the selected callback.
- Thread Pool: schedules work on the process's thread pool. It avoids explicitly creating a dedicated thread, although it introduces a dependency on the pool's initialisation and lifecycle.
- Fibers: transforms the thread flow into a cooperative context and switches to a fiber that runs the routine. This provides control over the transfer, but requires particular care with the thread's return path and state.
In tests performed in environments with advanced security solutions, callback-based execution has produced good results.
Compiling and reviewing the output
When we click Generate, SideFinder attempts to locate a compatible compiler toolchain. It can use Visual Studio Build Tools through cl.exe or a MinGW-w64 installation with g++.exe. If it finds a compiler, it automatically builds the project and opens its directory; otherwise, it keeps the source code and build files so that we can compile them manually.
Inside the output directory, we can find the compiled binaries ready to be copied and pasted.
In our example, the output folder contains three artefacts:
VERSION.dll: the generated proxy DLL, using the name that BgInfo will attempt to load.notVERSION.dll: the renamed legitimate library, which preserves the original functionality.test.dat: the file where the supplied shellcode is stored in encrypted form.
The three files are placed alongside Bginfo64.exe. When the program starts, Windows resolves VERSION.dll from that directory, the proxy forwards the functions to notVERSION.dll, and our routine retrieves the contents of test.dat.
As a result, we can see that the program loads correctly while preserving its functionality and stability, and a calculator also appears as a result of executing the instructions contained in the shellcode.
For a complete reference to every option and help resolving compilation or loading errors, see the full SideFinder guide.