本帖最后由 ttegame 于 2021-04-18 23:33 编辑
PE学习笔记系列 PE学习笔记一 PE介绍 PE学习笔记二 PE文件的两种状态 PE学习笔记三 DOS部分 PE学习笔记四 PE文件头之标准PE头 PE学习笔记五 PE文件头之扩展PE头 PE学习笔记六 节表和节 PE学习笔记七 RVA与FOA转换 PE学习笔记八 实战之HOOK程序添加弹窗 PE学习笔记九 实战之HOOK程序添加弹窗续 PE学习笔记十 扩大节 PE学习笔记十一 新增节 PE学习笔记十二 修正内存对齐 PE学习笔记十三 合并节 PE学习笔记十四 导出表 PE学习笔记十五 导入表 PE学习笔记十六 代码重定位 PE学习笔记十七 重定位表 继续具体学习PE的各个结构细节,前面学完了DOS部首,接着学习PE文件头 由于PE文件头的内容较多,故要拆分为多个笔记,此笔记主要为标准PE头 PE文件头PE文件头结构
两种PE文件头PE文件头的结构有两种,分别对应32位的程序和64位的程序,它们的差异在于扩展PE头的结构 PE文件头结构 | 说明 |
---|
_IMAGE_NT_HEADERS | 32位程序对应的PE文件头结构 | _IMAGE_NT_HEADERS64 | 64位程序对应的PE文件头结构 |
_IMAGE_NT_HEADERS | 对应C中的结构体(类型) | 说明 |
---|
"PE",0,0 | DOWRD | PE标识 | IMAGE_FILE_HEADER | IMAGE_FILE_HEADER | 标准PE头 | IMAGE_OPTIONAL_HEADER32 | IMAGE_OPTIONAL_HEADER32 | 扩展PE头 32位 |
_IMAGE_NT_HEADERS64 | 对应C中的结构体(类型) | 说明 |
---|
"PE",0,0 | DOWRD | PE标识,固定值不可变 | IMAGE_FILE_HEADER | IMAGE_FILE_HEADER | 标准PE头 | IMAGE_OPTIONAL_HEADER64 | IMAGE_OPTIONAL_HEADER64 | 扩展PE头 64位 |
结构体截图
结构体代码32位结构体typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; //PE文件头标识
IMAGE_FILE_HEADER FileHeader; //标准PE头
IMAGE_OPTIONAL_HEADER32 OptionalHeader; //扩展PE头 32位
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
64位结构体typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature; //PE文件头标识
IMAGE_FILE_HEADER FileHeader; //标准PE头
IMAGE_OPTIONAL_HEADER64 OptionalHeader; //扩展PE头 64位
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
PE文件头标志实例分析根据DOS MZ头的最后一个成员找到PE文件头的首部,也就是PE文件头标志的首部 可以看到,PE文件头标志固定为 50 45 00 00 ,对应ASCII为“PE ” ,是用来判断文件是否为PE文件的标识之一,还有一个PE标识为MZ头 PE文件头标志 | 对应C语言变量 | 数据宽度 | 值 | 说明 |
---|
"PE",0,0 | Signature | DWORD(4字节) | 50 45 00 00对应ASCII为“PE ” | PE文件标识 |
标准PE头结构体截图
结构体代码typedef struct _IMAGE_FILE_HEADER {
WORD Machine;//可以运行在什么样的CPU上 任意:0 Intel 386以及后续:14C x64:8664
WORD NumberOfSections;//表示节的数量
DWORD TimeDateStamp;//编译器填写的时间戳 与文件属性里面(创建时间、修改时间)无关
DWORD PointerToSymbolTable;//调试相关
DWORD NumberOfSymbols;//调试相关
WORD SizeOfOptionalHeader;//可选PE头的大小(32位PE文件:0xE0 64位PE文件:0xF0)
WORD Characteristics;//文件属性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
成员详情成员 | 数据宽度 | 说明 | 值 |
---|
Machine | WORD(2字节) | 程序支持的CPU | 任意:0 Intel 386以及后续:14C x64:8664 | NumberOfSections | WORD(2字节) | 节的数量 | 不大于96 | TimeDateStamp | DWORD(4字节) | 编译器填写的时间戳 | 与文件属性里面(创建时间、修改时间)无关 | PointerToSymbolTable | DWORD(4字节) | 指向符号表 | 调试相关 | NumberOfSymbols | DWORD(4字节) | 符号表中的符号个数 | 调试相关 | SizeOfOptionalHeader | WORD(2字节) | 可选PE头结构大小 | 32位PE文件:0xE0 64位PE文件:0xF0 | Characteristics | WORD(2字节) | 文件属性 | 由数据位拼接而成,详见下方 |
Machine计算机的体系结构类型。映像文件只能在指定的计算机或模拟指定计算机的系统上运行。此成员可以是以下值之一: 值 | 含义 |
---|
宏定义IMAGE_FILE_MACHINE_I386 = 0x014c | x86 | 宏定义IMAGE_FILE_MACHINE_IA64 = 0x0200 | Intel IPF | 宏定义IMAGE_FILE_MACHINE_AMD64 = 0x8664 | x64 |
IA64:就是所谓的安腾(Itanium)(IPF),Intel跟HP联合折腾的一种64-bits全新架构,与x86系列不兼容
NumberOfSections节数。这表示紧跟在PE文件头后面的节表的大小。请注意,Windows加载程序将节数限制为96。
TimeDateStampImage时间戳的低32位。这表示链接器创建Image的日期和时间。根据系统时钟,该值以自1970年1月1日午夜(00:00:00)后经过的秒数表示。
PointerToSymbolTable符号表的偏移量,以字节为单位,如果不存在COFF符号表,则为零。 COFF是指通用对象文件格式,在Microsoft 实现叫做可移植可执行 (PE) 文件格式,在Linux上的实现叫做(可执行与可链接)ELF文件格式;COFF全拼为:Common Object File Format
NumberOfSymbols符号表中的符号数
扩展PE头的大小,以字节为单位。对于对象文件(object files),此值应为0。 32位的PE文件默认值为0xE0 64位PE文件默认值为0xF0 该值可变
CharacteristicsImage的文件属性,其值对应的数据位含义为:
Characteristics的数据宽度为WORD(2字节=16 bits) 假设Characteristics的十六进制为0102,分析其文件属性 首先将十六进制转化为二进制:0000 0001 0000 0010 此时可以发现数据位1和8的位置的值为1(数据位由0开始),对照上面可得出:文件属性为 文件是可执行的、只在32位平台上运行
实例分析紧跟着上面PE文件头标志的实例分析,继续分析标准PE头对应的各个属性 根据标准PE头各个成员的数据宽度不难得出标准PE头的总宽度为:20字节(4个WORD+3个DWORD=4×2+3×4=20) 因此从前面PE文件头标志后再数20个字节都是标准PE头的数据
4C 01 05 00 6B 01 AE 55 00 00 00 00 00 00 00 00 E0 00 02 01
得到: 成员 | 说明 | 值 |
---|
Machine | x86 | 14C | NumberOfSections | 有5个节 | 5 | TimeDateStamp | 编译器填充的时间戳 | 55 AE 01 6B | PointerToSymbolTable | 调试相关 | 00 00 00 00 | NumberOfSymbols | 调试相关 | 00 00 00 00 | SizeOfOptionalHeader | 可选PE头结构大小为E0 | E0 | Characteristics | 文件属性为 文件可执行且只在32位平台上运行 | 102 |
自写代码解析PE文件头因为有人提议用VS2019来编写,于是这里改成VS2019中的代码,但其实在VC6中也通用 #include <windows.h>
#include<stdio.h>
//在VC6这个比较旧的环境里,没有定义64位的这个宏,需要自己定义,在VS2019中无需自己定义
#define IMAGE_FILE_MACHINE_AMD64 0x8664
int main(int argc, char* argv[])
{
//创建DOS对应的结构体指针
_IMAGE_DOS_HEADER* dos;
//读取文件,返回文件句柄
HANDLE hFile = CreateFileA("C:\\Users\\sixonezero\\Desktop\\dbgview64.exe", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, 0);
//根据文件句柄创建映射
HANDLE hMap = CreateFileMappingA(hFile, NULL, PAGE_READONLY, 0, 0, 0);
//映射内容
LPVOID pFile = MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0);
//类型转换,用结构体的方式来读取
dos = (_IMAGE_DOS_HEADER*)pFile;
//输出dos->e_magic,以十六进制输出
printf("dos->e_magic:%X\n", dos->e_magic);
//创建指向PE文件头标志的指针
DWORD* peId;
//让PE文件头标志指针指向其对应的地址=DOS首地址+偏移
peId = (DWORD*)((UINT)dos + dos->e_lfanew);
//输出PE文件头标志,其值应为4550,否则不是PE文件
printf("peId:%X\n", *peId);
//创建标准PE头对应的结构体指针
_IMAGE_FILE_HEADER* file;
//让标准PE头指针指向其对应的地址=PE文件头标志地址+PE文件头标志大小
file = (_IMAGE_FILE_HEADER*)((UINT)peId + sizeof(DWORD));
//输出file->Machine
printf("file->Machine:%X\n", file->Machine);
//根据file->Machine判断程序为 x86或IPF或x64
switch (file->Machine) {
//程序为32位
case IMAGE_FILE_MACHINE_I386:
{
printf("x86 program\n");
//确定程序为32位则扩展PE头确定为_IMAGE_OPTIONAL_HEADER
//创建扩展PE头对应的结构体指针
_IMAGE_OPTIONAL_HEADER* opt;
//让扩展PE头指针指向其对应的地址=标准PE头地址+标准PE头大小
opt = (_IMAGE_OPTIONAL_HEADER*)((UINT)file + sizeof(_IMAGE_FILE_HEADER));
//输出opt->Magic
printf("opt->Magic:%X\n", opt->Magic);
break;
}
//程序为IPF
case IMAGE_FILE_MACHINE_IA64:
printf("IPF program\n");
break;
//程序为64位
case IMAGE_FILE_MACHINE_AMD64:
{
printf("x64 program\n");
//确定程序为64位则扩展PE头确定为_IMAGE_OPTIONAL_HEADER64
//创建扩展PE头对应的结构体指针
_IMAGE_OPTIONAL_HEADER64* opt;
//让扩展PE头指针指向其对应的地址=标准PE头地址+标准PE头大小
opt = (_IMAGE_OPTIONAL_HEADER64*)((UINT)file + sizeof(_IMAGE_FILE_HEADER));
//输出opt->Magic
printf("opt->Magic:%X\n", opt->Magic);
break;
}
default:
break;
}
return 0;
}
运行结果分别演示32位程序和64位程序的运行结果 32位程序
64位程序代码小解代码中判断程序是32位或64位是看file->Machine的值来进行判断的,但其实这里并不一定准确,实际上应当判断opt->Magic才最为准确的。但关于扩展PE头的内容留作之后,这里为了学习标准PE头,故先采用这种方式进行判断,后面也会修正为使用opt->Magic来判断程序为32位或64位
代码中大部分都有注释,并不难理解,主要说明一下 让指针指向对应地址 的代码 //让PE文件头标志指针指向其对应的地址=DOS首地址+偏移
peId = (DWORD*)((UINT)dos + dos->e_lfanew);
//让标准PE头指针指向其对应的地址=PE文件头标志地址+PE文件头标志大小
file = (_IMAGE_FILE_HEADER*)((UINT)peId + sizeof(DWORD));
//让扩展PE头指针指向其对应的地址=标准PE头地址+标准PE头大小
opt = (_IMAGE_OPTIONAL_HEADER*)((UINT)file + sizeof(_IMAGE_FILE_HEADER));
//让扩展PE头指针指向其对应的地址=标准PE头地址+标准PE头大小
opt = (_IMAGE_OPTIONAL_HEADER64*)((UINT)file + sizeof(_IMAGE_FILE_HEADER));
指针的地址 = 首地址 + 偏移 这个没有什么好说的,主要在指针前的一个(UINT)强制类型转换 为什么要在指针前加一个(UINT)的强制类型转换? 这就涉及到指针的加减问题了,详解可参考:指针的加减 这里简单引用一下指针加减的结论: 无论是指针的加亦或是减(这里只演示了加法,但减法同理),其加或减的单位为去掉一个*后的数据宽度 也就是实际增减的数值=去掉一个*后的数据宽度 × 增减的数值
上面的指针都是一级结构体指针,DWORD,_IMAGE_FILE_HEADER,_IMAGE_OPTIONAL_HEADER,_IMAGE_OPTIONAL_HEADER64 去掉一个*后的数据宽度为结构体的大小,但是我们这里想要进行的增减的单位应该为字节,而不是结构体的大小,于是要将指针类型强转为UINT(无符号整数)类型(数据宽度为字节),使得其每次增减的单位为字节
总结- PE文件头的起始位置由DOS MZ头的最后一个成员确定
- PE文件头标志固定ASCII为“PE ”,若不是则说明该文件非PE文件
- 标准PE头的第一个成员Machine可以判断程序为32位或64位
- 标准PE头的第二个成员NumberOfSections表示后面节的个数
- 可选PE头结构大小可变,且在标准PE头的第六个成员SizeOfOptionalHeader指定
- 标准PE头的最后一个成员Characteristics说明了该文件的属性
|