羽夏逆向指引—— Hook

寂靜的羽夏發表於2022-03-28

寫在前面

  此係列是本人一個字一個字碼出來的,包括示例和實驗截圖。可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閒錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並宣告我的個人資訊和本人部落格地址即可,但必須事先通知我

你如果是從中間插過來看的,請仔細閱讀 羽夏逆向指引——序 ,方便學習本教程。

簡述

  在軟體安全對抗方面,還是在外掛和反外掛方面,你可能經常聽到Hook這個名詞,中文翻譯就是鉤子。所謂掛鉤子就是試圖改變程式碼程式的原有流程,執行到自己的程式碼區域,這就是掛鉤子的作用。在看本篇介紹之前,最好先來閱讀 羽夏筆記——Hook攻防基礎羽夏筆記——硬編碼(32位),以防下面相關操作可能有些看不懂,本篇討論32位的,64位的實現是一樣的。
  Hook實現無非常用如下類似組合:

//組合1
jmp 0x4001200;

//組合2
push 0x4001200;
ret;

//組合3
sub esp,4;
mov [esp],0x4001200;
ret;

//組合4
call 0x4001200;

//組合5
mov eax,0x4001200;
jmp eax;

//組合6
mov eax,0x4001200;
call eax;

  上面的地址都是我假設的我讓流程跑到0x4001200這個地址。
  但是,程式存的並不是上面的彙編程式碼,而是實實在在的硬編碼。jmp分為長跳和短跳,它的彙編指令編碼方式是不同的。如果直接跳轉到目標地址,還需要特定的演算法進行轉化,由於在 羽夏筆記——硬編碼(32位) 介紹了,這裡我就不贅述了,我們來看看幾個在3環比較常見的Hook以及它們的示例。

InlineHook

  內聯鉤子,就是直接在程式不用的空間直接寫硬編碼。因為程式不可能是緊湊的,每一個函式都有一定的空間可以利用,我們可以看如下圖:

羽夏逆向指引—— Hook

  如果你注入別人的程式需要保證有充分的空間來儲存你的內聯鉤子程式碼。僅展示一下原理,我們就偷懶就Hook自己程式的函式,實驗思路如下:
  先看看我們Hook的地址的彙編情況:

羽夏逆向指引—— Hook

  我們用jmp來實現Hook,先看看受影響的彙編程式碼:

羽夏逆向指引—— Hook

  確認好受影響的彙編指令後,我們就可以寫程式碼了:

#include <iostream>
#include <Windows.h>

using namespace std;

UINT HookAddr = 0;
char shellcode[] = { 0xE9,0,0,0,0 };

void __stdcall HookProc(LPCWSTR Caption, LPCWSTR Text)
{
    wcout << "Capiton : " << Caption << endl << "Text : " << Text << endl;
}

void __declspec(naked) HookDispatcher()
{
    _asm
    {
        /*執行 Hook 處理*/
        mov eax, [esp + 0xC];    //Caption
        mov ebx, [esp + 0x8];    //Text
        push ebx;
        push eax;
        call HookProc;

        /*補充被損壞的硬編碼*/
        push ebp;    
        mov ebp, esp;

        /*回去執行*/
        mov eax, [HookAddr];
        add eax, 5;
        jmp eax;
    }
}


int main(int argc, char* argv[])
{
    HMODULE lib = LoadLibrary(L"user32.dll");
    if (lib)
    {
        FARPROC msgboxW = GetProcAddress(lib, "MessageBoxW");
        if (msgboxW)
        {
            HookAddr = (UINT)msgboxW;

            //構造 ShellCode
            UINT dest = (UINT)HookDispatcher - HookAddr - 5;
            memcpy_s(&shellcode[1], 4, &dest, sizeof(UINT));

            if (WriteProcessMemory((HANDLE)-1, (LPVOID)HookAddr, shellcode, sizeof(shellcode), NULL))
            {
                MessageBoxW(NULL, L"This is the text!!!", L"Caption", MB_ICONINFORMATION);    //呼叫測試
            }
        }
    }

    system("pause");
    return 0;
}

  這個實現的功能就是攔截字串引數,只要程式呼叫了MessageBoxW函式,就會被攔截。

IATHook

  對於Windows的可執行程式,呼叫系統的API並不是直接呼叫對應的函式地址,而是通過間接的方式來進行的,如下是實際情況:

push        40h  
push        offset string L"Caption" (0405220h)  
push        offset string L"This is the tex\x4000\0\0\0" (0405230h)  
push        0  
call        dword ptr [__imp__MessageBoxW@16 (04050B0h)] 

  如果我們修改了這個地址,我們就可以實現對該函式的掛鉤,由於這裡需要PE結構的知識,所以請詳細學習之後再回來看看這部分程式碼:

#include <iostream>
#include <Windows.h>

using namespace std;

typedef  int (*WINAPI MsgBoxW)(
    _In_opt_ HWND hWnd,
    _In_opt_ LPCWSTR lpText,
    _In_opt_ LPCWSTR lpCaption,
    _In_ UINT uType);

MsgBoxW msgboxw;

void WINAPI HookProc(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType)
{
    wcout << "Capiton : " << lpCaption << endl << "Text : " << lpText << endl;
    msgboxw(hWnd, lpText, lpCaption, uType);
}


int main(int argc, char* argv[])
{

    HMODULE lib = LoadLibrary(L"user32.dll");
    if (!lib)
    {
        cout << "LoadLibrary Error!" << endl;
        system("pause");
        return 0;
    }

    msgboxw = (MsgBoxW)GetProcAddress(lib, "MessageBoxW");
    if (!msgboxw)
    {
        cout << "GetProcAddress Error!" << endl;
        system("pause");
        return 0;
    }

    UINT pdos = (UINT)GetModuleHandle(NULL);
    PIMAGE_NT_HEADERS pfile = (PIMAGE_NT_HEADERS)(pdos + ((PIMAGE_DOS_HEADER)pdos)->e_lfanew);
    UINT optHeaderSize = pfile->FileHeader.SizeOfOptionalHeader;
    auto iat = pfile->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
    auto importd = (PIMAGE_IMPORT_DESCRIPTOR)(pdos + iat.VirtualAddress);
    IMAGE_IMPORT_DESCRIPTOR emptyImport = { 0 };

    for (;memcmp(&emptyImport, &importd, sizeof(IMAGE_IMPORT_DESCRIPTOR)); importd++)
    {
        auto item = *importd;
        if (_stricmp((char*)(pdos + item.Name), "user32.dll"))    //不區分大小寫比較
            continue;

        auto othunk = (PIMAGE_THUNK_DATA)(pdos + item.OriginalFirstThunk);
        IMAGE_THUNK_DATA emptyThunk = { 0 };

        for (int i = 0; ; i++)
        {
            auto iitem = othunk[i];

            if (!memcmp(&othunk[i], &emptyThunk, sizeof(IMAGE_THUNK_DATA)))
                break;

            if (iitem.u1.Ordinal & 0x80000000)
                continue;

            auto byname = (PIMAGE_IMPORT_BY_NAME)(pdos + iitem.u1.AddressOfData);
            if (!strcmp((char*)&byname->Name, "MessageBoxW"))
            {
                auto thunk = (PIMAGE_THUNK_DATA)(pdos + item.FirstThunk);
                DWORD old;
                if (VirtualProtect(&thunk[i].u1.Function, sizeof(UINT), PAGE_READWRITE, &old))
                {
                    thunk[i].u1.Function = (UINT)HookProc;
                    VirtualProtect(&thunk[i].u1.Function, sizeof(UINT), old, &old);
                    MessageBoxW(NULL, L"This is the text!!!", L"Caption", MB_ICONINFORMATION);    //呼叫測試
                }
                goto EndProc;
            }
        }
    }
EndProc:
    system("pause");
    return 0;
}

  針對於如上的Hook,我們可以有一些反制措施,比如載入PE完畢後直接抹掉INT表,因為這個表就沒啥用處了,呼叫函式都是IAT表。

虛表 Hook

  在C++物件導向使用帶有虛擬函式的就會有這東西,我們可以通過除錯視窗的區域性變數來觀察情況:

羽夏逆向指引—— Hook

  具體的測試程式碼如下所示:

#include <iostream>
#include <Windows.h>

using namespace std;

void HookProc()
{
    cout << "HookProc" <<endl;
}


class MyClass
{
public:
    virtual void Test()
    {
        cout << "MyClass" << endl;
    }

private:

};

class MyClassSub :MyClass
{
public:
    void Test()
    {
        cout << "MyClassSub" << endl;
    }

private:

};


int main(int argc, char* argv[])
{
    MyClassSub* cls = new MyClassSub();
    cls->Test();

    UINT* vfptr = (UINT*)*(UINT*)cls;
    DWORD old;
    if (VirtualProtect(vfptr,sizeof(UINT),PAGE_READWRITE,&old))
    {
        *vfptr = (UINT)HookProc;
        VirtualProtect(vfptr, sizeof(UINT), old, &old);
        cls->Test();
    }

    system("pause");
    return 0;
}

  如果第二次輸出的是HookProc,說明我們的虛表鉤子實現成功。

異常 Hook

  在外掛補丁層面,有一些基於異常實現的鉤子。在這裡我們實現硬體斷點配合VEH實現掛鉤:

#include <iostream>
#include <Windows.h>

using namespace std;

void HookProc()
{
    cout << "HookProc" << endl;
}

void Proc()
{
    cout << "Proc" << endl;
}

LONG NTAPI VECTORED_EXCEPTION_HANDLER(struct _EXCEPTION_POINTERS* ExceptionInfo)
{
    if (ExceptionInfo->ExceptionRecord->ExceptionCode== EXCEPTION_SINGLE_STEP &&
        ExceptionInfo->ExceptionRecord->ExceptionAddress == (PVOID)Proc)
    {
        ExceptionInfo->ContextRecord->Eip = (DWORD)HookProc;
        return EXCEPTION_CONTINUE_EXECUTION;
    }
    return EXCEPTION_CONTINUE_SEARCH;
}

PVOID pveh;

DWORD WINAPI THREAD_START_ROUTINE(LPVOID lpThreadParameter)
{
    auto tid = (DWORD)lpThreadParameter;    //獲取主程式的執行緒 ID

    Proc();    //測試函式
    pveh = AddVectoredExceptionHandler(1, VECTORED_EXCEPTION_HANDLER);
    
    HANDLE hthread = OpenThread(THREAD_ALL_ACCESS, FALSE, tid);
    if (hthread)
    {
        CONTEXT context;
        context.ContextFlags = CONTEXT_ALL;
        SuspendThread(hthread);
        GetThreadContext(hthread, &context);
        context.Dr0 = (DWORD)Proc;
        context.Dr7 |= 1;
        SetThreadContext(hthread, &context);
        ResumeThread(hthread);
        CloseHandle(hthread);
    }
    return 0;
}

int main(int argc, char* argv[])
{
    HANDLE hthread = CreateThread(NULL, NULL, THREAD_START_ROUTINE, (LPVOID)GetCurrentThreadId(), 0, NULL);
    if (hthread)
    {
        WaitForSingleObject(hthread, -1);
        Proc();    //呼叫測試
        CloseHandle(hthread);
    }
    if (pveh) RemoveVectoredExceptionHandler(pveh);
    system("pause");
    return 0;
}

  有關基於異常的鉤子,我就介紹這麼多。

下一篇

  羽夏逆向指引——注入

相關文章