gpt4 book ai didi

4.3IATHook挂钩技术

转载 作者:我是一只小鸟 更新时间:2023-09-15 15:05:01 41 4
gpt4 key购买 nike

IAT(Import Address Table)Hook是一种针对Windows操作系统的API Hooking 技术,用于修改应用程序对动态链接库(DLL)中导入函数的调用。IAT是一个数据结构,其中包含了应用程序在运行时使用的导入函数的地址.

IAT Hook的原理是通过修改IAT中的函数指针,将原本要调用的函数指向另一个自定义的函数。这样,在应用程序执行时,当调用被钩子的函数时,实际上会执行自定义的函数。通过IAT Hook,我们可以拦截和修改应用程序的函数调用,以实现一些自定义的行为,比如记录日志、修改函数参数或返回值等.

IAT Hook的步骤通常包括以下几个步骤:

  • 获取目标函数的地址:通过遍历模块的导入表,找到目标函数在DLL中的地址。
  • 保存原始函数地址:将目标函数的地址保存下来,以便后续恢复。
  • 修改IAT表项:将目标函数在IAT中对应的函数指针修改为自定义函数的地址。
  • 实现自定义函数:编写自定义的函数,该函数会在被钩子函数被调用时执行。
  • 调用原始函数:在自定义函数中,可以选择是否调用原始的被钩子函数。

该技术常用于实现一些系统级的功能,例如API监控、函数跟踪、代码注入等,接下来笔者将具体分析 IAT Hook 的实现原理,并编写一个 DLL 注入文件,实现 IAT Hook 替换 MessageBox 弹窗的功能.

回到顶部

分析导入表结构

在早些年系统中运行的都是 DOS 应用,所以 DOS 头结构就是在那个年代产生的,那时候还没有 PE 结构的概念,不过软件行业发展到今天 DOS 头部分的功能已经无意义了,但为了最大的兼容性微软还是保留了 DOS 文件头,有些软件在识别程序是不是可执行文件的时候通常会读取 PE 文件的前两个字节来判断是不是MZ.

上图就是PE文件中的DOS部分,典型的DOS开头 ASCII 字符串 MZ 幻数,MZ是 Mark Zbikowski 的缩写, Mark Zbikowski 是 MS-DOS 的主要开发者之一,很显然这个人给微软做出了巨大的贡献.

在DOS格式部分我们只需要关注标红部分,标红部分是一个偏移值 000000F8h 该偏移值指向了PE文件中的标绿部分 00004550 指向PE字符串的位置,此外标黄部分为DOS提示信息,当我们在DOS模式下执行一个可执行文件时会弹出 This program cannot be run in DOS mode. 提示信息.

上图中在PE字符串开头位置向后偏移1字节,就能看到黄色的 014C 此处代表的是机器类别的十六进制表示形式,在向后偏移1个字节是紫色的 0006 代表的是程序中的区段数,继续向后偏移1字节会看到蓝色的 5DB93874 此处是一个时间戳,代表的是自 1970年1月1日 至当前时间的总秒数,继续向后可看到灰色的 000C 此处代表的是链接器的具体版本.

上图中我们以PE字符串为单位向后偏移36字节,即可看到文件偏移为120处的内容,此处的内容是我们要重点研究的对象.

在文件FOA偏移为 120 的位置,可以看到标红色的地址 0001121C 此处代表的是程序装入内存后的入口点(虚拟地址),而紧随其后的橙色部分 00001000 就是代码段的基址,其后的粉色部分是数据段基址,在数据基址向后偏移1字节可看到紫色的 00400000 此处就是程序的建议装入地址,如果编译器没有开启基址随机化的话,此处默认就是 00400000 ,开启随机化后建议装入地址与实际地址将不符合.

继续向下文件FOA偏移为 130 的位置,第一处浅蓝色部分 00001000 为区段之间的对齐值,深蓝色部分 00002000 为文件对其值.


上面只简单的介绍了PE结构的基本内容,在PE结构的开头我们知道了区段的数量是 6 个,接着我们可以在PE字符串向下偏移 244 个字节的位置就能够找到区段块,区块内容如下:

上图可以看到,我分别用不同的颜色标注了这六个不同的区段,区段的开头一般以 .xxx 为标识符其所对应的机器码是 2E ,其中每个区块分别占用 40 个字节的存储空间.

我们以 .text 节为例子,解释下不同块的含义,第一处绿色的位置就是区段名称该名称总长度限制在 8 字节以内,第二处深红色标签为虚拟大小,第三处深紫色标签为虚拟偏移,第四处蓝色标签为实际大小,第五处绿色标签为区段的属性,其它的节区属性与此相同,此处就不再赘述了.


接着继续看一下导入表,导出表,基址重定位表,IAT表,这些表位于PE字符串向后偏移116个字节的位置,如下我已经将重要的字段备注了颜色:

首先第一处浅红色部分就是导出表的地址与大小,默认情况下只有DLL文件才会导出函数所以此处为零,第二处深红色位置为导入表地址而后面的黄色部分则为导入表的大小,继续向下第三处浅蓝色部分则为资源表地址与大小,第四处棕色部分就是基址重定位表的地址,默认情况下只有DLL文件才会重定位,最下方的蓝色部分是 IAT 表的地址,后面的黄色为 IAT 表的大小.

此时我们重点关注一下导入表RVA地址 0001A1E0 我们通过该地址计算一下导入表对应到文件中的位置.

计算公式:FOA = 导入RVA表地址 - 虚拟偏移 + 实际偏移 = > 0001A1E0 - 11000 + 400 = 95E0 。

通过计算可得知,导入表位置对应到文件中的位置是 0x95E0 ,我们直接跟随过去但此时你会惊奇的发现这里全部都是0,这是因为 Windows 装载器在加载时会动态的获取第三方函数的地址并自动的填充到这些位置处,我们并没有运行EXE文件所以也就不会填充,为了方便演示,我们将程序拖入 x64dbg 让其运行起来,然后来看一个重要的结构.

                        
                          typedef struct _IMAGE_IMPORT_DESCRIPTOR
{
    union
    {
        DWORD   Characteristics;
        DWORD   OriginalFirstThunk;     // 指向导入表名称的RVA
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp;              // 默认为0(非重点)
    DWORD   ForwarderChain;             // 默认为0(非重点)
    DWORD   Name;                       // 指向DLL名字的RVA
    DWORD   FirstThunk;                 // 导入地址表IAT的RVA
} IMAGE_IMPORT_DESCRIPTOR;

                        
                      

该 IMAGE_IMPORT_DESCRIPTOR 导入表结构的大小为 4*5 = 20 个字节的空间,导入表结构结束的位置通常会通过使用一串连续的 4*5 个 0 表示结束,接下来我们将从后向前逐一分析这个数据结构所对应到程序中的位置.


通过上面对导入表的分析我们知道了导入表RVA地址为 0001A1E0 此时我们还知道 ImageBase 地址是 00400000 两个地址相加即可得到导入表的虚拟VA地址 0041a1e0 ,此时我们可以直接通过 x64dbg 的数据窗口定位到 0041a1e0 可看到如下地址组合,结合 IMAGE_IMPORT_DESCRIPTOR 结构来分析.

如上所示,可以看到该程序一共有3个导入结构分别是红紫黄色部分,最后是一串零结尾的字符串,标志着导入表的结束,我们以第1段红色部分为例,最后一个地址偏移 0001A15C 对应的就是导入表中的 FirstThunk 字段,我们将其加上 ImageBase 地址,定位过去发现该地址刚好是 LoadIconW 的函数地址,那么我们有理由相信紧随其后的地址应该是下一个外部函数的地址,而事实也正是如此.

接着我们继续来分析 IMAGE_IMPORT_DESCRIPTOR 导入结构中的 Name 字段,其对应的是第一张图中的红色部分 0001A54A 将该偏移与基址 00400000 相加后直接定位过去,可以看到 0041A54A 对应的字符串正是 USER32.dll 动态链接库,而后面会有两个 00 标志着字符串的结束.

最后我们来分析 IMAGE_IMPORT_DESCRIPTOR 中最复杂的一个字段 OriginalFirstThunk 为什么说它复杂呢?是因为他的内部并不是一个数值而是嵌套了另一个结构体 IMAGE_THUNK_DATA ,我们先来看一下微软对该结构的定义:

                        
                          typedef struct _IMAGE_THUNK_DATA32
{
    union
    {
        DWORD ForwarderString;        // PBYTE 
        DWORD Function;               // PDWORD
        DWORD Ordinal;                // 序号
        DWORD AddressOfData;          // 指向 PIMAGE_IMPORT_BY_NAME
    } u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;

                        
                      

接着来找到 OriginalFirstThunk 字段在内存中的位置,由第一张图可知,图中的标红部分第一个四字节 0001A38C 就是它。我们加上基址 00400000 然后直接怼过去,并结合上方的结构定义研究一下; 。

该结构中我们需要关注 AddressOfData 结构成员,该成员中的数据最高位(红色)如果为1(去掉1)说明是函数的导出序号,而如果最高位为0则说明是一个指向 IMAGE_IMPROT_BY_NAME 结构(导入表)的RVA(蓝色)地址,此处因为我们找的是导入表所以最高位全部为零.

我们以上图中的第一个RVA地址 0001A53E 与基址相加,来看下该 AddressOfData 字段中所指向的内容是什么.

上图黄色部分是编译器生成的,而蓝色部分则为 LoadIconW 字符串与 FirstThunk 中的 0041A15C 地址指针是相互对应的,而最后面的 00 则表明字符串的结束,对比以下结构声明就很好理解了.

                        
                          typedef struct _IMAGE_IMPORT_BY_NAME
{
    WORD    Hint;           // 编译器生成的
    CHAR   Name[1];         // 函数名称,以0结尾的字符串
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

                        
                      

为了能更加充分的理解,笔者为大家用 Excel 画了一张图,如下所示:

如上图 IMAGE_IMPORT_DESCRIPTO 导入表结构中的 FirstThunk 和 OriginalFirstThunk 分别指向两个相同的 IMAGE_THUNK_DATA 结构,其中内存 INT(Improt Name Table) 表中存储的就是导入函数的名称,而 IAT(Improt Address Table) 表中存放的是导入函数的地址,他们都共同指向 IMAGE_IMPORT_BY_NAME 结构,而之所以使用两份 IMAGE_THUNK_DATA 结构,是为了最后还可以留下一份备份数据用来反过来查询地址所对应的导入函数名,看了这张图再结合上面的实验相信你已经理解了; 。

回到顶部

实现导入表劫持

在之前的内容中我们已经分析了导入表结构,接着我们将实现对导入表的劫持功能,我们需要使用 IAT Hook 就必须要首先找到导入表中特定的函数地址,首先我们先实现枚举定位功能,通过枚举程序中的 IMAGE_IMPORT_DESCRIPTOR 结构在其中找到对应的导入模块 user32.dll 并在该模块内寻找对应的函数名 MessageBox ,通过使用双层循环即可实现对特定导入函数的枚举,如下是一段枚举导入表函数的功能; 。

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

#pragma comment (lib, "Dbghelp")

int main(int argc, char* argv[])
{
  // 打开文件
  HANDLE hFile = CreateFile("d://lyshark.exe", GENERIC_READ, FILE_SHARE_READ,
    NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

  // 创建内存映射
  HANDLE hMap = CreateFileMapping(hFile, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, 0);
  LPVOID lpBase = MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0);
  
  // 得到DOS头部
  PIMAGE_DOS_HEADER pDosHdr = (PIMAGE_DOS_HEADER)lpBase;
  if (pDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
  {
    UnmapViewOfFile(lpBase);
    return -1;
  }

  // 得到NT头部
  PIMAGE_NT_HEADERS pNtHdr = (PIMAGE_NT_HEADERS)((DWORD)lpBase + pDosHdr->e_lfanew);
  if (pNtHdr->Signature != IMAGE_NT_SIGNATURE)
  {
    return -1;
  }

  DWORD dwNum = 0;

  // 数据目录表
  PIMAGE_IMPORT_DESCRIPTOR pImpDes = (PIMAGE_IMPORT_DESCRIPTOR)
    ImageDirectoryEntryToData(lpBase, TRUE, IMAGE_DIRECTORY_ENTRY_IMPORT, &dwNum);

  PIMAGE_IMPORT_DESCRIPTOR pTmpImpDes = pImpDes;

  // 枚举导入表
  while (pTmpImpDes->Name)
  {
    printf("[*] 链接库名称: %s \n", (DWORD)lpBase + (DWORD)pTmpImpDes->Name);
    PIMAGE_THUNK_DATA thunk = (PIMAGE_THUNK_DATA)(pTmpImpDes->FirstThunk + (DWORD)lpBase);

    int index = 0;
    while (thunk->u1.Function)
    {
      if (thunk->u1.Ordinal & IMAGE_ORDINAL_FLAG)
      {
        printf("导入序号: %08X \r\n", thunk->u1.Ordinal & 0xFFFF);
      }
      else
      {
        PIMAGE_IMPORT_BY_NAME pImName = (PIMAGE_IMPORT_BY_NAME)thunk->u1.Function;
        printf("函数名称: %-30s \t", (DWORD)lpBase + pImName->Name);
        DWORD dwAddr = (DWORD)((DWORD *)((DWORD)pNtHdr->OptionalHeader.ImageBase
          + pTmpImpDes->FirstThunk) + index);
        printf("导入地址: 0x%08x \r\n", dwAddr);
      }
      thunk++;
      index++;
    }
    pTmpImpDes++;
  }

  system("pause");
  return 0;
}

                        
                      

读者可自行编译并运行上方代码片段,当运行后即可输出 d://lyshark.exe 程序中所有的导入库与该库中的导入函数信息,输出效果如下图所示; 。

当有了枚举导入表功能,则下一步是寻找特定函数的导入地址,以 MessageBoxA 函数为例,该函数的导入地址是 0x0047d3a0 此时我们只需要在此处进行挂钩,并转向即可实现劫持效果,具体来说这个流程如下所示; 。

  • 首先需要编写DLL文件,在DLL文件中找出 MessageBox 的原函数地址。
  • 接着通过代码的方式找到 DOS/NT/FILE-Optional 头偏移地址。
  • 通过 DataDirectory[1] 数组得到导入表的起始 RVA 并与 ImageBase 基址相加得到 VA 内存地址。
  • 循环遍历导入表中的 IAT 表,找到与 MessageBox 地址相同的4字节位置。
  • 找到后通过 VirtualProtect 设置内存属性可读写,并将自己的函数地址写入到目标 IAT 表中。
  • 没有找到的话直接 pFirstThunk++ 循环遍历后面的4字节位置,直到找到为止。
  • 最后将自身弹窗回调函数 MyMessageBoxA 与原函数做替换,则此时即可实现劫持功能。

通过上述开发流程,读者应该可以自行编写出这段劫持代码,如下代码则是完整的劫持实现,我们通过自定义 MyMessageBoxA 函数,并通过 IATHook() 实现对内存中导入函数地址的替换,此时当有新的访问时则会自动跳转到自定义函数上执行,执行结束后既跳转回 OldMessageBoxA 原函数上返回.

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

#pragma comment (lib, "Dbghelp")

typedef int(WINAPI *pfMessageBoxA)(HWND, LPCSTR, LPCSTR, UINT);
pfMessageBoxA OldMessageBoxA = NULL;

// 我们自己的回调函数
int WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType)
{
  return OldMessageBoxA(hWnd, "hello lyshark", lpCaption, uType);
}

// 得到进程NT头部
PIMAGE_NT_HEADERS GetLocalNtHead()
{
  DWORD dwTemp = NULL;
  PIMAGE_DOS_HEADER pDosHead = NULL;
  PIMAGE_NT_HEADERS pNtHead = NULL;

  // 取自身ImageBase
  HMODULE ImageBase = GetModuleHandle(NULL);

  // 取pDosHead地址
  pDosHead = (PIMAGE_DOS_HEADER)(DWORD)ImageBase;
  dwTemp = (DWORD)pDosHead + (DWORD)pDosHead->e_lfanew;

  // 取出NtHead头地址
  pNtHead = (PIMAGE_NT_HEADERS)dwTemp;
  return pNtHead;
}

// 劫持函数
void IATHook()
{
  PVOID pFuncAddress = NULL;

  // 取Hook函数地址
  pFuncAddress = GetProcAddress(GetModuleHandleA("user32.dll"), "MessageBoxA");

  // 保存原函数指针
  OldMessageBoxA = (pfMessageBoxA)pFuncAddress;

  // 获取到程序自身NtHead
  PIMAGE_NT_HEADERS pNtHead = GetLocalNtHead();
  PIMAGE_FILE_HEADER pFileHead = (PIMAGE_FILE_HEADER)&pNtHead->FileHeader;
  PIMAGE_OPTIONAL_HEADER pOpHead = (PIMAGE_OPTIONAL_HEADER)&pNtHead->OptionalHeader;

  // 找出导入表偏移
  DWORD dwInputTable = pOpHead->DataDirectory[1].VirtualAddress;
  DWORD dwTemp = (DWORD)GetModuleHandle(NULL) + dwInputTable;
  PIMAGE_IMPORT_DESCRIPTOR pImport = (PIMAGE_IMPORT_DESCRIPTOR)dwTemp;
  PIMAGE_IMPORT_DESCRIPTOR pCurrent = pImport;

  // 导入表子表,IAT存储函数地址表
  DWORD *pFirstThunk;

  // 遍历导入表
  while (pCurrent->Characteristics && pCurrent->FirstThunk != NULL)
  {
    // 找到内存中的导入表
    dwTemp = pCurrent->FirstThunk + (DWORD)GetModuleHandle(NULL);

    // 赋值 pFirstThunk
    pFirstThunk = (DWORD *)dwTemp;

    // 不为NULl说明没有结束
    while (*(DWORD*)pFirstThunk != NULL)
    {

      // 相等则找到了
      if (*(DWORD*)pFirstThunk == (DWORD)OldMessageBoxA)
      {
        DWORD oldProtected;

        // 开启写权限
        VirtualProtect(pFirstThunk, 0x1000, PAGE_EXECUTE_READWRITE, &oldProtected);
        dwTemp = (DWORD)MyMessageBoxA;
        
        // 将MyMessageBox地址拷贝替换
        memcpy(pFirstThunk, (DWORD *)&dwTemp, 4);

        // 关闭写保护
        VirtualProtect(pFirstThunk, 0x1000, oldProtected, &oldProtected);
      }

      // 继续递增循环
      pFirstThunk++;
    }

    // 每次是加1个导入表结构
    pCurrent++;
  }
}

BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
  switch (ul_reason_for_call)
  {
  case DLL_PROCESS_ATTACH:
    // 进程被加载后执行
    IATHook();
    break;
  case DLL_THREAD_ATTACH:
    // 线程被创建后加载
    break;
  case DLL_THREAD_DETACH:
    // 正常退出执行的代码
    break;
  case DLL_PROCESS_DETACH:
    // 进程卸载本Dll后执行的代码
    break;
  }
  return TRUE;
}

                        
                      

编译上方代码片段,并生成一个 hook.dll 文件,通过使用注入器将该模块注入到指定进程中,此时再次点击弹窗提示会发现功能已经被替换了,打开 x64dbg 也可看到模块已经被注入,如下图所示; 。

本文作者: 王瑞 本文链接: https://www.lyshark.com/post/f4e2e05e.html 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处! 。

最后此篇关于4.3IATHook挂钩技术的文章就讲到这里了,如果你想了解更多关于4.3IATHook挂钩技术的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。

41 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com