PE文件格式分析
该部分为恶意代码检测课程的笔记备份。
概况
PE文件主要分这几部分:MZ头部、DOS Stub、NT头(包含文件标识、PE文件头、可选文件头)、节表、节。整体结构图如下:
头与节划分如下:
注意:图中所说的“PE文件头“是指NT头。但为了区分子项PE文件头本作业中所有PE文件头均为NT头的子项IMAGE_File_HEADER,而图中的”PE文件头“均用NT头表示。
MZ头部
用记事本打开任何一个镜像文件,其头2个字节必为字符串“MZ”,这是Mark Zbikowski的姓名缩写,他是最初的MS-DOS设计者之一。然后是一些在MS-DOS下的一些参数,这些参数是在MS-DOS下运行该程序时要用到的。在这些参数的末尾也就是文件的偏移0x3C(第60字节)处是是一个4字节的PE文件签名的偏移地址。该地址有一个专用名称叫做“E_lfanew”。
MZ头部由IMGAE_DOS_HEADER定义,共64字节,MZ头部具体参数如下表所示:
PE文件内容 | 字段名 | 含义 | 偏移量 | 长度 |
---|---|---|---|---|
5A4DH | e_magic | DOS文件可执行标记,MZ作为识别标志 | 00H | WORD |
0090H | e_cblp | 文件最后页字节数 | 02H | WORD |
0003H | e_cp | 文件页数 | 04H | WORD |
0000H | e_crlc | 重定位表指针数 | 06H | WORD |
0004H | e_cparhdr | 头部尺寸 | 08H | WORD |
0000H | e_minalloc | 最小附加段大小 | 0AH | WORD |
FFFFH | e_maxalloc | 最大附加段大小 | 0CH | WORD |
0000H | e_ss | DOS代码的初始化堆栈SS | 0EH | WORD |
00B8H | e_sp | DOS代码初始化堆栈指针SP | 10H | WORD |
0000H | e_csum | 补码校验值 | 12H | WORD |
0000H | e_ip | DOS代码初始化指令入口(指针IP) | 14H | WORD |
0000H | e_cs | DOS代码的初始化堆栈入口CS | 16H | WORD |
0004H | e_lfarlc | 重定位表字节偏移量 | 18H | WORD |
0000H | e_ovno | 覆盖号 | 1AH | WORD |
00000000 00000000H | e_res[4] | 保留字 | 1CH | 4 WORD |
0000H | e_oemid | OEM标识符 | 24H | WORD |
0000H | e_oeminfo | OEM信息 | 26H | WORD |
00000000000000000000 00000000000000000000H | e_res2[10] | 保留字,e_res2[10] | 29H | 10 WORD |
000000B0H | e_lfanew | PE头相对于文件的偏移地址,指出了真正的PE文件头在文件中的位置,这个位置总是以8字节为单位对齐的 | 3CH | LONG |
DOS Stub
紧跟着E_lfanew的是一个MS-DOS程序。那是一个运行于MS-DOS下的合法应用程序。当可执行文件(一般指exe、com文件)运行于MS-DOS下时,这个程序显示“This program cannot be run in DOS mode(此程序不能在DOS模式下运行)”这条消息。
MZ头后是一个整个DOS Stub字节块,其内容随使用的链接器的不同而不同,PE中并没有与之相关的结构。
NT头
NT头由IMAGE_NT_HEADERS结构定义,共248字节,也有观点认为为244字节(不包括PE文件标志),NT头的具体参数如下:
偏移量 | 字段名 | 长度 | 含义 |
---|---|---|---|
00H | Signature | DWORD | PE文件标识,在一个PE文件中该字段被设置为4550H |
04H | FileHeader | IMAGE_FILE_HEADER | PE文件头 |
18H | OptionalHeader | IMAGE_OPTIONAL_HEADER32 | 可选文件头 |
PE文件头
PE文件头,共20字节,其偏移量相对于NT头而言的具体参数如下:
PE文件内容 | 字段名 | 含义 | 偏移量 | 长度 |
---|---|---|---|---|
014CH | Machine | 运行平台,可执行文件的目标CPU类型 | 04H | WORD |
0003H | NumberOfSections | 文件的区块数目 | 06H | WORD |
428F4D9BH | TimeDateStamp | 文件创建日期和时间,表明文件是何时被创建的,这个值是自1970年1月1日以来用格林威治时间(GMT)计算的秒数 | 08H | DWORD |
00000000H | PointerToSymbolTable | 指向符号表(主要用于调试),COFF 符号表的文件偏移位置 | 0CH | DWORD |
00000000H | NumberOfSymbols | 符号表中符号个数,如果有COFF 符号表,它代表其中的符号数目,如果想找到COFF 符号表的结束位置,则需要这个变量。 | 10H | DWORD |
00E0H | SizeOfOptionalHeader | IMAGE_OPTIONAL_HEADER32 结构大小 | 14H | WORD |
010FH | Characteristics | 文件属性,有选择的通过几个值可以运算得到 | 16H | WORD |
PE文件头的Machine字段参数如下表所示:
大小 | 预定义值 | 含义 |
---|---|---|
0x014C | IMAGE_FILE_MACHINE_I386 | x86平台 |
0x0200 | IMAGE_FILE_MACHINE_IA64 | Intel Itanium平台 |
0x8664 | IMAGE_FILE_MACHINE_AMD64 | x64平台 |
PE文件头的Characteristics字段参数如下表所示:
大小 | 预定义值 | 含义 |
---|---|---|
0x0001 | IMAGE_FILE_RELOCS_STRIPPED | 重定位表从文件中剥离 |
0x0002 | IMAGE_FILE_EXECUTABLE_IMAGE | 可执行文件 |
0x0004 | IMAGE_FILE_LINE_NUMS_STRIPPED | COFF行号从文件中剥离 |
0x0008 | IMAGE_FILE_LOCAL_SYMS_STRIPPED | COFF符号表入口从文件中剥离 |
0x0010 | IMAGE_FILE_AGGRESIVE_WS_TRIM | 积极修剪工作集,过时了 |
0x0020 | IMAGE_FILE_LARGE_ADDRESS_AWARE | 应用能处理大于2G的地址 |
0x0080 | IMAGE_FILE_BYTES_REVERSED_LO | 保留字 |
0x0100 | IMAGE_FILE_32BIT_MACHINE | 电脑支持32位字 |
0x0200 | IMAGE_FILE_DEBUG_STRIPPED | 调试信息移动并存储到其他地方 |
0x0400 | IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP | 若为可移动媒体则从swap中拷贝运行 |
0x0800 | IMAGE_FILE_NET_RUN_FROM_SWAP | 若为网络则 从swap中拷贝运行 |
大小 | 预定义值 | 含义 |
---|---|---|
0x1000 | IMAGE_FILE_SYSTEM | 文件是系统文件 |
0x2000 | IMAGE_FILE_DLL | 文件是DLL文件,不能直接运行 |
0x4000 | IMAGE_FILE_UP_SYSTEM_ONLY | 文件只能运行在单处理器计算机中 |
0x8000 | IMAGE_FILE_BYTES_REVERSED_HI | 保留字 |
可选文件头
可选文件头由IMAGE_OPTIONAL_HEADER结构定义,共224字节,其偏移量相对于NT头的具体参数如下:
PE文件内容 | 字段名 | 含义 | 偏移量 | 长度 |
---|---|---|---|---|
010BH | Magic | 标志字, ROM 映像(0107H),普通可执行文件(010BH) | 18H | WORD |
05H | MajorLinkerVersion | 链接程序的主版本号 | 1AH | BYTE |
0CH | MinorLinkerVersion | 链接程序的次版本号 | 1BH | BYTE |
00002000H | SizeOfCode | 所有含代码的节的总大小 | 1CH | DWORD |
00004000H | SizeOfInitializedData | 所有含已初始化数据的节的总大小 | 20H | DWORD |
00000000H | SizeOfUninitializedData | 所有含未初始化数据的节的大小 | 24H | DWORD |
00001000H | AddressOfEntryPoint | 程序执行入口RVA,指出文件被执行时的入口地址,可执行文件上附加了一段代码并想首先被执行,只需将其指向附加代码 | 28H | DWORD |
00001000H | BaseOfCode | 代码的区块的起始RVA | 2CH | DWORD |
00002000H | BaseOfData | 数据的区块的起始RVA | 30H | DWORD |
00040000H | ImageBase | 程序的首选装载地址,指出文件的优先装入地址 | 34H | DWORD |
00001000H | SectionAlignment | 内存中的区块的对齐大小,每个节被装入的地址必定是本字段指定数值的整数倍 | 38H | DWORD |
00000200H | FileAlignment | 文件中的区块的对齐大小,指定了节存储在磁盘文件中时的对齐单位 | 3CH | DWORD |
0004H | MajorOperatingSystemVersion | 要求操作系统最低版本号的主版本号 | 40H | WORD |
0000H | MinorOperatingSystemVersion | 要求操作系统最低版本号的副版本号 | 42H | WORD |
0000H | MajorImageVersion | 可运行于操作系统的主版本号 | 44H | WORD |
0000H | MinorImageVersion | 可运行于操作系统的次版本号 | 46H | WORD |
0004H | MajorSubsystemVersion | 要求最低子系统版本的主版本号 | 48H | WORD |
0000H | MinorSubsystemVersion | 要求最低子系统版本的次版本号 | 4AH | WORD |
00000000H | Win32VersionValue | 莫须有字段,不被病毒利用的话一般为0 | 4CH | DWORD |
00004000H | SizeOfImage | 映像装入内存后的总尺寸 | 50H | DWORD |
00000400H | SizeOfHeaders | 所有头 + 区块表的尺寸大小 | 54H | DWORD |
00000000H | CheckSum | 映像的校检和 | 58H | DWORD |
0002H | Subsystem | 可执行文件期望的子系统,决定了系统如何为程序建立初始的界面 | 5CH | WORD |
0000H | DllCharacteristics | DllMain()函数何时被调用,默认为 0 | 5EH | WORD |
00100000H | SizeOfStackReserve | 初始化时的栈大小 | 60H | DWORD |
00001000H | SizeOfStackCommit | 初始化时实际提交的栈大小 | 64H | DWORD |
00100000H | SizeOfHeapReserve | 初始化时保留的堆大小 | 68H | DWORD |
00001000H | SizeOfHeapCommit | 初始化时实际提交的堆大小 | 6CH | DWORD |
00000000H | LoaderFlags | 与调试有关,默认为 0 | 70H | DWORD |
00000010H | NumberOfRvaAndSizes | 下边数据目录的项数,这个字段自Windows NT 发布以来一直是16 | 74H | DWORD |
- | DataDirectory | 数据目录表,由16个相同的IMAGE_DATA_DIRECTORY结构组成 | 78H | 16 IMAGE_DATA_DIRECTORY |
可选文件头Subsystem字段具体参数如下表所示:
取值 | 预定义值 | 含义 |
---|---|---|
0 | IMAGE_SUBSYSTEM_UNKNOWN | 未知子系统 |
1 | IMAGE_SUBSYSTEM_NATIVE | 不需要子系统 |
2 | IMAGE_SUBSYSTEM_WINDOWS_GUI | Windows图形界面 |
3 | IMAGE_SUBSYSTEM_WINDOWS_CUI | Windows控制台界面 |
5 | IMAGE_SUBSYSTEM_OS2_CUI | OS2控制台界面 |
7 | IMAGE_SUBSYSTEM_POSIX_CUI | POSIX控制台界面 |
8 | IMAGE_SUBSYSTEM_NATIVE_WINDOWS | 不需要Windows子系统 |
9 | IMAGE_SUBSYSTEM_WINDOWS_CE_GUI | WIndows CE图形界面 |
可选文件头的DataDirectory字段的具体参数如下表所示:
PE文件内容 | 预定义值 | 对应数据块 | 索引 |
---|---|---|---|
0000000000000000H | IMAGE_DIRECTORY_ENTRY_EXPORT | 导出表入口 | 0 |
0000003C00002014H | IMAGE_DIRECTORY_ENTRY_IMPORT | 引入表入口 | 1 |
0000000000000000H | IMAGE_DIRECTORY_ENTRY_RESOURCE | 资源 | 2 |
0000000000000000H | IMAGE_DIRECTORY_ENTRY_EXCEPTION | 异常 | 3 |
0000000000000000H | IMAGE_DIRECTORY_ENTRY_SECURITY | 安全 | 4 |
0000000000000000H | IMAGE_DIRECTORY_ENTRY_BASERELOC | 重定位表 | 5 |
0000000000000000H | IMAGE_DIRECTORY_ENTRY_DEBUG | 调试信息 | 6 |
0000000000000000H | IMAGE_DIRECTORY_ENTRY_ARCHITECTURE | 版权信息 | 7 |
0000000000000000H | IMAGE_DIRECTORY_ENTRY_GLOBALPTR | - | 8 |
0000000000000000H | IMAGE_DIRECTORY_ENTRY_TLS | 线程分配本地存储 | 9 |
0000000000000000H | IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG | 加载设置 | 10 |
0000000000000000H | IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT | 绑定引入表入口 | 11 |
0000001400002000H | IMAGE_DIRECTORY_ENTRY_IAT | 引入函数地址表入口 | 12 |
0000000000000000H | IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT | 延时引入表入口 | 13 |
0000000000000000H | IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR | COM描述符 | 14 |
0000000000000000H | 未使用 | 15 |
IMAGE_DATA_DIRECTORY结构用伪代码描述如下:
typedef struct IMAGE_DATA_DIRECTORY{
DWORD VirtualAddress;
DWORD Size;
}IMAGE_DATA_DIRECTORY;
节表
PE文件中所有节的属性都被定义在节表中,节表由一系列的IMAGE_SECTION_HEADER结构排列而成,每个结构用来描述一个节,结构的排列顺序和它们描述的节在文件中的排列顺序是一致的。
节表中 IMAGE_SECTION_HEADER 结构的总数总是由PE文件头 IMAGE_NT_HEADERS 结构中的 FileHeader.NumberOfSections 字段来指定的。全部有效结构的最后以一个空的IMAGE_SECTION_HEADER结构作为结束,所以节表中IMAGE_SECTION_HEADER结构数量等于节的数量加一。
其中,IMAGE_SECTION_HEADER结构共占用40字节,具体参数如下表所示:
.text表头 | .rdata表头 | .data表头 | 字段名 | 含义 | 偏移量 | 长度 |
---|---|---|---|---|---|---|
00000074 7865742EH | 00006174 6164722EH | 00000061 7461642EH | Name[IMAGE_SIZEOF_SHORT_NAME] | 节区名称,通过8位的ASCII 码名,用来定义区块的名称 | 00H | 8 BYTE |
00000046H | 000000A6H | 0000008EH | Misc | 联合结构,用于记录物理地址或者真实长度,一般取后者 | 08H | DWORD |
00001000H | 00002000H | 00003000H | VirtualAddress | 节区的RVA地址,按照内存页来对齐,数值总是 SectionAlignment 的值的整数倍。 | 0CH | DWORD |
00000200H | 00000200H | 00000200H | SizeOfRawData | 在文件中对齐后的尺寸,在磁盘中所占的大小 | 10H | DWORD |
00000400H | 00000600H | 00000800H | PointerToRawData | 在文件中的偏移量,数值是从文件头开始算起的偏移量 | 14H | DWORD |
00000000H | 00000000H | 00000000H | PointerToRelocations | 重定位的偏移量,在OBJ 文件中使用以表示本区块重定位信息的偏移值。 | 18H | DWORD |
00000000H | 00000000H | 00000000H | PointerToLinenumbers | 行号表的偏移量(供调试使用) | 1CH | DWORD |
0000H | 0000H | 0000H | NumberOfRelocations | 在OBJ文件中使用,重定位项数目 | 1EH | WORD |
0000H | 0000H | 0000H | NumberOfLinenumbers | 行号表中行号的数目 | 20H | WORD |
60000020H | 40000040H | C0000040H | Characteristics | 节属性如可读,可写,可执行等 | 24H | DWORD |
IMAGE_SECTION_HEADER结构的Characteristics字段具体参数如下:
大小 | 预定义值 | 含义 |
---|---|---|
0x00000040 | IMAGE_SCN_CNT_INITIALIZED_DATA | 该区块包含以初始化的数据 |
0x00000080 | IMAGE_SCN_CNT_UNINITIALIZED_DATA | 该区块包含未初始化的数据 |
0x02000000 | IMAGE_SCN_MEM_DISCARDABLE | 该区块可被丢弃,因为当它一旦被装入后, 进程就不在需要它了,典型的如重定位区块 |
0x10000000 | IMAGE_SCN_MEM_SHARED | 该区块为共享区块 |
0x20000000 | IMAGE_SCN_MEM_EXECUTE | 该区块可以执行。通常当0x00000020被设置 时候,该标志也被设置 |
0x40000000 | IMAGE_SCN_MEM_READ | 该区块可读,可执行文件中的区块总是设置该 标志 |
0x80000000 | IMAGE_SCN_MEM_WRITE | 该区块可写 |
0x00000020 | MAGE_SCN_CNT_CODE | 包含代码,常与 0x10000000一起设置 |
IMAGE_SECTION_HEADER结构的Msic联合结构用伪代码描述如下:
typedef union Misc{
DWORD PyhsicalAddress;//物理地址
DWORD VirtualSize;//真实长度
}Misc;
节
通常,节的数据在逻辑上是关联的。PE文件至少具有两个节:代码节和数据节。一般常见命名的节区及其描述如下表所示:
节区名称 | 描述 |
---|---|
.text | 默认的代码节区,内容全是指令代码。链接程序把所有目标文件的.text块链接成一个大的.text块 |
.data | 默认的读/写数据节区,全局变量、静态变量放在这里 |
.rdata | 默认的只读数据节区,程序中很少用到该节区,一般用于在微软的链接程序产生的EXE中存放调试目录,或者用于存放说明字符串 |
.idata | 包含其他外来的DLL函数以及数据信息 |
.edata | 输出表。当创建一个输出API或者数据的可执行文件时链接程序会创建一个.EXP文件,其中包含一个.edata节区,其最终加入到可执行文件中 |
.rsrc | 资源,包含模块的全部资源,只读的节区,无法合并 |
.bss | 未初始化数据,很少使用 |
.crt | 用于支持C++g运行时所添加的数据 |
.tls | 线程局部存储器,用于支持通过deeplspec(thread)声明的线程局部存储变量的数据 |
.reloc | 可执行文件的基址重定位,基址重定位一般为DLL所需要 |
.sdata | 相对于全局指针的可被定位的“短的”读/写数据 |
.srdata | 相对于全局指针的可被定位的“短的”只读数据 |
.pdata | 异常表,包含一个CPU特定的IMAGE_RUNTIME_FUNCTION_ENTRY结构数组,由DataDirectory的IMAGE_DIRECTORY_ENTRY_EXCEPTION指向它 |
.debug$S | OBJ文件中的Codeview格式的符号 |
.debug$T | OBJ文件中的Codeview格式的类型记录 |
.debug$P | 当使用预编译的头时,可以在OBJ文件中找到 |
.drectve | 包含链接程序命令,只能在OBJ文件中找到 |
.didat | 延迟装入的输入数据 |
引入函数节
引入函数节一般为.rdata节,其前面三部分为:IMPORT Address Table、引入目录表(IMPORT Directory Table)、IMPORT Name Table、IMPORT Hints/Names & DLL Name。
位置 | 定义 | 描述 |
---|---|---|
红色位置 | IMPORT Address Table | 在IAT中记录的地址,每个函数的地址以DWORD大小为一项,每一DLL的地址以4字节的0结尾,一个DLL中可能存在多个函数的地址,同一DLL多个函数接连在一起。 |
黄色区域 | IMPORT Directory Table | 在引入目录表中,如果有n个DLL,则有n+1个IMAGE_IMPORT_DESCRIPTOR项,大小为(n+1)⋅ 20字节。在引入目录表中最后一个IMAGE_IMPORT_DESCRIPTOR项全为0,即最后20字节的0结尾。 |
蓝色区域 | IMPORT Name Table | 每个函数指向名字的地址以DWORD大小为一项,每一DLL的地址以4字节的0结尾,一个DLL中可能存在多个函数名地址,其顺序与IAT不一定相同(尤其要注意),同一DLL多个函数连在一起。 |
紫色区域 | IMPORT Hints/Names & DLL Name | 记录DLL名字、函数名字字符串等信息 |
引出函数节
引出函数节一般为.edata节,这是PE文件中向其他程序提供调用函数列表、函数所在位置的地址以及具体代码实现的部分。
在引出函数中,需要引出目录表。引出目录表的结构由IMAGE_EXPORT_DIRECTORY定义,引出目录表的结构用伪代码描述如下:
typedef strcut IMAGE_EXPORT_DIRECTORY{
DWORD Characteristics;
DWORD TimeDataStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions;
DWORD AddressOfNames;
DWORD AddressOfNameOrdinals;
}_IMAGE_EXPORT_DIRECTORY;
其具体参数如下表所示:
字段名 | 描述 | 偏移量 | 大小 |
---|---|---|---|
Characteristics | 特征,一般为0 | 00H | DWORD |
TimeDataStamp | 文件生成时间 | 04H | DWORD |
MajorVersion | 主版本号 | 08H | WORD |
MinorVersion | 次版本号 | 0AH | WORD |
Name | 指向DLL的名字 | 0CH | DWORD |
Base | 开始的序列号 | 10H | DWORD |
NumberOfFunctions | 函数地址数组的项数 | 14H | DWORD |
NumberOfNames | 名字地址数组的项数 | 18H | DWORD |
AddressOfFunctions | 指向函数地址数组的导出函数地址表(EAT,Export Address Table) | 1CH | DWORD |
AddressOfNames | 指向函数名数组的导出函数名地址表(ENT,Export Address Table) | 20H | DWORD |
AddressOfNameOrdinals | 指向函数索引序列号数组的函数序号表(EOT,Export Oridinal Table) | 24H | DWORD |
导出地址表结构如下:
typedef struct IMAGE_Export_Address_Table{
union{
DWORD dwExportRVA;//指向导出地址
DwORD dwFurwarderRVA;//指向另一个DLL中某个API函数名
}
}
导出名字表结构如下:
typedef struct IMAGE_Export_Name_Table{
DWORD dwPointer;
}_IMAGE_Export_Name_Table;
导出序号表结构如下:
typedef struct IMAGE_Export_Ordinal_Table{
WORD dwOrdinal;//保存各导出函数的函数地址在导出地址表的序
}_IMAGE_Export_Ordinal_Table;
与导入函数的双桥结构不同的是,导出函数名字表和导出地址表不是一一对应关系的。因为一个函数可能有多个名字,有的函数没名字而通过序号导出。
导入函数表与导入机制
引入目录表(或称函数引入表),位于可选文件头的DataDirectory数组IMAGE_DIRECTORY_ENTRY_IMPORT索引。 程序需要执行dll相关代码,则相关代码指令必须存在于进程地址空间,操作系统在加载的时候会根据引入目录表的描述将需要调用的函数指令加载到进程空间。
导入描述符
引入目录表实际上是一个 IMAGE_IMPORT_DESCRIPTOR 结构数组,表中数据的起始部分是多组导入描述符结构,每个结构包含PE文件引入函数的一个相关DLL的信息。其具体参数如下表所示:
IMAGE_IMPORT_DESCRIPTOR 结构通过伪代码描述如下:
IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; //导入名字表的地址(RVA)
};
DWORD TimeDateStamp; //时间戳
DWORD ForwarderChain; //链表的前一个结构
DWORD Name; //指向链接库的指针
DWORD FirstThunk; //导入地址表的地址 (RVA)
} IMAGE_IMPORT_DESCRIPTOR;
偏移量 | 字段名 | 长度 | 含义 |
---|---|---|---|
00H | Characteristics/OriginalFirstThunk | DWORD | 联合结构,现在一般用于描述OriginalFirstThunk,即指向引入名字表(INT)的地址 |
04H | TimeDataStamp | DWORD | 时间戳。映象绑定前,这个值是0,绑定后是引入模块的时间戳 |
08H | ForwarderChain | DWORD | 链表的前一个结构,转发链。如果没有转发器,这个值是-1。 |
偏移量 | 字段名 | 长度 | 含义 |
---|---|---|---|
0CH | Name | DWORD | 指向链接库的指针,指向引入模块的名字 |
10H | FirstThunk | DWORD | 指向输入地址表(IAT)的地址 |
函数引入过程
在程序执行时,PE文件通过可选文件头的IMAGE_DIRECTORY_ENTRY_IMPORT定位到函数引入节的引入目录表,对引入目录表项进行遍历查找,根据INT表定位函数在IAT中的位置,再通过IAT表绑定引入后的VA地址进行内存调用。
以PE文件中的MessageBox函数为例,在图中我们知道MessageBox字符串入口为例:
根据PE文件头部可知.rdata开始位置文件偏移量为600H,内存地址为2000H,又通过ASCII码可以知道其地址为68CH,因此我们知道内存地址应该为208CH,根据蓝色部分INT表查询可知第二个DLL的第一项为其内存地址,因此函数真实位置再跟据IAT表可知为208CH,同时根据双桥结构及其关系,我们也能看出来这个可执行程序未进行函数的绑定引入。
双桥结构及其关系
每一个结构IMAGE_IMPORT_DESCRIPTOR都对应的是一个唯一的dll文件。以及dll中的每个函数都可以通过”编号-名称”的方式找到,这就是引入目录表的双桥结构,如图所示:
OriginalFirstThunk指向的数组中每一项为一个结构,此结构的名称是IMAGE_THUNK_DATA。该结构实际上只是一个双字,但在不同的时刻却拥有不同的解释。该字段有两种解释:这个值是INT的地址,INT(Import Name Table)是一个存储了库文件函数名称的表。在装载的时候,PE装载器读取OriginalFirstThun获得INT,然后通过函数名称去获取函数的地址。
FirstThunk指向的是IAT(Import Address Table),也是IMAGE_THUNK_DATA,但是对于程序的装载,并不使用IAT进行函数的寻址,在通过INT寻址之后,将找到的地址填充到IAT中。后期需要用到函数的时候,可以使用GetProcAddressAPI函数进行获取。
而在IMAGE_THUNK_DATA结构数组中,则存放着指向IMAGE_IMPORT_BY_NAME结构数组的指针。
IMAGE_THUNK_DATA结构用伪代码描述如下:
typedef struct IMAGE_THUNK_DATA{
union{
DWORD ForwarderString;
DWORD Function;
DWORD Ordinal;//序号
DWORD AddressOfData;//指向IMAGE_IMPORT_BY_NAME
}
}IMAGE_THUNK_DATA;
IMAGE_THUNK_DATA结构字段具体说明如下表所示:
字段名 | 描述 |
---|---|
ForwarderString | 指向一个转向者字符串RVA |
Function | 被引入的函数的内存地址 |
Ordinal | 被引入的API函数序号 |
AddressOfData | 被引入的API的Hint和函数名字字符串信息 |
需要注意的是:当IMAGE_THUNK_DATA的数据最高位为0时通过函数名引入,指向IMPORT Hints/Names;为1时则通过序号引入函数。
IMAGE_IMPORT_BY_NAME结构用伪代码描述如下:
typedef struct IMAGE_IMPORT_BY_NAME {
WORD Hint; //对dll中的每个函数进行标号,该值不是必须的
BYTE Name; //函数名称
} IMAGE_IMPORT_BY_NAME;
绑定引入前后双桥结构会发生变化,其变化如图所示:
绑定引入后,IAT中的地址从指向IMAGE_IMPORT_BY_NAME变为指向函数的VA地址。而INT仍然指向函数名地址。
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步