通过给出DLLName来定位DLL路径

Locate DLL path by giving a DLLName

本文关键字:定位 DLL 路径 DLLName      更新时间:2023-10-16

如果我做了

LoadLibrary("MyTest.dll")

Windows将从"C:TestFolderTestMyTest.dll"找到并加载它,因为"C:TestFolderTest"%PATH%文件夹中。

如何模拟相同的函数?我需要通过传递MyTest.dll作为函数的参数来定位C:TestFolderTestMyTest.dll(C:TestFolderTest%PATH%中)。有这样的API吗?还是一个函数?

注:我不能做LoadLibrary,然后GetModuleHandle和查找路径,有时这个DLL可能是恶意的DLL,我不能加载它。所以我需要找到PATH而不需要加载它

要加载DLL而不运行任何恶意代码,请使用LoadLibraryExDONT_RESOLVE_DLL_REFERENCESLOAD_LIBRARY_AS_DATAFILE标志。

那么你可以使用GetModuleFileName

您还应该阅读所有其他标志,它们允许您执行Windows所能执行的所有各种搜索。

这个问题的公认答案并不适用于所有情况。更具体地说,使用GetModuleFileNameLOAD_LIBRARY_AS_DATAFILE只会在库已经加载之前没有这个标志的情况下工作。例如,它可以工作在一个像KERNEL32.DLL这样的库中,这个库已经被进程加载了,但是它不能工作在你自己的库第一次被加载到进程中。

这是因为,引用The Old New Thing,通过LOAD_LIBRARY_AS_DATAFILE(或类似标志)加载的库不会在任何驯鹿模块游戏中发挥作用。

如果您加载带有LOAD_LIBRARY_AS_DATA-FILE标志的库,则它没有正常意义上的装载。事实上,它保存完好不入账。加载一个库LOAD_LIBRARY_AS_DATA-FILE、LOAD_LIBRARY_AS_DATA-FILE_EXCLUSIVE或LOAD_LIBRARY_AS_IMAGE_RESOURCE标志(或在(未来),然后库被映射到进程地址空间,但它不是一个真正的模块。像Get-Module-Handle,Get-Module-File-Name, Enum-Process-Modules和Create-Toolhelp32-Snapshot将看不到库,因为它是从未进入加载模块的数据库。

此时,您可能只使用GetModuleHandle,因为它只适用于先前加载的库。显然不是理想的,并且实际上没有回答在不执行DllMain的情况下获取路径的问题。

另一个标志DONT_RESOLVE_DLL_REFERENCES呢?嗯,从技术上讲,它是可行的。然而,你会在微软文档中注意到以下注释:

不要使用这个值;它只提供向后兼容性。如果您计划仅访问DLL中的数据或资源,请使用LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE或LOAD_LIBRARY_AS_IMAGE_RESOURCE或者两者兼有。

这个标志只是为了向后兼容而提供的,这是有充分理由的。DONT_RESOLVE_DLL_REFERENCES是一个定时炸弹。

有人调用GetModuleHandle来查看DLL是否存在是很常见的如果是,则使用GetProcAddress获取过程地址和调用它。如果DLL是用DONT_RESOLVE_DLL_REFERENCES加载的,GetModuleHandle都将成功,但是结果函数将成功调用时崩溃。执行此操作的代码不知道DLL是什么加载了DONT_RESOLVE_DLL_REFERENCES;它没有办法保护本身。

其他线程将看到库已加载。如果它们试图使用加载的库(这是完全正常的),它们将使程序崩溃,因为它实际上还没有初始化。所以这个标志,虽然它与GetModuleFileName一起工作,但会导致程序不稳定。还是不理想

那么,如果我们不能将DONT_RESOLVE_DLL_REFERENCESLOAD_LIBRARY_AS_DATAFILEGetModuleFileName一起使用,那么解决方案是什么?那么,解决方案是不使用GetModuleFileName,而是使用GetMappedFileName

在这一点上,如果您知道GetMappedFileName的作用,您可能会感到困惑。通常,GetMappedFileName用于从使用文件映射API创建的文件映射中获取文件名。好吧,秘密是在引擎盖下,图像加载是由MapViewOfFile完成的。Dbghelp文档巧妙地暗示了这一点——例如,ImageNtHeader文档规定图像库必须是…

通过调用映射到内存中的映像的基址MapViewOfFile函数

这意味着模块句柄不仅是指向模块的指针,也是映射文件指针。然而,与GetModuleFileName不同的是,GetMappedFileName没有"驯鹿模块游戏"的概念。所以它甚至可以与LoadLibraryExLOAD_LIBARY_AS_DATAFILE标志一起工作。不仅如此,GetMappedFileNameGetModuleFileName有更多的好处。

你可能不知道的是,简单地用LoadLibrary加载一个库并不会独占地锁定DLL文件。你自己试试:写一个简单的程序,用LoadLibrary加载你自己的库,然后在程序运行时,将DLL文件剪切并粘贴到不同的位置。只要没有其他应用程序对DLL文件有锁,这就可以工作(是的,无论Windows版本如何都一直工作)。无论DLL文件的新位置如何,文件映射API都会继续运行。

然而,当你调用GetModuleFileName时,它将始终返回DLL文件的路径,无论何时使用LoadLibrary加载库。这有安全隐患。可以将DLL文件剪切并粘贴到新位置,并将另一个DLL文件放在旧位置。如果使用GetModuleFileName返回的路径再次加载库,实际上可能导致加载完全不同的DLL文件。因此,GetModuleFileName仅用于显示名称或获取传递给LoadLibrary的DLL文件名,而不能依赖于当前文件路径。

GetMappedFileName没有这样的问题,因为它没有LoadLibrary何时被调用的概念。它返回文件的最新路径,即使它在加载时已被移动。

有一个小缺点:GetMappedFileName返回一个设备路径,格式为DeviceHarddiskVolume1Example.DLL。谢天谢地,这是一个可以解决的问题。我们可以使用QueryDosDevice将设备路径转换为驱动器路径。

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只是以位掩码的形式获得可用驱动器(如C:, D:等)的列表(其中第一个位对应于a:,第二个位对应于B:等),然后我们循环遍历可用驱动器,获得它们的路径,并将它们与映射文件名中的路径进行比较。这个函数的结果是一个可以传递给CreateFile函数的路径。

关于这些设备路径是否不区分大小写,我能找到的唯一来源是这本书,它声称它们曾经是区分大小写的,但在Windows XP中是不区分大小写的。我假设你的目标不再是Windows 9x,所以我只是比较它们不区分大小写。

编辑:在写完这个之后,我才意识到有一种更好的方法来写这个函数。只需在路径前面加上"\?GLOBALROOT"该文件可以直接由CreateFile打开,而不需要经过QueryDosDevice。据我所知,自Windows NT以来,这一直有效,所以这样做没有缺点,但我保留了最初的实现。下面是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;
}

稍等:这可能还不够。如果你的意图是,就像我一样,试图获得一个文件句柄到DLL文件,但是使用DLL搜索路径,那么只需获得路径并将其传递给CreateFile,就可以打开一个文件系统竞争条件,就像这个LiveOverflow视频中解释的那样。这样的技术可能会被黑客滥用,因此句柄实际上并没有指向我们想要的文件。没有任何GetMappedFileHandle函数,我们该怎么办?

我想了一会儿,这是我想出的变通办法。我们的想法是,我们调用自己的getFilePathNameFromMappedView函数一次,只是为了获得传递给CreateFile的路径,并使用FILE_SHARE_READ标志专门锁定文件。然而,我们随后通过对getFilePathNameFromMappedView的第二次调用来确认文件实际上仍然在那里。如果路径匹配,知道该路径上的文件现在被锁定,我们就可以确定得到的句柄指向实际加载的库。但是,如果在对CreateFile的调用完成之前移动了文件,则路径将不匹配,因为GetMappedFileName返回文件的最新路径。到那时,我们可以再试一次。我使用scope_guard是为了确保句柄在失败时关闭。

inline bool stringsCaseInsensitiveEqual(const char* leftHandSide, const char* rightHandSide) {
return !_stricmp(leftHandSide, rightHandSide);
}
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 (stringsCaseInsensitiveEqual(filePathName.c_str(), filePathName2.c_str())) {
fileCloseHandleScopeExit.dismiss();
return result;
}
}
// if an error occured, return
if (!result) {
return result;
}
}
return false;
}

那么我们可以这样称呼它

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;
}

这只是我想到的一些东西,所以如果有问题,请在回复中告诉我。

一旦你有了一个文件的句柄,它就可以被传递给GetFileInformationByHandle,以确认它与另一个进程中加载的库是相同的,然后用CloseHandle关闭。