梳理下PE的文件结构
PE整体结构
PE结构可以大致分为:
- DOS部分
- NT头
- 节表(块表)
- 节数据(块数据)
- 调试信息
DOS头
1 | typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header |
IMAGE_DOS_HEADER结构体的大小为64字节。在该结构中必须知道两个重要成员: e_magic与e_lfanew
- e_magic: DOS签名(signature)
- e_lfanew: 只是NT头的偏移
主要通过它里面的e_magic和e_lfanew来判断该文件是否是PE文件格式。
NT头
PE文件头由PE文件头标志,标准PE头,扩展PE头三部分组成。PE文件头标志自然是50 40 00 00,也就是’PE’,我们从结构体的角度看一下PE文件头的详细信息
1 | typedef struct _IMAGE_NT_HEADERS { |
文件头
文件头是表现文件大致属性的IMAGE_FILE_HEADER结构体。
1 | typedef struct _IMAGE_FILE_HEADER { |
这个结构体主要包含了,CPU的Machine码,节区数量,需要装载的可选头的大小和文件属性等信息。
可选头
可选头包含了很多信息,重点关注:
- 代码起始地址 AdressOfEntryPoint
- 加载基址 ImageBase。执行PE文件时,PE装载器先创建进程,再将文件载入内存,然后再把EIP寄存器的值设为ImgeBase+AddressOfEntryPoint
- 文件对齐和节区对齐的值
- PE头的大小和Image的大小
- IMAGE_DATA_DIRECTORY的表(包含了很多重要的表,例如导入表导出表等)
1 | typedef struct _IMAGE_OPTIONAL_HEADER { |
节区头
节区头是由IMAGE_SECTION_HEADER结构体组成的数组,每个结构体对应一个节区。
1 | typedef struct _IMAGE_SECTION_HEADER { |
主要包含了节区的偏移地址,对齐和节的属性等信息
导出表
导出表(Export Table)一般是DLL文件用的比较多,exe文件很少有导出表,导出表的数据结构如下
1 | typedef struct _IMAGE_EXPORT_DIRECTORY { |
导入表
1 | typedef struct _IMAGE_IMPORT_DESCRIPTOR { |
可以看到,OriginalFirstThunk 和 FirstThunk 指向的内容分别是 INT 和 IAT ,但实际上 INT 和 IAT 的内容是一样的,所以他们指向的内容是一样的,只是方式不同而已,下图可以完美的解释
但是上图只是PE文件加载前的情况,PE文件一旦运行起来,就会变成下图的情况
PE-Viewer
1 | // ConsoleApplication3.cpp : 定义控制台应用程序的入口点。 |