文章目录
- PE文件
- DOS头部
- PE/NT头解析
- 区段头/区块表解析
- 数据目录表(存放在某个区段)
- 1. 导出表
- 2. 导入表
- 3. 重定位表
PE文件
- PE文件:PE文件是在windows平台可执行的文件。
- 包括:.exe(可执行程序),.dll(动态链接库),.sys(驱动程序)
DOS头部
-
概念:
- DOS头部共64个字节
- DOS头部是一个结构体,包含如下字段:
-
重要字段:
- e_magic(前两个字节):DOS头签名,可执行文件的标志.(固定的值:4D 5A)
- e_lfanew(后四个字节):PE头的偏移位置
-
DOS头解析:
#include<Windows.h> #include<iostream> // 获取DOS头 PIMAGE_DOS_HEADER get_pe_dos_header(char* pBuff) { return (PIMAGE_DOS_HEADER)pBuff; } // 解析DOS头 void parse_pedos_header(const char* path) { DWORD fSize = 0; char* pBuff = NULL; DWORD dwReadSize = 0; // 1.打开文件 HANDLE hfile = CreateFileA( path, // 文件路径 GENERIC_READ, // 打开文件读方式 FILE_SHARE_READ,// 共享方式 NULL, // 安全属性 OPEN_EXISTING, // 创建标志 FILE_ATTRIBUTE_NORMAL, //默认创建 NULL ); // 1.2 获取文件大小 fSize = GetFileSize(hfile, NULL); // 获取文件大小 pBuff = new char[fSize]; // 申请文件大小空间 // 2. 读取到内存中 ReadFile(hfile, pBuff, fSize, &dwReadSize, NULL); // 3. 解析PE文件的DOS头部 PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pBuff; //PIMAGE_DOS_HEADER pDos = get_pe_dos_header(pBuff); //pDos->e_magic; //DOS头签名,0x5A4D //pDos->e_lfanew; // PE头的偏移 // 4. 判断是否是PE文件 if (pDos->e_magic != IMAGE_DOS_SIGNATURE) { printf("这不是一个PE文件\n"); } printf("e_lfanew=%d\n", pDos->e_lfanew); // 5. 关闭文件句柄 CloseHandle(hfile); } int main() { parse_pedos_header("crackme1.exe"); }
-
其他:
- 程序在硬盘中和在内存中存储状态不一样,在内存中的对齐值比在硬盘中大
PE/NT头解析
-
概念
- NT头是一个结构体,包含三个字段:
- NT头是一个结构体,包含三个字段:
-
重要字段:
- Signature(前4个字节):NT头签名,PE标志,固定值 50 45 00 00
- FileHeader(20个字节):文件头
- OptionalHeader(大小不固定):可选PE头
-
NT头解析:
- FileHeader(文件头,共20字节):
重要字段:
- Machine(2个字节):程序允许的CPU型号,如果为0表示可以在任何CPU上执行。若为0x014C,表示能在386及后续的CPU上运行
- NumberOfSections(2个字节):文件中存在的区段的数量
- TimeDateStamp(4个字节):时间戳
- SizeOfOptionalHeader(2个字节):可选PE头的大小,32位PE文件默认E0,64位默认F0
- Characteristics(2个字节):文件属性,每个位有不同的含义
- OptionalHeader(可选PE头)
重要字段:
- Magic(2个字节):表示是32位PE文件还是64位PE文件。0x010B表示32位,0x020B表示64位
- SizeOfCode(4个字节):所有代码的总大小,按照FileAlignment对齐后的大小
- SizeOfInitializedData(4个字节):已初始化的数据大小,按照FileAlignment对齐
- SizeOfUninitializedData(4个字节):未初始化的数据大小 按照FileAlignment对齐
- AddressOfEntryPoint(4个字节):程序入口 OEP(偏移地址)
- BaseOfCode(4个字节):代码开始地址
- BaseOfData(4个字节):数据开始地址
- ImageBase(4个字节):内存镜像地址(程序被加载到内存后的起始地址(绝对地址))
- SectionAlignment(4个字节):内存对齐大小
- FileAlignment(4个字节):文件对齐大小
- SizeOfImage(4个字节):文件在内存中的大小,按SectionAlignment对齐后的大小
- SizeOfHeaders(4个字节):DOS头+NT头+标准PE头+可选PE头+区段头,按照FileAlignment对齐后的大小
- NumberOfRvaAndSizes(4个字节):数据目录表的个数
- DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]:存放数据目录表
- FileHeader(文件头,共20字节):
区段头/区块表解析
-
概念:
- 区段头由多个结构体组成,有多少个区段就有多少个对应的的结构体:
- 区段头由多个结构体组成,有多少个区段就有多少个对应的的结构体:
-
重要字段:
- Name[IMAGE_SIZEOF_SHORT_NAME(8)](8个字节):区段名称,注意此处跟字符串不一样,不会以0结尾
- union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc(8个字节):该区段在内存中的真实大小(未对齐) - VirtualAddress(4个字节):区段在内存中的偏移位置,+ImageBase 为真正的地址。
- SizeOfRawData(4个字节):区段在文件中对齐后大小
- PointerToRawData(4个字节):区段在文件中的偏移位置
- Characteristics(4个字节):区段属性 (是否可读 是否可写 是否可执行等等)
数据目录表(存放在某个区段)
-
概念:
可选PE头中包含两个字段:
- NumberOfRvaAndSizes(4个字节):数据目录表的个数
- DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]:存放数据目录表
1. 导出表
-
概念:
- 导出表存放该程序中定义的可供外部使用的函数
- 导出表存放该程序中定义的可供外部使用的函数
-
重要字段:
- DWORD Name:指向导出表文件名 RVA -->FOA+FileBuff=char *name;
- DWORD Base:导出函数起始序号
- DWORD NumberOfFunctions:导出函数个数
- DWORD NumberOfNames:以名称导出函数个数
- DWORD AddressOfFunctions:导出函数地址表 RVA–>FOA +FileBuff
- DWORD AddressOfNames:导出函数名称表 // RVA from base of image
- DWORD AddressOfNameOrdinals:导出函数序号表 // RVA from base of image
FOA(文件偏移)与RVA(内存偏移)的转换:
数据的RVA - 区段地址的RVA = 数据的FOA - 区段地址的FOA = 数据距离区段起始位置有多远,注意无论是RVA还是FOA都是偏移
- 数据的FOA = 数据的RVA - 区段地址的RVA + 区段地址的FOA
- 数据的RVA = 数据的FOA - 区段地址的FOA + 区段地址的RVA
2. 导入表
-
知识点准备:
- 调用dll文件函数原理:
程序在调用dll文件函数时,并不是把dll文件函数的代码编译到当前文件中,而是把dll文件对应的函数地址保存到了当前文件中
- 一个进程空间中的exe、dll文件如何被加载到内存中:
exe文件先导入内存,然后再导入dll文件到内存中
- HMODULE hModule = LoadLibraryW(L"Dll1.dll"):将dll文件加载到内存中,并将ImageBase的值存放到hModule中
- GetProcAddress(hModule, “my_export2”):从dll文件中拿到对应的函数的地址
- 将得到的函数地址填到exe文件中的对应位置
- exe文件调用的动态链接库在内存中与在硬盘中有什么不同
内存中,exe文件中的 dll文件的对应的函数地址 在exe硬盘中 存放的是一个RVA,该RVA转为FOA后,发现存放的是对应的函数名称
- 在硬盘中,exe文件存放使用的dll文件中的函数名称(准确的说是一个RVA值,该RVA转为FOA后,发现存放的是对应的函数名称)
- 在内存中,exe文件存放使用的dll文件中的函数地址
- 调用dll文件函数原理:
-
概念:
- 导入表存放该程序使用的外部函数的信息:
- 导入表存放该程序使用的外部函数的信息:
-
重要字段:
- union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA,指向(IMAGE_THUNK_DATA,导入名称表)结构体数组,Import name table,简称INT
} DUMMYUNIONNAME;(8个字节)
- TimeDateStamp:时间戳。为-1时,IAT在硬盘存储时会存放函数的地址而不是名称(由于存放地址可能会造成冲突,因此需要让所有dll文件导入内存的位置固定)
- Name:RVA,存放dll的名称
- FirstThunk:RVA,IAT 导入地址表 IMAGE_THUNK_DATA数组。在硬盘存储时,一般和INT一样(但不绝对);在内存存储时,会存函数的在内存中的地址。
- union {