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

时间:2019-04-26 07:10:56

标签: c++ wininet winhttp

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

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

症状与该博客的上一个常见问题解答完全相同:WinHTTP Questions: About Callbacks

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

2 个答案:

答案 0 :(得分: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_REQUEST_COMPLETE   最终之前的手柄上的指示   出现 INTERNET_STATUS_HANDLE_CLOSING 指示   手柄完全关闭。

如此INTERNET_STATUS_HANDLE_CLOSING-将是通过句柄进行的最后回调。 最终呼叫。正是在这个时候,我们需要取消引用 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,而无需返回该跳转的位置。但必须返回到调用回调的位置。这在 c / c ++ 中是不可能的,但在 asm 代码中则可能。当然,每个平台目标都需要单独的 asm 文件( x86,x64 最低要求)

因此接下来是正确而优雅的解决方案:INTERNET_STATUS_CALLBACK回调函数无论如何都必须与某个类相关联,我们在其中保存了句柄上下文。这必须是类中的静态函数,并且 DWORD_PTR dwContext-与hInternet关联的应用程序定义的上下文值必须指向类的实例(实际上是 this 指针)。通常这是通过以下方式完成的:

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 - TRUE ,如果这是最终调用,则返回(dwInternetStatus == INTERNET_STATUS_HANDLE_CLOSING ),否则为 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以引用 DLL 。请注意,此处调用FreeLibrary是安全正确的,因为调用SendRequest的人必须具有 DLL - DLL 的引用,在此通话期间。因此,当我们从此函数调用FreeLibrary时-我们保证不释放对 DLL 的最后一个引用(我们从GetModuleHandleExW调用中释放引用,但在执行此函数期间,在 DLL 上存在其他引用。此函数的调用者(直接或间接)对此很在意)。在适当的位置 dwContext 中,我们传递 this 指针。

现在是最后一部分-_StatusCallback的实现。

用于 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

用于 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

答案 1 :(得分:0)

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