A customer distributed a program and included its supporting DLLs in the same directory, because the application directory is the application bundle.
They worried about the case that the user deletes one of the supporting DLLs, and then when the program tries to load that DLL, a rogue copy somewhere else on the PATH gets loaded instead. They want to reject loading the DLL from anywhere other than the application directory.
You can accomplish this by explicitly calling LoadÂLibraryÂEx
with the LOAD_
flag, which says that the function should look only in the application directory for the DLL. If it’s not there, it gives up without searching any other directories. After you load the library, you can use GetÂProcÂAddress
to get the functions.
Unfortunately, this is rather cumbersome since you have to switch from implicit loading to explicit loading, so you don’t get the convenience of import libraries.
You might think that you can get the convenience back by using the /DEPENDENTLOADFLAG linker option with the value 0x200
(the numeric value of LOAD_
), but the problem is that the dependent load flag applies to all DLLs loaded via import tables, and that includes kernel32 and other DLLs you probably wanted to load from the system32 directory.
Now, the system32 directory is writable only by administrators, so we could consider that a “safe” directory, because if somebody attacks that directory, they have already taken over the system. Therefore, you could use the /DEPENDENTLOADFLAG linker option with the value 0xA00
, which is the numeric value of LOAD_
. Alternatively, you could use the value 0x1000
, which is the numeric value of LOAD_
, which includes the application directory, the system32 directory, and any directories added by AddÂDllÂDirectory
and SetÂDllÂDirectory
.
But wait, what is the issue we are trying to defend against? The stated scenario is “The user deletes a DLL from the application directory.” In that case, the user already has write permission into the application directory, so instead of deleting the DLL, they can just replace it with a malicious DLL. Restricting the load to the application directory does not prevent a malicious DLL from being loaded.
But maybe your goal is not to create a security boundary but just to contain the scope of an error. If the user accidentally deletes the DLL from the application directory, at least prevent somebody else from injecting a DLL into the process by planting a DLL on the path.
Now, the directories on the path fall into two categories. You have the directories on the global path, and the directories that are specific to a single user. If an attacker can plant a DLL into a directory on the global path, then that means that they have gained write permission onto the global path. To do this without administrator privileges requires that the global path contain a directory writable by non-administrators, which is an insecure configuration, so we are in the case of creating an insecure system and then being surprised that it is insecure. Instead of planting a rogue DLL on the path, the attacker could just plant, say, a rogue notepad.exe
, and steal all your attempts to run notepad.
The other case is that the directory under attack is a directory on the per-user path. The user chose to add that directory, and if they added a directory that is writable by non-administrators other than the current user, they have once again created an insecure system because they have granted non-administrators the ability to inject things into their path.
The only attacks against rogue DLLs on the path assume that the system has already been compromised. So this issue is not about protecting a secure system but rather trying to protect from an already-compromised system.
The way this is presented, this doesn’t sound like a security issue, it sounds more like a developer wanting to avoid DLL Hell.
This would especially be a problem if they’re using a third-party DLL that is installed by multiple apps and the user decides to move only the .exe to another location, not realizing the DLL is a necessary component, thereby causing extremely weird (possibly indeterminate) crashes if it happens to grab a DLL from another app’s install that incorrectly installed it on a global search path.
While a security feature could fix this usability issue (loading only DLLs signed by...
The customer request is less about security and more about preventing collateral loading.
Imagine a user who installs App A, but during installation, chooses Custom Setup, and opts not to install
ffmpeg.dll
(or component that includesffmpeg.dll
). FFmpeg, however, is a popular free and open-source solution. Many different apps may ship with custom copies offfmpeg.dll
. It is reasonable to assume that one such app, say App B, comes with an incompatible copy offfmpeg.dll
that App A wouldn’t want to load.If
ffmpeg.dll
is required for functionality, the installer shouldn’t give the user the option to not install it. This is Windows, not Linux. If you need a DLL, provide it yourself. However, if theffmpeg.dll
requiring component is optional, the installer should leave a note for the application to not allow that functionality. Your scenario isn’t a Windows problem, but an installer/application bug.I've had a somewhat legitimate reason for this before. We were shipping a copy of a newer version of onnxruntime.dll than was included in windows at the time. We got some crash reports that didn't make sense, so we added some code to log the md5 sum of the onnxruntime.dll that we had loaded. Sure enough, the md5 didn't match. We never did figure out why it was loading the wrong one (only for a tiny minority of users, I might add), but explicitly loading it using LoadLibraryW and the full path fixed the issue. Luckily onnxruntime already uses the...
My guess: this is less about security, more about reliability and supportability -- I've seen this flavor of DLL-hell with libraries like DbgHelp.dll, which have changed a lot over the past 20 years, in binary-incompatible ways, but are used and redist'ed fairly ubiquitously -- including by other DLLs you may end up loading, directly or indirectly (thinking of things like FPS counters and VR/XR frameworks).
It's tough when your app crashes due to an incompatible DLL conflict, and even tougher when it's your crash-handler that's crashing because it can't use DbgHelp.dll to log a stack trace, so your customer has...
That doesn’t work _at all_.
I filed a vulnerability report after discovering that User32.dll loads additional DLLs dynamically and throws out the value of /DEPENDENTLOADFLAG that the application was linked with. The vulnerability got rejected. So we live in the world where you can’t restrict the DLL load order for security.
The application bundle is responsible only for the bundle. The app isn’t responsible for insecure system configurations outside the bundle that were not made by the app or installer itself. I don’t see why apps shouldn’t harden their DLL loading (and the directory permissions) due to the possibility of insecure configs outside of the bundle.