PE学习

0x1 基础概念:

EXE文件和DLL文件基本上只是语义上的区别, 唯一区别是有一个标识字段指出EXE或DLL, 常见的PE文件格式有:DLL,EXE,OCX,SYS, SCR, CPL, OBJ等
64位的PE文件格式, 做了简单的修饰, 叫PE32+/PE+, 32位字段扩展位64字段
PE格式的定义地方在 winnt.h 头文件中我们能在其中找到PE文件的定义 如下图VC的路径查找
image-20200507103628502
VA是进程虚拟内存的绝对地址, RVA是相对虚拟地址 RVA+ImageBase = VA
32位的Windows OS中, 各进程都分配有4GB的虚拟内存, 所以VA范围: 00000000 ~ FFFFFFFF

PE文件总体框架.

image-20200520084527063

PE文件执行顺序.

1.执行一个PE文件时, PE装载器首先会找DOS头签名(MZ),检查是否有效, 然后是DOS头里的找 e_lfanew(最后一个成员, 指示PE头的), 如果找到, 则直接跳转.
2.找到PE头, 开始检查PE头信息属性是否有效, 如果有效, 就跳转到PE头尾部.
3.紧跟PE头尾部的是节表, PE装载器开始读取节表中记录了每个属性的信息. 平且采用文件映射将这些节映射到内存. 文件映射: 在执行一个PE文件的时候,Windows并不在一开始就将整个文件读入内存,而是采用与内存映射的机制,也就是说,Windows装载器在装载的时候仅仅建立好虚拟地址和PE文件之间的映射关系,只有真正执行到某个内存页中的指令或者访问某一页中的数据时,这个页面才会被从磁盘提交到物理内存,这种机制使文件装入的速度和文件大小没有太大的关系
4.PE文件映射入内存后, PE装载器继续处理一些逻辑结构, 如输入表的修正.

0x2 MS-DOS头部及DOS存根

DOS头的作用是兼容MS-DOS操作系统中的可执行文件, 该结构体大小为64字节(0x40)
2个重要成员 e_magic(DOS头第一个成员): DOS签名(4D5A -> ASCII值 MZ) e_lfanew(DOS头最后一个成员): 指示NT头的偏移, 从这里找到PE头(取决于DOS存根大小)
DOS存根是DOS头与PE文件头中间部分的内容, 为16位的汇编指令组成, 既有代码也有数据, 大小不固定
我们知道DOS存根的内容是当我们的程序在DOS环境中运行时执行的代码, 也就是给一个提示信息:This is program cannot be run in DOS mode, 那我们是可以随便将其内容修改为自己想填充的东西, 反正不会影响在window os中的运行, 但记住这个大小是不能修改的, 会影响后面指令索引地址跟着出错, 最后程序崩溃(刚开始学习时在一道逆向题中, 就犯了这个错) 如下图所示OD程序, 重要字段已标出(DOS存根从0x40 - 0x1FF)
image-20200508155307169

0x3 NT头

分别介绍3个结构体

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct _IMAGE_NT_HEADERS
{
DOWORD Signature;
//PE头的标志 50450000

IMAGE_FILE_HEADER FileHeader;
//文件头 size: 0xF8 记载文件的大部分属性

IMAGE_OPTIONAL_HEADER32 OptionalHeader;
//可选头 very important

}IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
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
typedef struct _IMAGE_FILE_HEADER
{
WORD Machine;
//指出该PE文件运行的平台,每个CPU都有唯一的标识码,一般0x14c(x86)
014C

WORD NumberOfSections;
//指出文件中存在的节区数量 注:这里的定义一定要等于实际
//大小, 不然程序会运行失败
0008

DWORD TimeDateStamp;
//PE文件的创建时间,一般有连接器填写 UTC(世界标准时间)进
//行存储 从1970年1月1日00:00:00算起的秒数值 我们可以用C
//语言的localtime()函数(时区也会转换)计算.
40B10868

DWORD PointerToSymbolTable;
//指向符号表COFF的指针, 用于调试信息. 发现每次看都是0
00000000

DWORD NumberOfSymbols;
//符号表数量. 发现每次看都是0
00000000

WORD SizeOfOptionalHeader;
//指出PE的IMAGE_OPTIONAL_HEADER32结构体或者
//PE+格式文件的IMAGE_OPTIONAL_HEADER64结构体的长度
//这两个结构体尺寸是不相同的,所以需要SizeOfOptionalHeader
//中指明大小 32位通常位: E0 64位通常为: F0 (不是绝对的)
//它们只是最小值,可能有更大的值
00E0

WORD Characteristics;
//标识文件的属性, 文件是否可运行, 是否为DLL文件等.
//二进制中每一位代表不同属性, 以 bit oR形式结合起来
//2个需要记住的值. 0002h:.exe文件 2000h: .dll文件
010E

} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

下图为OD程序的文件头. 在上面每个成员下面依次标出.

image-20200507203805570

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
typedef struct _DATA_DIRECTORY////定义了DataDirectory的结构体 
{
DOWORD VirtualAddress;
//该结构体的RVA

DOWORD Size;
//该结构体的大小
}IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16

typedef struct _IMAGE_OPTIONAL_HEADER
{
WORD Magic;
//这个可选头的类型 PE: 10Bh PE+: 20Bh 可以依次区分是32位还是64位
010B

BYTE MajorLinkerVersion;
//链接器的版本号(不重要)
05

BYTE MinorLinkerVersion;
//链接器的小版本号(不重要)
00

DWORD SizeOfCode;
//代码段的长度
000AF000

DWORD SizeOfInitializedData;
//初始化的数据长度
0008EC00

DWORD SizeOfUninitializedData;
//未初始化的数据长度
00000000

DWORD AddressOfEntryPoint;
//程序EP的RVA, 指出程序最先执行代码的起始地址 (很重要)
00000100

DWORD BaseOfCode;
//代码段起始地址的RVA
00000100

DWORD BaseOfData;
//数据段起始地址的RVA
000B0000

DWORD ImageBase;
//VA: 0~FFFFFFFF(32位系统).PE文件加载到虚拟内存时, 指出文件优先装入地址
//EXE, DLL文件被装载到0~7FFFFFFF
//SYS文件载入内核内存的 80000000~FFFFFFFF
//执行PE文件时,PE装载器会把EIP设置为: ImageBase+AddressOfEntrypoint
00400000

DWORD SectionAlignment;
//节在内存中的最小单位 (对齐单位) 一般为: 1000h
00001000

DWORD FileAlignment;
//节在磁盘文件中的最小单位 (对齐单位) 一般为: 200h
//一般SectionAlignment <= FileAlignment,节省储存空间.
00000200

WORD MajorOperatingSystemVersion;
//操作系统主版本号(不重要)
0004

WORD MinorOperatingSystemVersion;
//操作系统小版本号(不重要)
0000

WORD MajorImageVersion;
//映象文件主版本号, 这个是开发者自己指定的,由连接器填写(不重要)
0000

WORD MinorImageVersion;
//映象文件小版本号(不重要)
0000

WORD MajorSubsystemVersion;
//子系统版本号
0004

WORD MinorSubsystemVersion;
//子系统小版本号
0000

DWORD Win32VersionValue;
//Win32版本值 目前看过的文件都是 0
00000000

DWORD SizeOfImage;
//指定PE image在虚拟内存中所占空间的大小 SectionAlignment的倍数
00180000

DWORD SizeOfHeaders;
//指出整个PE头的大小(FileAlignment整数倍)
//它也是从文件的开头到第一节的原始数据的偏移量, 可以找到第一节区
00000600

DWORD CheckSum;
//映象文件的校验和 目的是为了防止载入无论如何都会冲突的、已损坏的二进制文件
00000000

WORD Subsystem;
//说明映像文件应运行于什么样的NT子系统之上
//该值用来区分系统驱动文件(*.sys)与普通的可执行文件(*.exe, *.dll)
//value: 1 含义: Driver文件 tips: 系统驱动(如: ntfs.sys)
//value: 2 含义: GUI文件 tips: 窗口应用程序(如: notepad.exe)
//value: 3 含义: CUI文件 tips: 控制台应用程序(如: cmd.exe)
0002

WORD DllCharacteristics;
//DLL的文件属性 如果是DLL文件,何时调用DLL文件的入口点
//一般的exe文件有以下2个属性:
//IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE(表示支
//持终端服务器)8000h IMAGE_DLLCHARACTERISTICS_NX_COMPAT
//(表示程序采用了)/NXCOMPAT编译100h (bit or 为 81000)
//但是开启了ASLR的程序会多一个
//IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE(DLL can move)
//40h的属性 (bit or 后为8140),那可以修改这里关闭ASLR
0000

DWORD SizeOfStackReserve;
//保留栈的大小 默认是1MB
00100000

DWORD SizeOfStackCommit;
//初始时指定栈大小 默认是4KB
00020000

DWORD SizeOfHeapReserve;
//保留堆的大小 默认是1MB
01000000

DWORD SizeOfHeapCommit;
//指定堆大小 默认是4K
00001000

DWORD LoaderFlags;
//看到的资料都是保留 value 为 0
00000000

DWORD NumberOfRvaAndSizes;
//数据目录的项数, 即指出了我们下面一个成员数组的个数
//虽然宏定义了#defineIMAGE_NUMBEROF_DIRECTORY_ENTRIES16
//但是PE装载器会通过此值来识别数组大小,说明数组大小也可能非16
00000010

IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
//很重要,一个数据目录,数组中的每一项记录了对于数据项的RVA及Size
//重点: EXPORT IMPORT, RESOURCE, TLS Direction

} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

image-20200509154933553

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // 输出表(导入表) (重要)
RVA:0010F000 Size:000012FA

#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // 输入表 (重要)
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // 资源目录 (重要)
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // 异常目录
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // 安全目录
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // 基址重定位表 (重要)
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // 调试目录
#define IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // 描述信息(版权信息之类)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 8 // 架构特定数据
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 9 // 机器值
#define IMAGE_DIRECTORY_ENTRY_TLS 10 // 线程级局部存储目录(重要)
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 11 // 载入配置目录
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 12 // 绑定输入目录
#define IMAGE_DIRECTORY_ENTRY_IAT 13 // 输入地址表
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 14 // 延迟加载导入描述符
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 15 // COM运行时描述符

PE文件中的code(代码), data(数据), resource(资源)等按照属性分类储存在不同的节区, (1)这样分类便于统一和查看 (2)这样可以在一定程度上保护程序的安全性, 因为如果把所有的代码数据放在一起的话, 当我们向数据区写数据时, 若输入超过缓冲区的大小, 那么就有可能会将其下的code(指令)覆盖掉, 造成应用程序崩溃. PE文件就可以把相似属性的的数据保存在一个被称为”节区”的地方, 然后为每个节区设置不同的特性,访问权限等.

0x4 节区头

节区头是由IMAGE_SECTION_HEADER结构体组成的数组, 每个结构体对应一个节区

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
#define IMAGE_SIZEOF_SHORT_NAME      8

typedef struct _IMAGE_SECTION_HEADER
{
BYTE NAME[IMAGE_SIZEOF_SHORT_NAME];
//节区的名字 8个字节
//如果所有的8字节都被用光,该字符串就没有0结束符,
//典型的名称.data .text .bss 形式 (.不是必须)
//节区名称都和节中的内容不一定相关,
//节名称没有严格要求前边带有“$”的相同名字的区块在载入时候将会被合并,
//在并,在合并之后的区块中,他们是照“$”后边的字符的字母顺序进行合并的。
//每个区块的名称都是唯一的,不能有同名的两个区块


union
{
DOWORD PhysicalAddress;
DOWORD VitualSize;
//内存中节区所占大小(实际初始了的数据大小, 未内存对齐)

}Misc;

DWORD VirtualAddress;
//内存中节区的起始地址(RVA). 开始没有值, 由SectionAlignment确定

DWORD SizeofRawData;
//磁盘文件中节区所占大小(对齐后的大小)

DWORD PointerToRawData;
//磁盘文件中节区的起始位置. 开始没有值, 由FileAlignment确定

DWORD PointerToRelocations;
//重定位指针 下面四个都是用于目标文件的信息

DWORD PointerToLinenumbers;
//行数指针

WORD NumberOfRelocations;
//重定位数

WORD NumberOfLinenumbers;
//行数

DWORD Characteristics;
//指定节的属性,权限. 由不同的值 bit or 而成
//0x20: 包含代码. 0x40: 包含初始化数据的节
//0x80: 包含未初始化数据的节 0x20000000: 可执行 (x)
//0x40000000: 可读 (r) 0x80000000: 可写 (w)

}IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

下图展示OD程序的各个节, 并将(.txt)节中的各成员值在上面依次标出 image-20200509170302585

由于每个节区都有内存地址到文件偏移间的映射(RAW-RVA). 我们可以通过节区的VirtualAddress与PointerToRawData来从RVA->RAW.

注: 由于VirtualSize是未对齐的大小,而SizeofRawData是对齐后的大小, 那么 VirtualAddress一般比SizeofRawData小. 但是也有例外, 就是当含有未初始化数据的节(如.bss), 在磁盘中未初始化数据是不占空间的, 但是到了内存, 未初始化的数据是要赋值占空间.


0x5 IMAGE_EXPORT_DIRECTORY 输出表

一般dll文件才有,DataDirectory[0]记录了RVA及Size.

用来描述模块(dll)中的导出函数的结构,如果一个模块导出了函数,那么这个函数会被记录在导出表中,从 库向其他PE文件提供服务

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
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
//通常为0
00000000

DWORD TimeDateStamp;
//创建时间, 不是很有效的值
00000000

WORD MajorVersion;
//主版本号
0000

WORD MinorVersion;
//小版本号
0000

DWORD Name;
//指向以0结尾的ASCII字符串(DLL名称)的RVA
//如(user32.dll, kernel32.dll)
0010F780

DWORD Base;
//基址, 一个输出项的序数就是函数地址数组中的索引值加base.
//base大多时候为1 , 说明第一个输出函数的序数为1
00000001

DWORD NumberOfFunctions;
//实际Export函数的个数
000000BC

DWORD NumberOfNames;
//Export函数中具名的函数个数(以名称来输出函数的数量)
000000BC

DWORD AddressOfFunctions;
//Export函数地址数组(数组个数: NumberOfFunctions)
0010F028

DWORD AddressOfNames;
//Export函数名称地址数组(数组个数:NumberOfNames)
0010F318

DWORD AddressOfNameOrdinals;
//指向函数名名称对应序数输出条目列表的RVA
//数组每个名称拥有一个相应的序数(数组个数:NumberOfNames)
0010F608

} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

从导出表中获得函数地址的API为: GetProcAddress()函数. 该API用来引用EAT来获取指定的API的地址.

注: (1) 导出函数也可能没有名称的, 这时只能通过序数导出 (2) 序数是指定某个输出函数的独一无二的16位数字(2个字节)

两种导出函数的方法:

一:按函数名字

(1)通过AddressOfNames找到函数名称数组. 使用strcmp()函数, 在(RVA)指针数组从索引值0开始依次与我们要找的函数名称对比,从而找到索引值 index_name

(2)通过AddressOfNameOrdinals找到存放函数序号的数组, 使用步骤(1)获得的index_name为索引值找到函数地址的序号(index_address)

(3)通过AddressOfFunctions找到函数地址数组(EAT), 在EAT中使用步骤(2)获得的index_address为索引值找到指定函数的RVA

二:按函数序号

(1)使用我们函数的序号减去 _IMAGE_EXPORT_DIRECTORY.Base 的值得到函数地址索引值index_address

(2)通过AddressOfFunctions找到函数地址数组(EAT), 在EAT中使用步骤(1)获得的index_address为索引值找到指定函数的RVA

下面依旧用OD程序来看导出表, 并将每个值标在上面每个成员下面, 通过上面IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]介绍, 已经标出导出表的RVA: 0010F000 Size: 000012FA. 再通过CFF Explorer 工具查看每个节的地址可以计算出输出表的 RAW : 00CE200

image-20200510143001536

1.查看输出表名称(RVA : 0010F780 -> RAW: 000CE980)

image-20200510151725862

image-20200510151945596

2.查找函数名称.

(1)AddressOfNames. (RVA: 0010F318 -> RAW: 000CE518)

image-20200510153257760

image-20200510154247725

由(RVA:0010F78C -> RAW: 000CE98C):

image-20200510154722496

现在已经找到了函数的名称, 下面模拟查看一个指定名称函数的RVA. 假设我们找的是Addsorteddata.(即第一个函数), (1)通过strcmp(). 得到它的索引值是0, 记为 index_name. (2)通过AddressOfNameOrdinals使用index_name找到函数的序数, 通过下图得到序数0, 记为index_address.

AddressOfNameOrdinals. (RVA: 0010F608 -> RAW: 000CE808):image-20200510160635396

(3)通过AddressOfFunctions函数地址数组(EAT), 使用index_address为索引值得到我们指定函数的RVA.

AddressOfFunctions(RVA: 0010F028 -> RAW: 000CE228):image-20200510161225077

到此, 得到我们指定输出函数Addsorteddata.的RVA: 00054EFC. 最后通过 OD载入OD看一下.image-20200510161939914

从这里也说明了, .exe文件也是可能有输出表的


0x6 IMAGE_IMPORT_DESCRIPTOP 输入表

记录PE文件要导入那些库文件 DataDirectory[1]记录了RVA及Size.

首先, 执行一个程序会有很多的函数是公用的,在动态链接库里(动态链接库, .dll文件总是附加在一个要执行的程序中, .dll文件中有说明库EAT的输出表), 如下图, 一个程序加载的部分 .dll文件.image-20200518132004979

我们的输入表记录了需要用到的函数名称, 通过在加载的动态链接库中搜索该函数得到实际的RVA, 再记录到输入表中, 供程序使用. 另外执行一个普通的程序一般需要多个库, 那导入多少库, 就会有多少个输入表结构体. 这就构成了结构体数组且结构体数组最后以 NULL 结束 (即每个导入的 DLL 都会成为数组中的一项).

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
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics
// 由于是一个联合, 如果这是该结构体数组的最后一
// 项, 那使用 Characteristics成员,且值为 0
// 否则使用下面一个成员

DWORD OriginalFirstThunk;
// INT(import name table)结构体数组的RVA
// 数组每个成员记录了要使用函数名称与序号

} DUMMYUNIONNAME;

DWORD TimeDateStamp;
// 映象绑定前,这个值是0,绑定后是导入模块的时间戳
// 据说可以用来确定输入表是否绑定从而是否需要重定位

DWORD ForwarderChain;
// 中转链, 输入函数列表中第一个中转的、32位的索引
// 如果没有转发链, 值为 -1

DWORD Name;
// DLL文件的名称(0结尾的ASCII码字符串)的32位的RVA,
// 所以一个导入模块对应一个这样的数组


DWORD FirstThunk;
//IAT(import address table)结构体数组的RVA

} IMAGE_IMPORT_DESCRIPTOR;


上面的 OriginalFirstThunk(INT), FirstThunk(IAT) 成员在PE文件加载前一般是都同时指向相同地址的 IMAGE_THUNK_DATA 数组.下面是 IMAGE_THUNK_DATA32的定义


typedef struct _IMAGE_THUNK_DATA32
{
//一个联合, 所以意味着每次只能使用一个成员
union
{
DWORD ForwarderString;
// 中转链,一个DLL文件能输出不定义在本DLL文件中却需从另一个
// DLL文件中的函数.

DWORD Function;
// 函数的地址

DWORD Ordinal;
// 函数的序数. 由于所有成员都是同一个地址, 当最高位为1时表
// 示列表中没有函数的名字信息, 只能通过本序数查找函数,
// 用低16位表示的序数, 因为最高位作为标志了。

DWORD AddressOfData;
// 同上, 由于所有成员都是同一个地址, 当最高位为0时, 则使用
// 本成员,用低31为表示 _IMAGE_IMPORT_BY_NAME结构的RVA

} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;


上面介绍的 AddressOfData成员的低31就记录指向下面所示的 _IMAGE_IMPORT_BY_NAME 结构体数组的地址(RVA), 数组中每个成员的前2个字节是函数的序数, 后面跟着长度不定的函数名称的字符串.


typedef struct _IMAGE_IMPORT_BY_NAME
{
WORD Hint;
// 函数的序数(即索引, 与输出表中讲的一样)
BYTE Name[1];
// 函数名称数组,记录函数的名称. 数量未定义即长度不定.

}IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

注: 上面所讲的 OriginalFirstThunk 成员(指针数组)的值是不能改写的, 通过它寻找函数的名称. 而 FirstThunk 成员(指针数组)的值在PE文件在被PE装载器时, PE装载器会通过 OriginalFirstThunk 得到函数的名称或者序数, 然后通过函数名称在加载的.dll文件的输出表中找到函数的实际地址, 然后替换到FirstThunk的一个值. 装载完成后, FirstThunk 数组就指向向了函数实际的地址. 另外上面的 TimeDateStamp 成员可以用来确定输入表是否绑定从而是否需要重定位, 如果它的值是0, 那么输入列表没有被绑定, 加载器总是要修复输入表. 否则输入被绑定, 但该时间戳的值必须和.dll文件头中 TimeDateStamp 的一样, 如果不一样, 仍会修正输入表, 就会进行下面的步骤.

导入函数输入到 IAT 的顺序

1.读取 _IMAGE_IMPORT_DESCRIPTOR中的name成员, 获取库名称字符串. 如(user32.dll)

2.装载相应的库. LoadLibrary(“user32.dll”)

3.读取_IMAGE_IMPORT_DESCRIPTOR中的 OriginalFirstThunk 成员, 得到 INT地址.

4.逐一读取 INT中数组的值, 获取相应的 IMAGE_IMPORT_BY_NAME地址(RVA)

5.使用 IMAGE_IMPORT_BY_NAME的Hint (ordinak/序数)或name项, 获取相应函数的起始地址.

GetProcAddress(“函数名称”)

6.读取 IAT 成员, 获得IAT地址.

7.将上面获得的函数地址输入相应的IAT数组值.

8.重复 步骤 4 -7, 直到INT结束.

图示一下, INT 与 IAT 关系 (技术太差了.png).

image-20200519174651226

下面实例查看OD程序的输入表.

1.首先从PE文件可选头的 DataDirectory[1].VirtualAdress 得到输入表的RVA: 10D000h 及size: 1c87h

image-20200519150821954

2.RVA: 10D000h -> RAW: (10D000-10D000+CC400) = CC400h

image-20200519151445783

3.找到输入表. 记录下对应成员的RVA.

image-20200519153011688

4.查看该输入表名称: ADVAPI32,DLL, RVA: 10D9C8 -> RAW: (10D9C8-10D000+CC400) = CCDC8

image-20200519153843705

5.查看 OriginalFirstThunk( INT ) RVA:10D0C8 -> RAW: (10D0C8-10D000+CC400) = CC4C8

image-20200519155230578

6.可以看到第一成员的最高位是 0, 则该值是IMAGE_IMPORT_BY_NAME的RVA.(RVA: 10DA33 -> RAW: CCE33)

image-20200519160556676

7.查看 FirstThunk( IAT ) RVA: 10D0E4 -> RAW: (10D0E4-10D000+CC400) = CC4E4

image-20200519163851829

8.从步骤7可以看到, PE装载器装载PE文件之前, INT与IAT各元素同时指向相同的地址.

9.再看 TimeDateStamp 成员的值为 0, 那就是输入表被绑定, 如果与该对应 .dll PE文件的文件头的 TimeDateStamp的值相同, 那这个输入表是不需要修正的.

10.从上面知道 IAT 的RVA: 10D0E4. 库名称: ADVAPI32,DLL另外使用一个OD载入这个OD程序看看. 可以看到加载该.dll文件文件的RVA是从 FC0000开始的, 而查看未被PE装载器装载前的状态, IAT的RVA是 10D0E4,所以显然这是需要PE装载器装载时对输入表修正的, 那也可推出他们的 TimeDateStamp 的值是不同的

image-20200519164915615

输入表与输出表联系还是比较大, 结合起来看看清楚很多.


-------------本文结束感谢您的阅读-------------