作者:The_Itach1@知道創宇404實驗室
日期:2022年12月23日

最近看了一下x86matthew關于hook方法的一篇文章https://www.x86matthew.com/view_post?id=stealth_hook,相對于傳統的一些hook方式,個人認為StealthHook的最大優點并不在于不修改內存保護,而是其隱蔽性,這種hook方式是難以檢測的,因為其沒有直接作用于目標函數。

此hook方式,實際上并沒有去hook目標函數,而是通過目標函數內的子函數,去獲取了進入目標函數時,棧上保存的返回地址,通過修改這個地址,即可劫持執行流程,在函數返回前,執行我們的代碼。

hook樣例-CreateFile

下面是其給出的例子。

#include <stdio.h>
#include <windows.h>

DWORD dwGlobal_OrigCreateFileReturnAddr = 0;
DWORD dwGlobal_OrigReferenceAddr = 0;

void __declspec(naked) ModifyReturnValue()
{
    // the original return address for the CreateFile call redirects to here
    _asm
    {
        // CreateFile complete - overwrite return value
        mov eax, 0x12345678

        // continue original execution flow (ecx is safe to overwrite at this point)
        mov ecx, dwGlobal_OrigCreateFileReturnAddr
        jmp ecx
    }
}

void __declspec(naked) HookStub()
{
    // the hooked global pointer nested within CreateFile redirects to here
    _asm
    {
        // store original CreateFile return address
        mov eax, dword ptr [esp + 0x100]
        mov dwGlobal_OrigCreateFileReturnAddr, eax

        // overwrite the CreateFile return address
        lea eax, ModifyReturnValue
        mov dword ptr [esp + 0x100], eax

        // continue original execution flow
        mov eax, dwGlobal_OrigReferenceAddr
        jmp eax
    }
}

DWORD InstallHook()
{
    BYTE *pModuleBase = NULL;
    BYTE *pHookAddr = NULL;

    // get base address of kernelbase.dll
    pModuleBase = (BYTE*)GetModuleHandle("kernelbase.dll");
    if(pModuleBase == NULL)
    {
        return 1;
    }

    // get ptr to function reference
    pHookAddr = pModuleBase + 0x1DF650;

    // store original value
    dwGlobal_OrigReferenceAddr = *(DWORD*)pHookAddr;

    // overwrite ptr to call HookStub
    *(DWORD*)pHookAddr = (DWORD)HookStub;

    return 0;
}

int main()
{
    HANDLE hFile = NULL;

    // create temporary file (without hook)
    printf("Creating file #1...\n");
    hFile = CreateFile("temp_file_1.txt", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    printf("hFile: 0x%X\n\n", hFile);

    // install hook
    printf("Installing hook...\n\n");
    if(InstallHook() != 0)
    {
        return 1;
    }

    // create temporary file (with hook)
    printf("Creating file #2...\n");
    hFile = CreateFile("temp_file_2.txt", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    printf("hFile: 0x%X\n\n", hFile);

    return 0;
}

上面的代碼的作用就是鉤取了CreatFile這個API函數,修改了其返回值為0x12345678,具體步驟如下。

  • Hook了kernelbase.dll+0x1DF650處的函數,這個函數是CreatFile內部會調用的一個子函數。
  • 在這個子函數執行前,將棧上CreatFile原本的返回地址保存下來,也就是[esp+0x100]的值,然后替換成了我們自己的函數ModifyReturnValue。
  • 子函數執行。
  • 最終會執行CreatFile函數最后的ret指令,但是此時棧上的返回地址以被修改,所以會先執行我們的函數,修改了eax,也就是返回值變成了0x12345678。
  • 最后mov eax, dwGlobal_OrigReferenceAddr jmp eax,回到真正的返回地址處。

下面來調試一下過程。

先是InstallHook()內部,Hook了一個子函數,其獲取EAT中了某一子函數的地址,并且將其替換為了HookStub。

然后到第二次調用CreateFile的開頭,我們查看一下,這時候ESP存放的返回地址是多少,實際上等下這里的值是會被修改的。

接著,我們本來會調用CreateFile內部的一個子函數,但是其已被我們hook現在變成了HookStub()函數,我們在HookStub()打斷點,發現其對棧偏移100處進行了修改,這個地址保存的就是原CreateFile返回到main函數的返回地址。

HookStub()內部將棧上的地址先進行保存到全局變量,然后修改為了我們自己的一個函數,最后jmp到真正的子函數處。

然后在CreatFile函數內部最后的ret指令處打個斷點,發現返回地址已被修改,不會跳轉到main函數了,而是跳轉到ModifyReturnValue()。

進入ModifyReturnValue(),發現其就是對eax(函數返回值)進行了修改,然后跳轉到真正應該返回的地址。

最后結果如下,hook后,調用CreatFile函數的返回值會被修改為0x12345678。

整個過程還是比較清晰,也不是很復雜的hook過程,問題就在于,如何獲取到子函數的地址,以及到目標函數的返回地址的棧偏移是多少,因為我們不可能自己去一個一個調試獲取。

為了解決這個問題,x86matthew師傅開發了一款工具,用來獲取可用的子函數地址,以及棧偏移。

StealthHook工具

其先是注冊了一個異常處理函數,用來處理EXCEPTION_SINGLE_STEP異常和EXCEPTION_ACCESS_VIOLATION異常。

LONG WINAPI ExceptionHandler(EXCEPTION_POINTERS* ExceptionInfo)
{
    NATIVE_VALUE dwReturnAddress = 0;

    // check exception code
    if (ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP)
    {
        if (dwGlobal_TraceStarted == 0)
        {
            //打在目標函數的硬件斷點和此時的eip是否一致
            if (CURRENT_EXCEPTION_INSTRUCTION_PTR != ExceptionInfo->ContextRecord->Dr0)
            {
                return EXCEPTION_CONTINUE_SEARCH;
            }

            //獲取當前ESP寄存器的值
            dwGlobal_InitialStackPtr = CURRENT_EXCEPTION_STACK_PTR;

            //返回地址處打硬件斷點
            ExceptionInfo->ContextRecord->Dr1 = *(NATIVE_VALUE*)dwGlobal_InitialStackPtr;

            // initial trace started
            dwGlobal_TraceStarted = 1;
        }

        // set debug control field
        ExceptionInfo->ContextRecord->Dr7 = DEBUG_REGISTER_EXEC_DR1;

        // check current instruction pointer
        if (CURRENT_EXCEPTION_INSTRUCTION_PTR == dwGlobal_Wow64TransitionStub)
        {
            // we have hit the wow64 transition stub - don't single step here, set a breakpoint on the current return address instead
            dwReturnAddress = *(NATIVE_VALUE*)CURRENT_EXCEPTION_STACK_PTR;
            ExceptionInfo->ContextRecord->Dr0 = dwReturnAddress;
            ExceptionInfo->ContextRecord->Dr7 |= DEBUG_REGISTER_EXEC_DR0;
        }
        else if (CURRENT_EXCEPTION_INSTRUCTION_PTR == ExceptionInfo->ContextRecord->Dr1)
        {
            //到達返回地址后,刪除所有斷點
            ExceptionInfo->ContextRecord->Dr7 = 0;
        }
        else
        {
            // scan all modules for the current instruction pointer
            ScanAllModulesForAddress(CURRENT_EXCEPTION_INSTRUCTION_PTR, CURRENT_EXCEPTION_STACK_PTR);

            // single step
            ExceptionInfo->ContextRecord->EFlags |= SINGLE_STEP_FLAG;
        }

        // continue execution
        return EXCEPTION_CONTINUE_EXECUTION;
    }
    else if (ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION)
    {
        // access violation - check if the eip matches the expected value
        if (CURRENT_EXCEPTION_INSTRUCTION_PTR != OVERWRITE_REFERENCE_ADDRESS_VALUE)
        {
            return EXCEPTION_CONTINUE_SEARCH;
        }

        // caught current hook successfully
        dwGlobal_CurrHookExecuted = 1;

        // restore correct instruction pointer
        CURRENT_EXCEPTION_INSTRUCTION_PTR = dwGlobal_OriginalReferenceValue;

        // continue execution
        return EXCEPTION_CONTINUE_EXECUTION;
    }

    return EXCEPTION_CONTINUE_SEARCH;
}

先不看這個異常處理,后面具體分析。

先看BeginTrace()函數,這個函數的參數就是目標函數的地址。

DWORD BeginTrace(BYTE* pTargetFunction)
{
    CONTEXT DebugThreadContext;

    // reset values
    dwGlobal_TraceStarted = 0;
    dwGlobal_SuccessfulHookCount = 0;
    dwGlobal_AddressCount = 0;

    // set initial debug context - hardware breakpoint on target function
    memset((void*)&DebugThreadContext, 0, sizeof(DebugThreadContext));
    DebugThreadContext.ContextFlags = CONTEXT_DEBUG_REGISTERS;
    DebugThreadContext.Dr0 = (NATIVE_VALUE)pTargetFunction;
    DebugThreadContext.Dr7 = DEBUG_REGISTER_EXEC_DR0;
    if (SetThreadContext(GetCurrentThread(), &DebugThreadContext) == 0)
    {
        return 1;
    }

    // execute the target function
    ExecuteTargetFunction();

    return 0;
}

其在目標函數地址處,打上了硬件斷點,這個異常會被我們自己的異常處理函數所捕獲,獲取了esp寄存器的值,并且在返回地址處又打了個硬件斷點。

if (dwGlobal_TraceStarted == 0)
{
    //打在目標函數的硬件斷點和此時的eip是否一致
    if (CURRENT_EXCEPTION_INSTRUCTION_PTR != ExceptionInfo->ContextRecord->Dr0)
    {
        return EXCEPTION_CONTINUE_SEARCH;
    }
    //獲取當前ESP寄存器的值
    dwGlobal_InitialStackPtr = CURRENT_EXCEPTION_STACK_PTR;

    //返回地址處打硬件斷點
    ExceptionInfo->ContextRecord->Dr1 = *(NATIVE_VALUE*)dwGlobal_InitialStackPtr;

    // initial trace started
    dwGlobal_TraceStarted = 1;
}

接著執行,ScanAllModulesForAddress()函數是用來掃描子函數的,其參數是當前eip和當前esp,然后將EFlags設置為了單步執行,意味著后面每執行一條匯編指令,都會觸發單步異常,從而進入這個異常處理函數。

// scan all modules for the current instruction pointer
ScanAllModulesForAddress(CURRENT_EXCEPTION_INSTRUCTION_PTR, CURRENT_EXCEPTION_STACK_PTR);

// single step
ExceptionInfo->ContextRecord->EFlags |= SINGLE_STEP_FLAG;

當進入到合適的子函數頭部中時,就會調用下面的遍歷過程,計算出其對應的dll以及函數地址,和棧偏移。下面兩個函數,一個遍歷模塊,一個遍歷EAT表,當遍歷出子函數時,就會用最初目標函數的的esp-現在的esp,從而得到棧偏移。

DWORD ScanModuleForAddress(BYTE* pModuleBase, char* pModuleName, NATIVE_VALUE dwAddr, NATIVE_VALUE dwStackPtr)
{
    IMAGE_DOS_HEADER* pImageDosHeader = NULL;
    IMAGE_NT_HEADERS* pImageNtHeader = NULL;
    IMAGE_SECTION_HEADER* pCurrSectionHeader = NULL;
    DWORD dwReadOffset = 0;
    BYTE* pCurrPtr = NULL;
    MEMORY_BASIC_INFORMATION MemoryBasicInfo;
    DWORD dwStackDelta = 0;

    // get dos header
    pImageDosHeader = (IMAGE_DOS_HEADER*)pModuleBase;
    if (pImageDosHeader->e_magic != 0x5A4D)
    {
        return 1;
    }

    // get nt header
    pImageNtHeader = (IMAGE_NT_HEADERS*)(pModuleBase + pImageDosHeader->e_lfanew);
    if (pImageNtHeader->Signature != IMAGE_NT_SIGNATURE)
    {
        return 1;
    }

    // loop through all sections
    for (DWORD i = 0; i < pImageNtHeader->FileHeader.NumberOfSections; i++)
    {
        // get current section header
        pCurrSectionHeader = (IMAGE_SECTION_HEADER*)((BYTE*)&pImageNtHeader->OptionalHeader + pImageNtHeader->FileHeader.SizeOfOptionalHeader + (i * sizeof(IMAGE_SECTION_HEADER)));

        // ignore executable sections
        if (pCurrSectionHeader->Characteristics & IMAGE_SCN_MEM_EXECUTE)
        {
            continue;
        }

        // scan current section for the target address
        dwReadOffset = pCurrSectionHeader->VirtualAddress;
        for (DWORD ii = 0; ii < pCurrSectionHeader->Misc.VirtualSize / sizeof(NATIVE_VALUE); ii++)
        {
            // check if the current value contains the target address
            pCurrPtr = pModuleBase + dwReadOffset;
            if (*(NATIVE_VALUE*)pCurrPtr == dwAddr)
            {
                // found target address - check memory protection
                memset((void*)&MemoryBasicInfo, 0, sizeof(MemoryBasicInfo));
                if (VirtualQuery(pCurrPtr, &MemoryBasicInfo, sizeof(MemoryBasicInfo)) != 0)
                {
                    // check if the current region is writable
                    if (MemoryBasicInfo.Protect == PAGE_EXECUTE_READWRITE || MemoryBasicInfo.Protect == PAGE_READWRITE)
                    {
                        // ensure the address list is not full
                        if (dwGlobal_AddressCount >= MAXIMUM_STORED_ADDRESS_COUNT)
                        {
                            printf("Error: Address list is full\n");
                            return 1;
                        }

                        // store current address in list
                        dwGlobal_AddressList[dwGlobal_AddressCount] = (NATIVE_VALUE)pCurrPtr;
                        dwGlobal_AddressCount++;

                        // calculate stack delta
                        dwStackDelta = (DWORD)(dwGlobal_InitialStackPtr - dwStackPtr);

                        printf("Instruction 0x%p referenced at %s!0x%p (sect: %s, virt_addr: 0x%X, stack delta: 0x%X)\n", (void*)dwAddr, pModuleName, (void*)pCurrPtr, pCurrSectionHeader->Name, dwReadOffset, dwStackDelta);
                    }
                }
            }

            // increase read offset
            dwReadOffset += sizeof(NATIVE_VALUE);
        }
    }

    return 0;
}

DWORD ScanAllModulesForAddress(NATIVE_VALUE dwAddr, NATIVE_VALUE dwStackPtr)
{
    DWORD dwPEB = 0;
    PEB* pPEB = NULL;
    LDR_DATA_TABLE_ENTRY* pCurrEntry = NULL;
    LIST_ENTRY* pCurrListEntry = NULL;
    DWORD dwEntryOffset = 0;
    char szModuleName[512];
    DWORD dwStringLength = 0;

    // get PEB ptr
#if _WIN64
    pPEB = (PEB*)__readgsqword(0x60);
#else
    pPEB = (PEB*)__readfsdword(0x30);
#endif

    // get InMemoryOrderLinks offset in structure
    dwEntryOffset = (DWORD)((BYTE*)&pCurrEntry->InLoadOrderLinks - (BYTE*)pCurrEntry);

    // get first link
    pCurrListEntry = pPEB->Ldr->InLoadOrderModuleList.Flink;

    // enumerate all modules
    for (;;)
    {
        // get ptr to current module entry
        pCurrEntry = (LDR_DATA_TABLE_ENTRY*)((BYTE*)pCurrListEntry - dwEntryOffset);

        // check if this is the final entry
        if (pCurrEntry->DllBase == 0)
        {
            // end of module list
            break;
        }

        // ignore main exe module
        if (pCurrEntry->DllBase != pGlobal_ExeBase)
        {
            // convert module name to ansi
            dwStringLength = pCurrEntry->BaseDllName.Length / sizeof(wchar_t);
            if (dwStringLength > sizeof(szModuleName) - 1)
            {
                dwStringLength = sizeof(szModuleName) - 1;
            }
            memset(szModuleName, 0, sizeof(szModuleName));
            wcstombs(szModuleName, pCurrEntry->BaseDllName.Buffer, dwStringLength);

            // scan current module
            ScanModuleForAddress((BYTE*)pCurrEntry->DllBase, szModuleName, dwAddr, dwStackPtr);
        }

        // get next module entry in list
        pCurrListEntry = pCurrListEntry->Flink;
    }

    return 0;
}

最終會將所有合適的子函數都保存到dwGlobal_AddressList[dwGlobal_AddressCount]這個全局數組中。

然后會驗證一下獲取到的這些子函數地址的可用性。

{
    // attempt to hook the target function at all referenced instructions found earlier
    for (DWORD i = 0; i < dwGlobal_AddressCount; i++)
    {
        printf("\nOverwriting reference: 0x%p...\n", (void*)dwGlobal_AddressList[i]);

        // reset flag
        dwGlobal_CurrHookExecuted = 0;

        // store original value
        dwGlobal_OriginalReferenceValue = *(NATIVE_VALUE*)dwGlobal_AddressList[i];

        // overwrite referenced value with placeholder value
        *(NATIVE_VALUE*)dwGlobal_AddressList[i] = OVERWRITE_REFERENCE_ADDRESS_VALUE;

        printf("Calling target function...\n");

        // execute target function
        ExecuteTargetFunction();

        // restore original value
        *(NATIVE_VALUE*)dwGlobal_AddressList[i] = dwGlobal_OriginalReferenceValue;

        // check if the hook was executed
        if (dwGlobal_CurrHookExecuted == 0)
        {
            // hook wasn't executed - ignore
            printf("Failed to catch hook\n");
        }
        else
        {
            // hook was executed - this address can be used to hook the target function
            printf("Hook caught successfully!\n");
            dwGlobal_SuccessfulHookCount++;
        }
    }

    return 0;
}

其將其函數地址修改為OVERWRITE_REFERENCE_ADDRESS_VALUE,導致這個子函數不可用,然后就會觸發EXCEPTION_ACCESS_VIOLATION異常,由我們的異常處理函數來處理,主要就是修改dwGlobal_CurrHookExecuted = 1,代表測試成功。

else if (ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION)
{
    // access violation - check if the eip matches the expected value
    if (CURRENT_EXCEPTION_INSTRUCTION_PTR != OVERWRITE_REFERENCE_ADDRESS_VALUE)
    {
        return EXCEPTION_CONTINUE_SEARCH;
    }

    // caught current hook successfully
    dwGlobal_CurrHookExecuted = 1;

    // restore correct instruction pointer
    CURRENT_EXCEPTION_INSTRUCTION_PTR = dwGlobal_OriginalReferenceValue;

    // continue execution
    return EXCEPTION_CONTINUE_EXECUTION;
}

總結

此hook方式的思路還是很新穎,同樣也存在一些缺點,那就是只能在目標函數執行完成后,修改流程,并且可能hook的子函數萬一被其他函數也調用了,這時候修改棧上的值,是否會有觸發崩潰可能性呢。

通過這個工具的代碼,也學到不少東西,異常處理,打硬件斷點等等,可惜的是每想要hook一個API函數,都必須要去修改一下源碼。

參考

https://www.x86matthew.com/view_post?id=stealth_hook


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/2035/