Table of Contents

Reference


  • 《加密与解密》(第四版)第 13 章 HOOK 技术
  • 《逆向工程核心原理》第 32 章 计算器显示中文数字

IAT HOOK 原理


根据文章A Journey Towards an Import Address Table (IAT) of an Executable File中的一张图:

若我们找到了想要 HOOK 函数在 IAT 表中的具体位置,我们就可以通过修改该位置(该位置存放的是指针)指针的值为我们自己编写的函数的地址(在此之前肯定要把这个函数先加载到进程空间),但该函数的参数必须与被 HOOK 的函数完全一致

对本进程进行 HOOK


完整源代码文件IATHookMsgBox.cpp(代码量 200+行这里就不完整贴出来了)及编译好的二进制可执行文件IATHookMsgBox.exe都能在《加密与解密》第 13 章的随书文件中找到,下载链接见我的另一篇文章

程序主函数

IATHookMsgBox.cpp主函数部分:

int main(int argc, char *argv[ ])
{
    BOOL bIsWow64 = IsWow64();
    printf("IsWow64 = %d\n",bIsWow64);
    ShowMsgBox("Before IAT Hook");
    IAT_InstallHook();
    ShowMsgBox("After  IAT Hook");
    IAT_UnInstallHook();
    ShowMsgBox("After  IAT Hook UnHooked");
    return 0;
}

根据函数名便能知道大致含义,主要还是看IAT_InstallHook函数

IAT_InstallHook 函数

函数IAT_InstallHook

BOOL IAT_InstallHook()
{
    BOOL bResult = FALSE ;
    HMODULE hCurExe = GetModuleHandle(NULL);
    PULONG_PTR pt ;
    ULONG_PTR OrginalAddr;
    bResult = InstallModuleIATHook(hCurExe,"user32.dll","MessageBoxA",(PVOID)My_MessageBoxA,&pt,&OrginalAddr);
    if (bResult)
    {
        printf("[*]Hook安装完毕! pThunk=0x%p  OriginalAddr = 0x%p\n",pt OrginalAddr);
        g_PointerToIATThunk = pt ;
        OldMessageBox = (PFN_MessageBoxA)OrginalAddr ;
    }
    return bResult;
}

函数InstallModuleIATHook,最重要的函数

//************************************
// FullName:    InstallModuleIATHook
// Description: 为指定模块安装IAT Hook
// Access:      public
// Returns:     BOOL
// Parameter:   HMODULE hModToHook , 待Hook的模块基址
// Parameter:   char * szModuleName , 目标函数所在模块的名字
// Parameter:   char * szFuncName , 目标函数的名字
// Parameter:   PVOID DetourFunc , Detour函数地址
// Parameter:   PULONG * pThunkPointer , 用以接收指向修改的位置的指针
// Parameter:   ULONG * pOriginalFuncAddr , 用以接收原始函数地址
//************************************
BOOL InstallModuleIATHook(
    HMODULE hModToHook,// IN
    char *szModuleName,// IN
    char *szFuncName,// IN
    PVOID DetourFunc,// IN
    PULONG_PTR *pThunkPointer,//OUT
    ULONG_PTR *pOriginalFuncAddr//OUT
    )
{
    PIMAGE_IMPORT_DESCRIPTOR  pImportDescriptor;
    PIMAGE_THUNK_DATA         pThunkData;
    ULONG ulSize;
    HMODULE hModule=0;
    ULONG_PTR TargetFunAddr;
    PULONG_PTR lpAddr;
    char *szModName;
    BOOL result = FALSE ;
    BOOL bRetn = FALSE;

    hModule = LoadLibrary(szModuleName);
    TargetFunAddr = (ULONG_PTR)GetProcAddress(hModule,szFuncName); //找到MessageBoxA函数的真实入口点
    printf("[*]Address of %s:0x%p\n",szFuncName,TargetFunAddr);
    printf("[*]Module To Hook at Base:0x%p\n",hModToHook); //被IAT HOOK的宿主程序的加载基址
    pImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)ImageDirectoryEntryToDat  (hModToHook, TRUE,IMAGE_DIRECTORY_ENTRY_IMPORT, &ulSize); //通过Windows API函数ImageDirectoryEntryToData找到导入表在内存中的位置
    printf("[*]Find ImportTable,Address:0x%p\n",pImportDescriptor);
    while (pImportDescriptor->FirstThunk) //找到MessageBoxA函数对应的USER32.dll
    {
        szModName = (char*)((PBYTE)hModToHook+pImportDescriptor->Name) ;
        printf("[*]Cur Module Name:%s\n",szModName);
        if (stricmp(szModName,szModuleName) != 0) //通过导入表数组元素中的Name字段判断当前对应的DLL是哪个
        {
            printf("[*]Module Name does not match, search next...\n");
            pImportDescriptor++;
            continue;
        }
        //程序的导入表处理完毕后OriginalFirstThunk可能是无效的,不能再根据名称来查找,而是遍历FirstThunk直接根据地址判断
        pThunkData = (PIMAGE_THUNK_DATA)((BYTE *)hModToHook + pImportDescriptor->FirstThunk);
        while(pThunkData->u1.Function)//找到USER32.dll中对应MessageBoxA的那个ThunkDate指针
        {
            lpAddr = (ULONG_PTR*)pThunkData;
            //找到了地址
            if((*lpAddr) == TargetFunAddr)
            {
                printf("[*]Find target address!\n");
                //通常情况下导入表所在内存页都是只读的,因此需要先修改内存页的属性为可写
                DWORD dwOldProtect;
                MEMORY_BASIC_INFORMATION  mbi;
                VirtualQuery(lpAddr,&mbi,sizeof(mbi));
                bRetn = VirtualProtect(mbi.BaseAddress,mbi.RegionSize,PAGE_EXECUTE_READWRITE,&dwOldProtect);
                if (bRetn)
                {
                    //内存页属性修改成功,继续下一步操作,先保存原始数据
                    if (pThunkPointer != NULL)
                    {
                        *pThunkPointer = lpAddr ;
                    }
                    if (pOriginalFuncAddr != NULL)
                    {
                        *pOriginalFuncAddr = *lpAddr ;
                    }
                    //修改地址
                    *lpAddr = (ULONG_PTR)DetourFunc;
                    result = TRUE ;
                    //恢复内存页的属性
                    VirtualProtect(mbi.BaseAddress,mbi.RegionSize,dwOldProtect,0);
                    printf("[*]Hook ok.\n");
                }

                break;
            }
            //---------
            pThunkData++;
        }
        pImportDescriptor++;
    }
    FreeLibrary(hModule);
    return result;
}

其实通读一遍IATHookMsgBox.cpp就能很好的理解整个 Hook 过程了

简化流程

程序加载到内存 -> DLL 被加载,其中的函数被导入,IAT 表中的的每个 Thunk 被刷新为指向了导入函数在内存中的真实地址 -> IAT_InstallHook 通过 windows 的 API 函数ImageDirectoryEntryToDat找到程序的导入表加载基址,然后遍历导入表并找到对应 MessageBoxAuser32.dll 文件的元素项 -> 通过该元素项中的FirstThunk项找到指向 MessageBoxA 的 Thunk -> 修改 Thunk 值,使其指向 My_MessageBoxA(由于是对本进程 HOOK,所以该函数已提前加载到内存中)达到 HOOK 的目的 -> IAT_UnInstallHook将 Thunk 的值改回原值,MessageBoxA函数调用恢复正常

程序运行

Hook 前:

Hook 后:

卸载 Hook 后:

动调验证

0x778CED60

此处是 MessageBoxA 加载到虚拟内存中的真实地址:

0x00510000

从内容可以看出,该值为该 PE 文件的加载基址

0x00598000

在 PE 工具中文件加载基址为0x400000,此时 Import Table 在内存中为0x488000

而从 winDBG 的!address命令来看,该文件的加载基址变为了0x510000,而 Import Table 也理应变为了0x598000

用 010Editor 打开验证,确实是输入表结构,然后就是一个一个元素进行 dll 名称的验证,直到找到USER32.dll这个 Entry

0x00598448

该地址中存的内容就是 Thunk 的内容

再反汇编0x00536ce4地址处的机器码,查看 Thunk 究竟指向了哪里,确实被改为了My_MessageBoxA而不是MessageBoxA

对其他进程进行 API HOOK(使计算器显示韩文数字)


这个其实就像是上面内容和我的上一篇DLL 注入文章的综合利用,即:

DLL 文件注入目标进程后,修改 IAT 来更改进程中调用的特定 API 的功能 ———— 《逆向工程核心原理》第 32 章

实验代码

具体代码可以参考《逆向工程核心原理》随书文件中第 32 章 计算器显示中文数字相关内容,原理在那本书中也讲的很清楚

这里我们需要编译两个程序

  • hookiat.dll(被注入到calc.exe中的 DLL 文件)
  • InjectDll.exe(将hookiat.dll注入到calc.exe中的程序)

还需要一个 windows 自带的计算器程序

  • calc.exe

Hook 的目标 API 函数为user32.SetWindowTextW

InjectDll.exe

其中的关键函数InjectDll

BOOL InjectDll(DWORD dwPID, LPCTSTR szDllName) //dwPID被注入的程序的PID,szDllName是注入的DLL文件
{
    HANDLE hProcess, hThread;
    LPVOID pRemoteBuf;
    DWORD dwBufSize = (DWORD)(_tcslen(szDllName) + 1) * sizeof(TCHAR);
    LPTHREAD_START_ROUTINE pThreadProc;

    if (!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID))) //按照被注入程序的PID打开被注入程序,并获得handler
    {
        DWORD dwErr = GetLastError();
        return FALSE;
    }

    pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize, MEM_COMMIT, PAGE_READWRITE); //请求一个可读可写的page?

    WriteProcessMemory(hProcess, pRemoteBuf, (LPVOID)szDllName, dwBufSize, NULL); //将注入DLL文件的路径及文件名写到这个才申请的page中??

    pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");
    hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, pRemoteBuf, 0, NULL); //创建一个线程执行LoadLibraryW函数,将即将注入的DLL文件Load进来
    WaitForSingleObject(hThread, INFINITE);

    CloseHandle(hThread);
    CloseHandle(hProcess);

    return TRUE;
}

hookiat.dll

LoadLibraryW函数 load 进calc.exe的 DLL,其中的 DllMain 函数

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    switch (fdwReason)
    {
    case DLL_PROCESS_ATTACH:
        // original API 주소 저장
        g_pOrgFunc = GetProcAddress(GetModuleHandle(L"user32.dll"),
                                    "SetWindowTextW");

        // # hook
        //   user32!SetWindowTextW() 를 hookiat!MySetWindowText() 로 후킹
        hook_iat("user32.dll", g_pOrgFunc, (PROC)MySetWindowTextW);
        break;

    case DLL_PROCESS_DETACH:
        // # unhook
        //   calc.exe 의 IAT 를 원래대로 복원
        hook_iat("user32.dll", (PROC)MySetWindowTextW, g_pOrgFunc);
        break;
    }

    return TRUE;
}

而其中的hook_iat函数就和之前 HOOK 自己进程中的处理差不多,都是通过遍历找到对应的 Thunk,并将其修改,这里就不复制过来了

我们要注意的是在hookiat.dllLoadLibraryW函数 load 进calc.exe后,我们的MySetWindowTextW也随之加载到了宿主函数的进程空间中,也就有了真实的地址

这里在贴上具体的MySetWindowTextW函数吧:

// 사용자 후킹 함수
BOOL WINAPI MySetWindowTextW(HWND hWnd, LPWSTR lpString)
{
    wchar_t *pNum = L"영일이삼사오육칠팔구";
    wchar_t temp[2] = {
        0,
    };
    int i = 0, nLen = 0, nIndex = 0;

    nLen = wcslen(lpString);
    for (i = 0; i < nLen; i++)
    {
        // '수'문자를 '한글'문자로 변환
        //   lpString 은 wide-character (2 byte) 문자열
        if (L'0' <= lpString[i] && lpString[i] <= L'9')
        {
            temp[0] = lpString[i];
            nIndex = _wtoi(temp);
            lpString[i] = pNum[nIndex];
        }
    }

    // user32!SetWindowTextW() API 호출
    //   (위에서 lpString 버퍼 내용을 변경하였음)
    return ((PFSETWINDOWTEXTW)g_pOrgFunc)(hWnd, lpString);
}

实验结果演示

进行 Hook:

可见,在数字显示栏显示了韩文,而不是本应显示的数字

取消 Hook:

取消后,框内可显示正常数字