通过提供DLL名称定位DLL路径

3

如果我执行

LoadLibrary("MyTest.dll")

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

如何模拟相同的功能?我需要通过将MyTest.dll作为参数传递给函数来定位C:\TestFolder\Test\MyTest.dllC:\TestFolder\Test\%PATH%中)。是否有这样的API或函数?

P.S. 我无法执行LoadLibrary,然后GetModuleHandle和查找路径,因为有时该DLL可能是恶意DLL,我不能加载它。因此,我需要在不加载它的情况下找到PATH。


这怎么能防止你加载恶意DLL呢?即使你这样做,最终你还是需要加载DLL,这样你又回到了起点。 - Cody Gray
@user3404070你的DLL是否如此受欢迎,以至于有人会制作恶意版本? - PaulMcKenzie
1
使用 ***GetModuleFileName()***。 - ryyker
@ryyker,GetModuleFileName需要将DLL加载到内存中,而我不想这样做。 - user3404070
3
你的问题是如何编写一个函数,它以文件名作为输入,检查 %PATH% 并查找是否存在某个路径中具有该名称的文件,然后返回该文件名? - mafso
显示剩余2条评论
2个回答

12

要在不运行任何恶意代码的情况下加载DLL,请使用带有DONT_RESOLVE_DLL_REFERENCESLOAD_LIBRARY_AS_DATAFILE标志的LoadLibraryEx

然后您可以使用GetModuleFileName

您还应该阅读有关所有其他标志的信息,这些标志允许您执行Windows可以执行的各种搜索。


5
在这种情况下,确实可以使用该方法。更一般的解决方案是PathFindOnPath - Cody Gray
2
不,你不需要在那里指定任何东西。他只想查看系统路径,如果你将第二个参数传递为“NULL”,它就会这样做。 - Cody Gray
不,我没有读评论。它们通常是噪音。我只读了文档,它确认它会搜索PATH环境变量中的文件夹。听起来这就是他想要的。现在我已经阅读了评论,他们说它不会搜索当前目录。文档没有确认或否认,所以我会假设这是正确的。你是对的,这与“LoadLibrary[Ex]”的行为不同。但那不是我理解问题所询问的。 - Cody Gray
1
区别在于,@user,有用于定位DLL的特殊规则。LoadLibrary函数考虑到了这些规则,因此在这种情况下是最合适的选择。PathFindOnPath函数在系统PATH上的文件夹中搜索(如果您将第二个参数传递为NULL),这是Windows搜索DLL的一个位置,但不是唯一的位置。因此,如果调用PathFindOnPath,则可能会得到与尝试加载它时不同的DLL文件。 - Cody Gray
1
@rua.kr:什么出了问题?你从LoadLibraryEx得到了一个指定标志的错误吗?你从GetModuleFileName得到了一个错误吗?你从GetModuleFileName得到了一个有效路径,但它是DLL的错误副本的路径吗?你尝试了与我的答案不同的方法,而那个不同的方法失败了吗? - Ben Voigt
显示剩余10条评论

0

对于这个问题的接受答案,在所有情况下都不起作用。更具体地说,使用 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_REFERENCESLOAD_LIBRARY_AS_DATAFILE,那么解决方案是什么呢?嗯,解决方案就是不使用GetModuleFileName,而是使用GetMappedFileName
此时,如果你知道GetMappedFileName的作用,可能会感到困惑。通常,GetMappedFileName用于从使用文件映射API创建的文件映射中获取文件名。其实,背后的秘密是图像加载是通过MapViewOfFile完成的。这在Dbghelp文档中略有暗示 - 例如,ImageNtHeader文档中指出图像基址必须是...

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

这意味着模块句柄不仅是指向模块的指针,还是一个映射文件指针。与GetModuleFileName不同的是,GetMappedFileName没有"驯鹿模块游戏"的概念,因此即使在LoadLibraryExLOAD_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;
    }

    // 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函数的路径。

关于这些设备路径是否区分大小写,我找到的唯一来源是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++) {
        // 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 关闭。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接