Re_Pe

复习可执行文件的文件结构——PE。PE文件是微软Windows操作系统上的程序文件,意为可移植的可执行的文件。PE的段头直接沿用的COFF 的段头结构。

1、PE的结构


DOS头: 是DOS命令窗口下可以执行,其实没有PE文件也是可以执行的(听说是老一辈习惯啦DOS命令下执行,就加上去啦)。
NT头: 是PE中最大的结构体啦,其中有签名,文件头和可选头。
节区头: 定义(代码,数据,资源等的大小,起始位置,权限等)

2、DOS头

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 .EXE header
WORD e_magic; // Magic number
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; // Checksum
WORD e_ip; // Initial IP value
WORD 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; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

e_magic:所有PE开头都有DOS签名 “MZ”,这是以一个名叫Mark Zbikowski的DOS可执行文件的设计者首字母命名的

e_lfanew:指向NT头的位置,long类型,占4个字节。例:

3、NT头

NT结构体

1
2
3
4
5
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER OptionalHeader;
} IMAGE_NT_HEADERS, *PIMAGE_NT_HEADERS;

第一个参数是: 一个PE标志。在一个有效的PE文件里,Signature字段被设置为00004550h。
第二个参数是: IMAGE_FILE_HEADER结构体。
第三个参数是: OptionalHeader结构体。

NT文件头

1
2
3
4
5
6
7
8
9
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;         //机器型号,哪个CPU可以跑的.重要.
WORD NumberOfSections;     //节的数量 .data , .text
DWORD TimeDateStamp;       //程序的编译时间,参考用,没有实际作用
DWORD PointerToSymbolTable;   //符号表地址,主要是给比人用的
DWORD NumberOfSymbols;       //符号表大小
WORD SizeOfOptionalHeader;   //可选头大小,这个字段很重要.才知道可选头是多大
WORD Characteristics;      //文件属性,描述文件信息的.
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

NT可选头结构体

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
typedef struct _IMAGE_OPTIONAL_HEADER
{
//
// Standard fields.
//
WORD Magic; // 标志字, ROM 映像(0107h),普通可执行文件(010Bh)
BYTE MajorLinkerVersion; // 链接程序的主版本号
BYTE MinorLinkerVersion; // 链接程序的次版本号
DWORD SizeOfCode; // 所有含代码的节的总大小
DWORD SizeOfInitializedData; // 所有含已初始化数据的节的总大小
DWORD SizeOfUninitializedData; // 所有含未初始化数据的节的大小
DWORD AddressOfEntryPoint; // 程序执行入口RVA
DWORD BaseOfCode; // 代码的区块的起始RVA
DWORD BaseOfData; // 数据的区块的起始RVA
DWORD ImageBase; // 程序的首选装载地址
DWORD SectionAlignment; // 内存中的区块的对齐大小
DWORD FileAlignment; // 文件中的区块的对齐大小
WORD MajorOperatingSystemVersion; // 要求操作系统最低版本号的主版本号
WORD MinorOperatingSystemVersion; // 要求操作系统最低版本号的副版本号
WORD MajorImageVersion; // 可运行于操作系统的主版本号
WORD MinorImageVersion; // 可运行于操作系统的次版本号
WORD MajorSubsystemVersion; // 要求最低子系统版本的主版本号
WORD MinorSubsystemVersion; // 要求最低子系统版本的次版本号
DWORD Win32VersionValue; // 莫须有字段,不被病毒利用的话一般为0
DWORD SizeOfImage; // 映像装入内存后的总尺寸
DWORD SizeOfHeaders; // 所有头+ 区块表的尺寸大小
DWORD CheckSum; // 映像的校检和
WORD Subsystem; // 可执行文件期望的子系统
WORD DllCharacteristics; // DllMain()函数何时被调用,默认为0
DWORD SizeOfStackReserve; // 初始化时的栈大小
DWORD SizeOfStackCommit; // 初始化时实际提交的栈大小
DWORD SizeOfHeapReserve; // 初始化时保留的堆大小
DWORD SizeOfHeapCommit; // 初始化时实际提交的堆大小
DWORD LoaderFlags; // 与调试有关,默认为0
DWORD NumberOfRvaAndSizes; // 下边数据目录的项数,这个字段自Windows NT 发布以来 // 一直是16
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
// 数据目录表
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

一共31个字段成员,6个重要的

RVA也叫作OEP
AddressOfEntryPoint 持有EP 的RVA 值
基址
SizeOfHeader PE 头的大小
Subsystem 用来区分系统驱动文件与普通可执行文件。
DataDirectory数组

重点是最后一个成员IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];

因为DataDirectory数组里保存了导入表(用了哪些dll),导出表,TLS(Thread Local Storage) Directory等RVA和大小的信息
倒数第二个变量决定NumberOfRvaAndSizes数组长度
在LoadPE工具中,文件头显示信息,如下:

DataDirectory数组

IMAGE_DATA_DIRCTORY结构如下:

1
2
3
4
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; //相对虚拟地址
DWORD Size;      //大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

一个是RVA,一个是大小

data directory数据目录在WINNT.H中定义为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 导出表
#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_GLOBALPTR 8 机器值
#define IMAGE_DIRECTORY_ENTRY_TLS 9 TLS目录
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 载入配值目录
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 绑定输入表
#define IMAGE_DIRECTORY_ENTRY_IAT 12 导入地址表
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 延迟载入描述
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 COM信息

在LoadPE工具中,数据目录显示信息,如下:

4、RVA和RAW

理解PE 最重要的一个部分就是理解文件从磁盘到内存地址的映射过程,做逆向的人员,只有熟练地掌握才能跟踪到程序的调用过程和位置,才能分析和寻找漏洞。
对于文件和内存的映射关系,其实很简单,他们通过一个简单的公式计算而来:

换算公式是这样的:
RAW - PointToRawData(磁盘文件中节区起始位置) = RVA(相对虚拟地址) - VirtualAddress

寻找过程就是先找到RVA 所在的段,然后根据公式计算出文件偏移。因为我们通过逆向工具,可以在内存中查找到所在的RVA,进而我们就可以计算出在文件中所在的位置,这样,就可以手动进行修改。

VA与RVA公式是这样的:

RVA = VA(虚拟地址) - ImageBase(基址)

结果:

RAW = VA - ImageBase - VirtualAddress + PointerToRawData

比如:
VA=0x003A20F4 , ImageBase =0x003A0000

可以看到0x20F4地址位于VirtualAddress 为0x2000的.rdata节,偏移为0x20F4 - 0x2000 = 0xF4
观察节表,.rdata的PointerToRawData为0xE00,字符串在磁盘中的地址为0xE00 + 0xF4 = 0xEF4
使用公式:
RAW = VA - ImageBase - VirtualAddress + PointerToRawData = 0x003A20F4 - 0x003A0000 - 0x2000 + 0xE00 = 0xEF4
用winhex打开二进制文件

5、IAT与EAT

IAT

一个普通PE文件的运行往往需要导入多个库文件,在PE文件运行时如何找到库文件中函数的准确入口是程序正确运行的保证。IAT就是提供这样保证的一个机制。IAT总得来说是一张表,表内存储着每个库文件函数在内存中的地址。
结构体

1
2
3
4
5
6
7
8
9
10
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; //导入表结束标志
DWORD OriginalFirstThunk; //RVA指向一个结构体数组(INT表)
};
DWORD TimeDateStamp; //时间戳
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name; //RVA指向dll名字,以0结尾
DWORD FirstThunk; //RVA指向一个结构体数组(IAT表)
} IMAGE_IMPORT_DESCRIPTOR, *PIMAGE_IMPORT_DESCRIPTOR;

第一个成员是一个联合体:一般给出的是OriginalFirstThunk的值,这个值是INT的地址,INT(Import Name Table)是一个存储了库文件函数名称的表
第二个成员是时间戳
第三个成员是ForwarderChain
第四个成员是Name,存储的是库名称字符数组的地址
第五个成员是FirstThunk,存储的是IAT表的地址

第一步:
PE加载器读取结构体成员的值,Name成员找到库名称,然后将库文件加载到内存中来。
第二步:
PE加载器读取OriginalFirstThunk值获得INT地址,然后依次读取INT各项的值,根据函数的标号获取函数的地址
第三步:
根据FirstThunk的值获取IAT的地址,将上一步获得地址送入IAT中存储。

理解:读取IID(结构体)成员name获取库名->load(库)->读取IID的成员,获取INT的地址->读取函数名并获取地址->读取IID的成员,获取IAT的地址->将得到函数地址存入IAT中->重复直到INT为NULL

EAT

EAT对应的结构体为IMAGE_EXPORT_DESCRIPTOR,位置信息存储在可选头DataDirectory[0]中。
一般PE文件此项值应为0,代表不存在这个表项,只有库文件,才会含有这个表项。
结构体成员包括特征值,时间戳,版本信息等。重要的成员是Name,存储着库文件的名字;Base存储着函数标号从哪里开始;NumberOfFunctions存储着函数的数量;NumberOfNames存储着函数名称的数量(一般情况下这两项相同);AddressOfFunctions函数地址数组的首地址;AddressOfNames函数名称地址数组的首地址;AdressOfNameOrdinals,存储着函数标号的地址信息。

个人思考:记得以前学习的时候,把VA与VirtualAddress看成一个相同的,导致转化的时候很矛盾,现在再看,真是自己可以静心好好的学,再次理解IAT的运行机制。

参考:
https://www.jianshu.com/p/af9766222816
https://www.cnblogs.com/aguoshaofang/p/5021759.html

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