Reputation:
If I do
LoadLibrary("MyTest.dll")
Windows will locate and load it from "C:\TestFolder\Test\MyTest.dll"
, because "C:\TestFolder\Test\"
is in %PATH%
folder.
How can I emulate same function? I need to locate C:\TestFolder\Test\MyTest.dll
(C:\TestFolder\Test\
is in %PATH%
) by passing MyTest.dll
as an argument to a function. Is there such an API? or a function?
P.S. I can't do LoadLibrary and then GetModuleHandle and finding Path, sometimes this DLL could be malicious DLL and I can't load it. So I need to find PATH without having to load it.
Upvotes: 3
Views: 874
Reputation: 932
The accepted answer to this question will not work in all scenarios. More specifically, using GetModuleFileName
together with LOAD_LIBRARY_AS_DATAFILE
will only work if the library was already loaded prior without this flag. For example, it will work for a library like KERNEL32.DLL which is already loaded by the process, but it will not work with your own library being loaded into the process for the first time.
This is because, to quote The Old New Thing, a library loaded via LOAD_LIBRARY_AS_DATAFILE (or similar flags) doesn't get to play in any reindeer module games.
If you load a library with the LOAD_LIBRARY_AS_DATAFILE flag, then it isn’t really loaded in any normal sense. In fact, it’s kept completely off the books. If you load a library with the LOAD_LIBRARY_AS_DATAFILE, LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE, or LOAD_LIBRARY_AS_IMAGE_RESOURCE flag (or any similar flag added in the future), then the library gets mapped into the process address space, but it is not a true module. Functions like GetModuleHandle, GetModuleFileName, EnumProcessModules and CreateToolhelp32Snapshot will not see the library, because it was never entered into the database of loaded modules.
At that point, you might as well just use GetModuleHandle
, since it'll only work with previously loaded libraries. Obviously not ideal, and doesn't actually answer the question of getting the path without executing DllMain.
What about the other flag, DONT_RESOLVE_DLL_REFERENCES
? Well, technically yes it will work. However, you'll notice in the Microsoft documentation the following note.
Do not use this value; it is provided only for backward compatibility. If you are planning to access only data or resources in the DLL, use LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE or LOAD_LIBRARY_AS_IMAGE_RESOURCE or both.
The flag is only provided for backwards compatibility, and that is for good reason. To quote The Old New Thing a second time, DONT_RESOLVE_DLL_REFERENCES is a time bomb.
It is common for somebody to call GetModuleHandle to see if a DLL is loaded, and if so, use GetProcAddress to get a procedure address and call it. If the DLL had been loaded with DONT_RESOLVE_DLL_REFERENCES, both the GetModuleHandle will succeed, but the resulting function will crash when called. The code doing this has no idea that the DLL was loaded with DONT_RESOLVE_DLL_REFERENCES; it has no way of protecting itself.
Other threads will see the library is loaded. If they attempt to use the loaded library, as would be entirely normal to do, they will crash the program because it has not actually been initialized. So this flag, while it does work with GetModuleFileName
, will cause instability in the program. Still not ideal.
So then, if we can't use DONT_RESOLVE_DLL_REFERENCES
or LOAD_LIBRARY_AS_DATAFILE
with GetModuleFileName
, then what is the solution? Well, the solution is to not use GetModuleFileName
- and instead use GetMappedFileName
.
At this point, if you know what GetMappedFileName
does, you may be confused. Normally, GetMappedFileName
is used to get a filename from a File Mapping, created with the File Mapping API. Well, the secret is that under the hood, image loading is accomplished with MapViewOfFile
. This is subtly hinted at by the Dbghelp documentation - for example, the ImageNtHeader documentation which states an image base must be...
The base address of an image that is mapped into memory by a call to the MapViewOfFile function.
This means that not only is a module handle a pointer to a module, but also a mapped file pointer. Unlike GetModuleFileName
however, GetMappedFileName
has no concept of "the reindeer module games," so it works even with the LOAD_LIBARY_AS_DATAFILE
flag of LoadLibraryEx
. Not only that, but GetMappedFileName
has an additional benefit over GetModuleFileName
.
Something you might not know is that simply loading a library with LoadLibrary does not exclusively lock the DLL file. Try it yourself: write a simple program which loads your own library with LoadLibrary
, and then while the program is running, cut and paste the DLL file to a different location. This will work (and yes, has always worked regardless of Windows version) so long as no other application has a lock on the DLL file. The File Mapping API just keeps on chugging, regardless of the DLL file's new location.
However, when you call GetModuleFileName
, it will always return the path of the DLL file as of whenever the library was loaded with LoadLibrary. This has security ramifications. It would be possible to cut and paste the DLL file to a new location, and put a different one at the old location. If the path returned by GetModuleFileName
is used to load the library again, it could actually result in loading a different DLL file altogether. As such, GetModuleFileName
is only useful for the purpose of displaying the name or getting the DLL file name passed to LoadLibrary
, and can't be depended upon for the current file path.
GetMappedFileName
has no such issue, because it has no concept of when LoadLibrary
was called. It returns an up to date path to the file, even if it has been moved while it's loaded.
There is one minor downside though: GetMappedFileName
returns a device path, in the format of \Device\HarddiskVolume1\Example.DLL
. The simplest way to resolve this is by prepending the path with the prefix "\\?\GLOBALROOT" so the file can be opened directly by CreateFile
. As far as I can tell, this has always worked since Windows NT.. Here is a simple implementation of getFilePathNameFromMappedView
.
bool getFilePathNameFromMappedView(HANDLE process, LPVOID mappedView, std::string &filePathName) {
if (!process) {
return false;
}
if (!mappedView) {
return false;
}
CHAR mappedFileName[MAX_PATH] = "";
if (!GetMappedFileName(process, mappedView, mappedFileName, MAX_PATH - 1)) {
return false;
}
filePathName = "\\\\?\\GLOBALROOT" + std::string(mappedFileName);
return true;
}
Another alternative is we can use QueryDosDevice
to turn the device path into a drive path. This method is slightly more complex but has the benefit of producing a more user friendly looking path. Here is the alternative implementation of getFilePathNameFromMappedView
.
bool getFilePathNameFromMappedView(HANDLE process, LPVOID mappedView, std::string &filePathName) {
if (!process) {
return false;
}
if (!mappedView) {
return false;
}
CHAR mappedFileName[MAX_PATH] = "";
if (!GetMappedFileName(process, mappedView, mappedFileName, MAX_PATH - 1)) {
return false;
}
// the mapped file name is a device path, we need a drive path
// https://learn.microsoft.com/en-us/windows/win32/fileio/defining-an-ms-dos-device-name
const SIZE_T DEVICE_NAME_SIZE = 3;
CHAR deviceName[DEVICE_NAME_SIZE] = "A:";
// the additional character is for the trailing slash we add
size_t targetPathLength = 0;
CHAR targetPath[MAX_PATH + 1] = "";
// find the MS-DOS Device Name
DWORD logicalDrives = GetLogicalDrives();
do {
if (logicalDrives & 1) {
if (!QueryDosDevice(deviceName, targetPath, MAX_PATH - 1)) {
return false;
}
// add a trailing slash
targetPathLength = strnlen_s(targetPath, MAX_PATH);
targetPath[targetPathLength++] = '\\';
// compare the Target Path to the Device Object Name in the Mapped File Name
// case insensitive
// https://flylib.com/books/en/4.168.1.23/1/
if (!_strnicmp(targetPath, mappedFileName, targetPathLength)) {
break;
}
}
deviceName[0]++;
} while (logicalDrives >>= 1);
if (!logicalDrives) {
return false;
}
// get the drive path
filePathName = std::string(deviceName) + "\\" + (mappedFileName + targetPathLength);
return true;
}
GetLogicalDrives
just gets a list of which drives are available (like C:, D:, etc.) in the form of a bitmask (where the first bit corresponds to A:, the second bit corresponds to B:, etc.) We then loop through the available drives, getting their paths, and comparing them against the one in the Mapped File Name. The result of this function is a path that could be passed to the CreateFile
function.
The only source I could find for whether these device paths are case-insensitive or not was this book claiming that they used to be case-sensitive, but are case-insensitive as of Windows XP. I'm going to assume you are not targeting Windows 9x anymore, so I just compare them case-insensitively.
Hold on a second though: this still may not be enough. If your intention is, as mine was, to try and get a file handle to the DLL file, but using the DLL search path, then just getting the path and passing it to CreateFile
opens us up to a filesystem race condition, like the kind explained in this LiveOverflow video. A technique like this could be abused by a hacker so the handle doesn't actually point to the file we want. There isn't any GetMappedFileHandle
function, so what can we do?
I thought about this for a while, and here is the workaround I came up with. The idea is that we call our own getFilePathNameFromMappedView
function once just to get the path to pass to CreateFile
, and exclusively lock the file in place with the FILE_SHARE_READ
flag. However, we then confirm, with a second call to getFilePathNameFromMappedView
, that the file is still actually there. If the paths match, knowing that the file at that path is now locked, we can know for sure the handle we got is to the library that was actually loaded. If the file was moved before the call to CreateFile
finished, however, the paths will not match, because GetMappedFileName
returns the up to date path to the file. At that point, we can try again. I'm using scope_guard in order to ensure the handle is closed upon failure.
inline bool stringEqualsCaseInsensitive(const char* str, const char* str2) {
return !_stricmp(str, str2);
}
inline bool closeHandle(HANDLE &handle) {
if (handle && handle != INVALID_HANDLE_VALUE) {
if (!CloseHandle(handle)) {
return false;
}
}
handle = NULL;
return true;
}
bool getHandleFromModuleHandle(HMODULE moduleHandle, HANDLE &file) {
if (!moduleHandle) {
return false;
}
bool result = true;
HANDLE currentProcess = GetCurrentProcess();
std::string filePathName = "";
std::string filePathName2 = "";
const int MAX_ATTEMPTS = 10;
for (int i = 0; i < MAX_ATTEMPTS; i++) {
// pass the Module Handle as a Mapped View
// to get its current path
if (!getFilePathNameFromMappedView(currentProcess, moduleHandle, filePathName)) {
return false;
}
{
// prevent the Example File from being written to, moved, renamed, or deleted
// by acquiring it and effectively locking it from other processes
file = CreateFile(filePathName.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (!file || file == INVALID_HANDLE_VALUE) {
return false;
}
MAKE_SCOPE_EXIT(fileCloseHandleScopeExit) {
if (!closeHandle(file)) {
result = false;
}
};
// we now know this path is now protected against race conditions
// but the path may have changed before we acquired it
// so ensure the File Path Name is the same as before
// so that we know the path we protected is for the Mapped View
if (!getFilePathNameFromMappedView(currentProcess, moduleHandle, filePathName2)) {
return false;
}
if (stringEqualsCaseInsensitive(filePathName.c_str(), filePathName2.c_str())) {
fileCloseHandleScopeExit.dismiss();
return result;
}
}
// if an error occured, return
if (!result) {
return result;
}
}
return false;
}
Then we can call it like this...
HMODULE exampleModuleHandle = LoadLibraryEx("Example.DLL", NULL, LOAD_LIBRARY_AS_DATAFILE);
if (!exampleModuleHandle) {
return false;
}
// we want this to be a handle to the Example File
HANDLE exampleFile = NULL;
if (!getHandleFromModuleHandle(exampleModuleHandle, exampleFile)) {
return false;
}
This is just something I thought of, so let me know in the responses if there are issues with it.
Once you have a handle to the file, it can then be passed to GetFileInformationByHandle
to confirm it is the same library as is loaded in another process, and subsequently closed with CloseHandle
.
Upvotes: 0
Reputation: 283634
To load the DLL without running any malicious code inside, use LoadLibraryEx
with the DONT_RESOLVE_DLL_REFERENCES
and LOAD_LIBRARY_AS_DATAFILE
flags.
Then you can use GetModuleFileName
.
You should also read about all the other flags, which allow you to perform all the various searches Windows is capable of.
Upvotes: 12