• 我们在哪一颗星上见过 ,以至如此相互思念 ;我们在哪一颗星上相互思念过,以至如此相互深爱
  • 我们在哪一颗星上分别 ,以至如此相互辉映 ;我们在哪一颗星上入睡 ,以至如此唤醒黎明
  • 认识世界 克服困难 洞悉所有 贴近生活 寻找珍爱 感受彼此

核心原理-第13章 PE文件格式

核心原理 云涯 5年前 (2019-08-19) 2176次浏览 0个评论

学习PE文件格式的过程中也整理一下有关进程、内存、DLL等内容。

1 PE文件格式

种类

主扩展名 种类 主扩展名

可执行系列

exe,scr

驱动程序系列

sys,vxd

库系列

dll,ocx,cpl,drv

对象文件系列

obj

1.1 基本概念

VA:虚拟地址

images

模块地址(image Base)

模块地址,就是exe加载到内存的时候,所在的地址,比如MZ位置,在那个位置,那么对应模块地址就是这个位置,在OD中的内存中查看就是PE头

images

RVA(relative Virtual Address) 相对虚拟地址偏移

假设我们找一个虚拟地址VA = 0X4001200 (虚拟地址),那么算出他的相对偏移,那么我们就要看他属于内存中哪个节区了

images

可以看出,是在401000 ~ 41500之间,那么我们就用401000即可.

RVA = VA – 401000 得出的就是相对于虚拟地址的偏移

简化

RVA = 401200 – 401000 = 200(RVA) 那么偏移就是200了

FA(RAW)(File Address) 或者叫做 FOA (File Ofseet Address)

FA就是文件中的地址,那么这个要看我们的节表了

images

节表(就是那个区)上面我们看了是.text 也就是代码区,正好是属于第一个节表,那么看第一个节表中的PointerToRawData成员即可.

VAtoRaw(虚拟地址,转化为文件偏移位置,就是虚拟地址的代码,在文件那个偏移位置存储)

首先你要明白 RVA 怎么计算,FA怎么看.

images

我要找40101A虚拟地址,在文件中的位置.

思路:

1.获得虚拟地址(VA) 现在是40101A

2.查看属于哪个节区表(点击内存查看,OllyDbg)

images

大于401000,小于402000,所以节区属于代码区,也就是.text这个区域

3.算出RVA(相对虚拟地址偏移)

RVA = VA – 内存中节区地址

代入得到:

RVA = 40101A – 401000

RVA = 1A (相对虚拟地址偏移是1A)

4.RVA + 文件中的(相同节表,比如上面是.text,那么文件中看的节表就是.text这个节表)节表中的PointerToRawData成员记录的大小 得出虚拟地址在文件中的偏移

1A + (文件中节表的偏移) = 实际虚拟地址在文件偏移记录的代码地址.

1A + 200 = 21A (虚拟地址在文件中的偏移)

200要查看节表,还记得上面我们算RVA的时候吗,找的是内存中节区的地址,而这个地址正好是.text代码区

那么在文件中我们也要找到这个位置.,节表是第一个,第一个就是,而表中存放的文件偏移就是200

images

那么现在去文件中的21A位置查看一下,看看是否是我们虚拟地址的代码.

images

正是我们要找的地址,那么由此可以得出物理地址的代码位置,在文件中存放的偏移在哪里.

总结:

其实很简单,首先看属于哪个节表的, 那么先算出RVA的值,然后让RVA + 文件中相同节表中的成员(PointRawData) 那么最终就是虚拟地址代码,在文件偏移的位置.

举个例子

VA = 401456

RVA = 401456 – (.text的位置当然这个你得自己看,可能不是,这里默认是了)401000 = 456

FA = 456 + (文件中节表中的PointRawData,我假设是200,这里具体看PE中怎么存储的)200 = 656(十六进制)

那么这个656文件偏移处,记录的就是 虚拟地址(VA)401456的二进制代码.

没优化过的公式

VA = 401234

Image Bae = 400000

RVA = 401234 – 400000 = 1234

VPK = (内存中节区首地址 – image base) – 文件中节区的偏移地址(PointerToRawData 字段)

(401000 – 400000 ) – 400(这个值自己看文件,不一定是400)= 1000 – 400 = C00(vpk);

FA = RVA – VPK = 1234 – C00 = 634

例子:

已经知道VA = 401456

计算FA位置

RVA = 401456 – 00400000 = 1456

VPK = (401000 – 400000) – 文件中PointerToRawData 字段

= 1000 – 400 = C00

FA = RVA – VPK

= 1456- C00 = 856

优化的公式

FA =  VA – 内存中节区地址 + 文件PointerToRawData 字段

列如VA = 401596

当然,节区你要看内存,上面已经说了怎么看.(怎么看节区表)

401596 – 401000 + 400

= 596 + 400

= 996 (FA)

如果按照上面的公式,我们再来计算一遍

VA = 401596

IMAGEbase = 400000

RVA = (虚拟地址 – 模块地址)

=401596 – 400000

= 1596

VPK = (节区表首地址- 模块地址) – 节表中的文件PointerToRawData 字段

= 401000 – 40000 – 400

= 1000 – 400

= C00 (vpk)

FA = RVA – VPK

= 1596 – C00

= 996

文件偏移,转为虚拟地址

首先计算文件偏移,我们需要知道文件的位置

比如

1.你要知道一个文件位置, (随便哪个都行,把它转换为内存虚拟地址)

2.我们要知道 文件偏移位置的大小,(也就是上面说的节表中的 PointerToRawData 字段)

3.我们要知道你给的文件位置属于哪个区,这个是根据 上面计算出来FA的首地址的出来的

已经知道FA = 996

计算公式为

VA = FA +imagebase(模块首地址) + VPK

VPK的值就是你要计算的

VPK = (内存中的节区表 – 模块地址) – PointerToRawData字段

代入公式得

VA = 996 + 40000 + (401000 – 400000 – 400)

= 40996 + C00

= 41596 (虚拟地址位置)

名称 描述
VA 进程虚拟内存的绝对地址
RVA 相对虚拟地址,指从某个基准位置(ImageBase)开始的相对地址
RVA + ImageBase = VA
节是PE文件中代码或数据的基本单元。原则上讲,节只分为“代码节”和“数据节”。
RAW

images

PE头:DDOS头到节区头是PE头的部分,其下节区称为PE体。各个节区头定义了各节区在文件或者内存的大小,属性,位置等

1.2 PE头

PE头由许多结构体组成。

1.2.1 DOS头

为了扩展已有的DOS EXE文件头,在最前面添加一个IMAGE_DOS_HEADER结构体,大小为64字节,两个重要成员

e_magic:DDOS签名,4D5A==>”MZ”

e_lfanew:指示NT头的偏移。

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;                      // Checksum
    WORD   e_ip;                        // Initial IP value
    WORD   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这里是8字节
    WORD   e_oemid;                     // OEM identifier (for e_oeminfo)
    WORD   e_oeminfo;                   // OEM information; e_oemid specific
    WORD   e_res2[10];                  // Reserved words这里是20字节
    LONG   e_lfanew;                    // File address of new exe header这里是4字节
  } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

images

1.2.2 DOS存根

可选项,大小不固定。DOS(16位)环境下运行程序会显示DOS存根里面的内容。

1.2.3 NT头

IMAGE_NT_HEADERS结构体由3个成员组成,第一个成员签名结构体,其值为50450000(”PE”)。另外两个成员为文件头,可选头。

typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;        // PE Signature ("PE"00)
    IMAGE_FILE_HEADER FileHeader;文件头
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;可选头
} IMAGE_NT_HEADER32, *PIMAGE_NT_HEADER32;

文件头(20字节):

文件头是表现文件大致属性的IMAGE_FILE_HEADER结构体,拥有四个成员,若设置不正确,将导致文件无法正常运行。

  • Machine:CPU拥有的唯一码,Intel x86为14C
  • NumberOfSections:用来指出文件中存在的节区数量
  • SizeOfOptionalHeader:用来指明IMAGE_OPTIONAL_HEADER32结构体的长度。
  • Characteristics:该字段用于表示文件属性,文件是否为可运行形态,是否是DLL文件等
struct _IMAGE_FILE_HEADER{
    0x00 WORD Machine;                  //※程序执行的CPU平台:0X0:任何平台,0X14C:intel i386及后续处理器
    0x02 WORD NumberOfSections;         //※PE文件中区块数量
    0x04 DWORD TimeDateStamp;           //时间戳:连接器产生此文件的时间距1969/12/31-16:00P:00的总秒数
    0x08 DWORD PointerToSymbolTable;  //COFF符号表格的偏移位置。此字段只对COFF除错信息有用
    0x0c DWORD NumberOfSymbols;       //COFF符号表格中的符号个数。该值和上一个值在release版本的程序里为0
    0x10 WORD SizeOfOptionalHeader;   //IMAGE_OPTIONAL_HEADER结构的大小(字节数):32位默认E0H,64位默认F0H(可修改)
    0x12 WORD Characteristics;          //※描述文件属性,eg:
                                        //单属性(只有1bit为1):#define IMAGE_FILE_DLL 0x2000  //File is a DLL.
                                        //组合属性(多个bit为1,单属性或运算):0X010F 可执行文件
};

可选头(32位224字节,64位240字节):

IMAGE_OPTIONAL_HEADER32/64需要关注以下成员

  • Magic:10B–>IMAGE_OPTIONAL_HEADER32 20B–>IMAGE_OPTIONAL_HEADER64
  • AddressOfEntryPoint:指出程序最先执行的代码的起始地址
  • ImageBase:PE文件被加载到内存时,它指明文件优先装入地址。

exe,dll文件被装载到用户内存0-7FFFFFFF中,SYS文件被装载入内核内存80000000-FFFFFFFF中。一般使用开发工具创建的exe的ImageBase为00400000,DLL文件的ImageBase为10000000。。。

struct _IMAGE_OPTIONAL_HEADER{
    0x00 WORD Magic;                    //※幻数(魔数),0x0107:ROM image,0x010B:32位PE,0X020B:64位PE 
    0x02 BYTE MajorLinkerVersion;     //连接器主版本号
    0x03 BYTE MinorLinkerVersion;     //连接器副版本号
    0x04 DWORD SizeOfCode;              //所有代码段的总和大小,注意:必须是FileAlignment的整数倍,存在但没用
    0x08 DWORD SizeOfInitializedData;   //已经初始化数据的大小,注意:必须是FileAlignment的整数倍,存在但没用
    0x0c DWORD SizeOfUninitializedData; //未经初始化数据的大小,注意:必须是FileAlignment的整数倍,存在但没用
    0x10 DWORD AddressOfEntryPoint;     //※程序入口地址OEP,这是一个RVA(Relative Virtual Address),通常会落在.textsection,此字段对于DLLs/EXEs都适用。
    0x14 DWORD BaseOfCode;              //代码段起始地址(代码基址),(代码的开始和程序无必然联系)
    0x18 DWORD BaseOfData;              //数据段起始地址(数据基址)
    0x1c DWORD ImageBase;               //※内存镜像基址(默认装入起始地址),默认为4000H
    0x20 DWORD SectionAlignment;        //※内存对齐:一旦映像到内存中,每一个section保证从一个「此值之倍数」的虚拟地址开始
    0x24 DWORD FileAlignment;           //※文件对齐:最初是200H,现在是1000H
    0x28 WORD MajorOperatingSystemVersion;    //所需操作系统主版本号
    0x2a WORD MinorOperatingSystemVersion;    //所需操作系统副版本号
    0x2c WORD MajorImageVersion;              //自定义主版本号,使用连接器的参数设置,eg:LINK /VERSION:2.0 myobj.obj
    0x2e WORD MinorImageVersion;              //自定义副版本号,使用连接器的参数设置
    0x30 WORD MajorSubsystemVersion;          //所需子系统主版本号,典型数值4.0(Windows 4.0/即Windows 95)
    0x32 WORD MinorSubsystemVersion;          //所需子系统副版本号
    0x34 DWORD Win32VersionValue;             //总是0
    0x38 DWORD SizeOfImage;         //※PE文件在内存中映像总大小,sizeof(ImageBuffer),SectionAlignment的倍数
    0x3c DWORD SizeOfHeaders;       //※DOS头(64B)+PE标记(4B)+标准PE头(20B)+可选PE头+节表的总大小,按照文件对齐(FileAlignment的倍数)
    0x40 DWORD CheckSum;            //PE文件CRC校验和,判断文件是否被修改
    0x44 WORD Subsystem;          //用户界面使用的子系统类型
    0x46 WORD DllCharacteristics;   //总是0
    0x48 DWORD SizeOfStackReserve;  //默认线程初始化栈的保留大小
    0x4c DWORD SizeOfStackCommit;   //初始化时实际提交的线程栈大小
    0x50 DWORD SizeOfHeapReserve;   //默认保留给初始化的process heap的虚拟内存大小
    0x54 DWORD SizeOfHeapCommit;    //初始化时实际提交的process heap大小
    0x58 DWORD LoaderFlags;       //总是0
    0x5c DWORD NumberOfRvaAndSizes; //目录项数目:总为0X00000010H(16)
    0x60 _IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];//#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
};

执行PE文件时,PE装载器先创建进程,再将文件载入内存,然后吧把EIP寄存器值设定为ImageBase + AddressOfEntryPoint

  • SectionAlignment:指定了节区在内存中的最小单位
  • FileAlignment:指明了节区在磁盘文件中的最小单位
  • SizeOflmage:加载PE文件到内存时,它指定了PE Image在虚拟机内存中所占空间的大小。
  • SizeOfHeader:用来指出整个PE文件头的大小。
  • Subsystem:用来区分系统驱动文件与普通文件。
  • NumberOfRvaAndSizes:指定DateDirectory(Image_OPENTIONAL_HEADER32结构体最后一个成员)数组的个数。
  • DataDirectory:IMAGE_DATADIRECTORY结构体组成的数组。

1.2.4 节区头

节区头定义了各个节区属性。代码区、数据区、资源区

DWORD VirtualSize:内存中节区所占大小
DWORD VirtualAddress:内存中节区起始地址(RVA)
DWORD SizeOfRawData:磁盘文件中节区所占大小
DWORD PointerToRawData:磁盘文件中节区起始位置
DWORD Characteristics:节区属性

由IMAGE_SECTION_HEADER结构体组成的数组,每个结构体对应一个节区

Name: 区块名。这是一个由8位的ASCII 码名,用来定义区块的名称。多数区块名都习惯性以一个“.”作为开头(例如:.text),这个“.” 实际上是不是必须的。值得我们注意的是,如果区块名超过 8 个字节,则没有最后的终止标志“NULL” 字节。并且前边带有一个“$” 的区块名字会从连接器那里得到特殊的待遇,前边带有“$” 的相同名字的区块在载入时候将会被合并,在合并之后的区块中,他们是按照“$” 后边的字符的字母顺序进行合并的。
另外每个区块的名称都是唯一的,不能有同名的两个区块。但事实上节的名称不代表任何含义,他的存在仅仅是为了正 规统一编程的时候方便程序员查看方便而设置的一个标记而已。所以将包含代码的区块命名为“.Data” 或者说将包含数据的区块命名为“.Code” 都是合法的。当我们要从PE 文件中读取需要的区块时候,不能以区块的名称作为定位的标准和依据,正确的方法是按照 IMAGE_OPTIONAL_HEADER32 结构中的数据目录字段结合进行定位。

Virtual Size:该区块表对应的区块的大小,这是区块的数据在没有进行对齐处理前的实际大小。

Virtual Address:该区块装载到内存中的RVA 地址。这个地址是按照内存页来对齐的,因此它的数值总是 SectionAlignment 的值的整数倍。在Microsoft 工具中,第一个快的默认 RVA 总为1000h。在OBJ 中,该字段没有意义地,并被设为0。

SizeOfRawData:该区块在磁盘中所占的大小。在可执行文件中,该字段是已经被FileAlignment 潜规则处理过的长度。

PointerToRawData:该区块在磁盘中的偏移。这个数值是从文件头开始算起的偏移量哦。

PointerToRelocations:这哥们在EXE文件中没有意义,在OBJ 文件中,表示本区块重定位信息的偏移值。(在OBJ 文件中如果不是零,它会指向一个IMAGE_RELOCATION 结构的数组)

PointerToLinenumbers:行号表在文件中的偏移值,文件的调试信息,于我们没用,鸡肋。

NumberOfRelocations:这哥们在EXE文件中也没有意义,在OBJ 文件中,是本区块在重定位表中的重定位数目来着。

NumberOfLinenumbers:该区块在行号表中的行号数目,鸡肋。

Characteristics:该区块的属性。该字段是按位来指出区块的属性(如代码/数据/可读/可写等)的标志。

1.3 IAT 导入地址表

IAT是一种表格,用来记录程序正在使用哪些库中的哪些函数

1.3.1 DLL

加载DLL的方式

  • 显示连接:程序使用DLL是加载,使用完毕后释放
  • 隐式连接:程序一开始就加载DLL,程序结束后释放

调用createfilew函数:01001104(程序.text节区)存储着7c8107F0的值,而这个值就是createfilew的地址。通过调用01001104里面的值来调用函数

1.3.2 IMAGE_IMPORT_DESCRIPTOR

该结构体中存储着PE文件要导入那些库文件

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
     union {
          DWORD Characteristics;         // 0 for terminating null import descriptor
          DWORD OriginalFirstThunk;   // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
     };
     DWORD TimeDateStamp;           // 0 if not bound,
                                                                // -1 if bound, and real date\time stamp
                                                                // in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
                                                                // O.W. date/time stamp of DLL bound to (Old BIND)
      DWORD ForwarderChain;           // -1 if no forwarders
      DWORD Name;                             // RVA,指向字符串,是这个可执行文件的名字。例如"ACE.dll"
      DWORD FirstThunk;                     // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;

我们着重关心两个指针,OriginalFirstThunk和FirstThunk。
OriginalFirstThunk和FirstThunk是两个DWORD值,存贮着两个RVA数值,其实它们就是两个指针。
OriginalFirstThunk和FirstThunk实际上都是指向同一个数组。
前者,我们称之为INT,而后者,我们称之为IAT.

IAT是一个IMAGE_THUNK_DATA类型的数组。有多少个函数被导入,这个数组就有多少个成员。该数组以0结尾。

typedef struct _IMAGE_THUNK_DATA32 {
     union {
           DWORD ForwarderString;          // 一个RVA地址,指向forwarder string 
           DWORD Function;                       // PDWORD,被导入的函数的入口地址
           DWORD Ordinal;                         // 该函数的序数
           DWORD AddressOfData;           // 一个RVA地址,指向IMAGE_IMPORT_BY_NAME
      } u1;
} IMAGE_THUNK_DATA32;

IMAGE_THUNK_DATA64与IMAGE_THUNK_DATA32的区别,仅仅是把DWORD换成了64位整数。

PIMAGE_IMPORT_BY_NAME是一个非常简单的结构,就两个成员。
typedef struct _IMAGE_IMPORT_BY_NAME {
     WORD Hint;            // 该函数的导出序数
     BYTE Name[1];      // 该函数的名字
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

而IMAGE_THUNK_DATA32就是一个非常魔术般的东西了。
struct IMAGE_THUNK_DATA的大小,恰好等于一个指针的大小。(32bit机器下是32bit,64bit机器下是64bit)

每一个IMAGE_THUNK_DATA对应着一个被导入的函数。
对于可执行文件而言,IAT中的IMAGE_THUNK_DATA中存储的要么是Ordinal,要么是AddressOfData。
怎么判断IMAGE_THUNK_DATA中存储的是Ordinal 还是 AddressOfData 呢?
众所周知,在32bit的机器上,地址空间是00000000-FFFFFFFF,
一般而言,其中00000000-7FFFFFFF是用户空间,其它是系统空间。
于是,看IMAGE_THUNK_DATA的最高位,如果是1,就是Ordinal,否则就是AddressOfData。

而INT和IAT中存储的本来应该是同样的数据。
然后说绑定(binding).
当一个可执行文件被绑定的时候,IAT中的IMAGE_THUNK_DATA被改写为(被导入的)该函数的实际地址。
这一步也许是交给链接器在链接的时候执行,也许是在该可执行文件载入的时候执行。
但是,如果,该可执行文件已经和dll绑定。但是这个dll后来又被更改了,这些被导入的函数依然在该dll中存在,但是实际地址已经改变了。还有,我们保留过一个IAT的副本,它就是INT.(这就是为什么我们称之为Original FirstThunk).根据INT中的内容,我们可以重建IAT表。

1.3.3 PE装载器把导入函数输入至IAT的顺序

  1. 读取IID的NAME成员,获取库名称字符串(kerned32.dll)
  2. 装载相应库—>LoadLibrary(“kernel32.dll”)
  3. 读取IID的OriginalFirstThunk成员,获取INT地址
  4. 逐一获取INT里面的值,获取相应的IMAGE_IMPORT_BY_NAME地址
  5. 使用IMAGE_IMPORT_BY_NAME获取相应函数的起始地址—>GetprocAddress(“GetCurrentThreadld”)
  6. 读取FirstThunk成员,获取IAT地址
  7. 将上面获得的函数地址输入相应的IAT数组值
  8. 重复以上4-7,直到INT结束

云涯历险记 , 版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权
转载请注明原文链接:核心原理-第13章 PE文件格式
喜欢 (1)

您必须 登录 才能发表评论!