lyl610abc 发表于 2021-3-29 12:56

PE文件笔记四 PE文件头之标准PE头

本帖最后由 lyl610abc 于 2021-4-3 21:23 编辑

继续PE系列笔记的更新

PE其它笔记索引可前往:
(https://www.52pojie.cn/thread-1391994-1-1.html)

------

继续具体学习PE的各个结构细节,前面学完了DOS部首,接着学习PE文件头

由于PE文件头的内容较多,故要拆分为多个笔记,此笔记主要为**标准PE头**

# PE文件头

## PE文件头结构

!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210328200236487.png)

------

!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210403212229062.png)

------

### 两种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位      |

------

### 结构体截图

!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210328204339552.png)

------

### 结构体代码

#### 32位结构体

```c
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位结构体

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

!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210328202904227.png)

可以看到,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头

### 结构体截图

!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210328204842798.png)

------

### 结构体代码

```c
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文件:0xE064位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文件:0xE064位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。

------

#### 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文件默认值为0xE064位PE文件默认值为0xF0        **该值可变**

------

#### Characteristics

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

!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210328211930866.png)

------

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

------

!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210328235126574.png)

------

```
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中也通用

```c
#include <windows.h>
#include<stdio.h>
//在VC6这个比较旧的环境里,没有定义64位的这个宏,需要自己定义,在VS2019中无需自己定义
#define IMAGE_FILE_MACHINE_AMD640x8664
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位程序

!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210330163616404.png)

------

#### 64位程序

!(https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/image-20210330163506517.png)

### 代码小解

代码中判断程序是32位或64位是看file->Machine的值来进行判断的,但其实这里**并不一定准确**,实际上**应当判断opt->Magic才最为准确的**。但关于扩展PE头的内容留作之后,这里为了学习标准PE头,故先采用这种方式进行判断,后面也会修正为使用opt->Magic来判断程序为32位或64位

------

代码中大部分都有注释,并不难理解,主要说明一下 让指针指向对应地址 的代码

```c
//让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)的强制类型转换?

这就涉及到指针的加减问题了,详解可参考:[指针的加减](https://www.52pojie.cn/thread-1387007-1-1.html#37278091_%E6%8C%87%E9%92%88%E7%9A%84%E5%8A%A0%E5%87%8F)

这里简单引用一下指针加减的结论:

无论是指针的加亦或是减(这里只演示了加法,但减法同理),其加或减的**单位**为去掉一个*后的数据宽度

也就是**实际增减的数值=去掉一个\*后的数据宽度 × 增减的数值**

------

上面的指针都是一级结构体指针,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说明了该文件的属性

# 附件

附上本笔记中分析的EverEdit文件:[点我下载](https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/EverEdit.zip?versionId=CAEQHhiBgID6g6vXxBciIDMxMjY5Y2Q0NGE5NTRkNmNiNTUwOGM0YjdmZTQxMTI3)

lyl610abc 发表于 2021-3-29 16:22

sam喵喵 发表于 2021-3-29 15:43
UP搞个完整列表吧,就像之前出的C笔记一样,看起来方便。

完整列表在PE文件笔记一那里,每个都搞完整列表的话我每次更新都得修改每个帖{:301_973:}

lyl610abc 发表于 2021-4-14 11:12

陈先森9944 发表于 2021-4-14 09:38
大佬PE要怎么学才好,我现在就是不会·敲代码,比如上面内个自写代码解析PE文件头,我不怎么知道每一行代码 ...

你可以从前面的逆向基础笔记逆向基础笔记二十 汇编 指针(一)开始学起,代码里涉及了前面关于指针等的知识

haoyu521 发表于 2021-3-29 13:08

看看学习一下

love514415 发表于 2021-3-29 13:13

没什么好说的了. 学就是了

wannabe 发表于 2021-3-29 14:40

谢谢大佬

debug_cat 发表于 2021-3-29 15:15

我也是上学的时候有你这么努力,我早就早就。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
秃头了。{:301_995:}

lyl610abc 发表于 2021-3-29 15:21

莫问刀 发表于 2021-3-29 15:15
我也是上学的时候有你这么努力,我早就早就。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。 ...

在忙项目差不多要做完了,所以有空摸鱼发帖子{:301_978:}

wk6663999 发表于 2021-3-29 15:31

感谢楼主分享!学习了!楼主总结的很到位!

sam喵喵 发表于 2021-3-29 15:43

UP搞个完整列表吧,就像之前出的C笔记一样,看起来方便。

sam喵喵 发表于 2021-3-29 16:25

lyl610abc 发表于 2021-3-29 16:22
完整列表在PE文件笔记一那里,每个都搞完整列表的话我每次更新都得修改每个帖

有就好,还有C笔记链接都在一起,感谢!
页: [1] 2 3 4
查看完整版本: PE文件笔记四 PE文件头之标准PE头