Windows下的hook技术学习

hook分类

20180806142903509

这张图非常经典,很多平台都能看到一模一样的分类。

根据《逆向工程核心原理》中的一个分类图:

image-20230723101207625

这篇用来记录应用层hook,内核还没学😅

1、消息hook类型

原理

消息机制算得上Windows中的最常见的常用的机制之一,比如当键盘敲击事件发生,这个事件就被添加到系统的消息队列中,然后操作系统判断哪个程序发生了事件,然后把这个事件转移到对应程序的私有队列中,程序检测到自己的队列中有新增消息,就检查消息进行响应,就完了。

这种hook技术又称Windows hook,这种类型的hook情况,Windows给出了一个官方的API:SetWindowsHookEx。这个常常和dll注入配合使用。

Hook分为全局和局部,全局一般用来实现dll注入(因为每一个进程的地址都是独立的,如果不将hook函数作为一个独立的地址,那么执行起来将非常麻烦),局部hook就是针对某一个线程进行hook。

HHOOK WINAPI SetWindowsHookEx(
__in int idHook, \\钩子类型
__in HOOKPROC lpfn, \\回调函数地址,也就是自定义的消息处理函数
__in HINSTANCE hMod, \\实例句柄
__in DWORD dwThreadId); \\线程ID,设置为0则为全局钩子,会影响所有的进程

使用这个api之后,当任意进程中生成指定的消息的时候,os就会把这个dll载进去,然后调用hook,这都是os自动进行的。

键盘 Hook 代码

代码来自《逆向工程核心原理》,下面是keylogger.dll的代码,通过另一个载入程序执行这个dll,然后就可以敲击键盘记录了。

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

HINSTANCE g_hInstance = NULL;
HHOOK g_hHook = NULL;
HWND g_hWnd = NULL;

#define DEF_PROCESS_NAME "notepad.exe"

// 这个回调函数用来处理键盘输入
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
    char szPath[MAX_PATH] = {0};
    char* p = NULL;

    if(nCode >= 0)
    {
        if(!(lParam & 0x80000000))
        {
            GetModuleFileNameA(NULL, szPath, MAX_PATH);
            // 取\\之后的字符,用来判断是不是目标进程notepad。
            p = strrchr(szPath, '\\');
        }
    }

    if( !_stricmp(p + 1, DEF_PROCESS_NAME) )
        return 1;

    // 不是就换下一个函数
    return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}

// 利用api启动挂钩
__declspec(dllexport) void StartHook()
{
    g_hHook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_hInstance, 0);
}

// 取消挂钩
__declspec(dllexport) void StopHook()
{
    // 先检查一下第一步挂钩是否成功,是被就不用了
    if(g_hHook)
    {
        UnhookWindowsHookEx(g_hHook);
        g_hHook = NULL;
    }
}

// 这里的dllmain实际上用不到, 写不写都无所谓了
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpvReserved)
{
    switch (dwReason)
    {
        case DLL_PROCESS_ATTACH:
            g_hInstance = hinstDLL;
            break;
        case DLL_PROCESS_DETACH:
            break;
    }
    return TRUE;
}

2、IAT Hook

这种方式主要是针对程序中的函数进行hook,而且是针对需要从外部调用的函数的进行HOOK。我认为与之对应的是inline hook

IAT是导入地址表,对应的EAT导出地址表也可以被hook

原理

1、修改被hook函数的在IAT表中的地址,让他指向自定义的函数。偷梁换柱。

2、根据这个原理,思路就是:找到目标函数iat中的位置--->修改iat中的函数的原来的地址---->改成自定义函数的地址---->正常调用原来的函数。

3、可以使用外部dll进行hook,也可以使用程序自身内部代码实现iathook。

Walkthrough

  1. 得到 PE baseaddr
  2. 利用 e_lfanew 地址获得NT头
  3. 得到NT头找到可选头
  4. 可选头的IDD数组
  5. 数组第二项为导入表

image-20230723163637596

  1. 根据偏移+基地址得到导入表
  2. 利用导入表里的name字段的值+基地址得到每个导入表的名称

导入表中的双桥结构

这个结构的主要的两个指针就是OriginalFirstThunk还有FirstThunk

IID的结构如下:

struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics; 
        DWORD   OriginalFirstThunk; //指向INT    桥1    
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp;
    DWORD   ForwarderChain;         
    DWORD   Name;                   //dll名称
    DWORD   FirstThunk;            //指向IAT    桥2
} IMAGE_IMPORT_DESCRIPTOR;

OriginalFirstThunk 和 FirstThunk 他们都是两个类型为IMAGE_THUNK_DATA 的数组

OriginalFirstThunk:

双字最高位为0,导入符号是一个数值,该数值是一个RVA。

双字最高位为1,导入符号是一个名称

PE 装载器首先搜索 OriginalFirstThunk ,找到之后加载程序迭代搜索数组中的每个指针,找到每个 IMAGE_IMPORT_BY_NAME 结构所指向的输入函数的地址,然后加载器用函数真正入口地址来替代由 FirstThunk 数组中的一个入口,因此我们称为输入地址表(IAT)。

image-20230723231335777

静态结构如下:

image-20230723231510423

其实就可以看作一个指向的是INT,另一个指向的是IAT。

实现代码

其实代码的关键就是确定要hook的函数,然后遍历导入表,修改地址,调用原函数。

核心:利用OriginalFirstThunkFirstThunk进行导入表的遍历。

#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
#include <winternl.h>
#include <winnt.h>

// 定义MessageBoxA函数原型  
typedef int (WINAPI* PrototypeMessageBox)(HWND, LPCSTR, LPCSTR, UINT);

// 原始MessageBoxA函数的地址  
PrototypeMessageBox originalMsgBox;

// Hook函数  
int hookedMessageBox(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType)
{
    MessageBoxW(NULL, TEXT("Hooked sucess"), TEXT("Title"), 0);

    return originalMsgBox(hWnd, lpText, lpCaption, uType);
}

int main()
{
    // 获取模块基地址
    HMODULE module = GetModuleHandle(NULL);

    // 获取DOS头
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)module;

    // 获取NT头 
    PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((BYTE*)module + dosHeader->e_lfanew);

    // 获取导入表目录
    PIMAGE_DATA_DIRECTORY importDir = &ntHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];

    // 获取导入表入口  
    PIMAGE_IMPORT_DESCRIPTOR importDesc = (PIMAGE_IMPORT_DESCRIPTOR)(importDir->VirtualAddress + (DWORD)module);

    // 遍历导入表
    while (importDesc->Name)
    {
        // 获取DLL名称
        LPCSTR dllName = (LPCSTR)importDesc->Name + (DWORD)module;
        // 程序在函数没用调用的时候就没用把对应的dll载入内存,这样强制载入,初始化iat表结束之后,才能进行后续的修改
        HMODULE dllHandle = LoadLibraryA(dllName);

        if (dllHandle)
        {
            // 获取原始和修正后的IAT
            PIMAGE_THUNK_DATA originalIAT = (PIMAGE_THUNK_DATA)((BYTE*)module + importDesc->OriginalFirstThunk);
            PIMAGE_THUNK_DATA firstIAT = (PIMAGE_THUNK_DATA)((BYTE*)module + importDesc->FirstThunk);

            // 遍历IAT查找MessageBoxA
            while (originalIAT->u1.AddressOfData)
            {
                PIMAGE_IMPORT_BY_NAME funcInfo = (PIMAGE_IMPORT_BY_NAME)((BYTE*)module + originalIAT->u1.AddressOfData);

                if (strcmp(funcInfo->Name, "MessageBoxA") == 0)
                {
                    // 保存原始地址
                    originalMsgBox = (PrototypeMessageBox)firstIAT->u1.Function;

                    // 修改内存权限,方便后续写入
                    DWORD oldProtect;
                    VirtualProtect(firstIAT, sizeof(PIMAGE_THUNK_DATA), PAGE_READWRITE, &oldProtect);

                    // 写入Hook地址
                    firstIAT->u1.Function = (DWORD)hookedMessageBox;

                    // 完成任务就直接退出循环
                    break;
                }
                originalIAT++;
                firstIAT++;
            }
        }
        importDesc++;
    }

    // 调用MessageBoxA
    originalMsgBox(NULL, "Hello", "Original", 0);
    return 0;
}

/*代码参考:https://www.ired.team/offensive-security/code-injection-process-injection/import-adress-table-iat-hooking*/

ps: 这里发现不少的定位偏移都是可以直接使用宏来实现,之前的代码中我还在苦哈哈的手动算偏移,程序里全是++--,通过上面的代码学到了多去找找对应的宏,微软肯定不会适配这么不好的。

2.1、EAT hook

这个和上面的思路基本一模一样,区别有两个:

  • 代码区别:更改使用IMAGE_DIRECTORY_ENTRY_EXPORT宏来定位导出表。
  • 使用区别:导出表该一个程序的,作用一个程序;导出表hook,直接改dll的,影响使用该dll的所有程序。

3、inline hook

顾名思义,就是直接改程序里的指令。类似于直接patch程序,但是是动态的。通过直接修改内存中任意函数的代码,将其劫持至Hook API。同时,它比IAT Hook的适用范围更广,因为只要是内存中有的函数它都能Hook,而后者只能Hook IAT表里存在的函数(有些程序会动态加载函数)。

被hook的位置

在进行hook的时候,最重要的就是找对hook的准确的地址,这样可以保证hook之后程序还可以正常执行,不会因为堆栈问题退出。注意的是hook过程中修改代码的位置不在调用函数中,而是在被调用函数的前几行。

能产生控制流分支的指令常见的无非call、jmp家族、ret这些,call和ret都会对栈产生影响,所以就用jmp进行hook最方便了。

思路

思路很简单,inline hook是通过直接修改进程代码来实现的,所以如果不考了稳定性,可以在一个程序的任意一个地方做hook方式,跳转到指定的代码段。

问题

有一个难点,hook的地址怎么找,虽然她可以在任意被执行的地方进行hook,但是如果确定地址呢?一般来说两种情况。

  • 自己hook自己
  • hook目标进程

hook自己的话,得到一些自定义的函数的地址是很方便的,其他位置的地址基本要靠偏移推测出来,所以实际上还是只能hook函数调用,无法任意地址修改。

hook目标进程,怎么找到对面的要修改jmp的地方的地址是大问题,一般操作都是针对win api进行的hook,对于对方自定义的函数调用hook实现比较困难,实在要hook只能人工介入手动分析算出偏移利用baseaddr进行计算从而进行hook。

应用场景

除了用在CTF出题中有些用,好像也没啥用了,想执行shellcode不如直接注入,而且操作其他进程也需要很多计算,自动化程度不高。

实现代码

使用jmp + 偏移地址的方式进行hook,去hook别的比较困难,这里就自己hook自己的方式,hook一个写好了但是没调用的函数,简单主要是。就32位的程序进hook,地址比较短。

这里需要注意的是,虽然jmp在汇编来看是jmp+dst_addr的格式,但是在实际字节码中,是jmp+offset-len的方式;第二个就是需要注意,0xeb和0xe9这两个的区别,虽然都是jmp指令,但是后面跟的地址不同。

0xE9 JMP 后面的四个字节是偏移

0xEB JMP 后面的二个字节是偏移

0xFF25 JMP 后面的四个字节是存放地址的地址

0xFF15 CALL 后面的四个字节是存放地址的地址

0xE8 CALL 后面的四个字节是地址

0x68 PUSH 后面的四个字节入栈

0x69 PUSH 后面的1个字节入栈

eb和e9这灵活搭配就行,可以先用eb这种目标地址短的方式跳到位置不那么紧张的地方,再用e9来实现跳转hook。

image-20230828162139858

还有需要注意的是,如果使用vs2022,第二次调用函数会被内联进去,所以hook的成功之后根本执行不到那里,导致看起来失败。

注意修改权限,可执行代码一般没有写的权限。先添加写权限,修改完事再恢复。这里不一定要用WriteProcessMemory函数,直接用memcpy也行。

DWORD oldProtect;
VirtualProtect(origin_func, 5, PAGE_EXECUTE_READWRITE, &oldProtect);
WriteProcessMemory(GetCurrentProcess(), (LPVOID)(addr_origfunc), newcode, 2, NULL);
VirtualProtect(origin_func, 5, oldProtect, &oldProtect);

4、热补丁 - HotFix HOOK

之前的方式需要修改程序然后再将程序修改回来,当进行全局Hook的时候,系统运行效率会受影响。而且,当一个线程尝试运行某段代码时,若另一个线程正在对该段代码进行“写”操作,这时就会程序冲突,最终引发一些错误。可以使用HotFix Hook(“热补丁”)方法弥补上述问题。

为啥叫热补丁,当目标进程不能停止的时候进行补丁操作的方式。这种方式并没有找到太多的解释和定义,他不是一种特定的技术,而是一类技术的概况,精细化到操作上可以按照如下理解:

差异 和inline hook

这是正常的调用一个api时候的汇编

image-20230828172812910

看到在push上面还有很多空白的地方可以操作,inline hook的方式就是直接e9+目标地址,hot fix使用的方法是 eb+5先跳到最上面那个ret下面,然后再进行e9的inline hook操作。

原因

这种方式更加安全,因为他并没有更改任何有意义的程序代码。inline hook会占用5字节的空间,这就遮挡了push和mov指令;hotfix方式只占用了mov edi,edi这个无意义的指令。即使是hook失败了,程序还是可以正常执行,不受影响。

没有版权,随便复制,免费的知识应该共享 all right reserved,powered by Gitbook该文章修订时间: 2023-08-28 17:34:43

results matching ""

    No results matching ""