最近对PE文件学习了下,整理下来加深影响。文中的ASLR,在另个帖子有总结:这里
目录
0x1 基础概念:
EXE文件和DLL文件基本上只是语义上的区别, 唯一区别是有一个标识字段 指出EXE或DLL, 常见的PE文件格式有:DLL,EXE,OCX,SYS, SCR, CPL, OBJ等
64位的PE文件格式, 做了简单的修饰, 叫PE32+/PE+, 32位字段扩展位64字段
PE格式的定义地方在 winnt.h
头文件中我们能在其中找到PE文件的定义 如下图VC的路径查找
VA是进程虚拟内存的绝对地址, RVA是相对虚拟地址 RVA+ImageBase = VA
32位的Windows OS中, 各进程都分配有4GB的虚拟内存, 所以VA范围: 00000000 ~ FFFFFFFF
PE文件总体框架.
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)
0x3 NT头 分别介绍3个结构体
1
2
3
4
5
6
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
typedef struct _IMAGE_FILE_HEADER
{
WORD Machine;
/
/
指出该PE文件运行的平台,每个CPU都有唯一的标识码,一般
0x14c
(x86)
014C
WORD NumberOfSections;
/
/
指出文件中存在的节区数量 注:这里的定义一定要等于实际
0008
/
/
的大小, 不然程序会运行失败
DWORD TimeDateStamp;
/
/
PE文件的创建时间,一般有连接器填写 UTC(世界标准时)进
40B10868
/
/
行存储 从
1970
年
1
月
1
日
00
:
00
:
00
算起的秒数值 我们可以用C
/
/
语言的localtime()函数(时区也会转换)计算.
DWORD PointerToSymbolTable;
/
/
指向符号表COFF的指针, 用于调试信息. 发现每次看都是
0
00000000
DWORD NumberOfSymbols;
/
/
符号表数量. 发现每次看都是
0
00000000
WORD SizeOfOptionalHeader;
/
/
指出PE的IMAGE_OPTIONAL_HEADER32结构体或
00E0
/
/
PE
+
格式文件的IMAGE_OPTIONAL_HEADER64结构体的长度
/
/
这两个结构体尺寸是不相同的,所以需要在
/
/
SizeOfOptionalHeader中指明大小,
32
位通常位: E0
/
/
64
位通常为:F0(不是绝对的)它们只是最小值,可能有更大的值
WORD Characteristics;
/
/
标识文件的属性, 文件是否可运行, 是否为DLL文件等.
010E
/
/
二进制中每一位代表不同属性, 以 bit oR形式结合起来
/
/
2
个需要记住的值.
0002h
:.exe文件
2000h
: .dll文件
} IMAGE_FILE_HEADER,
*
PIMAGE_FILE_HEADER;
下图为OD程序的文件头. 在上面每个成员下面依次标出.
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
typedef struct _DATA_DIRECTORY
/
/
定义了DataDirectory的结构体
{
DOWORD VirtualAddress;
/
/
该结构体的RVA
DOWORD Size;
/
/
该结构体的大小
}IMAGE_DATA_DIRECTORY,
*
PIMAGE_DATA_DIRECTORY;
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文件加载到虚拟内存时, 指出文件优先装入地址
00400000
/
/
EXE, DLL文件被装载到
0
~
7FFFFFFF
/
/
SYS文件载入内核内存的
80000000
~FFFFFFFF
/
/
执行PE文件时,PE装载器会把EIP设置为: ImageBase
+
AddressOfEntrypoint
DWORD SectionAlignment;
/
/
节在内存中的最小单位 (对齐单位) 一般为:
1000h
00001000
DWORD FileAlignment;
/
/
节在磁盘文件中的最小单位 (对齐单位) 一般为:
200h
00000200
/
/
一般SectionAlignment <
=
FileAlignment,节省储存空间.
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子系统之上
0002
/
/
该值用来区分系统驱动文件(
*
.sys)与普通的可执行文件(
*
.exe,
*
.dll)
/
/
value:
1
含义: Driver文件 tips: 系统驱动(如: ntfs.sys)
/
/
value:
2
含义: GUI文件 tips: 窗口应用程序(如: notepad.exe)
/
/
value:
3
含义: CUI文件 tips: 控制台应用程序(如: cmd.exe)
WORD DllCharacteristics;
/
/
DLL的文件属性 如果是DLL文件,何时调用DLL文件的入口点
0000
/
/
一般的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
DWORD SizeOfStackReserve;
/
/
保留栈的大小 默认是
1MB
00100000
DWORD SizeOfStackCommit;
/
/
初始时指定栈大小 默认是
4KB
00020000
DWORD SizeOfHeapReserve;
/
/
保留堆的大小 默认是
1MB
01000000
DWORD SizeOfHeapCommit;
/
/
指定堆大小 默认是
4K
00001000
DWORD LoaderFlags;
/
/
看到的资料都是保留 value 为
0
00000000
DWORD NumberOfRvaAndSizes;
/
/
数据目录的项数, 即指出了我们下面一个成员数组的个数
00000010
/
/
虽然宏定义了
/
/
但是PE装载器会通过此值来识别数组大小,说明数组大小也可能非
16
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
/
/
很重要
/
/
数据目录, 重点: EXPORT
/
/
IMPORT, RESOURCE, TLS Direction
} IMAGE_OPTIONAL_HEADER32,
*
PIMAGE_OPTIONAL_HEADER32;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
RVA:
0010F000
Size:
000012FA
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
typedef struct _IMAGE_SECTION_HEADER
{
BYTE NAME[IMAGE_SIZEOF_SHORT_NAME];
/
/
节区的名字
8
个字节
2E74657874000000
/
/
如果所有的
8
字节都被用光,该字符串就没有
0
结束符
/
/
典型的名称.data .text .bss 形式 (.不是必须)
/
/
节区名称都和节中的内容不一定相关,节名称没有严格要
/
/
求,前边带有“$”的相同名字的区块在载入时候将会被合
/
/
并,在合并之后的区块中,他们是按照“$”后边的字符
/
/
的字符的字母顺序进行合并的。每个区块的名称都是唯
/
/
一的,不能有同名的两个区块.
union
{
DOWORD PhysicalAddress;
DOWORD VitualSize;
/
/
内存中节区所占大小(实际初始了的数据大小, 未内存对齐)
000AF000
}Misc;
DWORD VirtualAddress;
/
/
内存中节区的起始地址(RVA). 开始没有值, 由SectionAlignment确定
00001000
DWORD SizeofRawData;
/
/
磁盘文件中节区所占大小(对齐后的大小)
000AE800
DWORD PointerToRawData;
/
/
磁盘文件中节区的起始位置. 开始没有值, 由FileAlignment确定
00000600
DWORD PointerToRelocations;
/
/
重定位指针 下面四个都是用于目标文件的信息
00000000
DWORD PointerToLinenumbers;
/
/
行数指针
00000000
WORD NumberOfRelocations;
/
/
重定位数
0000
WORD NumberOfLinenumbers;
/
/
行数
0000
DWORD Characteristics;
/
/
指定节的属性,权限. 由不同的值 bit
or
而成
60000020
/
/
0x20
: 包含代码.
0x40
: 包含初始化数据的节
/
/
0x80
: 包含未初始化数据的节
0x20000000
: 可执行 (x)
/
/
0x40000000
: 可读 (r)
0x80000000
: 可写 (w)
}IMAGE_SECTION_HEADER,
*
PIMAGE_SECTION_HEADER;
下图展示OD程序的各个节, 并将(.txt)节中的各成员值在上面依次标出
由于每个节区都有内存地址到文件偏移间的映射(RAW-RVA). 我们可以通过节区的VirtualAddress与PointerToRawData来从RVA->RAW.
注: 由于VirtualAddress是未对齐的大小,而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
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
/
/
通常为
0
00000000
DWORD TimeDateStamp;
/
/
创建时间, 不是很有效的值
00000000
WORD MajorVersion;
/
/
主版本号
0000
WORD MinorVersion;
/
/
小版本号
0000
DWORD Name;
/
/
指向以
0
结尾的ASCII字符串(DLL名称)的RVA
0010F780
/
/
如(user32.dll, kernel32.dll)
DWORD Base;
/
/
基址, 一个输出项的序数就是函数地址数组中的索引值加base.
00000001
/
/
base大多时候为
1
, 说明第一个输出函数的序数为
1
DWORD NumberOfFunctions;
/
/
实际Export函数的个数
000000BC
DWORD NumberOfNames;
/
/
Export函数中具名的函数个数(以名称来输出函数的数量)
000000BC
DWORD AddressOfFunctions;
/
/
Export函数地址数组(数组个数: NumberOfFunctions)
0010F028
DWORD AddressOfNames;
/
/
Export函数名称地址数组(数组个数:NumberOfNames)
0010F318
DWORD AddressOfNameOrdinals;
/
/
指向函数名名称对应序数输出条目列表的RVA
0010F608
/
/
数组每个名称拥有一个相应的序数(数组个数:NumberOfNames)
} 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
1.查看输出表名称(RVA : 0010F780 -> RAW: 000CE980 )
2.查找函数名称.
(1)AddressOfNames . (RVA: 0010F318 -> RAW: 000CE518 )
由(RVA:0010F78C -> RAW: 000CE98C ):
现在已经找到了函数的名称, 下面模拟查看一个指定名称函数的RVA. 假设我们找的是Addsorteddata. (即第一个函数), (1)通过strcmp(). 得到它的索引值是0, 记为 index_name. (2)通过AddressOfNameOrdinals使用index_name找到函数的序数, 通过下图得到序数0, 记为index_address.
AddressOfNameOrdinals . (RVA: 0010F608 -> RAW: 000CE808 ):
(3)通过AddressOfFunctions函数地址数组(EAT), 使用index_address为索引值得到我们指定函数的RVA.
AddressOfFunctions(RVA: 0010F028 -> RAW: 000CE228 ):
到此, 得到我们指定输出函数Addsorteddata. 的RVA: 00054EFC . 最后通过 OD载入OD看一下.
从这里也说明了, .exe文件也是可能有输出表的
0x6 IMAGE_IMPORT_DESCRIPTOP 输入表 记录PE文件要导入那些库文件 DataDirectory[1]记录了RVA及Size.
首先, 执行一个程序会有很多的函数是公用的,在动态链接库里(动态链接库, .dll文件总是附加在一个要执行的程序中, .dll文件中有说明库EAT的输出表), 如下图, 一个程序加载的部分 .dll文件.
我们的输入表记录了需要用到的函数名称, 通过在加载的动态链接库中搜索该函数得到实际的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
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).
下面实例查看OD程序的输入表.
1.首先从PE文件可选头的 DataDirectory[1].VirtualAdress 得到输入表的RVA: 10D000h 及size: 1c87h
2.RVA: 10D000h -> RAW: (10D000-10D000+CC400) = CC400h
3.找到输入表. 记录下对应成员的RVA.
4.查看该输入表名称: ADVAPI32,DLL , RVA: 10D9C8 -> RAW: (10D9C8-10D000+CC400) = CCDC8
5.查看 OriginalFirstThunk( INT ) RVA:10D0C8 -> RAW: (10D0C8-10D000+CC400) = CC4C8
6.可以看到第一成员的最高位是 0, 则该值是IMAGE_IMPORT_BY_NAME的RVA.(RVA: 10DA33 -> RAW: CCE33 )
7.查看 FirstThunk( IAT ) RVA: 10D0E4 -> RAW: (10D0E4-10D000+CC400) = CC4E4
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 的值是不同的
输入表与输出表联系还是比较大, 结合以来看看清楚很多.
PE
[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法
最后于 2020-9-17 23:07
被BIX编辑
,原因:
上传的附件: