- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
IAT(Import Address Table)Hook是一种针对Windows操作系统的API Hooking 技术,用于修改应用程序对动态链接库(DLL)中导入函数的调用。IAT是一个数据结构,其中包含了应用程序在运行时使用的导入函数的地址.
IAT Hook的原理是通过修改IAT中的函数指针,将原本要调用的函数指向另一个自定义的函数。这样,在应用程序执行时,当调用被钩子的函数时,实际上会执行自定义的函数。通过IAT Hook,我们可以拦截和修改应用程序的函数调用,以实现一些自定义的行为,比如记录日志、修改函数参数或返回值等.
IAT Hook的步骤通常包括以下几个步骤:
该技术常用于实现一些系统级的功能,例如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 此时我们只需要在此处进行挂钩,并转向即可实现劫持效果,具体来说这个流程如下所示; 。
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的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
原谅那个疯狂的标题... 我试图理解面向对象编程中继承与接口(interface)的概念。所以我试图将它与我已经知道的东西联系起来,这就是 CSS。 在 CSS 中,您可以选择在允许元素“继承”样式的
我有一个 C 函数,它返回一个表示二进制数据的 unsigned char*。我在文档中注意到 SWIG 有一个很好的类型映射来处理二进制数据作为 C 函数的输入,但是当 C 函数返回二进制数据及其无
过去遇到过几次类似的问题,想知道用什么语言(方法)来解决类似的问题(我是J2EE/java开发人员): 问题:在一组可能的单词中,根据给定的规则(假设单词可以是 A 和 X 的组合,并且始终以 X 开
这个问题不太可能帮助任何 future 的访客;它只与一个小地理区域、一个特定时刻或一个非常狭窄的情况相关,而这些情况通常不适用于互联网的全局受众。如需帮助使这个问题更广泛地适用,visit the
如果我们已经开发了自己的ORM框架并且该框架在过去的几年中运行良好,那么为什么我们要为即将到来的软件项目学习和使用全新的.net技术,例如LINQ或Entity Framework或NHibernat
即使听起来很奇怪,我相信每个人在处理具有大量自定义组件的大型应用程序时都遇到过此类问题。某个地方生成了 AV,但应用程序仍在继续执行,稍后会出现错误。我不是在谈论多线程应用程序。只是关于通用的单线程应
我正在设计一个新项目,我正在尝试找出将数据/事件从服务器应用程序推送到客户端应用程序(即 WPF 应用程序)的方法。 我知道的两个是: 发布/订阅(即 NServiceBus) Full Duplex
关闭。这个问题不满足Stack Overflow guidelines .它目前不接受答案。 想改善这个问题吗?更新问题,使其成为 on-topic对于堆栈溢出。 5年前关闭。 Improve thi
这个问题在这里已经有了答案: C# .NET: How to check if we're running on battery? (6 个答案) 关闭 9 年前。 我发现许多 API 可以帮助确定
没有 JQUERY!我有一个下拉列表,用户可以在其中选择日期、月份和年份。我创建以下代码并使用 setFullYear 将这些值传递到变量中。有时我还会向这个变量添加天数,这就是变量 ev_num 的
我有一个控件,我想在表单和打印时以不同的方式绘制它。这是我做的方式: private void printDocument1_PrintPage(object sender, System.Drawi
我正在尝试确定从扫描文档中提取手写数据的最佳方法。 手写数据位于特定的方框区域。我生成了文档的数字版本,因此我知道方框区域的坐标,并且如果需要还可以生成文档的其他变体(即被屏蔽以使字段更容易提取的版本
背景 对于基于音乐的问题,我深表歉意,但细节并没有那么重要。我正在按顺序浏览一个 midi 文件,我正在寻找一种有效的方法来查找数据中的模式以找到称为连音符的东西。见下图: 连音符上方有数字(3 或
经验丰富的 Java 新手,寻求您的智慧: 如果无法确保在对象超出范围时执行某些特定的 block 代码,那么还有哪些其他方法可以提供相同的功能?(看起来 finalize 显然不是那个意思) 一个典
我正在玩一个小的油漆应用程序。我想创建不同的画笔提示(不仅仅是简单的线条)。基本思想是沿着鼠标移动重复(冲压)画笔 Nib 。因为鼠标移动不会为鼠标移动的每个像素分派(dispatch)所需的事件。我
我正在制作时间表应用程序。重要的类是: Period id: int clazz: Clazz SubjectTeacher subject: String teac
关闭。这个问题需要更多 focused .它目前不接受答案。 想要改进这个问题吗? 更新问题,使其只关注一个问题 editing this post . 关闭 4 年前。 Improve this q
我有一个奇怪的任务要解决。我们有一个小型视频窗口(如 300x200 像素,256 色调色板)和 44kHz 2ch 声音在服务器上播放。我们需要将此流视频发送给一些客户端(1,2.. 最多 10 个
我很确定我在这里遗漏了一些东西,因为我对 Shapeless 还很陌生并且我正在学习,但是 Aux 技术实际上什么时候开始需要 ?我看到它是用来暴露一个 type通过将其提升为另一个“同伴”的签名来声
微软有什么理由仍然坚持使用 COM 技术(Office 组件仍然是 COM)……当所有用 COM 完成的事情都可以用 .Net 以更好、更有效的方式完成时 最佳答案 因为它需要一个 长完全重写Offi
我是一名优秀的程序员,十分优秀!