Another technique for Fixing DLL Preloading attacks
Back in February, 2008, I posted on DLL preloading attacks and how to avoid them here. It seems that the problem has recently gotten a lot of attention – currently called "Binary Planting". You can read more about that at the MSRC blog, the SWI blog, on this ZDNET blog post, an update, and a Computerworld article.
In addition to the advice I gave previously, I recently thought of a simple way to avoid problems. The technique is simply to push the current working directory, set it to something safe, like c:\windows\system32, call LoadLibrary, and then reset it to the previous current working directory. Note that if you can call SetDllDirectory(""), as I documented in my previous post, then you should do that, as it is easier and will solve the problem for all your LoadLibrary calls. If you cannot do that, here's a code sample that will fix individual calls for you:
HMODULE SafeLoadLibrary(const wchar_t* wzFileName)
{
static wchar_t wzSystem[MAX_PATH];
wchar_t wzCurDir[MAX_PATH];
HMODULE hMod = NULL;
if(wzSystem[0] == L'\0')
{
if(GetSystemDirectory(wzSystem, _countof(wzSystem)) == 0)
return NULL;
}
// Now get the actual current working directory
if(GetCurrentDirectory(_countof(wzCurDir), wzCurDir) == 0)
wzCurDir[0] = L'\0';
SetCurrentDirectory(wzSystem);
hMod = LoadLibrary(wzFileName);
SetCurrentDirectory(wzCurDir);
return hMod;
}
A nice feature of this code is that if the library you're loading might not have this fix and loads additional DLLs, it's been made safe by your changing the current working directory to something that's known good. A drawback of this code is that if some other thread is changing your current working directory, the two threads could have a race condition. This would be an unusual problem, but something to be aware of.
Obviously, if you need the current working directory in your search path, then this won't help, and it is up to you to ensure that the current working directory isn't something under an attacker's control.
Comments
Anonymous
August 23, 2010
But can we count on kernel32.dll to be safe? Otherwise the attacker could simply change the return value of GetSystemDirectory(). [dcl] A nuance I didn't explain (and should have) is that kernel32.dll has to be loaded in your process, because that's the DLL that exposes LoadLibrary, and you don't get far without LoadLibrary! The very first place LoadLibrary looks is in the DLLs that are already loaded in the process, so it must find kernel32.dll safely. I'm not sure I understand the entire issue here. Are DLLs in system directories guaranteed to be found before the ones in the current directory? [dcl] For all modern versions of the OS, yes. On Windows 2000, it depended on your service pack level and an OS setting, and anything earlier than that, it would find CWD very early. So if you're loading a DLL that is always present in the system(32) directory, it is always safe, assuming a relatively recent OS version.Anonymous
August 23, 2010
static wchar_t wzSystem[MAX_PATH]={0}; ? [dcl] The language standard says that all globals and statics get initialized, and failing a constructor, they get initialized to zero. Also, MAX_PATH limit is evil [dcl] Not really. Try installing Windows into a directory that won't fit in MAX_PATH, and my prediction is that it won't go well. You could potentially have issues with the CWD, and if you're worried about it, write the code to fall back and allocate the buffer. While this is fairly close to production quality, it is sample code, and could use more error checking and one might want to make it more robust.Anonymous
August 24, 2010
Is this how you would use SetDllDirectory? typedef BOOL (WINAPI PSDLLD)(__in_opt LPCWSTR); HMODULE SafeLoadLibrary_1(const wchar_t wzFileName) { HMODULE hMod = NULL; static PSDLLD pSetDllDirectory = NULL; if (NULL == pSetDllDirectory) { pSetDllDirectory = (PSDLLD)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "SetDllDirectoryW"); } if (NULL != pSetDllDirectory) { if (pSetDllDirectory(L"")) // remove '.' from search order { hMod = LoadLibrary(wzFileName); pSetDllDirectory(NULL); // restore default search order } } return hMod; } [dcl] Well, you could. Unless you're supporting Windows 2000 or XP gold, you can skip getting the proc address of SetDllDirectory. The problem with this is that it is process-wide and not thread safe. What I'd really recommend instead is just call SetDllDirectory("") as the first thing you do in main() or WinMain(), and just run like that. If you have problems with doing that, and sometimes need the option of using the CWD, then you can't do that, and need to take some other approach, like the example.Anonymous
August 28, 2010
Thanks for the new SafeLoadLibrary function, David! Do you think SetCurrentDirectory(wzSystem) will always succeed, in practice? I can imagine that a limited account user might be prohibited to change the current directory to c:windowssystem32. (While the user should still be allowed to load the DLL, wzFileName.) What do you think?- Anonymous
March 22, 2018
Sorry about not seeing this previously - I doubt that can happen. A user has to have read access, or can't run any system DLLs. BTW, if you do see this reply, SafeInt is still around!
- Anonymous