这段时间学了PE文件,将我学习的一点心得和理解发给大家。
要学习PE文件首先就要了解PE文件的结构。
PE文件的结构 分为PE头和PE体以下是PE头的详细结构
typedef IMAGE_DOS_HEADER STRUCT
{
+0000h WORD e_magic //EXE标志,“MZ”
+0002h WORD e_cblp // 最后(部分)页中的字节数
+0004h WORD e_cp //文件中全部和部分页数
+0006h WORD e_crlc //重定位表中的指针数
+0008h WORD e_cparhdr //头部尺寸,以段落为单位
+000ah WORD e_minalloc //所需最小附加段
+000ch WORD e_maxalloc //所需最大附加段
+000eh WORD e_ss //DOS代码的初始化堆栈SS
+0010h WORD e_sp //DOS代码的初始化堆栈指针SP
+0012h WORD e_csum // 补码校验值
+0014h WORD e_ip //DOS代码的初始化指令入口[指针IP]
+0016h WORD e_cs //DOS代码的初始堆栈入口 [寄存器CS值]
+0018h WORD e_lfarlc // 重定位表的字节偏移量
+001ah WORD e_ovno //覆盖号
+001ch WORD e_res[4] //保留字
+0024h WORD e_oemid //OEM标识符
+0026h WORD e_oeminfo //OEM信息
+0029h WORD e_res2[10] //保留字
+003ch DWORD e_lfanew //PE头相对于文件的偏移地址 指向PE头
} IMAGE_DOS_HEADER ENDS,IMAGE_DOS-HEADER, *PIMAGE_DOS_HEADER;
typedefstruct_IMAGE_NT_HEADERS
{
+00h DWORD Signature //PE头文件标识,“PE\0\0”
+04h IMAGE_FILE_HEADER FileHeader //PE标准头
+18h IMAGE_OPTIONAL_HEADER32 OptionalHeader //PE扩展头
} IMAGE_NT_HEADERS,*PIMAGE_NT_HEADERS32;
typedefstruct_IMAGE_FILE_HEADER
{
+04h WORD Machine;//运行平台
+06h WORD NumberOfSections;//文件的区块数目(PE中节的数量)
+08h DWORD TimeDateStamp;//文件创建日期和时间
+0Ch DWORD PointerToSymbolTable;//指向符号表(主要用于调试)
+10h DWORD NumberOfSymbols;//符号表中符号个数(同上)
+14h WORD SizeOfOptionalHeader;//IMAGE_OPTIONAL_HEADER32 结构大小(长度)
+16h WORD Characteristics;//文件属性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
typedef struct _IMAGE_OPTIONAL_HEADER
{
//
// Standard fields.
//
+18h WORD Magic; // ROM 映像(0107h),普通可执行文件(010Bh)
// x32: 10B, x64: 20B
+1Ah BYTE MajorLinkerVersion; // 链接程序的主版本号
+1Bh BYTE MinorLinkerVersion; // 链接程序的次版本号
+1Ch DWORD SizeOfCode; // 所有含代码的节的总大小
+20h DWORD SizeOfInitializedData; // 所有含已初始化数据的节的总大小
+24h DWORD SizeOfUninitializedData; // 所有含未初始化数据的节的大小
+28h DWORD AddressOfEntryPoint; // 程序执行入口RVA
+2Ch DWORD BaseOfCode; // 代码的区块的起始RVA
+30h DWORD BaseOfData; // 数据的区块的起始RVA
//-------------------------------------------------------------
// NT additional fields. 以下是属于NT结构增加的领域。
//-------------------------------------------------------------
+34h DWORD ImageBase; // 程序的首选装载地址(程序建议装载地址)
+38h DWORD SectionAlignment; // 内存中的区块的对齐大小
+3Ch DWORD FileAlignment; // 文件中的区块的对齐大小
+40h WORD MajorOperatingSystemVersion; // 要求操作系统最低版本号的主版本号
+42h WORD MinorOperatingSystemVersion; // 要求操作系统最低版本号的副版本号
+44h WORD MajorImageVersion; // 可运行于操作系统的主版本号
+46h WORD MinorImageVersion; // 可运行于操作系统的次版本号
+48h WORD MajorSubsystemVersion; // 要求最低子系统版本的主版本号
+4Ah WORD MinorSubsystemVersion; // 要求最低子系统版本的次版本号
+4Ch DWORD Win32VersionValue; // 莫须有字段,不被病毒利用的话一般为0
+50h DWORD SizeOfImage; // 映像装入内存后的总尺寸(内存中的整个PE映像尺寸)
+54h DWORD SizeOfHeaders; // 所有头 + 区块表的尺寸大小
+58h DWORD CheckSum; // 映像的校检和
+5Ch WORD Subsystem; // 可执行文件期望的子系统
+5Eh WORD DllCharacteristics; // DllMain()函数何时被调用,默认为 0(DLL文件特性)
+60h DWORD SizeOfStackReserve; // 初始化时的栈大小
+64h DWORD SizeOfStackCommit; // 初始化时实际提交的栈大小
+68h DWORD SizeOfHeapReserve; // 初始化时保留的堆大小
+6Ch DWORD SizeOfHeapCommit; // 初始化时实际提交的堆大小
+70h DWORD LoaderFlags; // 与调试有关,默认为 0
+74h DWORD NumberOfRvaAndSizes; // 下边数据目录的项数,这个字段自Windows NT 发布以来 // 一直是16
+78h IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];// 数据目录表
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
typedef struct _IMAGE_DATA_DIRECTORY
{
DWORD VirtualAddress; //0000h - 数据得起始RVA
DWORD isize; //0004h - 数据块的长度
}IMAGE_DATA_DIRECTORY,*PIMAGE_DATA_DIRECTORY;
PE体主要就是包含各个节表中的信息。
在这里我就不做过多的介绍了 下面开始不如今天的正题。
导入表
什么导入表,导入表就是程序引用动态链接库中的函数的各项信息。
在windows中 每个程序不可能调用一个API函数,就把API函数的代码给放到 PE文件中,那样会损失大量的空间所以动态链接库孕育而生。
当然如果你静态编译的话情况可能就会不同
下面我们继续,导入表在PE文件中的什么位置
在 _IMAGE_OPTIONAL_HEADER 结构中有一个成员 叫做 DataDirectory[ IMAGE_NUMBEROF_DIRECTORY_ENTRIES ]
这个成员是一个数组 里面的成员的位置已经被windows设置好了 如下所示
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 导出表地址和大小
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 导入表地址和大小
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 资源目录资源表地址和大小
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 异常目录异常表地址和大小
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 安全目录属性证书数据地址和大小
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 基地址重定位表地址和大小
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 调试信息地址和大小
#define IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 预留为0
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 指向全局指针寄存器的值
#define IMAGE_DIRECTORY_ENTRY_TLS 9 线程句柄存储地址和大小
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 加载配置表地址和大小
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 绑定导入表地址和大小
#define IMAGE_DIRECTORY_ENTRY_IAT 12 导入函数地址表地址和大小
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 延迟导入表地址和大小
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 COM信息
#define Reserved 15系统保留
那么我们现在开始 在PE文件中找到导入表
以一个简单的helloworld.exe程序为例
我所选中的就是PE头所有结构的信息。
让我们根据上面所提供的结构 来找到导入表的RVA。
这里RVA指的是 虚拟内存相对偏移
假设虚拟地址是0x400000 那么从 这个位置开始 偏移 4000 等于 0x404000
这4000就是相对于0x400000的RVA
首先我们找到 _IMAGE_OPTIONAL_HEADER结构中 一个已经预先设置好的值 0x10 十进制就是16
在前面的结构描述中 这个值一直就是被设置好的。
下面我们在PE文件中定位
这里我们成功找到了这个预先被设置好的一个值,0x00000010在这个值后面就是刚才我说的那个数组
我所选中的就是这个PE文件的数据目录
根据之前贴出来的数组结构信息我们可以发现 数组每个成员都是8个字节 所以我们略过第一个成员
直接访问第二个成员也就是导入表
箭头所指就是导入表的起始RVA还有 数据的大小
现在我们拿到导入表的RVA 00002010 记住这里是RVA 是加载在内存中之后的偏移 所以我们需要进行转换才能直接访问到正确的信息
接着在紧跟着PE标准头结构之后 就是节区信息
在这里面我们找到导入表的节区信息
.rdata或者.idata
.rdata可能会合并 .idata
下面是节区的结构信息
typedefstruct_IMAGE_SECTION_HEADER {
+0h BYTE Name[IMAGE_SIZEOF_SHORT_NAME];//节表名称,如“.text” 8个字节节名 //IMAGE_SIZEOF_SHORT_NAME=8
union
+8h
{
DWORD PhysicalAddress;//物理地址 节区的尺寸
DWORD VirtualSize;//真实长度,这两个值是一个联合结构,可以使用其中的任何一个,一般是取后一个
} Misc;
+ch DWORD VirtualAddress;//节区的 RVA 地址
+10h DWORD SizeOfRawData;//在文件中对齐后的尺寸
+14h DWORD PointerToRawData;//在文件中的偏移量
+18h DWORD PointerToRelocations;//在OBJ文件中使用,重定位的偏移
+1ch DWORD PointerToLinenumbers;//行号表的偏移(供调试使用)
+1eh WORD NumberOfRelocations;//在OBJ文件中使用,重定位项数目
+20h WORD NumberOfLinenumbers;//行号表中行号的数目
+24h DWORD Characteristics;//节属性如可读,可写,可执行等
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
下面我们来找到节区中对我们有用的几个关键成员
VirtualSize 和
VirtualAddress 和
SizeOfRawData 和
PointerToRawData
正好这四个成员是接近的
00000092是 内存中节区所占大小
00002000是 内存中节区的起始RVA (注意这里的起始代表着这个节区最开始的位置)
00000200是 文件中节区的大小
00000600是 文件中的节区起始偏移
得到了以上的信息下面我们就可以查看导入表的信息了
但是在这之前我们要进行一下 转换将RVA转换成 FOA(文件偏移)
首先用导入表的起始RVA减去 节区的起始RVA
00002010 -
00002000 = 00000010 得到实际的偏移
00000010 + 00000600 = 00000610 然后再加上文件节区的起始偏移就得到了 FOA
这里我说选中的就是 导入表结构体的各种信息
struct_IMAGE_IMPORT_DESCRIPTOR
{
union
{
DWORD Characteristics;
DWORD OriginalFirstThunk; //INT表
} DUMMYUNIONNAME;
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name;//库名
DWORD FirstThunk; //IAT表
} IMAGE_IMPORT_DESCRIPTOR;
同时导入表的结构体同样是一个数组
下面我们来获得导入表的信息
00002054 是INT表的RVA FOA 654
0000206a 是库名 FOA 66A
00002008 是IAT表的RVA FOA 608
0000204c 是INT表的RVA FOA 64c
00002084 是库名 FOA 684
00002000 是IAT表的RVA FOA 600
首先我们来看导入表结构体的第一个成员包含的是什么信息
来看第一个偏移 654
我们发现INT表保存的是一个 RVA下面我们将 RVA 转换成 FOA 65c
可以看见INT表指向的 数据包含了两个结构
(注:这里INT表指向的RVA地址实际上是一个数组)
前两个字节包含的时函数编号
后面的字符串就是函数名
导入表结构的NAME成员 偏移是 66A
这里我们可以见看 NAME所指向的DLL名
接着我们来查看 IAT表
相信表哥们可以发现一个细节
IAT表指向的地址 和 INT 表所指向的值是一样的。
到这里我们打开OD
我们发现在PE文件装载进内存之后 值就发生了改变
让我们看看这个地址又是什么
在这里我们可以发现 IAT表中的信息已经被改成了 DLL链接库中函数的地址
从上图中可以发现 750b0380是函数的入口地址
可以发现JMP的位置 就是函数的入口地址
由此我们可以发现IAT表在文件中和INT表是一样的,但是被加载进内存之后就会被替换掉变成DLL导入函数中的实际地址
IAT表在文件中是处于一种站位置的状态。
新手第一次写,各位大佬见笑了