对于这个问题的接受答案,在所有情况下都不起作用。更具体地说,使用 GetModuleFileName
结合 LOAD_LIBRARY_AS_DATAFILE
只有在库文件在没有此标志的情况下已加载时才起作用。例如,它适用于已经被进程加载的库文件,比如 KERNEL32.DLL,但是当你自己的库文件第一次加载到进程中时,它将无法工作。
这是因为,引用《The Old New Thing》的话来说,通过 LOAD_LIBRARY_AS_DATAFILE(或类似的标志)加载的库文件不能参与任何驯鹿模块游戏。
如果您使用LOAD_LIBRARY_AS_DATAFILE标志加载库,那么它实际上并没有以任何正常的方式加载。事实上,它完全不会被记录在册。如果您使用LOAD_LIBRARY_AS_DATAFILE、LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE或LOAD_LIBRARY_AS_IMAGE_RESOURCE标志(或将来添加的任何类似标志),则该库将映射到进程地址空间,但它不是一个真正的模块。像GetModuleHandle、GetModuleFileName、EnumProcessModules和CreateToolhelp32Snapshot这样的函数将看不到该库,因为它从未被输入到已加载模块的数据库中。
在这种情况下,您可能会考虑使用GetModuleHandle,因为它只能用于先前加载的库。显然这并不理想,并且实际上并没有回答如何在不执行DllMain的情况下获取路径的问题。
那么其他标志DONT_RESOLVE_DLL_REFERENCES呢?技术上说,它是可以工作的。然而,在Microsoft文档中您会注意到以下注释。
不要使用这个值,它仅用于向后兼容。
如果您计划只访问DLL中的数据或资源,请使用LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE或LOAD_LIBRARY_AS_IMAGE_RESOURCE,或者两者都使用。
该标志仅提供向后兼容性,并且这是有充分理由的。引用《The Old New Thing》的话来说,DONT_RESOLVE_DLL_REFERENCES是一个定时炸弹。
通常有人调用GetModuleHandle来查看DLL是否已加载,如果是,则使用GetProcAddress获取过程地址并调用它。如果使用DONT_RESOLVE_DLL_REFERENCES加载了DLL,那么GetModuleHandle将成功,但调用时结果函数将崩溃。执行此操作的代码不知道DLL是使用DONT_RESOLVE_DLL_REFERENCES加载的;它无法保护自己。
其他线程将看到库已加载。如果它们尝试使用已加载的库,这是完全正常的操作,它们将导致程序崩溃,因为实际上它还没有被初始化。因此,虽然这个标志在使用
GetModuleFileName
时确实起作用,但会导致程序不稳定。仍然不理想。
那么,如果我们不能在
GetModuleFileName
中使用
DONT_RESOLVE_DLL_REFERENCES
或
LOAD_LIBRARY_AS_DATAFILE
,那么解决方案是什么呢?嗯,解决方案就是不使用
GetModuleFileName
,而是使用
GetMappedFileName
。
此时,如果你知道
GetMappedFileName
的作用,可能会感到困惑。通常,
GetMappedFileName
用于从使用
文件映射API创建的文件映射中获取文件名。其实,背后的秘密是图像加载是通过
MapViewOfFile
完成的。这在Dbghelp文档中略有暗示 - 例如,
ImageNtHeader文档中指出图像基址必须是...
通过调用MapViewOfFile函数将映射到内存中的图像的基地址。
这意味着模块句柄不仅是指向模块的指针,还是一个映射文件指针。与
GetModuleFileName
不同的是,
GetMappedFileName
没有"驯鹿模块游戏"的概念,因此即使在
LoadLibraryEx
的
LOAD_LIBARY_AS_DATAFILE
标志下也可以正常工作。不仅如此,
GetMappedFileName
还比
GetModuleFileName
有额外的好处。
你可能不知道的是,仅仅使用LoadLibrary加载库并不会独占锁定DLL文件。自己试试吧:编写一个简单的程序,用
LoadLibrary
加载自己的库,然后在程序运行时将DLL文件剪切并粘贴到其他位置。只要没有其他应用程序锁定DLL文件,这将起作用(是的,无论Windows版本如何,这一直都有效)。文件映射API会继续工作,不管DLL文件的新位置在哪里。
然而,当你调用
GetModuleFileName
时,它总是会返回DLL文件的路径,即使是在使用LoadLibrary加载库的时候。这会带来安全问题。可以将DLL文件剪切并粘贴到新位置,并在旧位置放置一个不同的DLL文件。如果使用
GetModuleFileName
返回的路径再次加载库,实际上可能会加载一个完全不同的DLL文件。因此,
GetModuleFileName
只适用于显示名称或获取传递给
LoadLibrary
的DLL文件名,并且不能依赖于当前文件路径。
GetMappedFileName
没有这个问题,因为它没有关于何时调用
LoadLibrary
的概念。它返回的是最新的文件路径,即使在加载过程中文件已被移动。
不过,有一个小缺点:
GetMappedFileName
返回的是设备路径,格式为
\Device\HarddiskVolume1\Example.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;
}
const SIZE_T DEVICE_NAME_SIZE = 3;
CHAR deviceName[DEVICE_NAME_SIZE] = "A:";
size_t targetPathLength = 0;
CHAR targetPath[MAX_PATH + 1] = "";
DWORD logicalDrives = GetLogicalDrives();
do {
if (logicalDrives & 1) {
if (!QueryDosDevice(deviceName, targetPath, MAX_PATH - 1)) {
return false;
}
targetPathLength = strnlen_s(targetPath, MAX_PATH);
targetPath[targetPathLength++] = '\\';
if (!_strnicmp(targetPath, mappedFileName, targetPathLength)) {
break;
}
}
deviceName[0]++;
} while (logicalDrives >>= 1);
if (!logicalDrives) {
return false;
}
filePathName = std::string(deviceName) + "\\" + (mappedFileName + targetPathLength);
return true;
}
GetLogicalDrives
只是获取可用驱动器的列表(如C:,D:等),以位掩码的形式返回(其中第一个位对应A:,第二个位对应B:等)。然后我们循环遍历可用驱动器,获取它们的路径,并将其与映射文件名进行比较。该函数的结果是一个可以传递给CreateFile
函数的路径。
关于这些设备路径是否区分大小写,我找到的唯一来源是this book,它声称在Windows XP之前它们是区分大小写的,但在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++) {
if (!getFilePathNameFromMappedView(currentProcess, moduleHandle, filePathName)) {
return false;
}
{
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;
}
};
if (!getFilePathNameFromMappedView(currentProcess, moduleHandle, filePathName2)) {
return false;
}
if (stringsCaseInsensitiveEqual(filePathName.c_str(), filePathName2.c_str())) {
fileCloseHandleScopeExit.dismiss();
return result;
}
}
if (!result) {
return result;
}
}
return false;
}
然后我们可以这样称呼它...
HMODULE exampleModuleHandle = LoadLibraryEx("Example.DLL", NULL, LOAD_LIBRARY_AS_DATAFILE);
if (!exampleModuleHandle) {
return false;
}
HANDLE exampleFile = NULL;
if (!getHandleFromModuleHandle(exampleModuleHandle, exampleFile)) {
return false;
}
这只是我想到的一些事情,如果有问题,请在回复中告诉我。
一旦您获得文件句柄,可以将其传递给 GetFileInformationByHandle
来确认它与另一个进程中加载的库相同,并随后使用 CloseHandle
关闭。
%PATH%
并查找是否存在某个路径中具有该名称的文件,然后返回该文件名? - mafso