PE文件格式 super版
←-----------VA
←------RVA(相对虚拟地址)
←------基地址(Imagebase)
◆在NT架构中就是句柄
ImageBase+RVA=VA
◆计算机中为了提高处理文件、内存、网络包的效率,使用“最小基本单位”这一概念。
◆PE头和各节区后都有一个趋于,成为NULL填充。文件/内存中的节区起始位置应该在各文件/内存的最小单位的倍数上,空白趋于用null填充
◆基地址在NT架构中就是句柄来获取,HMODULEGetModuleHandle(LPCTSTR IpMouduleName)
◆32位windowos中,各进程分配有的4G虚拟内存,因此进程中VA值范围是00000000~FFFFFFFF
一。DOS头(IMAGE_DOS_HEADER结构体)[size:64byte]
Reserved words
Reserved words
结构体中有两个重要的数据成员。
第一个为e_magic,这个必须为MZ,即4D5A。
另一个是e_lfanew,这个成员的值为IMAGE_NT_HEADERS的偏移(指向PE文件头)。
其中,*e_lfanew这个字段的值: PE Header 在磁盘文件中相对于文件开始的偏移地址.
注释 :long---4字节相当于dword
一。DOS根存[size:不定]
DOS根存是16位的汇编指令,32为系统会自动忽略,但可在debug中运行
二。NT头 (IMAGE_NT_HEADERS)[size:248byte]15行
◆+0h DWORD Signature; //4 bytes PE文件头 标志 :(e_lfanew)->‘PE’
(1)。DWORD Signature; [size:20 byte] 标志 : 50 45 00 00-------→PE
(2)。IMAGE_FILE_HEADER FileHeader [size:20 byte]
typedef struct _IMAGE_FILE_HEADER {
1). Machine 代表了CPU的类型 //运行平台
#define IMAGE_FILE_MACHINE_UNKNOWN 0
#define 0x0248 //APLHA64
2) NumberOfSections: 代表区块的数目,区块表紧跟在IMAGE_NT_HEADERS后面, 区块表大概是一个链表结构,链表长度由NumberOfSection的数值决定.
3) TimeDataStamp: 表明文件的创建时间
4) SizeOfOptionalHeader: 是IMAGE_NT_HEADERS的另一个子结构IMAGE_OPTIONAL_HEADER的大小 (E0 00即十进制224byte)
5) Characteristics: 代表文件的属性EXE文件一般是0100h DLL文件一般是210Eh,多种属性可以用或运算同时拥有
#define IMAGE_FILE_NET_RUN_FROM_SWAP 0x0800 //如果图像在网络上,请将其复制并从交换文件运行。
#define IMAGE_FILE_SYSTEM 0x1000 // 系统文件
……..8000 标志已经过时
*属性0F 01即10F 属于32位机器、重定位信息被移除、文件可执行、行号被移除 、符号被移除
(3)。 IMAGE_OPTIONAL_HEADER32 OptionalHeader [size:224 byte]
紧接IMAGE_FILE_HEADER之后,IMAGE_OPTIONAL_HEADER的大小由IMAGE_FILE_HEADER中倒数第二个成员(SizeOfOptionalHeader)指定. IMAGE_OPTIONAL_HEADER结构体如下:
在IMAGE_OPTIONAL_HEADER32结构体中需要关注的成员。这些值是文件运行必须的,设置错误会导致文件无法正常运行。
1) Magic:32位可执行文件来:0x010B
64位可执行文件来:0x020B2) AddressOfEntryPoint: 程序执行的入口RVA地址 (指程序最先执行的代码位置)
3) ImageBase: 建议的装载地址
进程虚拟内存的范围00000000~FFFFFFFF,EXE、DLL文件被装载到用户内存0~07FFFFFF,SYS文件被装载到内核内存80000000~FFFFFFFF。一般而言,一般开发工具(VB/VC++/Delphi),创建的EXE文件ImageBase为00400000,DLL为10000000
执行PE文件时,PE装载器先创建进程,再将文件载入内存,然后保存EIP=ImageBase+AddressOfEntryPoint
4)SectionAlignment:为内存中节的对齐大小,一般为0×00001000 FileAlignment:为PE文件中节的对齐大小(200??)
也就是说,每个节被装入的地址必定是本字段指定数值的整数倍。
5) SizeOfImage:映像文件的大小 (虚拟内存中大小)
6)SizeOfHeaders:整个PE头的大小。该值必须为FileAlignment的整数倍。(文件开始到第一节区的偏移量)
7)Subsystem字段:区分系统驱动文件sys和普通可执行文件exe、dll
1 系统驱动(sys)
2 窗口应用程序(notepad.exe)
3 控制台应用程序(cmd)
8)NumberOfRvaAndSizes:指定datadirectory的数组个数
9) DataDirectory为数据目录表数组,比较重要:共有16个表项
Size = sizeof(_IMAGE_DATA_DIRECTORY) * 16
sizeof(_IMAGE_DATA_DIRECTORY) = 8 bytes
_IMAGE_DATA_DIRECTORY结构体以及成员定义:
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
// Directory Entries
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
三。节区头(IMAGE_SECTION_HEADER)[size:40byte]两行半
Section Header 数量:_IMAGE_FILE_HEADER结构体中NumberOfSections成员。
Section Header 定位:紧跟在IMAGE_NT_HEADERS后面
1) Name[IMAGE_SIZEOF_SHORT_NAME]:
这是一个由8位的ASCII 码名,用来定义区块的名称。(其实并没有限制必须使用ascii,可以放任何值,甚至可以用null填充)
多数区块名都习惯性以一个“.”作为开头(例如:.text),这个“.” 实际上是不是必须的。
值得我们注意的是,如果区块名超过 8 个字节,则没有最后的终止标志“NULL” 字节。2) Virtual Size:表对应的区块的大小,这是区块的数据在没有进行对齐处理前的实际大小。
参考RVA to RWA (内存地址与文件偏移间的映射)[偏移是在硬盘的概念]
Windows 装载器在装载DOS部分、PE文件头部分和节表(区块表)部分是不进行任何特殊处理的,而在装载节(区块)的时候则会自动按节(区块)的属性做不同的处理。
步骤:
1)查找RVA所在节区(.text / .rsrc /.data)
2)根据公式计算:
RAW-PointToRawData =RVA-VirtualAddress
RAW=RVA-VirtualAddress+PointToRawData
**(这个VirtualAddress是节区头的VirtualAddress,其实就是距离基地址的RVA)
IAT保存的内容与window操作系统的核心进程、内存、dll结构相关
IAT是一种表格,用来记录程序正在使用哪些库,哪些函数
------------------------------------------------------------------------------------------------------------------------------------------
参考:DLL“动态链接库”,需要的时候调用
◆加载DLL方式
1.“显式链接”,程序使用DLL时加载,使用完毕后释放内存
2.“隐式链接 ”,程序开始的时候加载DLL,程序终止时释放占用的内存
◆所有API调用均采用通过某处地址的值来实现,(如:call dword ptr DS:[01001104],就是通过DS:[0111104]的值,该值就是加载DLL的函数地址)
*因为系统的不同(dos、xp、9x、vista、7等) kernel32.dll的版本各不相同,对应的函数地址都不同,因此为了兼容,将个版本确切的函数地址存在某个特定地址,如编译器把CreatFileW()函数的实际地址存在了01001104,并通过call dword ptr DS:[01001104]来调用,编译器并不存在call7C8107F0(某个版本CreatFileW()函数的实际地址)
还有原因是重定向,DLL加载到内存中,位置可能因前一个dll占用了而重定向,实际中无法保证dll被加载到指定的ImageBase(但exe却能准确加载到自身ImageBase),还有一个原因是,PE头中表示地址不实用VA而是RVA(相对虚拟地址)
------------------------------------------------------------------------------------------------------------------------------------------
IMAGE_IMPORT_DESCRIPTOR [SIZE:20 bytes]
(记录这PE要导入哪些库文件,向库提供服务/函数)
执行一个普通程序时往往需要导入多个库,导入多少库就存在多少个 IMAGE_IMPORT_DESCRIPTOR结构体
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
*其中OriginalFirstThunk和FirstThunk非常类似,指向两个本质上相同的数组IMAGE_THUNK_DATA。
INT IMAGE_IMPORT_BY_NAME
IAT输入顺序 :
1.读取IID的Name成员,获取库名称字符串(“kernel32.dll”)
2.装载相应库。→LoadLibrary(“kernel32.dll”)
3.读取IID的OriginalFirstThunk成员,获取INT地址
4.逐一读取INT中数组的值,获取相应 IMAGE_IMPORT_BY_NAME地址(RVA)
5.使用 IMAGE_IMPORT_BY_NAME的Hint(ordinal)或者Name项,获取相应函数的起始地址
→GetProcAddress(GetCurrentThreadld)
6.读取IID的FirstThunk(IAT)成员,获取IAT地址
7.重复4~7直到INT结束(遇到NULL时)
定位查找IMAGE_IMPORT_DESCRIPTO结构
在节区头rdata看到pointertorawdata为4000
阴影部分即IID ,IID的大小为20字节
看第一个OriginalFirstThunk 70 44 00 00
即4470RVA
看最后一个FirstThunk:4000(RVA)转为RAW即4000
获取dll调用的所有函数(就是用上面的INT和IAT来看函数)
IMAGE_IMPORT_DESCRIPTOR中的第一个参数和最后一个参数,original_first_thunk 和first_thunk分别指向了INT(输入名称表)IAT(输入地址表)这两个表里面分别记录了指向调用函数名的地址,和此函数在dll中的序号(序号用来快速索引dll中的函数)
需要注意的地方
INT 和IAT数组在一开始的时候,里面存放的地址都是一样的,他们都是指向所调用函数的名字的字符串。而在加载到内存的时候,IAT的值会发生变换,它的值存放的是dll中函数实际被调用的地址,在加载到内存后,就只需要保存IAT就可以了,利用它来调用函数
四。EAT(IMAGE_EXPORT_DIRECTORY)
1.可以在IMAGE_OPTIONAL_HEADER找到EXPORT TABLE的RVA
结构
如何获得函数地址?
GetProcAddress()操作原理
1.利用“AdressOfName”成员转到“函数名称数组” [4个字节组成的数组]
2. “函数名称数组” 存储着字符串地址。通过比较(stcmp)字符串,查找指定的函数名称(此时数组的索引称为name_index)
[索引到是第0、1、2、3。。。。个]
3. 利用AddressOfNameOrdinals成员,转到Ordinal数组
5.利用AddressOfFunctions成员转到“函数地址数组(EAT)” [4个字节组成的数组]
6.在函数地址数组中将刚刚求的的ordinal用作数组索引,获得指定函数的起始地址。
理解:如 通过AdressOfName找到第2个是目标数组,到AddressOfNameOrdinals数组找2个数组,记住该值,在AddressOfFunctions数组用该值找 函数的地址
对于没有函数名称的导出函数,可以通过Ordinals查找它们的地址。从Ordinals值中减去IMAGE_EXPORT_DIRECTORY的base值得到一个数,使用该数作为“函数地址数组”的索引,即可查到相应的函数地址。
最后:
1)EAT提到的ordinal究竟是什么
把ordinal想成目标导出函数的编号就好了,有时有些函数不会对外公开函数名,仅用编号(ordinal) ,导入并使用这类函数的时候,先用ordinal查找相应的函数地址后在调用比如下面示例(1)通过函数名称来获取函数地址,示例(2)则使用函数的ordinal来获取函数地址
示例(1) pFunc=GetProcAddress( "TestFunc" )
示例(2)pFunc=GetProcAddress(5);