PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)

前言

本博客系统讲解了PE文件结构。

PE文件结构其实不复杂,但内容较多,希望朋友们能“沉心静气做学问”。


目录

  • 前言
  • 1 PE文件及其表现形式
  • 2 PE文件格式与恶意软件的关系
  • 3 PE文件格式的总体结构
    • 3.1 PE文件原始数据
    • 3.2 相关恶意代码分析工具
      • 3.2.1 PE文件格式查看工具1-`PEView`
      • 3.2.2 PE文件格式查看工具2-`Stud_PE`
        • 使用前的设置
      • 3.2.3 PE程序调试工具-`Ollydbg`
        • 使用前的设置
      • 3.2.4 16进制文件编辑工具-UltraEdit
    • 3.3 DOS-MZ文件头(0x40)+DOS Stub(IMAGE_DOS_HEADER/MS-DOS Stub Program)
    • 3.4 PE文件头(IMAGE_NT_HEADERS)
      • 3.4.1 字串“PE\0\0”
      • 3.4.2 文件头-IMAGE_FILE_HEADER(映像文件头)
      • 3.4.3 可选头-IMAGE_OPTIONAL_HEADER(可选映像头)(可选文件头)
        • 如何修改程序的入口地址?
        • DataDirectory - 数据目录表
    • 3.5 节表(区块表)(IMAGE_SECTION_HEADER)
      • 3.5.1 IMAGE_SECTION_HEADER 的结构定义
    • 3.6 区块(节)
      • 3.6.1 区块名称及其意义
      • 3.6.2 文件偏移和RVA
    • 3.7 代码节 .text
    • 3.8 已初始化的数据节 .data
    • 3.9 未初始化的数据节 .bbs
    • 3.10 引入函数节 .rdata:PE文件的引入函数机制
      • 3.10.1 引入目录表 (输入表)(IMPORT Directory Table)
        • 从上面内容可以总结出引入目录表中的从属关系(由高到低排序)
      • 3.10.2 引入名字表(IMPORT Name Table)
        • 实际操作:将通过`函数名`引入函数改成通过`序号`引入函数
      • 3.10.3 IMPORT Hints/Names &DLL names
      • 3.10.4 引入地址表 IAT ( IMPORT Address Table)
    • 3.11 引出函数节 .edata:DLL文件的函数引出机制
      • 3.11.1 引出目录表(导出表、输出表)IMAGE_EXPORT_DIRECTORY
        • 如何定位 Export Directory Table-引出目录表
      • 3.11.2 导出地址表 -EXPORT ADDRESS Table
        • 导出地址表的data两种含义
      • 3.11.3 导出名字表 -EXPORT Name Table
      • 3.11.4 导出序号表 -EXPORT Oridinal Table
      • 3.11.5 如何通过函数名定位函数导出地址?
        • 通过函数名定位函数的地址,下面给出实际操作步骤:
    • 3.12 资源节 .rsrc:文件资源索引、定位与修改
    • 3.13 重定位节 .reloc :镜像地址改变后的地址自动修正
  • 4 思考题
  • 5 实验题

1 PE文件及其表现形式

  • 可移植的可执行文件(PE,Portable Executable File),Win32平台可执行文件使用的一种格式

  • 其他EXE文件格式:

    • DOS:MZ格式
    • Windows 3.0/3.1
      • NE,New Executable
      • 16位Windows可执行文件格式
  • 可执行程序的不同形态(以QQ为例)

    • 用户眼中的QQ.
      PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第1张图片

    • 本质上:
      PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第2张图片

2 PE文件格式与恶意软件的关系

  • 何为文件感染?[或控制权获取]

    • 使目标PE文件具备[或启动]病毒功能[或目标程序]
    • 不破坏目标PE文件原有功能和外在形态(如图标)等
  • 病毒代码如何与目标PE文件融为一体?

    • 代码植入
      需要了解PE文件中哪里可以植入代码。
    • 控制权获取
    • 图标更改

3 PE文件格式的总体结构

PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第3张图片

PE文件至少包含两个段,即数据段和代码段。Windows NT 的应用程序有9个预定义的段,分别为 .text 、.bss 、.rdata 、.data 、.pdata 和.debug 段,这些段并不是都是必须的,当然,也可以根据需要定义更多的段(比如一些加壳程序)

在应用程序中最常出现的段有以下6种:

中文名 英文名
.执行代码段 通常 .text (Microsoft)或 CODE(Borland)命名;
.数据段 通常以 .data 、.rdata 或 .bss(Microsoft)
.资源段 通常以 .rsrc命名
.导出表 通常以 .edata命名
.导入表 通常以 .idata命名
.调试信息段 通常以 .debug命名

3.1 PE文件原始数据

使用UltraEdit打开之后看到的是PE文件的十六进制代码。
以text.exe为例:

3.2 相关恶意代码分析工具

看雪学院论坛可以下载。

PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第4张图片

3.2.1 PE文件格式查看工具1-PEView

可按照PE文件格式对目标文件的各字段进行详细解析。

3.2.2 PE文件格式查看工具2-Stud_PE

可按照PE文件格式对目标文件的各字段进行详细解析。与 PEView 功能类似。

使用前的设置

设置之后可以使用右键用Stud_PE打开PE文件。

PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第5张图片

3.2.3 PE程序调试工具-Ollydbg

跟踪目标程序的执行过程,属于用户态调试工具。无法调试内核程序。

使用前的设置

设置之后可以使用右键用Ollydbg打开PE文件。

PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第6张图片 PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第7张图片

3.2.4 16进制文件编辑工具-UltraEdit

可对目标文件进行16进制查看和修改

PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第8张图片

3.3 DOS-MZ文件头(0x40)+DOS Stub(IMAGE_DOS_HEADER/MS-DOS Stub Program)

typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
	WORD e_magic;      // Magic number
	WORD e_cblp;      // Bytes on last page of file
	WORD e_cp;      // Pages in file
	WORD e_crlc;      // Relocations
	WORD e_cparhdr;     // Size of header in paragraphs
	WORD e_minalloc;     // Minimum extra paragraphs needed
	WORD e_maxalloc;     // Maximum extra paragraphs needed
	WORD e_ss;      // Initial (relative) SS value
	WORD e_sp;      // Initial SP value
	WORD e_csum;      // ChecksumWORD e_ip;      // Initial IP valueWORD e_cs;      // Initial (relative) CS value
	WORD e_lfarlc;     // File address of relocation table
	WORD e_ovno;      // Overlay number
	WORD e_res[4];     // Reserved words
	WORD e_oemid;      // OEM identifier (for e_oeminfo)
	WORD e_oeminfo;     // OEM information; e_oemid specific
	WORD e_res2[10];     // Reserved words
	LONG e_lfanew;     // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

所有的PE文件都是以一个64字节的DOS头(MZ文件头)开始。这个DOS头只是为了兼容早期的DOS操作系统。

  • 该结构体中需要掌握的字段只有 2 个,分别是第一个字段 e_magic 和最后一个字段 e_lfanew 字段
    • e_magic 字段:
      DOS 可执行文件的标识符,占用 2 字节。该位置保存着的字符是“MZ”。
    #define IMAGE_DOS_SIGNATURE    0x5A4D // MZ
    
    • e_lfanew 字段:
      保存着PE头的起始位置。
  • 作用:
    • 定位PE文件头开始位置,也可用于PE文件合法性检测
    • DOS系统下运行PE文件时,将提示用户:“This program cannot be run in DOS mode”!
PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第9张图片

更正:上图中的“PE文件”表述应为“PE文件头"。特此说明。”

  • 使用PEView上图内容:
    PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第10张图片

3.4 PE文件头(IMAGE_NT_HEADERS)

IMAGE_NT_HEADERS是一个宏,其定义如下:

#ifdef _WIN64

typedef IMAGE_NT_HEADERS64     IMAGE_NT_HEADERS;

typedef PIMAGE_NT_HEADERS64    PIMAGE_NT_HEADERS;

#define IMAGE_FIRST_SECTION(ntheader)  IMAGE_FIRST_SECTION64(ntheader)

#else

typedef IMAGE_NT_HEADERS32     IMAGE_NT_HEADERS;

typedef PIMAGE_NT_HEADERS32    PIMAGE_NT_HEADERS;

#define IMAGE_FIRST_SECTION(ntheader) IMAGE_FIRST_SECTION32(ntheader)

#endif

该头分为32位和64位两个版本,其定义依赖于是否定义了_WIN64。这里只讨论32位的PE文件格式,来看一下IMAGE_NT_HEADERS32的定义,如下:

typedef struct _IMAGE_NT_HEADERS {
	DWORD Signature; //该结构体中的Signature就是PE标识符,标识该文件是否是PE文件。该部分占4字节,即“50 45 0000”。
	IMAGE_FILE_HEADER FileHeader;
	IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

如何简单判断一个文件是否为PE文件?
首先要判断DOS头部的开始字节是否是“MZ”。如果是“MZ”头部,则通过DOS头部找到PE头部,接着判断PE头部的前四个字节是否为“PE\0\0”。如果是的话,则说明该文件是一个有效的PE文件。

PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第11张图片

可以看到,开始位置正是 00B0h

  • PE header 由三部分组成【开始于000000B0】
    • 字串“PE\0\0”(Signature):
      以此识别给定文件是否为有效PE文件。
    • 映像文件头(FileHeader):
      结构域包含了关于PE文件物理分布的信息。
    • 可选映像头(OptionalHeader)
      PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第12张图片

3.4.1 字串“PE\0\0”

以此识别给定文件是否为有效PE文件。
PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第13张图片

  • Signature 一 dword类型,值为
    50h, 45h, 00h, 00h(PE\0\0)。
    • 本域为PE标记,可以此识别给定文件是否为有效PE文件
      但有时仅仅凭字串是不够的。

3.4.2 文件头-IMAGE_FILE_HEADER(映像文件头)

文件头结构体IMAGE_FILE_HEADERIMAGE_NT_HEADERS结构体中的一个结构体,紧接在PE标识符的后面。IMAGE_FILE_HEADER结构体的大小为20字节,起始位置为0x000000CC,结束位置在0x000000DF。结构域包含了关于PE文件物理分布的信息。

PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第14张图片
PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第15张图片
  • 该结构域包含了关于PE文件物理分布的信息
    • 比如节数目、后续可选文件头大小机器类型等。
  • 映像文件头的结构
    包含了PE文件的一些基础信息。
//
//File header format.
//

typedef struct _IMAGE_FILE_HEADER {
	WORD Machine;
	WORD NumberOfSections;
	DWORD TimeDateStamp;
	DWORD PointerToSymbolTable;
	DWORD NumberOfSymbols;
	WORD SizeOfOptionalHeader;
	WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

#define IMAGE_SIZEOF_FILE_HEADER   20
PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第16张图片 PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第17张图片
  • Machine: 该字段是WORD类型,占用2字节。该字段表示可执行文件的目标CPU类型。
    PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第18张图片
  • NumberOfSections:该字段是WORD类型,占用两个字节。该字段表示PE文件的节区的个数。
  • TimeDataStamp:该字段表明文件是何时被创建的,这个值是自1970年1月1日以来用格林威治时间计算的秒数。
  • PointerToSymbolTable:该字段很少被使用,这里不做介绍。
  • NumberOfSymbols:该字段很少被使用,这里不做介绍。
  • SizeOfOptionalHeader:该字段为WORD类型,占用两个字节。该字段指定IMAGE_OPTION AL_HEADER结构的大小。注意,在计算IMAGE_OPTIONAL_HEADER的大小时,应该从IMAGE_FILE_HEADER结构中的SizeOfOptionalHeader字段指定的值来获取,而不应该直接使用 sizeof ( IMAGE_OPTIONAL_HEADER )来计算。
  • Characteristics:该字段为WORD类型,占用2字节。该字段指定文件的类型。
    PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第19张图片

3.4.3 可选头-IMAGE_OPTIONAL_HEADER(可选映像头)(可选文件头)

IMAGE_OPTINAL_HEADER在几乎所有的参考书中都被称作“可选头”。虽然被称作可选头,但是该头部不是一个可选的,而是一个必须存在的头,不可以没有。该头被称作“可选头”的原因是在该头的数据目录数组中,有的数据目录项是可有可无的,数据目录项部分是可选的,因此称为“可选头”。它定义了PE文件的很多关键信息。大小可从 文件头-IMAGE_FILE_HEADER 中得知。可选头紧挨着文件头,文件头的结束位置在 0x000000DF,那么可选头的起始位置为0x000000E0

PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第20张图片

IMAGE_OPTIONAL_HEADER是一个宏,其定义如下:

#ifdef _WIN64

typedef IMAGE_OPTIONAL_HEADER64  IMAGE_OPTIONAL_HEADER;

typedef PIMAGE_OPTIONAL_HEADER64  PIMAGE_OPTIONAL_HEADER;

#define IMAGE_SIZEOF_NT_OPTIONAL_HEADER IMAGE_SIZEOF_NT_OPTIONAL64_HEADER

#define IMAGE_NT_OPTIONAL_HDR_MAGIC IMAGE_NT_OPTIONAL_HDR64_MAGIC

#else

typedef IMAGE_OPTIONAL_HEADER32  IMAGE_OPTIONAL_HEADER;

typedef PIMAGE_OPTIONAL_HEADER32  PIMAGE_OPTIONAL_HEADER;

#define IMAGE_SIZEOF_NT_OPTIONAL_HEADER IMAGE_SIZEOF_NT_OPTIONAL32_HEADER

#define IMAGE_NT_OPTIONAL_HDR_MAGIC IMAGE_NT_OPTIONAL_HDR32_MAGIC

#endif

32位版本和64位版本的选择是根据是否定义了_WIN64而决定的,这里只讨论其32位的版本。IMAGE_OPTIONAL_HEADER32的定义如下:

//
//Optional header format.
//

typedef struct _IMAGE_OPTIONAL_HEADER {

	//
	//Standard fields.
	//

	WORD Magic;BYTE MajorLinkerVersion;
	BYTE MinorLinkerVersion;
	DWORD SizeOfCode;
	DWORD SizeOfInitializedData;
	DWORD SizeOfUninitializedData;
	DWORD AddressOfEntryPoint;
	DWORD BaseOfCode;
	DWORD BaseOfData;

	//
	//NT additional fields.
	//

	DWORD ImageBase;
	DWORD SectionAlignment;
	DWORD FileAlignment;
	WORD MajorOperatingSystemVersion;
	WORD MinorOperatingSystemVersion;
	WORD MajorImageVersion;
	WORD MinorImageVersion;
	WORD MajorSubsystemVersion;
	WORD MinorSubsystemVersion;
	DWORD Win32VersionValue;
	DWORD SizeOfImage;
	DWORD SizeOfHeaders;
	DWORD CheckSum;WORD Subsystem;
	WORD DllCharacteristics;
	DWORD SizeOfStackReserve;
	DWORD SizeOfStackCommit;
	DWORD SizeOfHeapReserve;
	DWORD SizeOfHeapCommit;
	DWORD LoaderFlags;
	DWORD NumberOfRvaAndSizes;
	IMAGE_DATA_DIRECTORY 	DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
  • 可选头的定位:

    • 起始位置:
      可选头的定位有一定的技巧性,起始位置的定位相对比较容易找到,按照 PE 标识开始寻找是非常简单的。
    • 结束位置:
      可选头的结尾后面跟的是第一项节表的名称,所以可选头的结束位置就在.text节的前一个位置。
  • 定义了PE文件的很多关键信息
    可选头是对文件头的一个补充。文件头主要描述文件的相关信息,而可选头主要用来管理 PE 文件被操作系统装载时所需要的信息。该头同样有 32 位版本与 64 位版本之分。
    PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第21张图片

    • 程序入口点(代码从哪里开始执行?)
      ImageBase+RVA
      • Relative Virtual Address(RVA),相对虚拟地址,它是相对内存中 ImageBase的偏移位置,可由此得知PE文件真正的装载地址。

    一般为ImageBase+ RVA,即00400000h+RVA

    • 内存镜像加载地址(ImageBase)
      PE文件在内存中的优先装载地址。对于大多数程序都是 00400000h77F40000h
    • 节在文件和内存中的对齐粒度
      比喻:桶的容量为100升,现有367升水,请问需要使用多少个桶?4个。
      • 问题:代码节的代码实际长度为0x46字节,在内存和文件中占多少字节?
        • 内存中节对齐粒度为0x1000字节,故占1000字节。
          PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第22张图片
        • 文件中节对齐粒度为0x200字节,故占200字节。

    正因为对齐粒度的存在,PE文件中才有有很多“00”字节(或是“CC”字节)。

    • 本程序在内存中的镜像大小文件头大小

如何修改程序的入口地址?

本小节为实际操作,讲述如何修改程序的入口地址

  • AddressOfEntryPoint 的作用

  • 实际操作:
    问题是否可以修改AddressOfEntryPoint指向任意代码?
    回答可以的。下面进行演示:

D8H处的4个字节处修改1000H1016H

更正:上图文字应为“将地址D8h处的 00 10修改成16 10之后...”。

  • 修改:
    使用UltraEdit修改
    PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第23张图片
  • 结果:
    PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第24张图片

    此时程序入口地址已变成 00401016h

DataDirectory - 数据目录表

DataDirectory是可选映像头的最后128个字节(16项 * 8 bytes),也是IMAGE_NT_HEADERS(PE文件头)的最后一部分数据。
它由16个IMAGE_DATA_DIRECTORY结构组成的数组构成,指向输出表、输入表、资源块、重定位 等数据目录项的RVA(相对虚拟地址)和大小。

IMAGE_DATA_DIRECTORY的结构如下:

//
//Directory format.
//

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;			//数据块的起始RVA
    DWORD   Size;					//数据块的长度
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
  • 使用 PEView 查看数据目录表:
    PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第25张图片

读图可知Import table(输入表)的地址为 00402014h ,大小为 003Ch

  • 使用 LordPE 查看数据目录表:
    注意。下图所查看的exe文件与上图查看的exe文件是不同的。
    PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第26张图片

在数据目录中,并不是所有的目录项都会有值,很多目录项的值都为 0。因为很多目录项的值为0,所以说数据目录项是可选的。

可选头的结构体介绍完了,各位可以按照该结构体中各成员变量的含义自行学习可选头中的十六进制值的含义。只有参考结构体的说明去对照分析PE文件格式中的十六进制值,才能更好、更快地掌握PE结构。

3.5 节表(区块表)(IMAGE_SECTION_HEADER)

PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第27张图片
  • 在PE文件头与原始数据之间存在一个区块表(Section Table),它是一个IMAGE_SECTION_HEADER结构数组,区块表包含每个块在映像中的信息(如位置、长度、属性),分别指向不同的区块实体。

举例:test.exe的代码节表(.text)
PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第28张图片

  • 全部有效结构最后以一个空的IMAGE_SECTION_HEADER结构作为结束,所以节表中总的IMAGE_SECTION_HEADER结构数量等于节的数量加一
  • 另外,节表中 IMAGE_SECTION_HEADER 结构的总数总是由PE文件头 IMAGE_NT_HEADERS->FileHeader.NumberOfSections字段来指定的。(因为节表的个数是节的个数+1)

3.5.1 IMAGE_SECTION_HEADER 的结构定义

typedef struct _IMAGE_SECTION_HEADER {
    Name						//8个字节的块名
    union						
    {
        DWORD PhysicalAddress;
        DWORD VirtualSize;
    } Misc;                     //区块尺寸
    DWORD VirtualAddress;		//区块的RVA地址
    DWORD SizeOfRawData;		//在文件中对齐后的尺寸
    DWORD PointerToRawData;		//在文件中偏移
    DWORD PointerToRelocations;	//在OBJ文件中使用,重定位的偏移
    DWORD PointerToLinenumbers;	//行号表的偏移(供调试使用地)
    WORD NumberOfRelocations;	//在OBJ文件中使用,重定位项数目
    WORD NumberOfLinenumbers;	//行号表中行号的数目
    DWORD Characteristics;		//区块属性如可读,可写,可执行等
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
IMAGE_SECTION_HEADER 的成员名称 IMAGE_SECTION_HEADER 的成员意义
Name 这是一个8位的ASCII(不是Unicode内码),用来定义块名,多数块名以,开始(如.Text),这个实际上不是必需的,注意如果块名超过了8个字节,则没有最后面的终止标志NULL字节,带有 的 区 块 的 名 字 会 从 编 译 器 里 将 带 有 的区块的名字会从编译器里将带有 的相同名字的区块被按字母顺序合并。
VirtualSize 指出实际的,被使用的区块大小,是区块在没有对齐处理前的实际大小.如果VirtualSize < SizeOfRawData,那么SizeOfRawData是可执行文件初始化数据的大小(SizeOfRawData – VirtualSize)的字节用0来填充。这个字段在OBJ文件中被设为0。
VirtualAddress 该块时装载到内存中的RVA,注意这个地址是按内存页对齐的,她总是SectionAlignment的整数倍,在工具中第一个块默认RVA为1000,在OBJ中为0。
SizeofRawData 该块在磁盘中所占的大小,在可执行文件中,该字段包括经过FileAlignment调整后块的长度。例如FileAlignment的大小为200h,如果VirtualSize中的块长度为19Ah个字节,这一块保存的长度为200h个字节。
PointerToRawData 该块是在磁盘文件中的偏移,程序编译或汇编后生成原始数据,这个字段用于给出原始数据块在文件的偏移,如果程序自装载PE或COFF文件(而不是由OS装载),这种情况,必须完全使用线性映像方法装入文件,需要在该块处找到块的数据。
PointerToRelocations 在PE中无意义
PointerToLinenumbers 行号表在文件中的偏移值,文件调试的信息
NumberOfRelocations 在PE中无意义
NumberOfLinenumbers 该块在行号表中的行号数目
Characteristics 块属性,(如代码/数据/可读/可写)的标志,这个值可通过链接器的/SECTION选项设置.下面是比较重要的标志:
  • Characteristics-节属性
    PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第29张图片
    • 使用Stud_PE查看:
      PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第30张图片

3.6 区块(节)

每个区块的名称都是唯一的,不能有同名的两个区块
但事实上节的名称不表示任何含义,他的存在仅仅是为了正规统一编程的时候方便程序员查看方便而设置的一个标记而已。所以将包含代码的区块命名为“.Data” (一般为.text)或者说将包含数据的区块命名为“.Code”(一般为.rdata等) 都是合法的。
当我们要从PE 文件中读取需要的区块的时候,不能以区块的名称作为定位的标准和依据,正确的方法是按照 IMAGE_OPTIONAL_HEADER32 结构中的数据目录字段结合进行定位

疑问: 可是 IMAGE_OPTIONAL_HEADER32 中并没有各区块的地址啊,咋定位?

3.6.1 区块名称及其意义

  • 常见的区块(节)
    • 代码节
    • 数据节
    • 引入函数节
    • 资源节等(如图标)
    • 引出函数节(DLL文件中常见)
    • 重定位节(DLL文件中常见)

3.6.2 文件偏移和RVA

由于一些PE文件为减少体积,磁盘对齐值不是一个内存页 1000h,而是 200h,当这类文件被映射到内存后,同一数据相对于文件头的偏移量在内存中和磁盘文件中是不同的,这样就存在着文件偏移地址与虚拟地址的转换问题。

PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第31张图片

从这张图可以看到:

  • DOS头部的起始地址(基地址)变成了00400000h
  • 块表与.text块之间用0填充了,因为.text块的起始地址变成了00401000h
  • .text块.rdata块、.data块三者之间用0填充了,因为要保证每一块的大小都是1000h

3.7 代码节 .text

PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第32张图片
  • 代码节一般名为.text或CODE,该节含有程序的可执行代码
  • 每个PE文件都有代码节
    PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第33张图片

3.8 已初始化的数据节 .data

PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第34张图片
  • 这个节一般取名为.data或DATA
  • 已初始化的数据节中放的是在编译时刻就已确定的数据。
    如test.exe中的字符串“PE入口点测试1:进入第 一入口位置00401000H!”
PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第35张图片

3.9 未初始化的数据节 .bbs

  • 节名称一般叫.bbs
  • 这个节里放有未初始化的全局变量和静态变量。
    • 例如“static int k;”

3.10 引入函数节 .rdata:PE文件的引入函数机制

函数引入机制。

3.10.1 引入目录表 (输入表)(IMPORT Directory Table)

PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第36张图片
PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第37张图片
  • 如何从PE文件定位到引入目录表(IDT)的起始位置?
    PE文件可选文件头的DataDirectory
    PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第38张图片

  • 引入目录表由一系列的 IMAGE_IMPORT_DESCRIPTOR结构组成

    • 结构的数量取决于程序要使用的DLL文件的数量,每一个 IMAGE_IMPORT_DESCRIPTOR 结构对应一个DLL文件
    • 在所有这些结构的最后,由一个内容全为0IMAGE_IMPORT_DESCRIPTOR 结构作为结束
      PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第39张图片
  • 引入目录表的组成部分-IMAGE_IMPORT_DESCRIPTOR结构是咋样的?

    • IMAGE_IMPORT_DESCRIPTOR 的数据结构
      PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第40张图片
      其中 OriginalFirstThunk 和 FirstThunk 得特别说明一下:
    • OriginalFirstThunk 和 FirstThunk
      • 其中 FirstThunk 指向引入函数节(.data)中的引入名字表(Import Name Table,与引入目录表平级)
      • 其中 OriginalFirstThunk 指向引入函数节(.data)中的引入地址表(Import Address Table,与引入目录表平级)
      • 都指向一个包含一系列 IMAGE_THUNK_DATA 结构的数组
        这个所谓的“数组”其实就是引入名字表引入地址表
        • 每个 IMAGE_THUNK_DATA 结构定义了一个导入函数(注意不是一个.dll)的信息, 实际为一个双字,不同时刻可能代表不同含义。
        • 以一个内容为0IMAGE_THUNK_DATA 结构作为结束。

    OriginalFirstThunk和FirstThunk :
    在文件中:它们所指向IMAGE_THUNK_DATA结构的数组所有值都是相同的;
    在内存中:FirstThunk所指向IMAGE_THUNK_DATA结构的数组的值会改变
    (下图是PE文件中的数据,不是内存中的,看得到 OriginalFirstThunk 和 FirstThunk 指向的数据都是相同的。)
    PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第41张图片
    注意:
    一个OriginalFirstThunk或FirstThunk只指向一个.dll。上图每一个红框中都含有两个.dll,所以分别由两个OriginalFirstThunk或FirstThunk指向其对应红框。

    • 一个IMAGE_THUNK_DATA结构数组(上图一个红框中有两个数组)为一个.dll中的所有导入函数数据:
      如:Data 0002064 + Data 00000000(Value 0080 ExitProcess + Kenel32.dll)
    • 一个IMAGE_THUNK_DATA结构是一个导入函数数据(双字):
      如:Data 00002064 (Value 0080 ExitProcess)

    补充:IMAGE_THUNK_DATA结构
    一个IMAGE_THUNK_DATA结构实际上就是一个双字,它在不同时刻有不同的含义。
    PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第42张图片

从上面内容可以总结出引入目录表中的从属关系(由高到低排序)

.rdata

IMPORT Directory Table

IMAGE_IMPORT_DESCRIPTOR

OriginalFirstThunk和FirstThunk(O…和 F…均指向一个包含一系列 IMAGE_THUNK_DATA 结构的数组(引入名字表和引入地址表))

3.10.2 引入名字表(IMPORT Name Table)

是上一节中的IMAGE_THUNK_DATA结构数组

PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第43张图片

实际操作:将通过函数名引入函数改成通过序号引入函数

1、
PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第44张图片

如图,将其修改为80002064。(00002064 -> 80002064)

2、
PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第45张图片
通过序号引入函数时无法在kenel32.dll中找到序号为8292(2064h)的函数,故程序无法执行。

3、
PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第46张图片
经过查找,函数ExitProcess序号为183(B7h)

4、
PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第47张图片

5、
PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第48张图片

现在程序正常运行!

3.10.3 IMPORT Hints/Names &DLL names

当IMAGE_THUNK_DATA结构(双字)的最高位为0时,表示函数以字符串类型的函数名方式输入,这时双字的值是一个RVA,指向一个(IAMGE_IMPORT_BY_NAME)结构,该结构定义如下:

STRUCT  IAMGE_IMPORT_BY_NAME
{
    DWORD Hint;    //本函数在其所驻留DLL的输出表中的序号 
    BYTE  name     //输入函数的函数名,函数名是一个ASCII码字符串,以NULL结尾
};
  • IMAGE_IMPORT_BY_NAME结构
    • 80 00为Hints,ExitProcess为引入函数名
    • 62 02 为Hints,wsprintfA为引入函数名
    • 9D 01 为Hints,MessageBoxA为引入函数名
  • DLL names字符串
    • kernel32.dll 为dll文件名
    • user32.dll 为dll文件名

3.10.4 引入地址表 IAT ( IMPORT Address Table)

PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第49张图片
  • DWORD数组[可通过可选文件头中的DataDirectory的第13项定位
    • 在文件中时,其内容与 Import Name Table 完全一样。
    • 在内存中时,每个双字中存放着对应引入函数的地址。
      此时这些值与其装入内存前(在文件中)的值一般会有所不同
      PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第50张图片

3.11 引出函数节 .edata:DLL文件的函数引出机制

引出函数节一般名为.edata,这是本文件向其他程序提供调用函数的列表,函数所在的地址及具体代码实现的区块。有时合并入.text节(如下图)。

  • 关键结构:引出目录表(导出表、输出表)

3.11.1 引出目录表(导出表、输出表)IMAGE_EXPORT_DIRECTORY

PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第51张图片
  • 上图的解析:
    PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第52张图片
    Base一般为1,函数的序号等于Base+引出序号表里的值

如何定位 Export Directory Table-引出目录表

DataDirectory第一项。
PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第53张图片

3.11.2 导出地址表 -EXPORT ADDRESS Table

PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第54张图片

导出地址表的data两种含义

  • dwExportRVA

    • 指向导出地址
  • dwForwarderRVA

    • 指向另外一个DLL中的某个API函数名。
    • 举例:Kernel32.AddVectoredExceptionHandler → NTDLL.RtlAddVectoredExceptionHandler

3.11.3 导出名字表 -EXPORT Name Table

PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第55张图片

3.11.4 导出序号表 -EXPORT Oridinal Table

该表保存的是各导出函数的函数地址在导出地址表的序号,但序号并不是data中的值,而是待寻找函数在导出序号表排第几个,例如AddAtomA()的排第个所以序号是2

PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第56张图片

为何需要导出序号表?
导出函数名字和导出地址表中的地址不是一一对应关系

  • 一个函数实现可能有多个名字
  • 某些函数没有名字,仅通过序号导出

3.11.5 如何通过函数名定位函数导出地址?

PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第57张图片

先通过AddressOfNames查到到函数名,然后通过AddressOfNameOrdinals查找到函数序号,再通过AddressOfFunctions找到函数的RVA。

通过函数名定位函数的地址,下面给出实际操作步骤:

任务:查找HashData函数。
操作:按照下图的顺序操作,一共分为4
PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第58张图片

  • 查找导出表起始地址:
    PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第59张图片
    PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第60张图片

  • 通过AddressOfNames查找函数对应的序号
    PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第61张图片

  • 通过AddressOfNameOrdinals序号查找函数的索引值

  • 通过AddressOfFunctions索引值找到函数的RVA
    PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第62张图片

3.12 资源节 .rsrc:文件资源索引、定位与修改

  • 资源节一般名为.rsrc

  • 这个节放有如图标、对话框等程序要用到的资源

  • 资源节是树形结构的,它有一个主目录,主目录下又有子目录,子目录下可以是子目录或数据。
    通常有3层目录(资源类型、资源标识符、资源语言ID),第4层是具体的资源
    PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第63张图片

  • 3个重要结构

    • 目录IMAGE_RESOURCE_DIRECTORY 结构
    PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第64张图片 PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第65张图片
    • 目录项IMAGE_RESOURCE_DIRECTORY_ENTRY 结构
      PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第66张图片

    • 数据项IMAGE_RESOURCE_DATA_ENTRY 结构
      以PEView.exe为例(我自己分析我自己)

    PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第67张图片 PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第68张图片
PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第69张图片
3个重要结构
  • 如何定位资源目录位置?
    可选文件头(也称可选映像头)的DataDirectory项。

  • 如何定位资源?
    PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第70张图片

  • 资源节的部分应用

    • 图标修改
    • 汉化

3.13 重定位节 .reloc :镜像地址改变后的地址自动修正

PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第71张图片

在exe文件中一般没有,但在dll文件中基本都会有。

  • 重定位节存放了一个重定位表,若装载器不是把程序装到程序编译时默认的基地址时,就需要这个重定位表来做一些调整
  • 重定位节以IMAGE_BASE_RELOCATION结构开始
    PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第72张图片

从上图我们能可以看到,定位项的数量是不定的。

  • VirtualAddress:是一个4KB(一页)的边界。该值加上后面的TypeOffset数组的成员便得到了需要重定位数据的地址。
  • SizeBlock:为这一结构块的大小。该大小减去前两项(VirtualAddress和SizeBlock本身)的字节数8便得到了第3项(下一项,也就是重定位项数组TypeOffset[])的大小,再除以2(因为重定位项大小为2个字节)即得到了重定位项的个数。
  • 重定位项:每项都是16位的,其中的最高4位代表了所需要的重定位类型,剩下的12位代表了页面中重定位地址的偏移量(delta)
    • 重定位的类型
      • MAGE_REL_BASED_HIGHLOW(3)
        偏移量(delta)添加到原来的偏移位置(RVA)的32位字段上,它是32位地址重定位的首选类型。
PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第73张图片

更正:上图中的“定位项”表述应为“重定位项数组”。

观察上图,可以发现重定位项数组中最高4位的值是3(代表MAGE_REL_BASED_HIGHLOW(3)),剩下的12位就是偏移量(delta)

  • 为什么需要重定位?
    PE文件中部分数据是以VA地址存储的,当PE文件无法加载到预期ImageBase时,这些地址就需要修正。

参考资料:
云课堂武大慕课
PE文件格式分析 https://blog.csdn.net/shitdbg/article/details/49734495

4 思考题

用于验收PE文件结构学习情况。

PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第74张图片

如何判断目标程序是否为合法PE文件?
使用PEView找到PE文件头(IMAGE_NT_HEADERS)的字串(Signature)
PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第75张图片
若其开头双字内容为 “50 45 00 00”,则说明给定文件是有效PE文件。

如果不使用引入函数节,如何使用外部DLL中的API函数?
暂时还不会。下次一定补上。
Kenel32.dll提供了GetProcAddress函数,用于获取指定函数的地址。该函数的具体是怎样实现的?
暂时还不会。下次一定补上。
熊猫烧香病毒感染其他PE文件后,目标文件图标会变成容易被用户差距的熊猫图案,为什么?如何解决问题?

5 实验题

PE文件结构详解精华(从头看下去就能大概了解PE文件结构了)_第76张图片

你可能感兴趣的:(网络空间安全)