
Why DLL Sideloading Undermines Code Signing: Lessons for Developers from the NIGHTFORGE Attack
The NIGHTFORGE loader report stands out for one simple reason: the signed binary is not the payload, it is the trust anchor. Once an attacker gets a trusted VMware component to launch, they do not need to counterfeit a signature on the malicious code. They only need a DLL loading mistake that lets the signed process pull in attacker-controlled code from the wrong place.
That distinction matters if you build Windows software, ship helper executables, or treat code signing as a security control. A signed EXE can be legitimate and still become the first stage of an intrusion.
Why this VMware-signed binary matters
The report says attackers abused a VMware-signed binary to sideload the NIGHTFORGE loader in espionage activity. I would not read that as “VMware signing failed.” I would read it as “the Windows loader did exactly what it was allowed to do.”
That is the ugly part of sideloading. Signing tells you the binary came from the expected publisher. It does not tell you:
- what DLLs it will load
- where those DLLs will come from
- whether those DLLs are trusted
- whether the process should have been allowed to run in that location at all
If a vendor ships an EXE that looks up a DLL by name, and an attacker can place a different DLL with that name in a search location the process uses, the signed EXE becomes the delivery vehicle. The signature still checks out. The runtime behavior does not.
For defenders, the impact is practical:
- application allowlists can be bypassed if they only inspect the signed parent process
- execution logs may show a trusted VMware binary while the malicious logic lives in an unsigned module
- “block unsigned EXEs” controls miss the real issue, because the unsigned code enters through a signed host
What DLL sideloading actually changes in the trust model
Signed EXE, unsigned DLL, and the loader search path
A Windows process is not one object of trust. It is a loader, a module graph, and a pile of file system assumptions.
A signed executable can be launched because its publisher is trusted. But once that executable starts, Windows resolves imports and later LoadLibrary calls using search rules. If the code asks for foo.dll, the loader decides where foo.dll should come from. If the attacker can influence that path, the signed process loads attacker code.
In practice, sideloading usually depends on one of these patterns:
- the EXE and a required DLL are shipped together
- the DLL name is hardcoded but the path is not
- the process starts in a writable directory
- a plugin folder, temp folder, or current directory is part of the search path
- the app uses a helper library that itself depends on another library by name
The signed EXE is still signed. The malicious DLL is not. The trust boundary is the gap between those two facts.
Why code signing stops at the binary boundary
Code signing answers a narrow question: “Who published this file at signing time?”
It does not answer:
- “What will this process load after startup?”
- “Will the app search user-writable directories?”
- “Will a helper process inherit a dangerous working directory?”
- “Will the code load plugins from ambient paths?”
- “Will a trusted binary execute untrusted modules in memory?”
That is why code signing is necessary but not sufficient.
A useful mental model is this:
| Layer | What signing tells you | What it does not tell you |
|---|---|---|
| EXE file | Publisher identity and tamper status | Loaded DLLs, runtime behavior |
| DLL file | Publisher identity and tamper status | How the host process selects it |
| Process | Nothing by itself | Whether it runs in a safe directory or with safe search rules |
If you treat the EXE signature as proof that the whole execution chain is trustworthy, sideloading breaks that assumption immediately.
Reconstructing the NIGHTFORGE delivery chain
The signed VMware component as the launch point
The report’s shape is familiar: a signed VMware binary is used as the initial executable, and the attacker places a malicious DLL so that the process loads it during startup or some helper action.
I would expect the chain to look something like this:
- The attacker gets a trusted-looking VMware-signed executable onto the target.
- The executable is launched from a directory the attacker controls or can influence.
- The process resolves a dependency by name instead of by an absolute path.
- Windows loads the attacker-controlled DLL from the search path.
- The DLL initializes and hands control to the NIGHTFORGE loader.
That is enough for code execution under a signed parent process. No signature forgery is required.
The important detail is not the vendor name. It is that the signed component looks plausible enough to run, and it loads a local dependency in a way the attacker can steer.
Where the malicious DLL fits in the execution flow
Once the signed binary loads the malicious DLL, the DLL can execute in several common ways:
DllMainruns on load- an exported initialization function is called by the host
- the DLL hooks into a plugin or extension interface
- a delay-loaded dependency triggers on first use
That means the malicious DLL does not need a loud, separate launcher. It can sit inside the normal startup path of the trusted process.
For defenders, that matters because telemetry often looks clean at first glance:
- parent process: signed VMware binary
- child process: maybe none, maybe a legitimate-looking helper
- network activity: may come later, after initialization
- malicious logic: buried in module load events
If you only review process creation, you miss the interesting part. You need module-load visibility.
What the report implies about operator intent and target selection
I would not assume the attackers chose VMware by accident. Trusted vendor binaries are useful for at least three reasons:
- they are less suspicious in logs
- they may already exist on the target
- they can inherit allowlist trust or blend into normal admin work
This looks more like operator intent than opportunism. The goal is not just execution. The goal is execution that survives basic triage.
That is a common espionage pattern: use a legitimate-signed process as cover, then place the malicious loader where the process will accept it. The victim sees a trusted executable. The operator gets code execution under that trust umbrella.
How Windows picks the wrong DLL
Current directory precedence and application-local loading
The Windows loader has a long history of “helpful” search behavior. That behavior becomes dangerous when the application uses a bare DLL name and the working directory or application directory is attacker-influenced.
A simplified version looks like this:
LoadLibraryA("helper.dll");
That call does not say where helper.dll lives. It delegates the decision to the loader. If the application directory, current directory, or another allowed search location contains an attacker-controlled helper.dll, Windows can load the wrong file.
A safer pattern is to make the location explicit:
LoadLibraryExW(
L"C:\\Program Files\\Vendor\\App\\helper.dll",
NULL,
LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS
);
That does not solve every problem, but it removes the ambiguity sideloading depends on.
Also worth remembering: “current directory” bugs are not always the old classic SetCurrentDirectory mistake. They can show up when:
- a service starts from a writable folder
- a shortcut launches an EXE with an unsafe working directory
- an updater extracts files into a user-controlled path
- a child process inherits a directory that was never meant to be part of trust
Side effects of weak manifesting, unsafe search order, and bundled helpers
The loader gets dangerous when the app is sloppy in more than one place.
Common combinations include:
- no manifest hardening
- legacy search behavior left enabled
- helper DLLs shipped next to the EXE
- plugins loaded from a folder that normal users can modify
- updater binaries that run from temp or cache locations
If the app was built in a world where “next to the EXE” meant “safe,” sideloading turns that assumption into a liability. The attack only needs one writable edge.
I usually think of it this way:
- a signed launcher is fine
- a signed launcher that loads local dependencies is common
- a signed launcher that loads local dependencies from writable locations is a risk
- a signed launcher that loads local dependencies from writable locations and runs at high privilege is a problem
Common developer mistakes that make sideloading possible
The mistakes I keep seeing are boring, which is why they survive so long:
- calling
LoadLibrarywith a relative name - relying on the default DLL search order
- shipping helpers in directories users can write to
- using temp folders for extracted components
- letting plugin paths be configured without validation
- assuming “signed parent process” means “safe module tree”
- forgetting that services, scheduled tasks, and installers often run from very different working directories
None of those look dramatic alone. Together, they create an easy sideloading path.
What to inspect in your own software
Inventory signed executables that load local DLLs
Start by finding your signed EXEs that load DLLs from the application directory or from a configurable path.
You can build a rough inventory with static inspection:
Get-ChildItem "C:\Program Files\YourApp" -Filter *.exe -Recurse |
ForEach-Object {
& dumpbin /dependents $_.FullName 2>$null
}
That does not prove a sideloading bug, but it tells you where to look.
Then check whether the executable is expected to run from a location users can modify. If the binary is trusted and the directory is writable, that combination deserves attention.
A second pass should identify binaries that are signed but load unsigned modules later. That is where the trust boundary gets interesting.
Check imports, delay-load behavior, and runtime plugin paths
Static imports are only part of the picture. Many apps do not import the risky DLL directly. They delay-load it or build the path at runtime.
Things I would audit:
- delay-load dependencies
- plugin directories
- extension folders
- helper executables that spawn other helpers
- runtime configuration for module search paths
- code that constructs DLL paths from environment variables or registry values
If you use a binary analysis tool, the question is simple: does the process ever ask for a module by name instead of by absolute path, and can that name be influenced?
For runtime validation, module-load telemetry is more useful than source code guesses.
Look for writable directories near trusted binaries
The easiest sideloading opportunities show up when trusted binaries live near writable files.
Check for:
%ProgramFiles%subdirectories with weak ACLs- app folders under
%LOCALAPPDATA% - extracted updater directories in
%TEMP% - shared application folders writable by non-admin users
- service directories that inherited loose permissions during install
A quick defensive check is:
icacls "C:\Program Files\Vendor\App"
You want to know whether ordinary users can drop a file next to a signed executable. If they can, and the executable loads local dependencies, you have a problem worth fixing.
Safer design patterns for Windows developers
Use absolute paths or restricted DLL directories
The first rule is to stop relying on ambient search order when you do not need it.
If you control the code, prefer:
- absolute paths for known dependencies
SetDefaultDllDirectoriesAddDllDirectoryLoadLibraryExwith search flags
A common hardening pattern looks like this:
#include <windows.h>
int main() {
SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_SYSTEM32 |
LOAD_LIBRARY_SEARCH_APPLICATION_DIR);
DLL_DIRECTORY_COOKIE cookie = AddDllDirectory(L"C:\\Program Files\\Vendor\\App\\plugins");
HMODULE h = LoadLibraryExW(L"trusted-plugin.dll", NULL,
LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR |
LOAD_LIBRARY_SEARCH_DEFAULT_DIRS);
if (cookie) {
RemoveDllDirectory(cookie);
}
return h ? 0 : 1;
}
The exact flags depend on your app, but the principle is the same: define the search space instead of inheriting one.
Sign the whole package, not just the launcher
A signed EXE is not a signed product. If the product depends on local DLLs, plugins, or helpers, the package matters.
That means you should:
- sign all shipped binaries, not just the launcher
- verify hashes during update and install
- avoid shipping unsigned modules next to trusted executables
- make sure the installer sets safe ACLs on all runtime directories
Signing everything does not eliminate sideloading, but it reduces the chance that one unsigned helper becomes the weak link in an otherwise trusted bundle.
Reduce privilege and isolate updater or helper processes
A lot of sideloading problems get worse because the trusted host runs with more privilege than it needs.
Good hardening steps include:
- keep update logic in a separate process
- run helpers with the minimum token possible
- avoid admin-owned files in user-writable directories
- separate plugin execution from the main privileged process
- do not let an installer or updater keep the same broad privileges for runtime work
If the hostile DLL lands in a low-privilege helper instead of a privileged service, the blast radius shrinks.
Prefer explicit plugin loading over ambient search paths
If your app supports plugins, make the trust rules boring and explicit.
That means:
- a fixed plugin directory
- strict ACLs on that directory
- a manifest of allowed plugin names or hashes
- signature validation for third-party extensions
- no loading from current directory, temp, or environment-controlled paths
The mistake is not “plugins exist.” The mistake is “plugins can come from anywhere the process feels like checking.”
Detection and response ideas for defenders
Hunt for signed parent processes spawning unexpected children
Sideloading does not always stay inside one process. Sometimes the malicious DLL launches a second stage, injects into another process, or drops a payload for later execution.
Hunt for:
- signed vendor processes that spawn unusual children
- new child processes from software that normally should not launch anything
- command lines that look like updater activity but happen outside maintenance windows
- processes started from user-writable or temp locations
If a VMware-signed binary is running from a path you do not expect, that is already worth triage.
Flag unsigned modules loaded by trusted vendors
This is where module-load telemetry pays off.
Useful signals include:
- a signed process loading an unsigned DLL
- a trusted vendor process loading a module from a user-writable directory
- a process loading a DLL from its current working directory instead of
System32 - a DLL name that matches a legitimate vendor dependency but has a different file hash or path
Sysmon Event ID 7 is useful here because it records image loads. ETW image-load telemetry can do the same at higher volume if you already collect it.
A simple triage rule is often enough:
| Signal | Why it matters |
|---|---|
| Signed process + unsigned module | Possible sideloading |
| Module path in temp/user profile | High risk |
| Module name matches vendor DLL but path does not | Naming collision |
| Process not normally observed on endpoint | Suspicious execution path |
Correlate execution from user-writable or temp locations
The source of the binary matters as much as the signature.
Look for execution from:
%TEMP%%APPDATA%%LOCALAPPDATA%- download folders
- removable media
- extracted archive paths
If a trusted vendor binary is being launched from a user-writable folder, ask why. Many sideloading campaigns depend on exactly that mismatch: a real signed binary in an untrusted place.
Also check file creation before execution. If a DLL appears next to a trusted binary shortly before the binary starts, that is often the clue.
Defensive testing checklist
Reproduce the search order in a lab safely
You do not need malware to test sideloading exposure. You need a harmless test harness and a predictable dependency name.
Build or choose a small internal app that loads a DLL by name, then verify where Windows resolves it from. In a lab, use a benign DLL that only logs when it is loaded.
Your goal is to answer:
- Does the process load from its application directory?
- Does the current directory matter?
- What happens if the working directory changes?
- What happens if the intended DLL is missing?
- Does the app fall back to another search location?
If you can make the app load the wrong benign DLL, you have reproduced the class of bug without doing anything unsafe.
Verify mitigation with ProcMon, ETW, or loader telemetry
Process Monitor is still one of the fastest ways to prove what happened.
A practical workflow:
- Filter on the process name.
- Watch
Load Imageevents. - Note the exact DLL path Windows chose.
- Compare that path to your intended directory.
- Repeat after applying hardening.
If you already collect ETW, image-load events give you a better at-scale view. The question is the same in both tools: did the process load a module from the path you expected?
You can also use endpoint telemetry to identify unsigned module loads under signed parents. That signal is more reliable than process creation alone.
Validate that hardening survives common edge cases
A fix that only works in the happy path is not a fix.
Test these cases:
- launching from a shortcut with an odd working directory
- service startup after reboot
- running as standard user versus admin
- 32-bit and 64-bit builds
- update mode and first-run mode
- plugin installation and uninstall
- missing dependency fallback
- network paths and mapped drives
- paths with spaces and non-ASCII characters
A lot of sideloading regressions come back through edge cases. The app works during normal testing, then breaks open when a launch path or working directory changes.
What this attack says about trust on Windows
The NIGHTFORGE report is a reminder that trust on Windows is layered, not absolute.
A signature tells you who published a file. It does not tell you what the process will load next. A trusted binary can become the delivery vehicle for untrusted code if the loader is allowed to search the wrong places.
That is why defenders should care about more than signatures:
- file origin
- module load path
- directory permissions
- loader flags
- child processes
- privilege boundaries
And that is why developers should stop treating DLL loading as an implementation detail. It is part of your security model.
If your software ships a signed EXE, loads local dependencies, and runs from a directory users can influence, you have already created the conditions sideloading needs. The fix is not a stronger signature. The fix is tighter control over how code enters the process in the first place.
Conclusion
The useful lesson from the VMware-signed NIGHTFORGE case is not that a specific vendor was “bypassed.” It is that code signing ends at the file boundary, while the loader keeps going.
If you build Windows software, audit every place your process resolves a DLL by name. If you defend Windows endpoints, hunt for trusted binaries loading modules from untrusted paths. And if you rely on signatures for allowlisting, remember that a signed parent process can still hide an unsigned child inside its own address space.
That is where sideloading wins: not by breaking trust, but by borrowing it.


