前言:学习PE文件
什么是PE文件?
PE(Portable Execute)——可移植的执行体,是Windows平台组织程序代码的一种文件格式。
常见的PE文件由exe、dll、sys、ocx、com
文件偏移地址(File offset)
——PE文件存储在磁盘上,各数据段的地址称为文件偏移地址或物理地址(raw offset)。文件偏移地址从PE文件的第一个字节开始计数,起始值为0
入口点和基地址
入口点(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文件总体层次结构
PE文件分为一下四个模块:
- DOS头
- PE文件头
- 节表
- 节内容
DOS头
在Windows NT之前的windows操作系统都是基于dos操作系统内核,为了兼容dos系统上可执行文件,windows NT在设计可执行文件时兼容了之前的格式
PE文件中的DOS实模式残留数据包括两部分:DOS头 + DOS stub
DOS头与DOS插装程序
——PE结构中紧随MZ文件头之后的DOS插装程序(DOS Stub)
——IMAGE_DOS_HEADER结构可以识别一个合法的DOS头
——该结构的e_lfanew(偏移60,32bites)成员定位PE开始的标志
0x00004550(“PE\0\0”)
——病毒通过”MZ”,”PE”这两个标志,初步判断当前程序是否是目标文件——PE文件
DOS头数据结构
DOS实模式残留数据——DOS STUB
DOS Stub是一段16位的程序,反汇编如下:
PE文件加载流程
主要步骤:
- 当PE文件运行时,PE加载器如果发现PE文件头偏移DOS MZ头,它就跳过到PE头
- PE加载器检查PE头部是否有效。如果有效的话,它转到PE头的结尾
- PE头后是段表。PE头读取有关节的信息,并使用文件映射将这些部分映射到内存中。这也给每个部分的属性中指定的段表
- PE文件映射到内存中之后,PE加载程序关注PE文件的逻辑部分,比如导入表
NT文件头
DOS头的最后一个字段指示NT头的位置。NT头分为三个部分
1 | typedef struct _IMAGE_NT_HEADERS{ |
PE文件头
1 | typedef struct _IMAGE_FLIE_HEADER{ |
——紧接着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 | typedef struct _IMAGE_FILE_HADAER{ |
PE文件头——可选映像头
在整个PE文件中占重要地位,以下为节选部分字段:
1 | typedef struct _IMAGE_OPTIONAL_HEADER{ |
在可选头的最后,有一个IMAGE_DATA_DIRECTORY的数组,其中IMAGE_NUMBEROF_DIRECTORY_ENTRIES被定义为16
1 | typedef struct _IMAGE_DATA_DIRECTORY{ |
可选头的16个结构意义分别为:
PE文件——节表
节通过节表实现索引。节的内容是要真正执行的程序和相关数据
病毒要在PE文件中增加病毒代码节或把病毒插入一个现有节时,需修改节表
节表是紧挨着NT映像头的一结构数组,其成员的数目NumberOFSections决定
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 | typedef struct _IMAGE_IMPORT_DESCRIPTOR{ |
引出函数节.edata
——本文件向其他程序提供的可调用函数列表
——一般用在dll中,exe文件很少用
——当PE装载器执行一个程序,它将相关dll都装入该进程的地址空间,然后根据主程序的引入函数信息,查找相关dll中的真实函数地址来修改主程序
1 | typedef struct _IMAGE_EXPORT_DIRECTORY{ |
PE文件格式应用
DLL/EXE要引出一个函数给其他DLL/EXE使用
可通过 :
—函数名引出
—序号引出
已知导出函数名,获取函数地址步骤:
- 定位到PE header
- 从数据目录表读取导出表的虚拟地址
- 定位导出表获取名字数目(NumberOfNames)
- 并行遍历AddressOfNames和AddressOfNameOrdinals指向的数组匹配名字
- 如果在AddressOfNames指向的数组中找到匹配名字,从AddressOfNames指向的数组中提取索引值
- 例如,若发现匹配名字的RVA存放在AddressOfNames的第6个元素,那就提取AddressOfNamesOrdinals数组的第6个元素作为索引值
- 如果遍历完NumberOfNames个元素,说明当前模块没有所要的名字