Skip to content
WindowsDeep Dive Published Updated 7 min readViews unavailable

Windows DLL Search Order: Secure Loading for Native Applications

Harden Win32 DLL loading with explicit search flags, absolute paths, process policy, dependency-aware tests, and clear Windows compatibility boundaries.

Windows DLL loading is a name-resolution operation, not simply a request to open one file. If an application loads a library by a bare name, the loader searches locations according to the process type, package state, redirection rules, loaded modules, and configured search policy. An attacker who can place a DLL in a searched directory may cause the application to load code that was not intended to run. This class of issue is commonly called DLL preloading or binary planting.

The defensive objective is to make each trust decision explicit: choose a trusted absolute path for the primary module, constrain where its dependencies may be found, avoid directories writable by less-trusted users, and test the actual dependency graph. A full path for the top-level DLL is necessary in many designs, but it does not by itself pin the locations of dependent DLLs. Dependencies are generally resolved by module name using the active search policy.

Search order is contextual

There is no single list that safely describes every process. Packaged applications have package-specific resolution rules. Unpackaged applications may be affected by DLL redirection, API sets, side-by-side manifests, the loaded-module list, Known DLLs, package dependency graphs on newer Windows 11 releases, and then application and system directories. Safe DLL search mode moves the current directory later in the default order; it does not make an unconstrained call equivalent to an explicit allowlist. The process environment and legacy search APIs can introduce additional differences.

Do not copy a search-order diagram into a threat model and stop there. Record whether the executable is packaged or unpackaged, how it is launched, whether a manifest redirects a dependency, and which API and flags load each module. Avoid using SearchPath to construct a path for LoadLibrary: Microsoft documents that the two functions do not necessarily use the same search order, which can make a check-then-load pattern unsafe.

Set a process policy, then make call-site choices

For an unpackaged process that does not need the current directory or PATH as implicit DLL locations, SetDefaultDllDirectories with LOAD_LIBRARY_SEARCH_DEFAULT_DIRS establishes a constrained default search set: the application directory, System32, and user directories added through AddDllDirectory or SetDllDirectory. The setting is process-wide and is not a reversible temporary scope. Initialize it early, before loading optional or third-party components, and review libraries that depend on older implicit search behavior.

For an individual load, LoadLibraryExW accepts explicit search flags. When the top-level DLL is addressed by an absolute path, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR makes its containing directory eligible for resolving that DLL’s dependencies. Combining it with LOAD_LIBRARY_SEARCH_DEFAULT_DIRS allows the documented default directories as well. In ordinary resolution, the explicit path identifies the primary file; applicable DLL redirection can still override it, as described below. The flags describe dependency resolution. Neither is a substitute for verifying that every permitted directory is trusted.

#define WIN32_LEAN_AND_MEAN
#include <windows.h>

BOOL configure_dll_search(void)
{
    return SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS);
}

HMODULE load_plugin(const wchar_t *absolute_plugin_path)
{
    return LoadLibraryExW(
        absolute_plugin_path,
        NULL,
        LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR |
            LOAD_LIBRARY_SEARCH_DEFAULT_DIRS);
}

The function names suggest a complete policy, but the caller still has to validate the path and manage the module lifetime. Check the return value of SetDefaultDllDirectories before loading plugins. If LoadLibraryExW returns NULL, capture GetLastError immediately and report the failure through the application’s normal error path. A production wrapper should also ensure the supplied path is absolute, canonicalize or otherwise validate the installation boundary appropriate to the application, and avoid accepting a path controlled by an untrusted caller.

An absolute path identifies the primary module in the ordinary case, but it is not unconditional. When DLL redirection is enabled for an unpackaged application, Microsoft documents that a .local redirection file can make LoadLibraryExW ignore the supplied path when a matching DLL exists beside the executable. Side-by-side manifests also affect binding for manifest-managed assemblies. The explicit search flags constrain dependency directories; they do not erase applicable loader-redirection or manifest-binding rules. Test the effective module path in the deployed application context rather than inferring it from the argument alone.

AddDllDirectory is a trust and lifetime decision

Use AddDllDirectory only for a directory that the application intentionally trusts. Its argument must be an absolute path. The directory is considered when the active default or per-call flags include user directories. The returned cookie is an ownership token; retain it and call RemoveDllDirectory when that directory should no longer participate in future DLL resolution.

Do not register a user-writable download, temporary, current-working, or shared extraction directory simply because it contains a plugin. A path that is writable by an attacker is not made safe by being explicit. If multiple user directories are added, Microsoft does not guarantee their relative search order. Avoid relying on which duplicate dependency wins; isolate plugin dependency sets, give private dependencies distinct names where practical, or package a dependency graph using the platform’s supported application model.

The cookie lifetime should match the loading design. The initial LoadLibraryExW operation resolves the module’s immediate dependencies before returning, but the loaded module or its code may perform later dynamic loads. Keep the directory registered while those future loads are expected, and remove it only when that behavior is finished. Do not unload a module or remove paths without understanding callbacks, delay-loaded dependencies, and shutdown order.

Keep loading out of DllMain

DLL entry points run under the loader lock. Avoid calling complex loader operations, initializing subsystems that may load more libraries, or waiting for other threads from DllMain. Perform optional module discovery and plugin activation from an ordinary application initialization path with explicit error handling. This reduces loader-lock deadlocks and makes the sequence observable in tests.

Authenticode validation and search-path hardening solve different problems. A signature can help establish publisher and file integrity according to the application’s trust policy, but it does not make a writable search directory safe or force the loader to select the intended file. Conversely, a constrained search path does not prove that the selected binary is authorized. Apply both controls when the threat model requires them.

Packaged apps, old systems, and API compatibility

The direct-call example above targets Windows 8 and Windows Server 2012 or later. Microsoft also documents these search flags and APIs on Windows 7, Windows Server 2008 R2, Windows Vista, and Windows Server 2008 when KB2533623 is installed, but requires applications on those older systems to obtain the relevant function addresses from Kernel32.dll with GetProcAddress. A runtime check cannot protect an older or unpatched system from a missing statically imported API. If legacy systems are in scope, use a separately tested dynamically resolved path, verify the required API addresses before passing the flags, and define a safe failure mode. Do not silently fall back to the legacy broad search order after a hardening API fails.

Packaged applications should use their package-aware loading model, such as LoadPackagedLibrary where applicable, instead of assuming the unpackaged process recipe covers package dependency resolution. Treat process type and package identity as inputs to the loader design. A library that supports both packaged and unpackaged deployment may need separate, explicitly tested code paths.

Test dependencies, not only the requested DLL

Create a disposable test application with a top-level plugin and at least one private dependency. Give the dependency the same basename as a harmless fixture in a decoy directory, then verify that the hardened load resolves only the intended file. Use Process Monitor or an equivalent trusted tracing setup to observe attempted paths, and test both successful loading and missing-dependency failures. Do not use a production executable, privileged service, or real third-party DLL as a search-order experiment.

Test an absolute top-level path with a missing dependency, an unexpected DLL placed in the current directory, an application directory that is not writable by ordinary users, and each supported package mode. Verify that process policy is established before plugin code runs and that the AddDllDirectory cookie remains valid only for the intended lifetime. Repeat the tests on the oldest supported Windows build and on current supported releases; a successful load on one developer workstation does not prove every deployed dependency is resolved safely.

The review should be able to answer: Which exact file is the primary module? Where may every dependency be found? Who can write each directory? Which process-wide defaults are active? What happens when a hardening API or dependency is unavailable? Those answers turn loader behavior from an ambient machine property into a reviewable application policy.

Related:

Sources:

Comments