DLL sideloading: tricks, traps and fixes - Part 1!
This blog post is a result of our investigation into the process that attackers use when sideloading malicious DLLs into .NET executables. We’ll describe how and under what circumstances an attacker can get a malicious .NET DLL to be loaded by a trusted (signed) .NET executable, and we’ll also describe some risk-mitigation strategies for you to consider.
There’s a lot to get through (quite a few months of work was done in the lead-up to this post) so we’ll break the post into two parts; this is part one.
In summary:
- DLL sideloading of Authenticode-signed .NET executables is feasible but may need to overcome strong-name signature validation.
- There are some scenarios where strong-name signature validation is not enforced, and these situations can be exploited by attackers.
- Even if strong-name signature validation is being enforced, other Authenticode-signed .NET executables (even Microsoft-signed ones) suitable for sideloading can be identified.
What’s this all about? Read on! We have animated demos and everything!
Introduction
The search order for loading .NET DLLs by .NET executables is quite different from its native counterpart. To summarise, the following locations are searched, when loading .NET DLLs, in the order below:
- The Global Assembly Cache (GAC).
- Locations specified by the <codebase> element in the application configuration file.
- Probing the application base and culture directories, as well as privatePath (specified in the configuration file) subdirectories below the application base.
We will ignore the GAC for this article as the requirements to place a DLL in the GAC are quite restrictive (e.g. it must be done with local administrative rights), and instead we’ll focus on the latter two locations in an attempt to find DLL sideloading opportunities that can be used when targeting an unprivileged user.
Since the application base directory is included as part of the default search path, if an application does not have a <codebase> element in its configuration file (or doesn’t have an app-specific config file at all) then it sounds like we could just place a malicious dependent-upon DLL in the same directory as the target application and it would be loaded by the target .exe. However, many .NET assemblies are strong-name signed, which can potentially make this approach impossible.
But before clarifying this, let’s discuss what makes a strong-name assembly.
PE and strong-name signing
A Portable Executable (PE) file is a structured binary format that Windows uses to load and execute .exe (native or .NET) and .dll files.
What is Strong-Name Signing?
Strong-name signing is a cryptographic mechanism in .NET used to uniquely identify an assembly and ensure its integrity. It involves:
- Generating a strong-name key pair (a public/private key pair).
- Embedding the public key and its token in the assembly metadata.
- Signing the assembly manifest (inside the PE file) using the private key.
What Does Strong-Name Signing Provide?
- Tamper detection → Ensures the assembly hasn’t been modified after signing.
- Unique identity → Helps the .NET runtime distinguish between different assemblies, even with the same name.
- Compatibility with the Global Assembly Cache (GAC) → Only strong-named assemblies can be placed in the GAC in .NET Framework.
Strong-Name Signing vs. Authenticode Signing
Now, it’s important to note that strong-name signing is different from Authenticode signing in several ways:
- Scope
- Only .NET assemblies (.dll or .exe) can be strong-name signed.
- Any PE file (native or .NET) can be Authenticode-signed.
- Signature Location & Coverage:
- In strong-name signing, most parts of the PE file are signed with the notable exception of the Authenticode signature (if one exists).
- In Authenticode signing, the entire PE file is signed (including the strong-name signature if it exists), and the signature is stored in the PE header.
- Trust & Verification:
- Strong-name signatures are verified using a raw embedded public key.
- Authenticode signatures are verified using X.509 certificates, which should chain up to a trusted root certificate authority (CA).
Why does this matter?
Of the previous three points, the last point is crucial because strong-name signing does NOT imply trust:
- A strong-name signature only prevents undetectable modification of the .NET assembly.
- Anyone can create a strong-name signature that validates correctly—it does not indicate that the assembly comes from a trusted publisher.
Well, you were warned!
Putting it together: .NET assembly loading
When you strong-name sign a .NET assembly, you get a “strong-named” assembly. So how are strong-name signatures related to .NET assembly loading? When an application references a dependent assembly, it uses the following metadata to locate the correct file:
- The assembly name.
- The assembly version.
- The assembly culture.
- The assembly public key token, which is the last 8 bytes (in reverse order) of the SHA1 hash of the public key associated with the strong-name signing private key if the assembly has been strong-name signed, or ‘null’ otherwise.
The reference to the public key token is what prevents strong-name assemblies from being tampered with. If an attacker wants their malicious .NET DLL to be loaded in place of the legitimate strong-name, signed DLL, then they need to have it signed by the private key associated with the advertised public key token. Of course, the attacker doesn’t have that signing key and so signature validation will fail.
As an aside: There is also the possibility of an attacker being able to create a valid strong-name signature with a different public/private key pair, but with a clashing SHA1 hash of the advertised public key which will also let the malicious DLL load, but this is obviously a very non-trivial challenge!
What if the assembly is not strong-name signed ? After all, developers have the choice as to whether or not to strong-name sign their assemblies. In that case, the public key token is null and not used to determine the version of the DLL to be loaded, and so sideloading is trivial. However, it appears that all of the core Microsoft .NET Framework DLLs are strong-name signed, so this makes them immune to undetectable tampering.
Having said all of the above, in 2008 Microsoft released an update to .NET 3.5 Service Pack 1 which under certain conditions disabled verification of strong-name signature during assembly loading for performance reasons. What conditions? All of the following:
- Fully trusted, without considering strongName (such as has MyComputer zone) evidence. Note that the word ‘considering’ appears to have been left out of the official doco, making it somewhat nonsensical. I’ve included the word as described in this blog post.
- Loaded into a fully trusted AppDomain.
- Loaded from a location under the ApplicationBase property of that AppDomain.
- Not delay-signed.
There is a bit to unpack there and a complete understanding of these conditions requires historical knowledge of Code Access Security (CAS) and how it changed from .NET 2.0 -> 4.0. For the interested reader, we recommend the series of blog posts from 2008 which describe the evolution of CAS, .NET sandboxing and strong-name signature verification, much better than the official documentation.
Which .NET DLLs to target?
The most common case where the conditions in the previous bullet-list are satisfied is when a standalone (not hosted in a process such as ASP.NET or Click-once as they typically use application domains which are not full-trust) executable and its dependent DLL are located in the same directory on a local machine, then strong-name signatures are not verified when running under .NET Framework >= 3.5 SP1 (which is deployed pretty much everywhere). So, in a sense, in such a scenario all .NET DLLs can be sideloaded.
What about an attacker who wants to load their malicious DLL from a remote server ? Will strong-name signatures be verified in such a case ? To test this out, assume we have a .NET executable executable.exe with a dependency dependency.dll, both strong-name signed. First, we use DNSpy to modify dependency.dll to break the strong-name signature:

Running DNSpy
Now, recompile from within DNSpy and confirm using the strong-name tool (sn.exe) that the strong-name signature of the modified DLL now fails validation:

Success! Failed signature validation
First note that if we host the modified dependency.dll on a remote HTTP(S) server, adjust the codebase to point to that remote server, and then attempt to execute executable.exe, we fail:

Serving up over HTTPS won’t work.
As shown above, strong-name signatures are indeed being verified for remote DLLs loaded over HTTP(S).
However as a comparison, if we host the modified dependency.dll on a remote WebDAV server, adjust the codebase property to point to that URL and run executable.exe locally, we see that the modified dependency.dll is loaded/executed successfully:

But WebDAV? DLL-loading happiness!
Since this DLL is not being loaded from a location under the ApplicationBase property of the default AppDomain, this does seem to be inconsistent with the conditions for the strong-name validation bypass described above (and may be a Microsoft bug?).
Aside: What about MotW?
PE files (like .exe and .dll) are most vulnerable to abuse (e.g., malware, unwanted code execution) and so Microsoft came up with Mark of the Web (MotW), a flag stored inside the file’s alternate data stream (ADS) on NTFS file systems. This flag tells Windows and other software (including browsers, PowerShell, .NET runtimes, etc.) that the file in question came from the Internet (an untrusted zone).
In the context of this post, it’s worth noting that the presence or absence of the MOTW on an executable (or DLL dependency) does not affect the conditions of the strong-name signature bypass: That is, if an executable and DLL dependency have the MOTW and they would normally be permitted to execute by Smart Screen (e.g. if they were a Microsoft-signed executable) then strong-name signatures are still not validated by default.
Summary of Part 1
In our investigation into DLL sideloading for .NET executables, we explored how attackers might load malicious .NET DLLs using trusted, signed .NET executables. The process for loading .NET DLLs differs significantly from native executables, with the search order prioritizing the Global Assembly Cache (GAC),locations specified by the `<codebase>` element in configuration files, and the current application base directory.
In conclusion (and Part 2 will be here next fortnight!) we describe:
- Local Sideloading Feasibility: In scenarios where .NET executables and their dependencies reside in the same directory, sideloading remains feasible due to the strong-name verification bypass.
- Remote Loading Limitations: While HTTP(S) enforces strong-name verification, WebDAV does not, highlighting a potential oversight in security protocols.
- Security Implications: Developers must be aware of these sideloading opportunities and ensure that critical .NET assemblies are strong-name signed and configured securely to prevent unauthorized code execution.
Part 1 of this investigation has underscored the importance of understanding .NET’s assembly loading mechanisms and potential vulnerabilities, emphasizing the need for robust security practices in application development and deployment.
Part 2 will follow shortly, and will look at performing .NET sideloading of legitimate Authenticode-signed executables.
See you there!!
dotSec is a professional cyber security organisation that was founded in 2000. Our idea was simple:
“Help organisations to treat security as a strategic asset, and they will operate with fewer risks and with a more certain budget, attracting more customers and becoming more successful than their more reactive and less strategic competitors.”
Now, with over 25 years of national and international experience behind us, that one idea has allowed us to assist clients across most industry sectors, and across all tiers of government.