PE格式解析

一、什么是PE文件

PE文件是可移植的可执行的文件,什么是可执行文件?

可执行文件是为进程创建所服务的,进程在运行之前,需要将该进程所需要运行的代码、该进程支持的相关数据等一个进程创建所必须的信息以某种格式存储在磁盘中,而这种格式就是可执行文件格式。简单来说就是在进程创建之前会将该进程创建所需要的信息以可执行文件格式存储在磁盘中,同样也可以说进程创建所需要的信息会在可执行文件中详细记录。有了该进程的详细记录,那么操作系统在创建进程的时候就可以根据该进程的详细记录来创建进程,也就是说可执行文件是进程创建的前身,包含了程序在运行的时候需要的所有信息。但可执行文件和进程不能完全划等号,因为进程是动态的,是会根据你的操作变化而变化的;而可执行文件是静态的,是没有执行权、是死板的、是不会根据你的变化而变化的。而可移植的是因为微软在早期的一个战略目标,他想要达到只要是PC端就会有他们的影子,可以见到当时微软的野心是十分的大的,所以想要在任何平台上都可以运行那可移植性就十分重要了!(复制的)

我们先来看一下PE文件的大致结构:

img

主要包括4个组成部分: – MS-DOS头(Disk Operation System)

– NT头(New Technology)

–Section table 表(包含了所有的section头)

– 所有的section实体(段实体)

image-20250515203523062

我们先粗略的了解一下每个部分的具体作用:

DOS 头

  DOS 头分为“MZ 头部”和“DOS 存根”。MZ 头部才是真正的DOS头部,由于开始处的两个字节为“MZ”,因此 DOS 头也可以叫做 MZ 头。该头部用于程序在 DOS 系统下加载,他的结构被定义为 IMAGE_DOS_HEADER。

  DOS 存根是一段简单的 DOS 程序,主要用于输出类似“This program cannot be run in DOS mode.”的提示字符串。

  DOS 头主要是为了可执行程序可以兼容 DOS 系统。DOS 存根程序可以通过连接参数进行修改,使可执行文件同时在 Windows 和 DOS 系统同时运行。

PE 头

  PE 头部保存着 Windows 系统加载可执行文件的重要信息。PE 头部又 IMAGE_NT_HEADERS 定义。IMAGE_NT_HEADERS 是由 IMAGE_NT_SIGNATRUE(宏定义)、IMAGE_FILE_HEADER 和 IMAGE_OPTIONAL_HEADER 多个结构体组成。

  PE 头部在 PE 文件中的位置是不固定的,由 DOS 头部的某个字段给出

Section table

  程序的组织按照各属性的不同而保存在不同的节中,在 PE 头部之后就是一个结构体数组构成的节表。节表中描述了各个节在整个文件中的位置与加载入内存后的位置,同时定义了节的属性(只读、可读写、可执行等)。描述节表的结构体是 IMAGE_SECTION_HEADER,如果 PE 文件中有 N 个节,那么节表就是由 N 个 IMAGE_SECTION_HEADER 组成的数组。

section

  可执行文件中的真正程序代码部分就保存在 PE 结构的节中,当然,数据、资源等内容也保存在节中。节表只是描述了节数据的起始地址、大小及属性等信息。

image-20250515204342854

二、详解

(一):MS-DOS头

这里先给出MS-DOS头的结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct _IMAGE_DOS_HEADER { // DOS-MZ文件头
WORD e_magic;      // DOS 可执行文件的标识符
WORD e_cblp;      // Bytes on last page of file
WORD e_cp;      // Pages in file
WORD e_crlc;      // Relocations
WORD e_cparhdr;     // Size of header in paragraphs
WORD e_minalloc;     // Minimum extra paragraphs needed
WORD e_maxalloc;     // Maximum extra paragraphs needed
WORD e_ss;      // Initial (relative) SS value
WORD e_sp;      // Initial SP value
WORD e_csum;  
// ChecksumWORD e_ip;      // Initial IP valueWORD e_cs;      
// Initial (relative) CS value
WORD e_lfarlc;     // File address of relocation table
WORD e_ovno;      // Overlay number
WORD e_res[4];     // Reserved words
WORD e_oemid;      // OEM identifier (for e_oeminfo)
WORD e_oeminfo;     // OEM information; e_oemid specific
WORD e_res2[10];     // Reserved words
LONG e_lfanew;     // PE 签名的文件偏移量
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

DOS文件头是为了兼容DOS系统所遗留下来的产物,如果使用的不是DOS系统,我们只需要重点关注两个部分:

​ 第一个:WORD e_magic; 这个是DOS文件的标识符,大小为word(两个字节),保存的内容是“MZ”,十六进制是0x5A4D。这个就是用来标识文件的。

​ 第二个:LONG e_lfanew; 这个存放的是PE签名的文件偏移量,用来指出PE文件头的位置,大小为long(4个字节)

image-20250515205611085

此示例中,PE签名的位置就是0xDB,存放的内容是“PE\0\0”,用于将该文件标识为 PE 格式图像文件。

对了,还有一个DOS存根,内容是类似于“This program cannot be run in DOS mode.”的提示字符串。

(二):PE文件头

先放一个结构体

1
2
3
4
5
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; //该结构体中的Signature就是PE签名,标识该文件是否是PE文件。该部分占4字节,即“50 45 0000”。
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

一共由3部分组成:

​ –PE签名,类型为DWORD,大小为四字节,存储在signature变量当中,用于将该文件标识为 PE 格式图像文件。

​ –COFF文件头,FileHeader标准PE头,类型为结构IMAGE_FILE_HEADER。

​ –可选标头,OptionalHeader为可选标头,又称扩展PE头

(2.1)标准PE头

看一下他的结构体

1
2
3
4
5
6
7
8
9
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; // 标识目标计算机类型的数字。
WORD NumberOfSections; // 区段数量。
DWORD TimeDateStamp; // 自1970年1月1日00:00起的秒数的低 32 位 (C 运行时time_t值) ,该值指示文件创建时间。
DWORD PointerToSymbolTable; // COFF符号表的文件偏移量,如果没有COFF符号表,则为零。映像的此值应为零,因为 COFF 调试信息已弃用。
DWORD NumberOfSymbols; // 符号表中的条目数。此数据可用于查找紧跟在符号表后面的字符串表。映像的此值应为零,因为 COFF 调试信息已弃用。
WORD SizeOfOptionalHeader; // 可选标头的大小,这是可执行文件所必需的。
WORD Characteristics; // 指示文件属性的标志。
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

先别懵逼,我先懵一会

一个4个变量需要我们注意(前两个与后两个):

Machine、NumberOfSections、SizeOfOptionalHeader、Characteristics

Machine:

这个表……,我是记不住,但是感觉也没有要记忆得必要

img

在文件中对应着相应的values看目标计算机的类型即可,本实例的values为(14Ch),也就是I386类型

NumberOfSections

大小为两字节,表示PE文件的区段数量,本示例显示的是00 05,意思就是PE文件的区段是5个

SizeOfOptionalHeader

作用是表示可选标头的大小,即扩展PE头的大小,这是可执行文件所必需的。下图中这个对应的是00 E0,我们将E0换算成10进制为224,SizeOfOptionalHeader变量中的数据对于32位PE文件通常为00E0h,对于64位PE文件通常为00F0h。上图我们可以看到SizeOfOptionalHeader变量当中的数据为00 E0,扩展PE头大小为224,文件为32位PE文件。

Characteristics

是指示文件属性的标志。

下图中的数字表示为01 0E,我们把他转化为二进制,即0000 0001 0000 1110,对应的位置在第1,2,3,8位,然后对照着表就可以得到文件属性了。

img
image-20250515211353160

(2.2)扩展PE头

我们知道,标准PE头中的SizeOfOptionalHeader用于标识扩展PE头的大小。

此标头是可选的,因为某些文件 (具体来说,对象文件) 没有它。 对于PE文件,此标头是必需的。

此表头分为3个部分:

img

COFF是通用对象文件格式,而通用对象文件格式是指可执行文件(映像)和对象文件 32 位编程的格式。

标准字段

前八个字段:为每个 COFF 实现而定义的标准字段

img

AddressOfEntryPoint,该字段的值位相对虚拟地址(加载到内存的地址),该值表明程序最先执行代码的起始地址,即程序入口点

Windows-Specific字段

接下来的 21 个字段: COFF 可选标头格式的扩展。

它们包含链接器和加载程序在 Windows 中所需的其他信息。

img
img

ImageBase,字段表明PE文件加载进内存时,文件将被优先装入的虚拟内存的地址。必须是 64 K 的倍数。这里64k换算成十六进制是10000,64k的十进制大小是65536,这里提一下加载到内存中的地址是以十六进制的格式存储的,所以可以看到那些默认的加载到内存中的第一个图像字节的首选地址都是10000的倍数。所以可以看到加载到内存当中PE文件的第一个字节的首选位置是10000的倍数

装载后,EIP = Image + AddressOfEntryPoint

内存对齐:

内存对齐就好像比喻将物品进行分类,一个盒子装一类物品。而计算机将数据以某种格式进行分划,被划分的每一组数据占据一页或多页空间,每组被划分的数据起始位置必须符合一定的要求。

PE文件从磁盘映射到内存:

PE 文件在执行的时候,映射到内存中的结构布局与该文件在磁盘中存储时的结构布局是一致的。Windows 装载器(又称 PE 装载器)在载入一个 PE 文件时,把磁盘中的 PE 文件映射到进程的内存地址空间,是一种从磁盘偏移地址到内存偏移地址的转换。

img

有的PE文件进程空间中的对齐大小和磁盘中的对齐大小有区别,

进程空间当中的对齐大小默认值为系统页面大小 0x1000 B(4KB)、

在磁盘中存储时的对齐大小默认值为磁盘页面大小 0x200 B(512B)

image-20250515215627360

SizeOfImage字段:将PE文件装载到内存(进程空间)中占多大

从数据管理角度来看,PE 文件大致分为两个部分:

一是 DOS 头、PE 头和节表属于构成可执行文件的数据管理结构和数据组织结构部分;按照SectionAlignment的对齐值进行对齐

二是区段,这是可执行文件真正的数据部分,包含着程序执行时真正的代码、数据、资源等内容。标准PE头有记录PE文件的区段个数的NumberOfSections字段,每个区段独享一段SectionAlignment的对齐值大小的内存。

这两部分的总和 == SizeOfImage

SizeOfHeaders字段:磁盘空间中PE文件头(第一部分)对齐后的大小

PE文件校验和字段:

可执行文件的数据通过校验得到一个四字节的校验值就会存储在该字段里面。

用来校验PE文件是不是系统文件、在Windows下是不是一个驱动等

Windows子系统字段:

不多说,自己查表看吧

img

DllCharacteristics字段:

处理方式与Characteristics一致

img

后面的四个字段是关于堆栈的保留和提交

每个字段占4或8个字节大小:分别为栈保留、栈提交、堆保留、堆提交

保留就是最多可以占用多少空间,提交就是现在有多少空间可以立即使用。

换个说法,保留就是最多有多少钱可以用,提交就是手上还有多少钱可以由自己支配。

提交的空间会动态增加,就是开始手上的钱用完了,就去银行取。

NumberOfRvaAndSizes字段:记录数据目录表的个数

可选标头数据目录表
1
2
3
4
typedef struct _IMAGE_DATA_DIRECTORY {
``DWORD VirtualAddress;
``DWORD Size;
} IMAGE_DATA_DIRECTORY, ``*``PIMAGE_DATA_DIRECTORY;

第一个字段 VirtualAddress 实际上是表的 RVA。 RVA 是加载表时相对于映像基址的表地址,也就是在内存中相对于映像基址的偏移地址。

第二个字段提供大小(以字节为单位)。

img
image-20250515221315633

基址重定位

需要重定位的数据位置 = ImageBase + VirtualAddress + TypeOffset低12位

​ = 0x10000000 + 0x1000 + 0x006

假设有一个dll实际加载到了0x72ab0000,它的ImageBase却是0x10000000,系统会通过下面的公式进行重定位

重定位后地址 = (真实加载基址-默认加载基址) + 需要进行重定位的地址

​ = (0x72ab0000 - 0x10000000) + 0x10002210

导入表

什么是导入表

当我们PE文件运行的时候,需要调用一些外部接口,导入表就是记录导入的那些外部接口的。

当操作系统装载可执行文件的时候,

  1. 首先会分析可执行文件需要哪些动态链接库,然后会分析每个动态链接库需要哪些函数
  2. 当把这些函数加载进内存后,会把这些函数加载到内存的所在地址填到操作系统和编译器约好的位置。
  3. 在约好的地方填写完所需函数的加载地址后,当编译器在编译产生代码的时候要调用操作系统函数或者调用第三方函数时就会到约好的地方去间接的访问所需函数的地址,这样就完成了一次外部接口的调用。

问:操作系统与编译器约好存放API地址的位置是什么:IAT(import address table/导入地址表)

img

导入表是由一个或多个IMAGE_IMPORT_DESCRIPTO结构(上面那张导入表结构图最左边的结构体)组成的,每个IMAGE_IMPORT_DESCRIPTO结构总大小为20字节,每个IMAGE_IMPORT_DESCRIPTO结构记录一个需要导入到内存的动态链接库的信息,当IMAGE_IMPORT_DESCRIPTO结构为零时表示导入表结束,所以我们可以通过IMAGE_IMPORT_DESCRIPTO结构是否为零来判断导入表是否结束

每20个字节是导入表中一个需要导入到内存中的动态链接库的信息(当IMAGE_IMPORT_DESCRIPTO结构为零时表示结束)

这个结构体由五个字段组成,每个字段各4字节大小,其中比较重要的是:

第一个字段——导入名称表、第四个字段——动态链接库名称、第五个字段——导入地址表。

img

我们先根据数据目录中记录的导入表的RVA跳转到对应位置: 我们使用第一个20字节进行示范(好像这个实例只有一个): 根据导入名称表字段所记录的RVA(就是前4个字节)(0x2D02B0),去看看导入名称表的模样:

来到RVA = 2D028,以每四个字节为一组指向该动态链接库当中一个需要加载到内存的API名称所在位置的RVA

image-20250516142027082
image-20250516145138598

跟着第一个过去看看:

这就是一个需要导入到内存的API名称,这个名称是IMAGE_IMPORT_BY_NAME结构,

image-20250516142312979
image-20250516145209267

我们来看一下IMAGE_IMPORT_BY_NAME结构:

1
2
3
4
5
6
7
8
typedef struct _IMAGE_IMPORT_BY_NAME {

WORD Hint; // 编译时需导入的函数序号,但操作系统不参考

BYTE Name[1]; // 需导入的函数名称

} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
// 在该结构后面会尾随零填充字节(如有必要出现在尾随 null 字节之后),以在偶数边界上对齐下一个条目。

我们获取到该动态链接库中一个要加载到内存的API名称后,将该API加载到内存中,通过GetProcAddress()系统API来获取该API加载到内存的地址,那这个地址存放在哪里呢?那就需要读取IMAGE_IMPORT_DESCRIPTO结构最后一个字段——导入地址表,我们还是跟着IMAGE_IMPORT_DESCRIPTO结构导入地址表字段所记录的RVA(0x2F16C),去一探究竟:

image-20250516145236939

扩展PE头里面DataDirArray(可选标头数据目录表)里面的Import记载导入表的RVA

–>定位到位置之后,每20个字节是导入表中一个需要导入到内存中的动态链接库的信息

(第一个字段——导入名称表、第四个字段——动态链接库名称、第五个字段——导入地址表)

–>根据第一个字段找到导入名称表,根据名称表查询相应的名称

–>根据第五个字段找到导入地址表(IAT Hook就是修改的这个位置)

三、节表

节表是用来描述数据块的,还记得上面讲的PE文件从磁盘映射到内存当中吗?而节表描述其实就是描述这种映射关系,描述磁盘中节数据在哪里、占多大空间,之后映射到内存中又在哪里,又占多少空间。节表的每一行实际上是一个节标题。 此表紧跟可选标头(如果有)。 此定位是必需的,因为文件头不包含指向节表的直接指针。 相反,节表的位置是通过计算标头后第一个字节的位置来确定的。 请确保使用文件标头中指定的可选标头的大小。

结合上文一起来看一下内存映像:

节表中的条目数由文件标头中的 NumberOfSections 字段提供。 节表中的条目从 1 (1) 开始编号。 代码和数据内存部分条目按链接器选择的顺序排列。

在图像文件中,节的 VA 必须由链接器分配,以便它们按升序和相邻,并且必须是可选标头中 SectionAlignment 值的倍数。

每个节标题 (节表项) 具有以下格式,每个条目总共 40 个字节。

img

第一个字段就是节名称,节名称就类似于注释一样,它描述一下这个数据块是做什么的。因为该字段的作用类似于注释,可以随意修改且不会对程序产生什么影响。注意:节数据所在不是找节名称!因为节名称可以随意修改没有参考价值,要找节数据要通过数据目录进行寻找。

在讲下面四个字段时,我们需要先了解这些信息:

VA:虚拟内存地址(Virtual Address),PE 文件被操作系统加载进内存后的地址。

RVA:相对虚拟地址(Relative Virual Address),相对于image(虚拟内存基址)的偏移地址。RVA=VA-应用程序实例句柄(该模块加载到线性进程空间中的首地址)。

句柄:句柄就是主键,主键是身份的唯一标识,所以句柄就是身份的唯一标识。

FOA:文件偏移地址(File Offset Address),和内存无关,它是指磁盘中某个位置距离文件头的偏移。

从文件偏移到相对虚拟地址:

因为文件的内存对齐与内存的内存对齐之间的差异所产生的

举个例子:

有的转换软件RVA转FOA是先找该RVA在哪个区段,还是在PE文件头。找到后会用RVA去减节(区段)基址相对于PE文件加载基址的偏移量,得到RVA相对于该节基址的偏移量。下一步获取到该节在文件中的基址,将之前得到的RVA相对于该节基址的偏移量与该节在文件中的基址进行相加就得到了FOA。

比如RVA为10FA,在PE文件的代码段当中,代码段的基址为00401000,PE文件的加载基址为00400000,得出代码段的基址相对于PE文件的加载基址为1000,用10FA减去1000得到FA,FA即为10FA相对于该节基址的偏移量。代码段在文件中的基址是400,那么FOA就是4FA。

知道了RVA转换成FOA的流程后,你想啊!内存对齐值比文件对齐值大的时候,那么有一大部分地址相对于节基址的偏移量比文件当中节的大小都大,那些RVA是没法转换成FOA的。

https://bbs.kanxue.com/thread-277677.htm

Hook学习

image-20250515155257995

注入Hook

Inline Hook

大致实现步骤: 1.获取API的函数地址

2.修改API的起始字节,jmp跳转到我们自己的函数

3.在自定义的函数中实现unhook,恢复原API的起始字节

image-20250516152303798

那么我们的思路就是: 1.调用 GetModuleHandle 来获取到模块的基址(.dll) 2.调用 GetProcAddress 获取到函数弹窗的基址 3.调用 VirtualProtect 来修改hookAPI前5个字节内存属性 4.计算 Dest - HookAPI - 5 重定位跳转地址,并Jmp跳转 5.在自己的函数里面实现一些操作,然后进行脱钩

我们来Hook一下lstrcmp这个比较函数:

1.调用 GetModuleHandle 来获取到模块的基址(.dll) 2.调用 GetProcAddress 获取到lstrcmp弹窗的基址
1
2
3
4
5
6
7
8
9
BYTE pJmpCode[6] = { 0xE9,0, };
char pwd[15];
char key[15];
int ret;
char buf[] = "real_pwd";
DWORD dwOldProtect, pOffset, dwWritenSize;
hKernel32 = GetModuleHandleA("kernel32.dll");
plstrcmp = GetProcAddress(hKernel32, "lstrcmp");
pEditFunc = (PBYTE)plstrcmp;
3.调用 VirtualProtect 来修改hookAPI前5个字节内存属性
1
VirtualProtect(pEditFunc, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect)

将这个页改为可读可写可执行,然后用&dwOldProtect记录原本的内存属性,用于后续的恢复

4.计算 Dest - HookAPI - 5 重定位跳转地址,并Jmp跳转
1
2
3
4
5
memcpy(pOrgByte, pEditFunc, 5);
pOffset = (ULONGLONG)Mylstrcmp - (ULONGLONG)plstrcmp - 5;
memcpy(&pJmpCode[1], &pOffset, 4);
memcpy(plstrcmp, &pJmpCode[0], 5);
VirtualProtect(plstrcmp, 5, dwOldProtect, &dwOldProtect);

先将lstrcmp的前5个字节在pOrgByte存储下来

计算Mylstrcmp - lstrcmp - 5 重定位跳转地址,存放在pOffset

将这个地址放在JMP 的后面,就实现了跳转到我们写的函数里面了

再一次VirtualProtect是为了恢复原来那部分的内存属性

5.在自己的函数里面实现一些操作,然后进行脱钩
1
2
3
4
5
6
int __stdcall Mylstrcmp(LPCTSTR lpString1, LPCTSTR lpString2)
{
printf("hook!\n");
unhook();
return 0;
}

看一下脱钩操作:

1
2
3
4
5
6
7
8
9
10
11
void unhook()
{
DWORD dwOldProtect;
PBYTE plstrcmp;
FARPROC pFunc;
pFunc = GetProcAddress(GetModuleHandleA("kernel32.dll"), "lstrcmp");
plstrcmp = (PBYTE)pFunc;
VirtualProtect(plstrcmp, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);
memcpy(plstrcmp, pOrgByte, 5);
VirtualProtect(plstrcmp, 5, dwOldProtect, &dwOldProtect);
}

操作和HOOK差不多,也很好理解

image-20250516154807276
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#include<stdio.h>
#include<windows.h>
#include<string.h>
#include<stdlib.h>
BYTE pOrgByte[5] = { 0, };
void unhook()

{
DWORD dwOldProtect;
PBYTE plstrcmp;
FARPROC pFunc;
pFunc = GetProcAddress(GetModuleHandleA("kernel32.dll"), "lstrcmp");
plstrcmp = (PBYTE)pFunc;
VirtualProtect(plstrcmp, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);
memcpy(plstrcmp, pOrgByte, 5);
VirtualProtect(plstrcmp, 5, dwOldProtect, &dwOldProtect);
}

int __stdcall Mylstrcmp(LPCTSTR lpString1, LPCTSTR lpString2)
{
printf("hook!\n");
unhook();
return 0;
}

int main()
{
HANDLE hFile;
HMODULE hKernel32;
FARPROC plstrcmp;
FILE* fp = NULL;
PBYTE pEditFunc;
BYTE pJmpCode[6] = { 0xE9,0, };
char pwd[15];
char key[15];
int ret;
DWORD dwOldProtect, pOffset, dwWritenSize;
char buf[] = "real_pwd";
hKernel32 = GetModuleHandleA("kernel32.dll");
plstrcmp = GetProcAddress(hKernel32, "lstrcmp");
pEditFunc = (PBYTE)plstrcmp;

if (VirtualProtect(pEditFunc, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect))
{
memcpy(pOrgByte, pEditFunc, 5);
pOffset = (ULONGLONG)Mylstrcmp - (ULONGLONG)plstrcmp - 5;
memcpy(&pJmpCode[1], &pOffset, 4);
memcpy(plstrcmp, &pJmpCode[0], 5);
VirtualProtect(plstrcmp, 5, dwOldProtect, &dwOldProtect);
}
hFile = CreateFileA("pwd.txt", GENERIC_ALL, 0, NULL, CREATE_ALWAYS, 0x80, NULL);

/*
if(hFile!=NULL)
printf("open1 success\n");
else
printf("open1 fail\n");
*/

WriteFile(hFile, buf, strlen(buf), &dwWritenSize, NULL);
CloseHandle(hFile);

if (errno_t err = fopen_s(&fp, "file.txt", "r"))
printf("open success\n");
else
printf("open fail\n");
if (fp != NULL)
fscanf_s(fp, "%s", pwd);
else
printf("scan fail\n");

printf("plz input key\n");
scanf_s("%s", key);

if (fp != NULL)
fclose(fp);

ret = lstrcmp((LPCTSTR)key, (LPCTSTR)pwd);

if (ret == 0)
printf("congratulations!\n");
else
printf("try again!\n");

system("Pause");
return 0;
}

未修改前: image-20250516155228654

修改后:

image-20250516155335998

脱钩后: image-20250516155524152

image-20250516155616119

IAT Hook

这个的思想就是修改目标API函数的地址,那么这个地址存放在哪里呢,请看上一部分

1.定位KERNEL32.dll里面IAT表的位置

2.在IAT表中寻找要Hook函数对应的地址条目

3.将WriteFile对应的地址条目替换为自己的函数地址

我们这个示例Hook KERNEL32.dll里面的WriteFile函数:

1.定位KERNEL32.dll里面IAT表的位置:

1
2
3
IMAGE_DOS_HEADER* pImgDosHdr = (IMAGE_DOS_HEADER*)hModule;
IMAGE_OPTIONAL_HEADER* pImgOptHdr = (IMAGE_OPTIONAL_HEADER*)((DWORD)hModule + pImgDosHdr->e_lfanew + 24);
IMAGE_IMPORT_DESCRIPTOR* pImgImportDes = (IMAGE_IMPORT_DESCRIPTOR*)((DWORD)hModule + pImgOptHdr->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);

首先获取DOS头,然后Dos头的e_lfanew字段段找到NT头,再偏移24字节定位到可选头(Optional Header)

从可选头的DataDirectory数组中获取导入目录表(Import Directory)的位置(VirtualAddress)

1
2
3
4
5
6
7
8
9
10
11
12
while (pImgImportDes->Characteristics != 0)
{
USES_CONVERSION;
LPCTSTR lpszName = ((LPCSTR)(DWORD)hModule + pImgImportDes->Name);
TargetLibraryName = lpszName;
if (TargetLibraryName.compare(pImageName) == 0)
{
pImgThunkData = (IMAGE_THUNK_DATA*)((DWORD)hModule + pImgImportDes->FirstThunk);
break;
}
pImgImportDes++;
}

遍历导入描述符(Import Descriptor)表,直到找到Characteristics为0的条目(表示结束)

对每个描述符获取DLL名称并与目标名称比较

找到匹配的DLL后,获取FirstThunk字段的地址,这就是该DLL的IAT表开始位置(本示例是kernel.dll)

2.在IAT表中寻找要Hook函数对应的地址条目 3.将WriteFile对应的地址条目替换为自己的函数地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
while ( pImgThunkData->u1.Function )
{
FuncAddress = ( LPDWORD ) & ( pImgThunkData->u1.Function );
if ( *FuncAddress == ( DWORD )pTargetFuncAddr )
{
VirtualProtect( FuncAddress, sizeof( DWORD ), PAGE_READWRITE, &OldProtect );
if ( !WriteProcessMemory( ( HANDLE ) - 1, FuncAddress, &pReplaceFuncAddr, 4, NULL ) )
{
return FALSE;
}
VirtualProtect( FuncAddress, sizeof( DWORD ), OldProtect, 0 );
return TRUE;
}
pImgThunkData++;
}
return FALSE;

在IAT表里面遍历寻找要替换的那个Hook函数(本示例是WriteFile)对应的地址,然后替换为自己的函数地址

剩余的操作其实与Inline Hook差不多

在MyWriteFile中,使用了p_WriteFile,会正常调用WriteFile函数

看一下整体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <tchar.h>
#include <string>
#include <atlbase.h>
BOOL IATHook(HMODULE hModule, LPCTSTR pImageName, LPCVOID pTargetFuncAddr, LPCVOID pReplaceFuncAddr )
{
IMAGE_DOS_HEADER* pImgDosHdr = ( IMAGE_DOS_HEADER* )hModule;
IMAGE_OPTIONAL_HEADER* pImgOptHdr = ( IMAGE_OPTIONAL_HEADER* )( ( DWORD )hModule + pImgDosHdr->e_lfanew + 24 );
IMAGE_IMPORT_DESCRIPTOR* pImgImportDes = ( IMAGE_IMPORT_DESCRIPTOR* )( ( DWORD )hModule + pImgOptHdr->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress );
IMAGE_THUNK_DATA* pImgThunkData = NULL;
std::string TargetLibraryName;
DWORD Value = 0;
DWORD OldProtect = 0;
DWORD NewProtect = 0;
LPDWORD FuncAddress = NULL;

while ( pImgImportDes->Characteristics != 0 )
{
USES_CONVERSION;
LPCTSTR lpszName = ( ( LPCSTR )( DWORD )hModule + pImgImportDes->Name ) ;
TargetLibraryName = lpszName;
if ( TargetLibraryName.compare( pImageName ) == 0 )
{
pImgThunkData = ( IMAGE_THUNK_DATA* )( ( DWORD )hModule + pImgImportDes->FirstThunk );
break;
}
pImgImportDes++;
}
if ( pImgThunkData == NULL )
{
return FALSE;
}
while ( pImgThunkData->u1.Function )
{
FuncAddress = ( LPDWORD ) & ( pImgThunkData->u1.Function );
if ( *FuncAddress == ( DWORD )pTargetFuncAddr )
{
VirtualProtect( FuncAddress, sizeof( DWORD ), PAGE_READWRITE, &OldProtect );
if ( !WriteProcessMemory( ( HANDLE ) - 1, FuncAddress, &pReplaceFuncAddr, 4, NULL ) )
{
return FALSE;
}
VirtualProtect( FuncAddress, sizeof( DWORD ), OldProtect, 0 );
return TRUE;
}
pImgThunkData++;
}
return FALSE;
}

typedef int ( WINAPI* pWriteFile )( HANDLE hFile, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite, LPDWORD lpNumberOfBytesWritten,LPOVERLAPPED lpOverlapped );
pWriteFile p_WriteFile = 0;

int WINAPI MyWriteFile( HANDLE hFile, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite, LPDWORD lpNumberOfBytesWritten,LPOVERLAPPED lpOverlapped )
{
char mybuffer[7]="hooked";
return p_WriteFile(hFile, mybuffer, sizeof(mybuffer), lpNumberOfBytesWritten, lpOverlapped);
}

BOOL Hook(HMODULE hModule,LPCTSTR pImageName,LPCSTR pTargetFuncName)
{
HMODULE hLib = LoadLibrary( pImageName );
if ( NULL != hLib )
{
p_WriteFile = ( pWriteFile )GetProcAddress( hLib, pTargetFuncName );
BOOL bRet = IATHook( hModule, pImageName, p_WriteFile, MyWriteFile );
FreeLibrary( hLib );
return bRet;
}

return FALSE;
}

int main()
{

char buffer[10]="a";
DWORD bytesWritten;

HANDLE fh = CreateFile("pwd.txt", GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);


if(fh == INVALID_HANDLE_VALUE){
printf("File handle not opened.\n");
system("Pause");
exit(EXIT_FAILURE);
} else {
printf("File handle opened.\n");
}
printf("Please set your password(no more than 10 letters):\n");
scanf("%s",&buffer);

Hook( GetModuleHandle( NULL ), _T( "KERNEL32.dll" ), ( "WriteFile" ) );

WriteFile(fh, buffer, sizeof(buffer), &bytesWritten, NULL);

system("Pause");
return 0;
}

修改前:

image-20250516171632057
image-20250516171610572

修改后:

image-20250516171423137
image-20250516171515261

调用了自己的函数,将”hooked”写进txt文件里面:

image-20250516172226067
image-20250516172332720

成功!!!!