学习PE文件格式的过程中也整理一下有关进程、内存、DLL等内容。
1 PE文件格式
种类 |
主扩展名 | 种类 | 主扩展名 |
可执行系列 |
exe,scr |
驱动程序系列 |
sys,vxd |
库系列 |
dll,ocx,cpl,drv |
对象文件系列 |
obj |
1.1 基本概念
VA:虚拟地址
模块地址(image Base)
模块地址,就是exe加载到内存的时候,所在的地址,比如MZ位置,在那个位置,那么对应模块地址就是这个位置,在OD中的内存中查看就是PE头
RVA(relative Virtual Address) 相对虚拟地址偏移
假设我们找一个虚拟地址VA = 0X4001200 (虚拟地址),那么算出他的相对偏移,那么我们就要看他属于内存中哪个节区了
可以看出,是在401000 ~ 41500之间,那么我们就用401000即可.
RVA = VA – 401000 得出的就是相对于虚拟地址的偏移
简化
RVA = 401200 – 401000 = 200(RVA) 那么偏移就是200了
FA(RAW)(File Address) 或者叫做 FOA (File Ofseet Address)
FA就是文件中的地址,那么这个要看我们的节表了
节表(就是那个区)上面我们看了是.text 也就是代码区,正好是属于第一个节表,那么看第一个节表中的PointerToRawData成员即可.
VAtoRaw(虚拟地址,转化为文件偏移位置,就是虚拟地址的代码,在文件那个偏移位置存储)
首先你要明白 RVA 怎么计算,FA怎么看.
我要找40101A虚拟地址,在文件中的位置.
思路:
1.获得虚拟地址(VA) 现在是40101A
2.查看属于哪个节区表(点击内存查看,OllyDbg)
大于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
那么现在去文件中的21A位置查看一下,看看是否是我们虚拟地址的代码.
正是我们要找的地址,那么由此可以得出物理地址的代码位置,在文件中存放的偏移在哪里.
总结:
其实很简单,首先看属于哪个节表的, 那么先算出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 |
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;
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的顺序
- 读取IID的NAME成员,获取库名称字符串(kerned32.dll)
- 装载相应库—>LoadLibrary(“kernel32.dll”)
- 读取IID的OriginalFirstThunk成员,获取INT地址
- 逐一获取INT里面的值,获取相应的IMAGE_IMPORT_BY_NAME地址
- 使用IMAGE_IMPORT_BY_NAME获取相应函数的起始地址—>GetprocAddress(“GetCurrentThreadld”)
- 读取FirstThunk成员,获取IAT地址
- 将上面获得的函数地址输入相应的IAT数组值
- 重复以上4-7,直到INT结束