0%

PE文件

前言:学习PE文件

什么是PE文件?

PE(Portable Execute)——可移植的执行体,是Windows平台组织程序代码的一种文件格式。

常见的PE文件由exe、dll、sys、ocx、com

文件偏移地址(File offset)

——PE文件存储在磁盘上,各数据段的地址称为文件偏移地址或物理地址(raw offset)。文件偏移地址从PE文件的第一个字节开始计数,起始值为0

image-20240424200652644

入口点和基地址

入口点(Entry Point)

——程序执行的第一行代码

基地址(Image base)

文件执行时将被映射到指定内存地址的存初始值,由PE文件本身决定

默认,exe:0x00400000,dll:0x1000000。用链接程序的/BASE选项改变该值

虚拟地址和相对虚拟地址

虚拟地址(Virtual Address,VA)

——保护模式下,程序访问存储器所使用的逻辑地址称为虚拟地址(VA),内存偏移地址(memory offset)

相对虚拟地址(Relative Virtual Address,RVA)

——指内存中相对于PE文件装入地址(基地址)的偏移量 RVA = VA - imagebase(基地址)

PE文件总体层次结构

image-20240424201705305

PE文件分为一下四个模块:

  • DOS头
  • PE文件头
  • 节表
  • 节内容

DOS头

在Windows NT之前的windows操作系统都是基于dos操作系统内核,为了兼容dos系统上可执行文件,windows NT在设计可执行文件时兼容了之前的格式

PE文件中的DOS实模式残留数据包括两部分:DOS头 + DOS stub

image-20240424202158152

DOS头与DOS插装程序

——PE结构中紧随MZ文件头之后的DOS插装程序(DOS Stub)

——IMAGE_DOS_HEADER结构可以识别一个合法的DOS头

——该结构的e_lfanew(偏移60,32bites)成员定位PE开始的标志

0x00004550(“PE\0\0”)

——病毒通过”MZ”,”PE”这两个标志,初步判断当前程序是否是目标文件——PE文件

DOS头数据结构

image-20240424202811732

DOS实模式残留数据——DOS STUB

DOS Stub是一段16位的程序,反汇编如下:

image-20240424203150788

PE文件加载流程

主要步骤:

  • 当PE文件运行时,PE加载器如果发现PE文件头偏移DOS MZ头,它就跳过到PE头
  • PE加载器检查PE头部是否有效。如果有效的话,它转到PE头的结尾
  • PE头后是段表。PE头读取有关节的信息,并使用文件映射将这些部分映射到内存中。这也给每个部分的属性中指定的段表
  • PE文件映射到内存中之后,PE加载程序关注PE文件的逻辑部分,比如导入表

NT文件头

DOS头的最后一个字段指示NT头的位置。NT头分为三个部分

1
2
3
4
5
typedef struct _IMAGE_NT_HEADERS{
DWORD Signature; //标识符
IMAGE_FILE_HEADER FileHeader; //PE 文件头
IMAGE_OPTIONAL_HEADER OptionalHeader; //可选头
}IMAGE_NT_HEADERS, *PIMAGE_NT_HEADERS;

image-20240424205109877

PE文件头

1
2
3
4
5
6
7
8
9
typedef struct _IMAGE_FLIE_HEADER{
WORD Machine; //运行平台
WORD NumberOfSections; //节数目
DWORD TimeDataStamp; //文件创建时间
DWORD PointerToSymbolTable; //指向符号表指针
DWORD NumberOfSymbols; //符号数目
WORD SizeOfOptionalHeader; //可选头长度
WORD Characteristics; //文件属性
}IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

image-20240424205645394

——紧接着DOS Stub的是PE header

——PE header是IMAGE_NT_HEDERS的简称,即NT映像头(PE文件头),存放PE整个文件信息分布的重要字段,包含了许多PE装载器用到的重要域。执行体在支持PE文件结构的操作中执行时

——PE装载器将从DOS MZ header中找到PE header的起始偏移量,从而跳过DOS Stub直接定位到真正的文件头PE header

PE文件头结构

IMAGE_NT_HEADERS STRUCT

Signature dd ?

FileHeader IMAGE_FILE_HEADER <>

OptionalHeader IMAGE_OPTIONAL_HEADER32 <>

IMAGE_NT_HEADER ENDS

字符串”PE\0\0”(Signature)(4H字节)

PE文件头——映像文件头

映像文件头,包含有PE文件的基本信息

1
2
3
4
5
6
7
8
typedef struct _IMAGE_FILE_HADAER{
WORD Machine; //0x04,该程序要执行的环境及平台
WORD NumberOfSections; //0x06,文件中节的个数
DWORD TimeDataStamp; //0x08,文件建立的时间
DWORD PointerToSymbels; //0x0c,COFF符号表的偏移
WORD SizeOfOptionalHeader; //0X14,可选头的长度
WORD Characteristics; //0x16,标志集合
}IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

PE文件头——可选映像头

在整个PE文件中占重要地位,以下为节选部分字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct _IMAGE_OPTIONAL_HEADER{
WORD Magic;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
IMAGE_DATA_DIRECTORY
DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRTES];
}IMAGE_OPTIONAL_HEADER, *PIMAGE_OPTIONAL_HEADER;

image-20240424230133843

在可选头的最后,有一个IMAGE_DATA_DIRECTORY的数组,其中IMAGE_NUMBEROF_DIRECTORY_ENTRIES被定义为16

1
2
3
4
typedef struct _IMAGE_DATA_DIRECTORY{
DWORD VirtualAddress; //RVA
DWORD Size; //大小
}IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

可选头的16个结构意义分别为:

image-20240424230705335

PE文件——节表

节通过节表实现索引节的内容是要真正执行的程序相关数据

病毒要在PE文件中增加病毒代码节或把病毒插入一个现有节时,需修改节表

节表是紧挨着NT映像头的一结构数组,其成员的数目NumberOFSections决定

image-20240424231645306

PE文件——节

——PE文件的程序内容划分成块,称之为Section(节)

——每个节是一块拥有共同属性,如代码/数据、读/写等

——节名称仅是区别不同节的符号而已,类似于”data”、”code”的命名,其属性设置决定节的特性和功能

代码节属性一般是”可执行”、”可读”和”节中包含代码”

数据节属性一般是”可读”、”可写”和”包含已初始化数据”

病毒在添加新节时,将新添加的节属性设置为可读、可写、可执行

典型拥有9个预定义节

.text、.bss、.rdata、.data、.rscr、.edata、.idata、.pdata、.debug

——可执行代码节:.text

——数据节:.bss、.rdata、.data

——资源节:.rsrc

——引出函数节:.edata

——引入函数节:idata

基本信息

PE文件的主体:节区

节区没有特定的格式,不同的节承载的内容不一样,也就有不同的数据结构

常见节如下:

  • .text 代码节(VC)
  • .code 代码节(VB/Delphi)
  • .data 数据节(一般存放已初始化的全局变量,静态变量)
  • .rdata 只读数据节(一般存放只读数据,如常量字符串,C++虚表)
  • .idata 输入数据表(一般用来存放IAT和导入表)
  • .bss 通常是指用来存放程序中未初始化的全局变量、静态变量
  • .textbss 节中同时包含代码和未初始化全局变量、静态变量
  • .rsrc 资源表
  • .reloc 重定位表

[在VC中,编译器为我们提供了自定义节的功能,使用预编译指令

#pragma data_seg(“.myseg”) //一般用于dll共享数据

#pragma code_seg(“myseg2”) //将指定代码段放入指定节中 ]

代码节.text
  • windows NT默认将所有可执行代码组成一个单独的节,名为”.text”或”.code”
  • .text节也包含在数据目录表中提到过的入口点
  • IAT存在于.text节中模块入口点之前IAT是一系列的跳转指令
引入函数节.idata

——从其他dll中引入的函数

——该节开始为IMAGE_IMPORT_DESCRIPTOR结构的结构数组,即引入表,数据目录表表项结构成员VirtualAddress包含引入表地址

——引入函数节可能会被病毒用来直接获取API函数地址

IMAGE_IMPORT_DESCRIPTOR的结构如下:

1
2
3
4
5
6
7
8
9
10
typedef struct _IMAGE_IMPORT_DESCRIPTOR{
union{
DWORD Characteristics;
DWORD OriginalFirstThunk; //IMAGE_THUNK_DATA数组的指针
};
DWORD TimeDateStamp; //文件建立时间
DWORD ForwarderChain; //一般为0
DWORD Name; //dll名字的指针
DWORD FirstThunk; //通常也是IMAGE_THUNK_DATA数组的指针
}IMAGE_IMPORT_DESCRIPTOR;
引出函数节.edata

——本文件向其他程序提供的可调用函数列表

——一般用在dll中,exe文件很少用

——当PE装载器执行一个程序,它将相关dll都装入该进程的地址空间,然后根据主程序的引入函数信息,查找相关dll中的真实函数地址来修改主程序

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct _IMAGE_EXPORT_DIRECTORY{
DWORD Characteristics; //一般为0
DWORD TimeDataStamp; //文件生成时间
WORD MajorVersion; //主版本号
WORD MinorVersion; //次版本号
DWORD Name; //指向dll的名字
DWORD Base; //基数,加上序数就是函数地址数组的索引值
DWORD NumberOfFunctions; //AddressOfFunctions数组的项数
DWORD NumberOfNames; //AddressOfNames数组的项数
DWORD AddressOfFunctions //RVA from base of image
DWORD AddressOfNames; //RVA from base of image
DWORD AddressOfNameOrdinals; //RVA from base of image
}IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

PE文件格式应用

DLL/EXE要引出一个函数给其他DLL/EXE使用

可通过 :

—函数名引出

—序号引出


已知导出函数名,获取函数地址步骤:

  • 定位到PE header
  • 从数据目录表读取导出表的虚拟地址
  • 定位导出表获取名字数目(NumberOfNames)
  • 并行遍历AddressOfNames和AddressOfNameOrdinals指向的数组匹配名字
  • 如果在AddressOfNames指向的数组中找到匹配名字,从AddressOfNames指向的数组中提取索引值
  • 例如,若发现匹配名字的RVA存放在AddressOfNames的第6个元素,那就提取AddressOfNamesOrdinals数组的第6个元素作为索引值
  • 如果遍历完NumberOfNames个元素,说明当前模块没有所要的名字