windows 如何隔离依赖关系,避免传递依赖关系解析?

hs1rzwqc  于 2023-08-07  发布在  Windows
关注(0)|答案(2)|浏览(146)

bounty已结束.回答此问题可获得+500声望奖励。赏金宽限期三小时后结束。sorush-r希望引起更多关注此问题。

我正在开发一个应用程序,它提供了一个插件接口,让客户在应用程序中开发他们的逻辑。然后在运行时动态加载插件。我们为插件提供了一个干净的C接口,以使事情尽可能地可移植。然而,我们最近发现了一个过渡依赖关系的问题:当一个插件链接到它自己的依赖项时,它恰好也是应用的依赖项,只加载应用附带的版本。
因此在下面的配置中,lib_b.dll是插件,它使用lib_a.dll作为私有依赖项。虽然因为Executable也链接到同一个库的不同版本,但它们的版本没有被选择。

+----------------------+              +-------------------------------+
    |                      | LoadLibrary  |                               |
    | Executable.exe ------+--------------+--> plugins                    |
    |  |                   |              |     |                         |
    |  +--> lib_a.dll (v1) |              |     +--> lib_b.dll            |
    |                      |              |           |                   |
    +----------------------+              |           +--> lib_a.dll (v2) |
                                          |                               |
                                          +-------------------------------+

字符串
我正在寻找一个解决方案,以隔离地址空间和符号的依赖关系,从我的应用程序。这个想法是让Executable只关心它在运行时从插件加载的符号,而不是插件内部使用的符号。
我们这样做load_library:

HMODULE h = ::LoadLibraryExA(".../plugins/library_b.dll", 
    NULL, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR);


在插件中唯一有趣的一点是这样达到的:

callback_type do_the_thing_in_b = GetProcAddress(h, "do_the_thing_in_b");
int answer = do_the_thing_in_b(3.14, 42);

更新

应用程序可以在任何时候重新编译或更改,但在编译时我们没有插件的信息。这个想法是,客户端可以创建自己的插件,并把它放在那里。
我们也不能修改插件。我们可以扫描该目录并做一些事情,但这是我们可以做的更改的范围。我们不能重新编译它们或决定它们的依赖结构。
插件通过接口库链接到我们的可执行文件直接从可执行文件调用函数**

agxfikkp

agxfikkp1#

总之,

  • 您有一个应用程序,它有一个为插件定义的接口
  • 您定义并控制该接口
  • 插件依赖于能够调用应用程序内部的某些函数
  • 插件和应用程序可能会有冲突的视图,哪一个是依赖项的正确版本,应用程序的视图获胜
  • 插件不调用其他插件中的函数

我认为你已经击中了“隔离地址空间”的要害,就像DaniilVlasenko在评论中建议的那样。这也是FireFox现在用标签所做的;每一个都是单独的过程。
如果您能够控制接口,那么您就能够将主可执行文件分离到一个进程中,并拥有一个可以加载单个插件的DLL(及其依赖项)的薄垫片进程。您将为每个插件启动其中一个垫片。进程将通过某种机器内RPC进行通信。这一切都是假设数据结构在可执行文件和插件之间不共享,而不是通过函数调用和返回。
那会是什么样子呢?
RPC(例如GPB-RPC)感觉不太对,因为您需要一个RPC以一种方式让可执行文件调用插件,但是需要允许插件反过来调用可执行文件中的函数。
我将使用类似ZeroMQ的东西作为主可执行文件和插件之间的传输,并使用类似GPB的东西来制定消息,1)指示要调用的函数,以及2)它的参数是什么。序列将如下所示:
1.每个插件有2个ZMQREQ/REP套接字对,一对从可执行程序到插件shim进程(调用前向),另一对以相反的方向运行(回调)。每个进程都以REQ和REP套接字结束。
1.在主可执行文件中制定一条消息,该消息将被发送到插件shim,以调用提供的函数
1.通过调用前转REQ套接字从主可执行文件发送该消息,然后继续轮询调用前转REQ套接字和回调REP套接字。这允许主可执行文件或者接收返回值,或者接收执行它自己的函数之一的请求

  1. shim进程在其调用转发REP套接字上接收它,对其进行解码,并调用插件DLL中的相关函数
    1.插件函数可能希望调用主可执行文件中的函数。您必须在shim进程中实现所有此类函数的存根版本。瘦垫片中的存根函数制定包含调用参数的消息,并通过其自己的回调REQ套接字将其发送回主可执行文件,然后在回调REQ套接字上的zmq_recv()上阻塞。
    1.主可执行文件同时轮询其调用转发REQ和回调REP套接字,ZMQ告诉它回调REP套接字上有一条消息。它读取这个消息,执行指定的函数,收集结果。
    1.结果通过回调REP套接字发送回shim进程。主可执行文件返回轮询两个套接字。
    1.在zmq_recv上阻塞的shim进程接收结果消息,并将结果返回给DLL插件中的调用函数。
    1.插件函数本身最终完成,并将结果返回给shim进程
    1.这个最终结果被打包成一条消息,通过shim中的调用转发REP套接字发送回主可执行文件
    1.这次主可执行程序被告知在其调用转发REQ套接字上有一条消息就绪-插件的回复。
    1.读取该消息,并将数据返回到主可执行文件中想要调用插件的任何内容。
    这允许的是
  • 在插件中调用函数的主可执行文件
  • 调用主可执行文件提供的函数任意次数(从到很多次)
  • 以获取要返回到主可执行文件的插件函数结果
  • 使插件DLL和主可执行文件存在于不同的进程中,并加载它们自己的首选依赖项。

ZMQ将是有用的,因为它听起来像是在主可执行文件和插件之间没有直接的客户机/服务器或RPC关系;它们更加相互依赖。ZMQ是Actor Model,它支持上述类型的模式。
ZeroMQ的一个免费赠品是,掌握了这一点,插件可以很容易地完全或组合在另一台机器上(即一些是本地的,一些是远程的,一些是在世界另一端的Linux上运行的,等等)。
显然,如果存在共享的数据结构,那么拥有两个独立的进程是没有帮助的,尽管我认为这可以通过将它们放在共享内存中来克服。但是,所有插件shim进程都必须与主可执行文件位于同一台机器上。

如果事情是多线程的,我不认为上面概述的模式真的改变了很多。您可能希望消息中的字段指示正在发生的事情。如果插件要操作由主可执行文件创建的信号量、互斥量等,则可能会有点困难。

8oomwypt

8oomwypt2#

在加载DLL期间,Windows总是调用函数RtlDosApplyFileIsolationRedirection_Ustr。它可以导出,所以很容易就可以上钩。使用此API,我们可以重定向(替换)戴尔名称。
所以首先尝试挂接这个API:

#ifdef _X86_

#pragma warning(disable: 4483) // Allow use of __identifier
#define __imp_RtlDosApplyFileIsolationRedirection_Ustr __identifier("_imp__RtlDosApplyFileIsolationRedirection_Ustr@36")
#endif

EXTERN_C extern PVOID __imp_RtlDosApplyFileIsolationRedirection_Ustr;

NTSTATUS
NTAPI
hook_RtlDosApplyFileIsolationRedirection_Ustr(_In_ ULONG Flags,
                                              _In_ PUNICODE_STRING OriginalName,
                                              _In_ PUNICODE_STRING Extension,
                                              _Out_opt_ PUNICODE_STRING StaticString,
                                              _Out_opt_ PUNICODE_STRING DynamicString,
                                              _Out_opt_ PUNICODE_STRING *NewName,
                                              _Out_opt_ PULONG NewFlags,
                                              _Out_opt_ PSIZE_T FilePathLength,
                                              _Out_opt_ PSIZE_T MaxPathSize);

ULONG dwError = DetourTransactionBegin();
if (NOERROR == dwError)
{
    //++ optional
    DetourThread* pti = 0;
    SuspendThreads(&pti);
    //--optional

    dwError = DetourAttach(&__imp_RtlDosApplyFileIsolationRedirection_Ustr,
         hook_RtlDosApplyFileIsolationRedirection_Ustr);

    dwError = NOERROR != dwError ? DetourTransactionAbort() : DetourTransactionCommit();

    //++optional
    Free(pti);
    //--optional
}

字符串
SuspendThreadsoptional)实现可以如下:

struct DetourThread
{
    DetourThread *      pNext;
    HANDLE              hThread;
};

void Free(_In_ DetourThread* next)
{
    if (DetourThread* pti = next)
    {
        do 
        {
            next = pti->pNext;

            NtClose(pti->hThread);

            delete pti;

        } while (pti = next);
    }
}

NTSTATUS SuspendThreads(_Out_ DetourThread** ppti)
{
    DetourThread* pti = 0;
    HANDLE ThreadHandle = 0, hThread;
    NTSTATUS status;
    BOOL bClose = FALSE;

    HANDLE UniqueThread = (HANDLE)GetCurrentThreadId();

loop:
    status = NtGetNextThread(NtCurrentProcess(), ThreadHandle, 
        THREAD_QUERY_LIMITED_INFORMATION|THREAD_SUSPEND_RESUME|THREAD_GET_CONTEXT|THREAD_SET_CONTEXT, 
        0, 0, &hThread);

    if (bClose)
    {
        NtClose(ThreadHandle);
        bClose = FALSE;
    }

    if (0 <= status)
    {
        ThreadHandle = hThread;

        THREAD_BASIC_INFORMATION tbi;

        if (0 <= (status = NtQueryInformationThread(hThread, ThreadBasicInformation, &tbi, sizeof(tbi), 0)))
        {
            if (tbi.ClientId.UniqueThread == UniqueThread)
            {
                bClose = TRUE;
                goto loop;
            }

            if (NOERROR == (status = DetourUpdateThread(hThread)))
            {
                status = STATUS_NO_MEMORY;

                if (DetourThread* next = new DetourThread)
                {
                    next->hThread = hThread;
                    next->pNext = pti;
                    pti = next;
                    goto loop;
                }

                ResumeThread(hThread);
            }
        }

        if (status == STATUS_THREAD_IS_TERMINATING)
        {
            bClose = TRUE;
            goto loop;
        }

        NtClose(hThread);
    }

    switch (status)
    {
    case STATUS_NO_MORE_ENTRIES:
    case STATUS_SUCCESS:
        *ppti = pti;
        return STATUS_SUCCESS;
    }

    Free(pti);

    *ppti = 0;
    return status;
}


DetourUpdateThread挂起并保存DetourThread列表中的线程句柄。并在DetourTransactionAbortDetourTransactionCommit中恢复它。但它不关闭保存的hThread。作为结果需要由self维护线程的附加列表,用于关闭它处理..(免费)
好的。让我们钩住RtlDosApplyFileIsolationRedirection_Ustr。现在我们需要实现hook_RtlDosApplyFileIsolationRedirection_Ustr
让我们有下一个API:

// set path to some plugin (A) folder. 
// pszPluginPath - relative path. like plugin/A/

BOOL SetPluginPath(_In_ PCWSTR pszPluginPath);

// return full path to current plugin (A) folder - some like */plugin/A/

PCWSTR AcquirePluginPath();

void ReleasePluginPath();

// called once on start
BOOL InitPluginPath();

// called once on exit
void FreePluginPath();


在下一个代码中包含加载插件:

if (SetPluginPath(L"plugins/A/"))
    {
        LoadLibraryW(L"some-plugin.dll");
        RemovePluginPath();
    }


假设./plugin/是应用程序文件夹内的文件夹(exe位于其中)并且它包含每个插件的子文件夹(AB,..)

/plugin
   /A
   /B


因此,对于这段代码,我们尝试加载./plugin/A/some-plugin.dll
如果some-plugin.dll有来自lib-Y.dll的静态依赖(或者在dll入口点调用LoadLibrary-这真的没问题)并且存在./plugin/A/lob-Y.dll文件-我们尝试准确地加载./plugin/A/lob-Y.dll。即使./lib-Y.dll或/和./plugin/B/lib-Y.dll已经在进程中加载。

NTSTATUS
NTAPI
hook_RtlDosApplyFileIsolationRedirection_Ustr(_In_ ULONG Flags,
                                              _In_ PUNICODE_STRING OriginalName,
                                              _In_ PUNICODE_STRING Extension,
                                              _Out_opt_ PUNICODE_STRING StaticString,
                                              _Out_opt_ PUNICODE_STRING DynamicString,
                                              _Out_opt_ PUNICODE_STRING *NewName,
                                              _Out_opt_ PULONG NewFlags,
                                              _Out_opt_ PSIZE_T FilePathLength,
                                              _Out_opt_ PSIZE_T MaxPathSize)
{
    if (DynamicString)
    {
        BOOLEAN fOk = FALSE;
        WCHAR lpLibFileName[MAX_PATH], *lpFilePart = 0;

        if (PCWSTR pszPluginPath = AcquirePluginPath())
        {
            if (!wcscpy_s(lpLibFileName, _countof(lpLibFileName), pszPluginPath))
            {
                size_t s = wcslen(lpLibFileName);

                lpFilePart = lpLibFileName + s;

                int len = swprintf_s(lpFilePart, _countof(lpLibFileName) - s, L"%wZ", OriginalName);

                if (0 < len)
                {
                    static const UNICODE_STRING dot = RTL_CONSTANT_STRING(L".");
                    USHORT u;
                    if (0 > RtlFindCharInUnicodeString(0, OriginalName, &dot, &u))
                    {
                        swprintf_s(lpFilePart + len, _countof(lpLibFileName) - s - len, L"%wZ", Extension);
                    }       

                    fOk = RtlDoesFileExists_U(lpLibFileName);
                }
            }
        }

        ReleasePluginPath();

        if (fOk)
        {
            if (RtlCreateUnicodeString(DynamicString, lpLibFileName))
            {
                if (NewName)
                {
                    *NewName = DynamicString;
                }

                if (NewFlags)
                {
                    *NewFlags = 0;
                }

                if (FilePathLength)
                {
                    *FilePathLength = lpFilePart - lpLibFileName;
                }

                if (MaxPathSize)
                {
                    *MaxPathSize = _countof(lpLibFileName);
                }

                return STATUS_SUCCESS;
            }

            return STATUS_NO_MEMORY;
        }
    }

    return RtlDosApplyFileIsolationRedirection_Ustr(Flags,
        OriginalName,
        Extension,
        StaticString,
        DynamicString,
        NewName,
        NewFlags,
        FilePathLength,
        MaxPathSize);
}


以及最后-插件路径API的实现:

SRWLOCK g_lock = RTL_SRWLOCK_INIT;
ULONG g_cchMaxPlugin = 0;
PWSTR g_pszPluginPath = 0, g_pszPluginRelativePath = 0, g_pszPluginName = 0;

void FreePluginPath()
{
    if (g_pszPluginPath)
    {
        if (g_pszPluginName)
        {
            __debugbreak();
        }

        delete [] g_pszPluginPath;
    }
}

BOOL InitPluginPath()
{
    enum { buf_size = MAXSHORT + 1 };

    if (PWSTR psz = new(nothrow) WCHAR[buf_size])
    {
        if (ULONG cch = GetModuleFileName(0, psz, buf_size))
        {
            if (NOERROR == GetLastError())
            {
                PWSTR FileName = psz + cch;
                g_cchMaxPlugin = buf_size - cch;

                do 
                {
                    switch (*--FileName)
                    {
                    case '\\':
                    case '/':
                        g_pszPluginPath = psz;
                        g_pszPluginRelativePath = FileName + 1;
                        return TRUE;

                    }
                } while (g_cchMaxPlugin++, --cch);
            }
        }
        delete [] psz;
    }

    return FALSE;
}

BOOL SetPluginPath(_In_ PCWSTR pszPluginPath)
{
    SIZE_T cch = wcslen(pszPluginPath);

    PWSTR pszPluginName = g_pszPluginRelativePath + cch;

    if (++cch > g_cchMaxPlugin)
    {
        return FALSE;
    }

    AcquireSRWLockExclusive(&g_lock);

    memcpy(g_pszPluginRelativePath, pszPluginPath, cch * sizeof(WCHAR));

    g_pszPluginName = pszPluginName;

    ReleaseSRWLockExclusive(&g_lock);

    return TRUE;
}

void ReleasePluginPath()
{
    ReleaseSRWLockShared(&g_lock);
}

PCWSTR AcquirePluginPath()
{
    AcquireSRWLockShared(&g_lock);
    return g_pszPluginName ? g_pszPluginPath : 0;
}

void RemovePluginPath()
{
    AcquireSRWLockExclusive(&g_lock);
    g_pszPluginName = 0;
    ReleaseSRWLockExclusive(&g_lock);
}


在开始时,我们必须调用InitPluginPath(),在退出时调用FreePluginPath()

相关问题