逆向工程核心原理第十三章
PE文件格式
介绍
- PE(Portable Executable)是Windows下的可执行文件,是微软在UNIX平台的COFF(Common Object File Format)基础上制作的。最初(正如Portable这个单词所代表那样)设计用来提高程序在不同操作系统的移植性,但实际上这种文件格式只有在Windows系列的操作系统能运行
PE文件格式
种类 | 主扩展名 | 种类 | 主扩展名 |
---|---|---|---|
可执行系列 | EXE、SRC | 驱动程序系列 | SYS、VXD |
库系列 | DLL、OCX、CPL、DRV | 对象文件系列 | OBJ |
除OBJ(对象)文件外,所有文件都是可执行的,DLL、SYS等虽然不能直接在Shell(Explorer.exe)运行,但可以使用其他方法(调试器、服务等)执行。
用十六进制编辑器打开win10的notepad.exe,查看PE文件的特征,下图为PE文件的头部分(PE header),notepad.exe运行所需要的DLL有哪些、需要多大的栈/堆内存这些信息就包含在这个PE头中
从DOS头(DOS header)到节区头(Section header)是PE头部分,其余的节区合称PE体(PE body)。文件中使用偏移(offset),内存中使用VA(Virtual Address,虚拟地址)来表示位置。文件加载到内存时,情况就会发生变化(节区的大小、位置等)。文件等内容一般可分为(.text)、数据(.data)、资源(.rsrc)节,分别保存。
各节区头定义了各节区在文件或内存中的大小、位置、属性等。PE头与各节区尾部存在一个区域,称为NULL填充(NULL padding)
VA与RVA
VA指的是进程虚拟内存的绝对地址,RVA(Relative Virtual Address,相对虚拟地址)指从某个基准位置(ImageBase)开始的相对地址,VA与RVA满足以下换算关系
1
RVA + ImageBase = VA
PE头
DOS头:为了兼容DOS,PE头最前面有个IMAGE_DOS_HEADER结构体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23typedef struct _IMAGE_DOS_HEADER
{
WORD e_magic; // DOS signature: 4D5A("MZ")
WORD e_cblp;
WORD e_cp;
WORD e_crlc;
WORD e_cparhdr;
WORD e_minalloc;
WORD e_maxalloc;
WORD e_ss;
WORD e_sp;
WORD e_csum;
WORD e_ip;
WORD e_cs;
WORD e_lfarlc;
WORD e_ovno;
WORD e_res[4];
WORD e_oemid;
WORD e_oeminfo;
WORD e_res2[10];
LONG e_lfanew; // offset to NT header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;IMAGE_DOS_HEADER结构体大小为64个字节,其中有两个比较重要的成员e_magic和e_lfanew
- e_magic: DOS签名(固定值,"4D5A"=>ASCII值"MZ"=>"Mark Zbikowski"首字母)
- e_lfanew: 指示NT头的偏移(根据不同文件拥有可变值)
用Hex Editor打开notepad.exe查看IMAGE_DOS_HEADER结构体,根据PE规范文件开始的2个字节为"4D5A",e_lfanew值为"00000100"而不是"00010000"(小端序存储,低位在前)
这里有个奇怪的地方,是这样的,同一个notepad.exe用010 editor和HexEdit打开,e_lfanew的值是不一样的,但两者都指向了正确位置,从下图可以看到010 editor的e_lfanew值为"000000F8",对比010 editor和Hex Editor两者,发现两者主要区别是从地址"000000E4"开始,010 editor要少一些00,从这点上来推测是跟NULL填充有关。另外试了WinHex,结果跟HexEdit一样;试了BeyondCompare,结果跟010 editor一样;所以这并不能推断出两者谁对谁错或者都是对的,这里记下问题,待后续学习解决。不过无论是010 editor还是HexEdit,e_lfanew值都是正确指向了NT头的位置。
DOS存根
DOS存根在DOS头下方,是个可选项,且大小不固定(即使没有DOS存根,文件也能正常运行)
DOS存根由代码与数据混合而成,下图展示了DOS存根所在位置,其中在40~4D区域为16位汇编指令,Windows中不会执行该命令,(因为被识别为PE文件所以完全忽视该代码),在DOS环境中则可以运行那部分代码(不认识PE文件格式,被识别为DOS EXE文件)
接下来测试下这部分dos命令,因为这里用的是win10,所以cmd命令行已经没有debug命令了,要像书中那样测试,就需要用到DOSBox和MS-DOS,来测试。
下载安装好DOSBox后,打开DOSBox,可以看到以下界面,DOSBox默认没有挂载我们的硬盘,需要我们自己挂载硬盘路径
运行命令
mount c c:\
,挂载C盘,出现以下信息说明挂载成功接下来我们进入MS-DOS的解压路径
1
2c: # 切换c盘
cd USERS\CC\DESKTOP\MS-DOS\V2.0\BIN # MS-DOS解压后的v2.0 bin所在路径执行
DEBUG.COM C:\WINDOWS\SYSTEM32\NOTEPAD.EXE
,如下图出现了16位汇编指令我们还可以用DOSBox直接运行notepad.exe,如下图,可以看到DOS下运行notepad.exe会出现"This program cannot be run in DOS mode."
利用以上特性就可以编写出可以同时在DOS和Windows运行的程序
NT头
NT头IMAGE_NT_HEADERS
1
2
3
4
5typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; // PE Signature : 50450000("PE"00)
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;NT头包含文件头和可选头,下面介绍文件头和可选头结构体
文件头(_IMAGE_FILE_HEADER)是表现文件大致属性的IMAGE_FILE_HEADER结构体
1
2
3
4
5
6
7
8
9
10typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;IMAGE_FILE_HEADER结构体有以下4种重要成员
- Machine: 每个CPU都有唯一的Machine码,兼容32位Intel x86芯片的Machine码为14C,兼容64位AMD x64芯片的Machine码为8664,分别用010 editor和Hex Edit打开C:\Windows\System32\notepad.exe,010 editor识别的机器码是8664,Hex Edit则14C,说明一个识别为64位,另一个则识别为32位,这里推测原因是:010 editor是64位的程序,Hex Edit为32位,notepad.exe文件在64位虚拟内存和32位虚拟内存中是有差异的。这种差异应该也是造成上面e_lfanew不一样的原因。
- NumberOfSections: 用来指出PE文件中存在的节区数量,该值一定要大于0,且当定义的节区数与实际节区不同时,将运行出错
- SizeOfOptionalHeader: IMAGE_NT_HEADERS结构体最后一个成员为IMAGE_OPTIONAL_HEADER32结构体,SizeOfOptionalHeader用来指出IMAGE_OPTIONAL_HEADER32结构体长度供PE装载器识别。PE32+文件中使用的是IMAGE_OPTIONAL_HEADER64,而不是IMAGE_OPTIONAL_HEADER32,两者大小不同,因此需要SizeOfOptionalHeader中指出结构体的大小。
- Characteristics: 该字段用于标识文件的属性(文件是否可执行、是否为DLL等信息),以bit OR形式组合起来
可选头(IMAGE_OPTIONAL_HEADER32)是PE头结构体中最大的
1
2
3
4typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;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
33typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;在IMAGE_OPTIONAL_HEADER32结构体中比较重要的成员,有以下几个:
Magic:为IMAGE_OPTIONAL_HEADER32时为10B,为IMAGE_OPTIONAL_HEADER64时为20B
AddressOfEntryPoint:EP的RVA值,这个值指出程序最先执行的代码起始地址,非常重要
ImageBase:32位系统中进程虚拟地址为0~FFFFFFFF,PE文件被加载到如此大的内存中时,ImageBase指出文件的优先装入地址。EXE、DLL被装载在0~7FFFFFFF中,SYS文件被载入内核内存的80000000~FFFFFFFF中。一般使用开发工具(VB/VC++/Delphi)创建好EXE文件后,其ImageBase值为00400000,DLL文件的ImageBase值为10000000(当然也可以为其他),执行PE文件时,PE装载器先创建进程,再把文件载入内存,然后把EIP寄存器的值设为ImageBase+AddressOfEntryPoint
SectionAlignment,FileAlignment:PE文件的Body部分划分为若干个节区,这些节区储存着不同类别的数据。FileAlignment指定了节区在磁盘文件中的最小单位,而SectionAlignment则指定了节区在内存中的最小单位(一个文件中FileAlignment的值与SectionAlignment的值可能相同也可能不相同)。磁盘文件或内存的节区大小必定为FileAlignment或SectionAlignment的整数倍。
SizeOfImage:该值指定了PE Image在虚拟内存中所占空间的大小
SizeOfHeaders:该值用来指出整个PE头的大小,值必须为FileAlignment的整数倍。第一节区所在位置与SizeOfHeaders距文件开始偏移的量相同。
Subsystem:该值用来区分系统驱动文件(*.sys)与普通的可执行文件(*.exe,*.dll)。Subsystem成员可拥有的值如下所示
值 含义 备注 1 Driver文件 系统驱动(如:ntfs.sys) 2 GUI文件 窗口应用程序(如:notepad.exe) 3 CUI文件 控制台应用程序(如:cmd.exe) NumberOfRvaAndSizes:用来指定DataDirectory(IMAGE_OPTIONAL_HEADER32结构体最后一个成员)数组的个数,供PE装载器识别
DataDirectory:由IMAGE_DATA_DIRECTORY结构体组成的数组,数组的每项都有被定义的值,DataDirectory的值如下,比较重要的是前两个
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
31DataDirectory[0] = EXPORT Directory
DataDirectory[1] = IMPORT Directory
DataDirectory[2] = RESOURCE Directory
DataDirectory[3] = EXCEPTION Directory
DataDirectory[4] = SECURITY Directory
DataDirectory[5] = BASERELOC Directory
DataDirectory[6] = DEBUG Directory
DataDirectory[7] = COPYRIGHT Directory
DataDirectory[8] = GLOBALPTR Directory
DataDirectory[9] = TLS Directory
DataDirectory[A] = LOAD_CONFIG Directory
DataDirectory[B] = BOUND_IMPORT Directory
DataDirectory[C] = IAT Directory
DataDirectory[D] = DELAY_IMPORT Directory
DataDirectory[E] = COM_DESCRIPTOR Directory
DataDirectory[F] = Reserved Directory
notepad.exe中IMAGE_OPTIONAL_HEADER32的结构体如下所示:
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[ IMAGE_OPTIONAL_HEADER ]
offset value description
=======================================================================================
00000108 010B magic
0000011A 0E major linker version
0000011B 14 minor linker version
0000011C 00022400 size of code
00000120 00007600 size of initialized data
00000124 00000000 size of uninitialized data
00000128 00021860 address of entry point
0000012C 00001000 base of code
00000130 00024000 base of data
00000134 00400000 image base
00000138 00001000 section alignment
0000013C 00000200 file alignment
00000140 000A major OS version
00000142 0000 minor OS version
00000144 000A major image version
00000146 0000 minor image version
00000148 000A major subsystem version
0000014A 0000 minor subsustem version
0000014C 00000000 win32 version value
00000150 0002E000 size of image
00000154 00000400 size of headers
00000158 0002C834 Checksum
0000015C 0002 subsystem
0000015E C140 DLL characteristic
00000160 00040000 size of stack reserve
00000164 00011000 size of stack commit
00000168 00100000 size of heap reserve
0000016C 00001000 size of heap commit
00000170 00000000 loader flags
00000174 00000010 number of directories
00000178 00000000 RVA of export directory
0000017C 00000000 size of export directory
00000180 0002647C RVA of import directory
00000184 0000021C size of import directory
00000188 0002A000 RVA of resource directory
0000018C 00000BD8 size of resource directory
00000190 00000000 RVA of exception directory
00000194 00000000 size of exception directory
00000198 00000000 RVA of security directory
0000019C 00000000 size of security directory
000001A0 0002B000 RVA of basereloc directory
000001A4 00002428 size of basereloc directory
000001A8 00004FA0 RVA of debug directory
000001AC 00000054 size of debug directory
000001B0 00000000 RVA of copyright directory
000001B4 00000000 size of copyright directory
000001B8 00000000 RVA of globallptr directory
000001BC 00000000 size of globallptr directory
000001C0 0000140C RVA of TLS directory
000001C4 00000018 size of TLS directory
000001C8 00001360 RVA of LOAD_CONFIG directory
000001CC 000000AC size of LOAD_CONFIG directory
000001F0 00000000 RVA of BOUND_IMPORT directory
000001F4 00000000 size of BOUND_IMPORT directory
000001F8 00026000 RVA of IAT directory
000001FC 00000478 size of IAT directory
000001E0 00022E48 RVA of DELAY_IMPORT directory
000001E4 000000E0 size of DELAY_IMPORT directory
000001E8 00000000 RVA of COM_DESCRIPTOR directory
000001EC 00000000 size of COM_DESCRIPTOR directory
000001F0 00000000 RVA of reserved directory
000001F4 00000000 size of reserved directory节区头:节区头是由IMAGE_SECTION_HEADER结构体组成的数组,每个结构体对应一个节区,节区头定义了各节区属性,不同内存属性的访问权限如下
类别 访问权限 code 执行,读取权限 data 非执行,读写权限 resource 非执行,读取权限 IMAGE_SECTION_HEADER结构体如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;IMAGE_SECTION_HEADER重要成员
项目 含义 VirtualSize 内存中节区所占大小 VirtualAddress 内存中节区起始地址(RVA) SizeOfRawData 磁盘文件中节区所占大小 PointerToRawData 磁盘文件中节区起始位置 Characteristics 节区属性(bit OR) VirtualAddress和PointerToRawData不带有任何值,分别由(定义在IMAGE_OPTIONAL_HEADER32中的)SectionAlignment,FileAlignment确定。
VirtualSize与SizeOfRawData一般有不同的值,即磁盘文件中节区的大小与加载到内存中的节区大小是不同的
Characteristics的值由多个值组合(bit OR)而成
Name成员:Name成员不像C语言中的字符串一样以NULL结束,并且没有"必须使用ASCII值"的限制。PE规范未明确规定节区的Name,所以可以向其中放入任何值,甚至可以填充NULL值,所以节区Name仅供参考,不能保证其百分之百地被用作某种信息。
notepad.exe的节区头数据,如下图所示
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[ IMAGE_SECTION_HEADER ]
offset value descriptrion
=======================================================================================
000001F8 2E746578 Name(.text)
000001FC 74000000
00000200 000223B8 virtual size
00000204 00001000 RVA
00000208 00022400 size of raw data
0000020C 00000400 offset to raw data
00000210 00000000 offset to relocations
00000214 00000000 offset to line numbers
00000218 0000 number of relocations
0000021A 0000 number of line numbers
0000021C 60000020 characteristics
00000220 2E646174 Name(.data)
00000224 61000000
00000228 00001F74 virtual size
0000022C 00024000 RVA
00000230 00000A00 size of raw data
00000234 00022800 offset to raw data
00000238 00000000 offset to relocations
0000023C 00000000 offset to line numbers
00000240 0000 number of relocations
00000242 0000 number of line numbers
00000244 C0000040 characteristics
00000248 2E696461 Name(.idata)
0000024C 74610000
00000250 0000214E virtual size
00000254 00026000 RVA
00000258 00002200 size of raw data
0000025C 00023200 offset to raw data
00000260 00000000 offset to relocations
00000264 00000000 offset to line numbers
00000268 0000 number of relocations
0000026A 0000 number of line numbers
0000026C 40000040 characteristics
00000270 2E646964 Name(.didat)
00000274 61740000
00000278 000000BC virtual size
0000027C 00029000 RVA
00000280 00000200 size of raw data
00000284 00025400 offset to raw data
00000288 00000000 offset to relocations
0000028C 00000000 offset to line numbers
00000290 0000 number of relocations
00000292 0000 number of line numbers
00000294 C0000040 characteristics
00000298 2E727372 Name(.rsrc)
0000029C 63000000
000002A0 00000BD8 virtual size
000002A4 0002A000 RVA
000002A8 00000C00 size of raw data
000002AC 00025600 offset to raw data
000002B0 00000000 offset to relocations
000002B4 00000000 offset to line numbers
000002B8 0000 number of relocations
000002BA 0000 number of line numbers
000002BC 40000040 characteristics
000002C0 2E72656C Name(.reloc)
000002C4 6F630000
000002C8 00002428 virtual size
000002CC 0002B000 RVA
000002D0 00002600 size of raw data
000002D4 00026200 offset to raw data
000002D8 00000000 offset to relocations
000002DC 00000000 offset to line numbers
000002E0 0000 number of relocations
000002E2 0000 number of line numbers
000002E4 42000040 characteristics
RVA to RAW
PE文件加载到内存时,每个节区都要能准确完成内存地址与文件偏移的映射,这种映射一般称为RVA to RAW,方法如下:
查找RVA所在节区
使用简单公式计算文件偏移(RAW)
根据IMAGE_SECTION_HEADER结构体,换算公式如下:
1
2
3RAW - PointerToRawData = RVA - VirtualAdress
RAW = RVA - VirtualAdress + PointerToRawData
下图为win10 notepad.exe的文件与内存间的映射关系
测试练习(注:这里notepad.exe文件与内存映射关系跟《逆向工程核心原理》书中不一样是因为notepad.exe的版本不一样)
Q1: RVA=5000时,File Offset=?
1
2
3
4A1:首先查找RVA值所在节区。
=> RVA 5000位于第一个节区(.text)(假设ImageBase为00400000)
使用公式换算如下:
=> RAW = 5000(RVA)-1000(VirtualAddress)+400(PointerToRawData)=4400Q2:RVA=13314时,File Offset=?
1
2
3
4A2:查找RVA值所在节区。
=> RVA 13314位于第一个节区(.text)
使用公式换算如下:
=> RAW = 13314(RVA)-1000(VirtualAddress)+400(PointerToRawData)=12714Q3:RVA=时25999,File Offset=?
1
2
3
4
5A3:查找RVA值所在节区。
=> RVA 25999位于第二节区(.data)
使用公式换算如下:
=> RAW = 25999(RVA)-24000(VA)+22800(PointerToRawData)=24799(X)
=> 计算结果为RAW=24799,但是该偏移在第三个节区(.rsrc)。RVA在第二个节区,而RAW在第三个节区,这显然是错误的。这种情况表明"无法定义与RVA(25999)相对应的RAW值"。出现这种情况的原因在于,第二个节区的VirtualSize值要比SizeOfRawData值大。
IAT
IAT(Import Address Table,导入地址表):IAT保存的内容与Windows操作系统的核心进程、内存、DLL结构等有关。
学习IAT前先学习一下DLL内容(Dynamic Link Library,动态链接库)的知识,16位DOS时代不存在DLL这一概念,只有"库"(Library)一说,编译器编译时会程序调用的外部函数二进制代码加入到程序中,这就造成了磁盘和内存含大量重复的问题,而DLL就是用来解决这一问题的。DLL概念如下:
- 不要把库包含到程序中,单独组成DLL文件,需要时调用即可
- 内存映射技术使加载后的DLL代码、资源在多个进程中实现共享
- 更新库时只要替换相关DLL文件即可,简便易行
加载DLL的方式有两种:一种是"显示链接"(Explicit Linking),程序使用DLL时加载,使用完后释放内存。另一种是"隐示链接"(Implicit Linking),程序运行时即一同加载DLL,程序终止时再释放占用内存
下图是调用CreateFileW()函数的代码,调用该函数并非直接调用,而是通过读取"763B0FD0"处的值("75F4FFC0")来间接调用而不是直接
call 75F4FFC0
。地址"763B0FD0"是notepad.exe的内存区域(更确切地说是IAT内存区域)IMAGE_IMPORT_DESCRIPTOR,IMAGE_IMPORT_DESCRIPTOR结构体中记录着PE文件要导入哪些库文件,IMAGE_IMPORT_DESCRIPTOR结构体如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; // INT(Import Name Table) address (RVA)
};
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name; // library name string address (RVA)
DWORD FirstThunk; // IAT(Import Address Table) address (RVA)
} IMAGE_IMPORT_DESCRIPTOR;
typedef sturct _IMAGE_IMPORT_BY_NAME {
WORD Hint; // ordinal
BYTE Name[1]; // function name string
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;执行一个普通程序时,往往需要导入多个库,导入多少库就存在多少个IMAGE_IMPORT_DESCRIPTOR结构体,这些结构体形成了数组,且结构体数组最后以NULL结构体结束。IMAGE_IMPORT_DESCRIPTOR中的重要成员如下表所示
- INT与IAT是长整型(4个字节数据类型)数组,以NULL结束
- INT中各元素的值为IMAGE_IMPORT_BY_NAME结构体指针(有时IAT也拥有相同的值)
- INT与IAT大小应相同
项 目 含 义 OriginalFirstThunk INT的地址(RVA) Name 库名称字符串的地址 FirstThunk IAT的地址(RVA) 下图描述了notepad.exe的kernel32.dll的IMAGE_IMPORT_DESCRIPTOR结构,图中INT和IAT指向了相同地址,但也有很多情况下它们是不一致的。
接下来了解下PE装载器把导入函数输入至IAT的顺序
读取IID的Name成员,获取库名称字符串"kernel32.dll"
装载相应库=>LoadLibrary("kernel32.dll")
读取IID的OriginalFirstThunk成员,获取INT地址
逐一读取INT中数组的值,获取相应IMAGE_IMPORT_BY_NAME地址(RVA)
使用IMAGE_IMPORT_BY_NAME的Hint(ordinal)或Name项,获取相应函数的起始地址=>GetProcAddress("GetCurrentThreadId")
读取IID的FirstThunk(IAT)成员,获得IAT地址
将上面获得的函数地址输入相应的IAT数组值
重复步骤4~7,直到INT结束(遇到NULL)
IMAGE_IMPORT_DESCRIPTOR结构体不位于PE头中,它位于PE体中,但查找其位置的信息在PE头中。IMAGE_OPTIONAL_HEADER32.DataDirectory[1].VirtualAddress的值即是IMAGE_IMPORT_DESCRIPTOR结构体数组的起始地址(RVA值)。IMAGE_IMPORT_DESCRIPTOR结构体数组也被称为IMPORT Directory Table
从上面展示IMAGE_OPTIONAL_HEADER32结构体的数据里面可以得知IMAGE_IMPORT_DESCRIPTOR数组的起始地址为"0002647C"(RVA)
因为RVA为"2647C"所以文件偏移为"2367C"(使用RVA To RAW公式计算)
下面查看IMAGE_IMPORT_DESCRIPTOR结构体数组的第一个元素的各个成员
文件偏移 成员 RVA RAW 2367C OriginalFirstThunk(INT) 00026700 00023900 23680 TimeDateStamp - - 23684 ForwarderChain - - 23688 Name 00027110 00024310 2368C FirstThunk(IAT) 00026068 00023268 库名称(Name),根据RVA"27110"转换得到RAW"24310",查看notepad.exe文件偏移"24310"处,可以看到字符串"KERNEL32.dll"
OriginalFirstThunk - INT,INT是一个包含导入函数信息(Ordinal,Name)的结构体指针数组,只有获得了这些信息才能在加载到进程内存的库中准确求得相应的函数的起始地址。跟踪OriginalFirstThunk成员RVA:"26700"=>RAW:"23900",INT以地址数组组成(以NULL结束)
IMAGE_IMPORT_BY_NAME,跟踪数组的第一个值"00026D3C"(RVA)=>"00023F3C"(RAW),其中文件偏移"00023F3C"处最初两个字节"02B0"为Ordinal,是库中函数的固有编号。Ordinal的后面可以看到导入的API函数名称字符串"GetProcAddress"(同C语言一样以'\0'结束)
FirstThunk - IAT(Import Address Table),IAT的RVA为"26068"则RAW为"23268",于INT类似IAT数组以"NULL"结尾,IAT第一个元素值为"00026D3C",与INT的值一样,但有很多情况两者不一样,该值无实际意义,notepad.exe加载到内存时,准确的地址会取代该值
下面使用Ollydbg查看notepad.exe的IAT,如下图,可以看到EP的地址为"00771860",说明这里实际运行时,ImageBase并不是文件中的默认值"00400000",这里可以反推处实际ImageBase为"00750000"(VA:"771860"- RVA:"21860"=ImageBase:"750000"),因此IAT在内存的地址为"00776068",从下图左下角的数据窗口中可以得到验证,里面"7634F550"即为kernel32.GetProcAddress函数的实际地址
EAT
Windows中"库"是为了方便其他程序调用而集中包含相关函数的文件(DLL/SYS)。WIN32 API是最具代表性的库,其中的kernel32.dll文件被称为最核心的库文件。
EAT是一种核心机制,它使不同的应用程序可以调用库文件中提供的函数。也就是说,只有通过EAT才能准确求得从相应库中导出函数的起始地址。与前面讲解的IAT一样,PE文件内的特定结构体(IMAGE_EXPORT_DIRECTORY)保存着导出信息,且PE文件中仅有一个结构体用来说明EAT的IMAGE_EXPORT_DIRECTORY结构体
下图为kernel32.dll文件的IMAGE_OPTIONAL_HEADER32.DataDirectory[0](第一个4字节为RVA,第二个4字节为Size成员变量),由于RVA值为"00092D30"故RAW为"00078D30"
1
2
3
4
5
6
7RVA RAW
.text 10000 1000
.rdata 80000 66000
.data B0000 91000
.didat C0000 92000
.rsrc D0000 93000
.reloc E0000 94000IMAGE_EXPORT_DIRECTORY:IMAGE_EXPORT_DIRECTORY结构体定义代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics; // creation time date stamp
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name; // address of library file name
DWORD Base; // ordinal base
DWORD NumberOfFunctions; // number of funcitons
DWORD NumberOfNames; // number of names
DWORD AddressOfFunctions; // address of function start address array
DWORD AddressOfNames; // address of function name string array
DWORD AddressOfNameOrdinals; // address of ordinal array
}IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;IMAGE_EXPORT_DIRECTORY结构体重要成员如下表:
项 目 含 义 NumberOfFunctions 实际Export函数的个数 NumberOfNames Export函数中具名的函数个数 AddressOfFunctions Export函数地址数组(数组元素个数=NumberOfFunctions) AddressOfNames 函数名称地址数组(数组元素个数=NumberOfNames) AddressOfNameOrdinals Ordinal地址数组(数组元素个数=NumberOfNames) 下图描述的是kernel32.dll文件的IMAGE_EXPORT_DIRECTORY结构体与整个EAT结构
用HexEditor进入kernel32.dll的"00078D30"偏移处,如下图所示
图中高亮部分为IMAGE_EXPORT_DIRECTORY结构体区域,该IMAGE_EXPORT_DIRECTORY的成员如下表所示
文件偏移 成 员 值 RAW 78D30 Characteristics 00000000 - 78D34 TimeDateStamp 56B90D14 - 78D36 MajorVersion 0000 - 78D38 MinorVersion 0000 - 78D3C Name 00096C1E 7CC1E 78D40 Base 00000001 - 78D44 NumberOfFunctions 00000647 - 78D48 NumberOfNames 00000647 - 78D4C AddressOfFunctions 00092D58 78D58 78D50 AddressOfNames 00094674 7A674 78D54 AddressOfNameOrdinals 00095F90 7BF90 函数名称数组:AddressOfNames成员的值为RVA:00094674,即RAW:7A674。用HexEditor查看该地址,如下图所示。此处为4字节RVA组成的数组,数组元素个数为NameberOfNames(0x647),逐一跟随所有RVA值即可发现函数名称的字符串
由第一个RVA"96C8A"=>"7CC8A",查看该处地址可以看到函数名称为"AcquireSRWLockExclusive"
接下来查找"AcquireSRWLockExclusive"函数的Ordinal值,AddressOfNameOrdinals成员的值为RVA:00095F90=>RAW:7BF90,如下图可以看到高亮部分为多个2字节的ordinal组成的数组(ordinal数组中的各元素大小为2个字节),从图可以知道对应的ordinal=3
函数地址数组 - EAT,最后查找"AcquireSRWLockExclusive"的实际地址,AddressOfFunctions成员的值为RVA:92D58=>RAW:78D58,将上面得到的ordinal=3用作下图数组的索引,得到RVA:96CA2
kernel32.dll ImageBase为"6B800000",所以AcquireSRWLockExclusive地址为"6B896CA2",这里因为暂时没找到ollydbg调试kernel32.dll的方法,因此无法用ollydbg验证