在一个可执行文件需要用到其余DLL
文件中的函数时,就需要用到导入表,用于记录需要引用的函数。例如我们编写的可执行文件需要用到CreateProcess
函数,就需要用到kernel32.dll
文件并且将其中的CreateProcess
函数的信息导入到我们的可执行文件中,然后再调用。
为了管理这些导入函数,就构建了一个导入表进行统一的管理,简单来说,当我们编写的可执行文件中使用到导入函数就会去导入表中去搜索找到指定的导入函数,获取该导入函数的地址并调用。
因此加载器再调用导入函数之前需要先找到导入表的所在处。在可执行文件映射到内存空间是,都是以Dos Header
开始的,在该头部存在elfanew
的字段,用于记录PE
文件头的偏移,在PE
文件头存在可选头的结构体,该结构体中存储数据目录项,其中就包括了导入表。因此在内存中我们需要通过Dos Header -> Nt Header -> Option Header -> Import Table
的顺序获取导入表。
这里使用《加密与解密》的图来看一下导入表的结构体,如下图。
可以看到导入表涉及的变量非常多,这里重点关注OriginalFirstThunk
、FistThunk
以及Name
Name
:指向导入库的名称。
OriginalFistThunk
:指向输入名称表,里面存储了导入函数的信息。
FirstThunk
:指向输入地址表,可以看到在初始化的时候OriginalFistThunk
与FirstThunk
指向的是同一块区域,即导入函数的信息。
输入名称表的结构体如下图,这里重点关注Ordinal
与AddressOfData
Ordinal
:记录函数的序号,即导入函数以序号存储
AdressOfData
:以函数命的形式记录导入函数
那么INT
与IAT
的区别在于,加载器会在从导入表中获取了导入函数名称后,会搜索该函数的名称并获取该函数的地址并填入到IAT
中,因此在经历了加载器后,IAT
中存储了实际地址。如下图。
输入地址表钩取技术就是通过修改输入地址表的地址值,因此当调用该导入函数时会跳转到被篡改的地址上。
在钩取之前的状态如下图
在钩取之后的状态如下图
因此总结一下输入地址表钩取技术的流程
确定需要钩取的导入函数
获取输入地址表的地址
在输入地址表中搜索需要钩取的导入函数地址并且将导入函数地址修改为自定义的函数
在处理完之后需要在自定义函数中重新调用被钩取的函数
首先确定可执行文件中存在什么导入函数,可以发现目标的可执行文件中导入了kernel32.dll
的系统库,并且导入的CreateProcessW
那么采用输入地址表钩取方法钩取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
中的函数地址,找到需要钩取的函数地址,找到后则修改为自定义函数的地址。
代码如下
...
//遍历导入表项
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;
}
}
}
...
这里需要注意的是,我们需要构建一个自定函数,该函数的返回类型与参数需要与钩取的函数一模一样,这样我们就可以获取所有参数的信息,然后篡改后重新传递给原始的导入函数,即可完成钩取。
代码如下,这里篡改原始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
选项打开。
由于钩取时需要先使用DLL
注入技术将自定义的DLL
文件注入进去,因此想要调试钩取过程则需要在DLL
附着的时候打下断点。
利用sxe ld:xxx.dll
即可在加载xxx.dll
的时候打下断点。
利用sxe ud:xxx.dll
即在卸载xxx.dll
的时候打下断点。
防止自定义函数中的变量被优化导致不方便单步调试,在Visual Studio
中可以选择关闭优化进行编译。