当前位置: 首页 > 技术干货 > 导入地址表钩取技术解析

导入地址表钩取技术解析

发表于:2024-06-05 16:48 作者: hope 阅读数(837人)

前置知识

导入表

在一个可执行文件需要用到其余DLL文件中的函数时,就需要用到导入表,用于记录需要引用的函数。例如我们编写的可执行文件需要用到CreateProcess函数,就需要用到kernel32.dll文件并且将其中的CreateProcess函数的信息导入到我们的可执行文件中,然后再调用。

为了管理这些导入函数,就构建了一个导入表进行统一的管理,简单来说,当我们编写的可执行文件中使用到导入函数就会去导入表中去搜索找到指定的导入函数,获取该导入函数的地址并调用。

image-20240424192254287

因此加载器再调用导入函数之前需要先找到导入表的所在处。在可执行文件映射到内存空间是,都是以Dos Header开始的,在该头部存在elfanew的字段,用于记录PE文件头的偏移,在PE文件头存在可选头的结构体,该结构体中存储数据目录项,其中就包括了导入表。因此在内存中我们需要通过Dos Header -> Nt Header -> Option Header -> Import Table的顺序获取导入表。

image-20240424193422922

这里使用《加密与解密》的图来看一下导入表的结构体,如下图。

image-20240424195938393

可以看到导入表涉及的变量非常多,这里重点关注OriginalFirstThunkFistThunk以及Name

  • Name:指向导入库的名称。

  • OriginalFistThunk:指向输入名称表,里面存储了导入函数的信息。

  • FirstThunk:指向输入地址表,可以看到在初始化的时候OriginalFistThunkFirstThunk指向的是同一块区域,即导入函数的信息。

输入名称表的结构体如下图,这里重点关注OrdinalAddressOfData

  • Ordinal:记录函数的序号,即导入函数以序号存储

  • AdressOfData:以函数命的形式记录导入函数

image-20240424201610702

那么INTIAT的区别在于,加载器会在从导入表中获取了导入函数名称后,会搜索该函数的名称并获取该函数的地址并填入到IAT中,因此在经历了加载器后,IAT中存储了实际地址。如下图。

image-20240424202850423

导入地址表钩取技术

输入地址表钩取技术就是通过修改输入地址表的地址值,因此当调用该导入函数时会跳转到被篡改的地址上。

在钩取之前的状态如下图

image-20240424204535373

在钩取之后的状态如下图

image-20240424204853835

因此总结一下输入地址表钩取技术的流程

  • 确定需要钩取的导入函数

  • 获取输入地址表的地址

  • 在输入地址表中搜索需要钩取的导入函数地址并且将导入函数地址修改为自定义的函数

  • 在处理完之后需要在自定义函数中重新调用被钩取的函数

确定需要钩取的导入函数

首先确定可执行文件中存在什么导入函数,可以发现目标的可执行文件中导入了kernel32.dll的系统库,并且导入的CreateProcessW

image-20240424205932029

那么采用输入地址表钩取方法钩取CreateProcessW函数。

获取导入地址表的地址

根据DOS Header -> Nt Header -> Option Header ->Import Table的顺序进行搜索,即可获取导入地址表的地址。

代码如下

...
       //获取当前进程的基地址
   hMod = GetModuleHandle(NULL);
   pBase = (PBYTE)hMod;
//进程的基地址是从DOS头开始的
   pImageDosHeader = (PIMAGE_DOS_HEADER)hMod;
//通过e_lfanew变量获取NT头的偏移,然后加上基地址及NT头的位置
   pImageNtHeaders = (PIMAGE_NT_HEADERS)(pBase + pImageDosHeader->e_lfanew);
//数据目录项下标为1的项是导入表
   pImageImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(pImageNtHeaders->OptionalHeader.DataDirectory[1].VirtualAddress + pBase)
...

获取导入函数地址并修改

在获取导入地址表的地址后,首先通过遍历导入表的结构体,提取其中的Name字段,判断是否为我们需要钩取的导入库名。在匹配完成后则选择继续遍历IAT中的函数地址,找到需要钩取的函数地址,找到后则修改为自定义函数的地址。

image-20240424212559365

代码如下

    ...
   //遍历导入表项
   for (; pImageImportDescriptor->Name; pImageImportDescriptor++)
  {
       //获取导入库的名称
       szLibName = (LPCSTR)(pImageImportDescriptor->Name + pBase);
       //比较导入库的名称,判断是否为kernel32.dll
       if (!_stricmp(szLibName, szDllName))
      {
           //获取IAT
           PIMAGE_THUNK_DATA pImageThunkData = (PIMAGE_THUNK_DATA)(pImageImportDescriptor->FirstThunk + pBase);
           //获取导入函数地址
           for (; pImageThunkData->u1.Function; pImageThunkData++)
          {
               //判断函数地址是否是需要钩取的函数地址,这里需要注意的是64位与32位地址的区别
               if (pImageThunkData->u1.Function == (ULONGLONG)pfnOrg)
              {
                   //修改IAT的权限为可写
                   VirtualProtect(&pImageThunkData->u1.Function, 4, PAGE_EXECUTE_READWRITE, &dwOldProtect);
                   //将原始的地址修改为自定义函数地址
                   pImageThunkData->u1.Function = (ULONGLONG)pfnNew;
                   //将权限恢复
                   VirtualProtect(&pImageThunkData->u1.Function, 4, dwOldProtect, &dwOldProtect);
                   return TRUE;
              }
          }
      }
      ...

在自定义函数中重新调用被钩取的函数

这里需要注意的是,我们需要构建一个自定函数,该函数的返回类型与参数需要与钩取的函数一模一样,这样我们就可以获取所有参数的信息,然后篡改后重新传递给原始的导入函数,即可完成钩取。

image-20240424213549505

代码如下,这里篡改原始CreateaProcessW函数的第一个参数,使计算器

...
   LPCWSTR applicationName = L"C:\\Windows\\System32\\calc.exe";
   
   return ((LPFN_CreateProcessW)g_pOrgFunc)(applicationName,
       lpCommandLine,
       lpProcessAttributes,
       lpThreadAttributes,
       bInheritHandles,
       dwCreationFlags,
       lpEnvironment,
       lpCurrentDirectory,
       lpStartupInfo,
       lpProcessInformation);
...

完整代码:https://github.com/h0pe-ay/HookTechnology/blob/main/Hook-IAT/iat.cpp

调试

刚开始使用的是xdbg调试,但是用的不太习惯,后面改用WinDbg还可以源码调试,这里记录一下需要用到的操作与指令。

符号表与源码加载

在设置中可以选择源码默认的目录以及符号表默认的目录,符号文件则是利用Visutal Studio编译生成的pdb文件。

其中srv*c:\Symbols*https://msdl.microsoft.com/download/symbols是下载官方的符号表文件,这里可以选择删掉只调试我们设置的文件。不然每次都需要下载一遍影响时间。

源码文件也可以在侧边栏选择Open source file选项打开。

image-20240425103000930

DLL加载调试

由于钩取时需要先使用DLL注入技术将自定义的DLL文件注入进去,因此想要调试钩取过程则需要在DLL附着的时候打下断点。

利用sxe ld:xxx.dll即可在加载xxx.dll的时候打下断点。

利用sxe ud:xxx.dll即在卸载xxx.dll的时候打下断点。

image-20240425103802758

关闭优化调试

防止自定义函数中的变量被优化导致不方便单步调试,在Visual Studio中可以选择关闭优化进行编译。

image-20240425104053895