How can I control the directory from which my delay-loaded DLL is loaded?


A customer had a DLL that is a COM in-process server. This DLL gets loaded by arbitrary client applications, and it also uses the /DELAYLOAD linker flag to delay-load many of the DLLs which it depends upon. The customer observed that these delay-loaded DLLs were being loaded according to the standard DLL-loading search algorithm.¹ The problem is that the DLL which they are dependent upon is not in the standard search path; it's in its own private directory.

The customer explained that they are working around this problem by providing a custom delay-load helper function which calls Set­Dll­Directory to add the private directory to the DLL search path.

The customer wanted to know whether Set­Dll­Directory affects the DLL search path for the entire process. The customer doesn't want to affect the DLL search path for the entire process because the DLL is a guest in the host process, so it shouldn't be changing the carpet. (Hey, at least they're not calling in a demolition team.) "If it affects only the DLL we need to load, then it looks like Set­Dll­Directory is what we need. But if it affects the entire process, then we would have to switch to Add­Dll­Directory."

Yes, the Set­Dll­Directory function affects the DLL search path for the entire process. It's not clear what the customer's mental model is for "affects only the DLL we need to load", seeing as you don't actually pass Set­Dll­Directory the name of the DLL you need to load, so it has no idea which DLL to apply this path to.

The customer's proposed alternative of using Add­Dll­Directory doesn't solve the problem, because it too affects the DLL search path for the entire process. Maybe they were thinking of calling Add­Dll­Directory to add the private directory, then calling Remove­Dll­Directory to remove it at some unspecified point in the future. But that creates a window in which the process DLL path has the private directory, and if another thread also calls Load­Library, it will see that other private directory, which is presumably unwanted.

The customer is making things too hard for themselves by manipulating the DLL search paths. Since they know what directory the DLL is in, they don't need to do any searching at all. When the notification handler is called, it is given a few pieces of information.

  • The reason why the handler is being called.
  • Information about the DLL being loaded.

Since the customer already has a custom handler, they can just write their custom handler like this:

FARPROC WINAPI delayHook(unsigned dliNotify, PDelayLoadInfo pdli)
{
 if (dliNotify == dliNotePreLoadLibrary &&
     StrCmpIC(pdli->szDll, "special.dll") == 0)
 {
  return LoadTheSpecialDll();
 }
 ...
}

HMODULE LoadTheSpecialDll()
{
 .. calculate the full path to the special DLL in its
 .. private directory
 return LoadLibrary(fullPathToSpecialDll);
}

If the notification handler is being told that we are about to load special.dll, then load the special DLL using whatever custom algorithm you need, and return that handle. The delay-load library will use that module instead of trying to load via the standard DLL search directory. There's no need to mess around with Get/Set­Dll­Directory, which is a good thing, since that avoids applying a global solution to a local problem.

¹ This is explained in the documentation, because it says that the default delay-load helper function calls the Load­Library function, which is subject to the standard DLL search path. Though technically, it calls Load­Library­Ex and passes a flags value of 0, which is functionally equivalent. You can see this and more in the file delayhlp.cpp in the VC\Include directory.

Comments (19)
  1. skSdnW says:

    Shell32 and friends should be changed to do this (force loading from system32) to stop .dll hijacking attacks on applications that are not calling SetDefaultDllDirectories.

    1. xcomcmdr says:

      No, that would break a lot of use-cases.

      For example, DLL hijacking is what enables a lot of fixes for old games (ie. put third-party library DDrawCompat, or nGlide, or dgVoodoo2, as "ddraw.dll"/"Glide2X.dll" alongside the game's executable, and watch this 90s game work on Windows 10 !).

      It would be a shame to get rid of that.

      1. skSdnW says:

        I'm talking about .dlls shell32/shlwapi will load (insecurely) behind your back inside certain functions, mainly version/userenv/crypt32/crypbase/comres/clbcatq/uxtheme/setupapi/apphelp/propsys.dll. I would imagine that most games link directly to the 3d functions, not with delay-loading.

        If MS decides to fix this they could disable the hardening if the "appname.exe.local" file is present.

        1. xcomcmdr says:

          Ah, my bad. When I googled "dll hijacking" it described what I described. Here lies my error.

    2. You are trying to secure the wrong side of the airtight hatchway. The bad guy can just plant a rogue version.dll, in which case your "fixes" to version.dll are useless because the "fixed' version.dll isn't even running.

      1. skSdnW says:

        No? Create a program that only links to kernel32 and shell32 and then call SHGetFileInfo or some other function that causes shell32 to delay-load a function in propsys.dll or version.dll and then the delay-loader in shell32 calls LoadLibrary without a full path and your application might load a evil .dll you never asked for, it is only loaded as a implementation detail inside Microsoft controlled code.

        To fix it the shell32 delay-load hook would try to load from a full path to system32 before falling back to normal LoadLibrary.

        1. I can't tell whether you are saying that "shell32 should use this technique when it loads other DLLs" or "other DLLs should use this technique when it loads shell32." Either way, you have lost. In the case where you want shell32 to use this technique, the bad guy simply places an evil shell32.dll in the same directory and that will be used instead of the real one. Any fixes you make to the real one have no effect because evil has already won.

          1. skSdnW says:

            Yes, shell32 should use this technique when it loads other DLLs. Shell32 is on the known-dlls list, version.dll and propsys.dll are not.

  2. Antonio Rodríguez says:

    Yes, technically Add­Dll­Directory is a global solution to a local problem, and thus, technically incorrect. But *adding* a directory controlled by the COM server to the DLL search path (instead of *replacing* the full path) may not break anything. It would only affect if the host process tries to load a non-existent DLL *and* a DLL with the exact same name exists in the COM server's directory. Which is, in the worst case, very unlikely.

  3. Martin Bonner says:

    I am puzzled how the customer got to where they were. We had a similar problem (DLL loaded via LoadLibrary, needs other DLLs that are in the same directory as the main DLL, and unfindable by the standard rules). The solution I found by googling was to switch to delay load, and explicitly LoadLibrary the path we wanted. I don't understand why the customer switched to delay load, but didn't do the explicit load.

  4. skSdnW says:

    @Raymond: Yes I know it is not a security feature but it does act like one. If you look at all the .dll hijacking vulnerabilities disclosed last year for both Microsoft and 3rd-party products ( http://seclists.org/fulldisclosure/2016/Aug/50# etc.) they all hijack DLLs that are not on the known-DLLs list.

    1. That still won't help because any DLLs that are non-delay-loaded will still be hijackable. You are trying to secure the wrong side of the airtight hatchway.

      1. skSdnW says:

        DLLs loaded by the application are the developers responsibility but the delay-loaded stuff deep inside the call stack of a public API is not and it would not be hard for Microsoft to fix.

        WiX for example had to add a workaround for the problem; manually load DLLs they never use directly early in WinMain before calling shell functions ( https://github.com/firegiant/wix3/blob/master/src/burn/stub/stub.cpp )

        1. I'm not talking about DLLs loaded by the app. I'm talking about DLLs linked (non-delay) by shell32. Those would still be plantable. Basically, in order for this to be effective, every DLL in the system needs to delay-load all of its dependencies.

          1. skSdnW says:

            That might be true in theory but as long as the application explicitly links to shell32 then there is no problem because shell32 is well known and "... the \KnownDlls sections are computed as the transitive closure of the DLLs listed in KnownDLLs. So if a DLL’s listed in KnownDLLs, all of the DLL’s that are statically linked with the DLL are ALSO listed in the \KnownDlls section"(1) This leads is back to the real world situation where the delay-loaded DLLs are the problem. Fixing it is just GetSystemDirectory + PathAppend + LoadLibrary in the delay-load callback.

            1: https://blogs.msdn.microsoft.com/larryosterman/2004/07/19/what-are-known-dlls-anyway/

          2. Like I said, you're trying to secure the wrong side of the airtight hatchway. If shell32 gets removed from the Known DLLs list (which it could, since it's just a performance setting, not a security setting), then your plan breaks down.

          3. skSdnW says:

            Even if applications only explicitly link to kernel32 and delay-load with callback hardening for everything else you still have to deal with kernel32. There is no PE/manifest setting to control where DLLs are allowed to load from so you basically have to rely on known-DLLs and/or the internals of CreateProcess to safely load kernel32 and ntdll for you.

            Shell32 is on the known-DLLs list in every version of Windows from Win95 to Win10 so my suggestion would fix all supported versions and MS could provide a manifest setting for vNext in the unlikely event that it is removed from the list. This is just the tip of the iceberg of course but it would make a big roadblock for evil hijackers if Microsoft did it, even if just for shell32, shlwapi? and advapi32.

  5. Bryce Wagner says:

    I do something similar to this in the .NET world using the AppDomain.AssemblyResolve event. But DllImport signatures within those delay loaded assemblies could theoretically end up pointing at the wrong native DLL. Is it possible to use this technique to explicitly load native DLL files under C#, or is this a C++ specific feature? It looks like it's a C++ linker symbol, but maybe I'm missing something.

Comments are closed.

Skip to main content