大神论坛

找回密码
快速注册
查看: 783 | 回复: 0

[原创] PE学习笔记四 PE文件头之标准PE头

主题

帖子

6

积分

初入江湖

UID
31
积分
6
精华
威望
12 点
违规
大神币
68 枚
注册时间
2021-04-10 16:01
发表于 2021-04-18 23:33
本帖最后由 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文件头结构

image-20210328200236487


image-20210403212229062


两种PE文件头

PE文件头的结构有两种,分别对应32位的程序和64位的程序,它们的差异在于扩展PE头的结构

PE文件头结构说明
_IMAGE_NT_HEADERS32位程序对应的PE文件头结构
_IMAGE_NT_HEADERS6464位程序对应的PE文件头结构
_IMAGE_NT_HEADERS对应C中的结构体(类型)说明
"PE",0,0DOWRDPE标识
IMAGE_FILE_HEADERIMAGE_FILE_HEADER标准PE头
IMAGE_OPTIONAL_HEADER32IMAGE_OPTIONAL_HEADER32扩展PE头 32位
_IMAGE_NT_HEADERS64对应C中的结构体(类型)说明
"PE",0,0DOWRDPE标识,固定值不可变
IMAGE_FILE_HEADERIMAGE_FILE_HEADER标准PE头
IMAGE_OPTIONAL_HEADER64IMAGE_OPTIONAL_HEADER64扩展PE头 64位

结构体截图

image-20210328204339552


结构体代码

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文件头标志的首部

image-20210328202904227

可以看到,PE文件头标志固定为 50 45 00 00 ,对应ASCII为“PE  ”  ,是用来判断文件是否为PE文件的标识之一,还有一个PE标识为MZ头

PE文件头标志对应C语言变量数据宽度说明
"PE",0,0SignatureDWORD(4字节)50 45 00 00对应ASCII为“PE  ”PE文件标识

标准PE头

结构体截图

image-20210328204842798


结构体代码

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;

成员详情

成员数据宽度说明
MachineWORD(2字节)程序支持的CPU任意:0    Intel 386以及后续:14C   x64:8664
NumberOfSectionsWORD(2字节)节的数量不大于96
TimeDateStampDWORD(4字节)编译器填写的时间戳与文件属性里面(创建时间、修改时间)无关
PointerToSymbolTableDWORD(4字节)指向符号表调试相关
NumberOfSymbolsDWORD(4字节)符号表中的符号个数调试相关
SizeOfOptionalHeaderWORD(2字节)可选PE头结构大小32位PE文件:0xE0  64位PE文件:0xF0
CharacteristicsWORD(2字节)文件属性由数据位拼接而成,详见下方

Machine

计算机的体系结构类型。映像文件只能在指定的计算机或模拟指定计算机的系统上运行。此成员可以是以下值之一:

含义
宏定义IMAGE_FILE_MACHINE_I386 =  0x014cx86
宏定义IMAGE_FILE_MACHINE_IA64 = 0x0200Intel IPF
宏定义IMAGE_FILE_MACHINE_AMD64 = 0x8664x64

IA64:就是所谓的安腾(Itanium)(IPF),Intel跟HP联合折腾的一种64-bits全新架构,与x86系列不兼容


NumberOfSections

节数。这表示紧跟在PE文件头后面的节表的大小。请注意,Windows加载程序将节数限制为96。


TimeDateStamp

Image时间戳的低32位。这表示链接器创建Image的日期和时间。根据系统时钟,该值以自1970年1月1日午夜(00:00:00)后经过的秒数表示。


PointerToSymbolTable

符号表的偏移量,以字节为单位,如果不存在COFF符号表,则为零。

COFF是指通用对象文件格式,在Microsoft 实现叫做可移植可执行 (PE) 文件格式,在Linux上的实现叫做(可执行与可链接)ELF文件格式;COFF全拼为:Common Object File Format


NumberOfSymbols

符号表中的符号数


SizeOfOptionalHeader

扩展PE头的大小,以字节为单位。对于对象文件(object files),此值应为0。

32位的PE文件默认值为0xE0  64位PE文件默认值为0xF0   该值可变


Characteristics

Image的文件属性,其值对应的数据位含义为:

image-20210328211930866


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头的数据


image-20210328235126574

4C 01 05 00 6B 01 AE 55 00 00 00 00 00 00 00 00 E0 00 02 01

得到:

成员说明
Machinex8614C
NumberOfSections有5个节5
TimeDateStamp编译器填充的时间戳55 AE 01 6B
PointerToSymbolTable调试相关00 00 00 00
NumberOfSymbols调试相关00 00 00 00
SizeOfOptionalHeader可选PE头结构大小为E0E0
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位程序

image-20210330163616404


64位程序

image-20210330163506517

代码小解

代码中判断程序是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说明了该文件的属性

返回顶部