如何安全地防止在Wininet中调用状态回调函数?

问题描述 投票:1回答:2

我们在DLL中使用WinInet来进行异步网络调用。

当应用程序退出时,我们使用InetSetStatusCallback(connect_handle, NULL);删除挂起的请求的已注册回调函数。但是,偶尔会在DLL卸载后调用回调函数,从而导致应用程序崩溃。

症状与本博客的最后一个FAQ完全相似:WinHTTP Questions: About Callbacks

我试图找到一种方法来安全地删除所有挂起请求的回调函数,以便在卸载DLL后WinInet不会调用它们。

c++ wininet winhttp
2个回答
1
投票

在我们将指针传递给回调函数之后 - 包含此回调函数的模块当然不能被卸载,直到可以调用回调为止。如果回调函数在EXE模块中,那么这当然不会有问题,它永远不会被卸载。但是在DLL的情况下我们需要在设置回调之前添加对DLL的引用,以防止DLL卸载。这很容易通过GetModuleHandleExW功能完成

HMODULE hmod;
GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (PCWSTR)&__ImageBase, &hmod);

好的,但是怎么不让DLL卸载?当我们的回调不再被调用时,我们需要取消引用它(调用FreeLibrary)。什么时候会这样?

InternetSetStatusCallback为某些句柄设置了回调函数。这个句柄当然应该通过InternetCloseHandle关闭并且:

在句柄被关闭的回调中调用InternetCloseHandle是安全的。如果为正在关闭的句柄注册了状态回调,并且使用非NULL上下文值创建了句柄,则将进行INTERNET_STATUS_HANDLE_CLOSING回调。此指示将是从句柄进行的最后一次回调,并指示句柄正在被销毁。

如果句柄或其任何子句柄的异步请求待处理,则句柄不能立即关闭,但它将无效。使用句柄尝试的任何新请求都将返回ERROR_INVALID_HANDLE通知。异步请求将使用INTERNET_STATUS_REQUEST_COMPLETE完成。应用程序必须准备好在最终的INTERNET_STATUS_HANDLE_CLOSING指示之前接收句柄上的任何INTERNET_STATUS_REQUEST_COMPLETE指示,这表示句柄已完全关闭。

所以INTERNET_STATUS_HANDLE_CLOSING - 将是一个句柄的最后一个回调。最后的电话。正是在这个时候我们需要defereference DLL模块 - 回调将不再被调用。不需要参考更多。

好。什么时候清楚。但怎么样?我们不能从这一点直接调用FreeLibrary,因为如果DLL存在于最后一个引用 - 它将在FreeLibrary调用中被卸载并且当我们返回时 - 我们返回到空间并崩溃。

我们也不能调用FreeLibraryAndExitThread - 因为我们在任意线程上。但是存在不正确但是在实际解决方案上工作:

ULONG WINAPI SafeUnload(void*)
{
    //Sleep(*);
    FreeLibraryAndExitThread((HMODULE)&__ImageBase, 0);
}

if (HANDLE hThread = CreateThread(0, 0, SafeUnload, 0, 0, 0))
{
    CloseHandle(hThread);
}

我们可以自己创建新的线程,并从这个线程调用FreeLibraryAndExitThread(这里这个调用是正确的)。但我怎么说 - 这在理论上是不正确的解决方案。这里存在种族 - 在我们从FreeLibraryAndExitThread调用返回之前,CreateThread调用可以执行和卸载DLL。结果我们在CreateThread之后崩溃(我们尝试执行已经卸载的代码)。当然这不太可能,但..

我们可以在调用Sleep之前插入FreeLibraryAndExitThread(一些延迟),但无论如何 - 理论上可以争夺任何延迟。再次具体延迟选择具体价值?!和DLL将被延迟卸载这个时间。不正确也不好。尽管这是相对简单的“解决方案”,但我不会使用它。

正确的解决方案是 - 调用(更确切地说是跳转)到FreeLibrary而不返回到该跳转的位置。但是hust回到了地方,从那里召唤了回叫。这在c / c ++中是不可能的,但在asm代码中是可能的。当然这需要为每个平台目标单独的asm文件(x86,x64如何最小)

所以接下来是正确而优雅的解决方案:无论如何,INTERNET_STATUS_CALLBACK回调函数必须与某个类关联,我们在其中持有句柄上下文。这必须是类中的静态函数和DWORD_PTR dwContext - 与hInternet关联的应用程序定义的上下文值必须指向类的实例(事实上这个指针)。通常这是以下一种方式完成的:

class REQUEST_CONTEXT 
{
  // ...
    static void WINAPI _StatusCallback(
        __in  HINTERNET hRequest,
        __in  DWORD_PTR dwContext,
        __in  DWORD dwInternetStatus,
        __in  LPVOID lpvStatusInformation,
        __in  DWORD dwStatusInformationLength
        )
    {
        reinterpret_cast<REQUEST_CONTEXT*>(dwContext)->StatusCallback(
            hRequest, 
            dwInternetStatus, 
            lpvStatusInformation, 
            dwStatusInformationLength
            );
    }

    void StatusCallback(
        __in  HINTERNET hRequest,
        __in  DWORD dwInternetStatus,
        __in  LPVOID lpvStatusInformation,
        __in  DWORD dwStatusInformationLength
        );
};

在这里我们需要在asm代码中实现_StatusCallback来调用FreeLibrary。和StatusCallback的返回值从void到BOOL - 如果这是最终调用(dwInternetStatus == INTERNET_STATUS_HANDLE_CLOSING)则返回TRUE,否则返回FALSE。

所以开始解决

// helper for get complex c++ names for use in asm code
#if 0
#define ASM_FUNCTION {__pragma(message(__FUNCDNAME__" proc\r\n" __FUNCDNAME__ " endp"))}
#define CPP_FUNCTION __pragma(message("extern " __FUNCDNAME__ " : PROC ; "  __FUNCSIG__))
#else
#define ASM_FUNCTION
#define CPP_FUNCTION
#endif

基类骨架:

class REQUEST_CONTEXT 
{
    // ...
    static void WINAPI _StatusCallback(
        __in  HINTERNET hRequest,
        __in  DWORD_PTR dwContext,
        __in  DWORD dwInternetStatus,
        __in  LPVOID lpvStatusInformation,
        __in  DWORD dwStatusInformationLength
        ) ASM_FUNCTION;

    BOOL StatusCallback(
        __in  HINTERNET hRequest,
        __in  DWORD dwInternetStatus,
        __in  LPVOID lpvStatusInformation,
        __in  DWORD dwStatusInformationLength
        );

    ULONG SendRequest(PCWSTR pwszObjectName);

    void OnRequestComplete(HINTERNET hRequest, INTERNET_ASYNC_RESULT* pres);
};

StatusCallback实施:

BOOL REQUEST_CONTEXT::StatusCallback(
                    __in  HINTERNET hRequest,
                    __in  DWORD dwInternetStatus,
                    __in  LPVOID lpvStatusInformation,
                    __in  DWORD dwStatusInformationLength
                    )
{
    CPP_FUNCTION;

    switch (dwInternetStatus)
    {
    case INTERNET_STATUS_HANDLE_CLOSING:
        Release();
        return TRUE;
    case INTERNET_STATUS_REQUEST_COMPLETE:
        OnRequestComplete(hRequest, (INTERNET_ASYNC_RESULT*)lpvStatusInformation);
        break;
    }

    return FALSE;
}

我们如何注册回调(在SendRequest的代码中为HttpOpenRequestW返回的句柄)

ULONG SendRequest(PCWSTR pwszObjectName)
{
    // ... some code ...
    AddRef();

    HMODULE hmod;
    if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (PCWSTR)&__ImageBase, &hmod))
    {
        if (HINTERNET hRequest = HttpOpenRequestW(..., (DWORD_PTR)this))
        {
            set_handle(hRequest);

            if (INTERNET_INVALID_STATUS_CALLBACK != InternetSetStatusCallback(hRequest, _StatusCallback))
            {
                INTERNET_ASYNC_RESULT ar;
                ar.dwResult = HttpSendRequestW(hRequest, 0, 0, 0, 0);
                ar.dwError = ar.dwResult ? NOERROR : GetLastError();

                if (ar.dwError != ERROR_IO_PENDING)
                {
                    OnRequestComplete(hRequest, &ar);
                }

                return NOERROR;
            }
        }

        FreeLibrary(hmod);
    }

    Release();

    return GetLastError();
}

所以这里我们在设置回调之前调用GetModuleHandleExW来添加对DLL的引用。如果设置回调失败 - 我们将FreeLibrary称为deference DLL。请注意,这里调用FreeLibrary是安全和正确的,因为调用SendRequest的人必须有DLL的引用 - 当然在此调用期间不能卸载DLL。所以当我们从这个函数调用FreeLibrary时 - 我们保证不会释放最后一个DLL引用(我们从GetModuleHandleExW调用中自由引用,但在此函数执行期间存在对DLL的附加引用。此函数的调用者(直接或间接)关心这个)。在位dwContext我们传递这个指针。

所以现在最后一部分 - _StatusCallback实施。

for x64(ml64 / c / Cp $(InputFileName) - > $(InputName).obj)

extern __imp_FreeLibrary:QWORD
extern __ImageBase:BYTE

; void REQUEST_CONTEXT::StatusCallback(void *,unsigned long,void *,unsigned long)
extern ?StatusCallback@REQUEST_CONTEXT@@QEAAHPEAXK0K@Z : PROC 

.CODE 

?_StatusCallback@REQUEST_CONTEXT@@SAXPEAX_KK0K@Z proc
    xchg rcx,rdx
    mov rax,[rsp + 28h]
    sub rsp,38h
    mov [rsp + 20h],rax
    call ?StatusCallback@REQUEST_CONTEXT@@QEAAHPEAXK0K@Z
    add rsp,38h
    test eax,eax
    jnz @@1
    ret
@@1:
    lea rcx, __ImageBase
    jmp __imp_FreeLibrary
?_StatusCallback@REQUEST_CONTEXT@@SAXPEAX_KK0K@Z endp

end

for x86(ml / c / Cp $(InputFileName) - > $(InputName).obj)

.MODEL FLAT

extern __imp__FreeLibrary@4:DWORD
extern ___ImageBase:BYTE

; int __thiscall REQUEST_CONTEXT::StatusCallback(void *,unsigned long,void *,unsigned long)
extern ?StatusCallback@REQUEST_CONTEXT@@QAEHPAXK0K@Z : PROC 

.CODE

?_StatusCallback@REQUEST_CONTEXT@@SGXPAXKK0K@Z proc
    mov ecx,[esp + 8]
    push [esp + 20]
    push [esp + 20]
    push [esp + 20]
    push [esp + 16]
    call ?StatusCallback@REQUEST_CONTEXT@@QAEHPAXK0K@Z
    test eax,eax
    jnz @@1
    ret 4*5
@@1:
    mov eax,[esp]
    lea edx, ___ImageBase
    add esp,4*4
    mov [esp],eax
    mov [esp + 4],edx
    jmp __imp__FreeLibrary@4
?_StatusCallback@REQUEST_CONTEXT@@SGXPAXKK0K@Z endp

end

0
投票

您很可能应该等待/取消所有挂起的异步调用。让它们活着并卸载代码并不好(正确)。

© www.soinside.com 2019 - 2024. All rights reserved.