1) Magic:32位可执行文件来:0x010B
64位可执行文件来:0x020B 2) 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后面
#define IMAGE_SIZEOF_SHORT_NAME 8 typedef struct _IMAGE_SECTION_HEADER { dword Name[IMAGE_SIZEOF_SHORT_NAME]; union { DWORD PhysicalAddress; DWORD VirtualSize; } Misc; DWORD VirtualAddress; //内存中偏移地址 DWORD SizeOfRawData; //PE文件中对其之后的大小 DWORD PointerToRawData;//为PE块区在PE文件中偏移 DWORD PointerToRelocations; DWORD PointerToLinenumbers; WORD NumberOfRelocations; WORD NumberOfLinenumbers; DWORD Characteristics; //块区的属性:可读、可写.. } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER; #define IMAGE_SIZEOF_SECTION_HEADER 40 重要数据成员 : 1) Name[IMAGE_SIZEOF_SHORT_NAME]:这是一个由8位的ASCII 码名,用来定义区块的名称。(其实并没有限制必须使用ascii,可以放任何值,甚至可以用null填充)
多数区块名都习惯性以一个“.”作为开头(例如:.text),这个“.” 实际上是不是必须的。
值得我们注意的是,如果区块名超过 8 个字节,则没有最后的终止标志“NULL” 字节。 2) Virtual Size:表对应的区块的大小,这是区块的数据在没有进行对齐处理前的实际大小。 3) Virtual Address:该区块装载到内存中的RVA 地址。 4) SizeOfRawData:该区块在磁盘中所占的大小。 5) PointerToRawData:该区块在磁盘中的偏移。 6) Characteristics:该区块的属性。(如代码/数据/可读/可写等)的标志。参考RVA to RWA (内存地址与文件偏移间的映射)[偏移是在硬盘的概念] Windows 装载器在装载DOS部分、PE文件头部分和节表(区块表)部分是不进行任何特殊处理的,而在装载节(区块)的时候则会自动按节(区块)的属性做不同的处理。 步骤: 1)查找RVA所在节区(.text / .rsrc /.data) 2)根据公式计算: RAW-PointToRawData =RVA-VirtualAddress RAW=RVA-VirtualAddress+PointToRawData **(这个VirtualAddress是节区头的VirtualAddress,其实就是距离基地址的RVA)
四。IAT(Import Address Table)在块里面 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 { union { 。。。。 DWORD OriginalFirstThunk; // RVA 指向INT ( IMAGE_IMPORT_BY_NAME ) }; DWORD TimeDateStamp; DWORD ForwarderChain; // -1 if no forwarders DWORD Name; //dll 名称 DWORD FirstThunk; //指向引入函数真实地址单元处的RVA IAT(IMAGE_ADDRESS_TABLE) } IMAGE_IMPORT_DESCRIPTOR; typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_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结构 在可选头看到importRVA 44 34 而4434位于rdata,rdata位于404000(在od里看到) 在节区头rdata看到pointertorawdata为4000 所以 RAW= RVA-VirtualAddress+ PointToRawData 4434-4000+4000=4434 阴影部分即IID ,IID的大小为20字节 看第一个 OriginalFirstThunk 70 44 00 00 即4470RVA 4470位于rdata,rdata位于404000(在od里看到) 在节区头rdata看到pointertorawdata为4000 所以 4470-4000+4000=4470 第一个值为46F0(RVA)进入该地址可以看到导入的API函数字符串 看最后一个FirstThunk:4000(RVA)转为RAW即4000 【*内容都可以在PE view看到】 获取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 结构 IMAGE_EXPORT_DIRECTORY STRUCT Characteristics DWORD ? ; 未使用,总是定义为0 TimeDateStamp DWORD ? ; 文件生成时间 MajorVersion WORD ? ; 未使用,总是定义为0 MinorVersion WORD ? ; 未使用,总是定义为0 Name DWORD ? ; 模块的真实名称 Base DWORD ? ; 基数,加上序数就是函数地址数组的索引值 NumberOfFunctions DWORD ? ; 导出函数的总数 NumberOfNames DWORD ? ; 以名称方式导出的函数的总数 AddressOfFunctions DWORD ? ; 指向输出函数地址的RVA AddressOfNames DWORD ? ; 指向输出函数名字的RVA AddressOfNameOrdinals DWORD ? ; 指向输出函数序号的RVA IMAGE_EXPORT_DIRECTORY ENDS 重要成员 NumberOfFunctions 实际export函数的个数 NumberOfNames export 函数中具名的函数个数 AddressOfFunctions export函数地址数组(数组元素个数=NumberOfFunctions) AddressOfNames 函数名称地址数组(数组元素个数=NumberOfNames) AddressOfNameOrdinals Ordinal地址数组(数组元素个数=NumberOfNames) 如何获得函数地址? GetProcAddress()操作原理 1.利用“AdressOfName”成员转到“函数名称数组” [4个字节组成的数组] 2. “函数名称数组” 存储着字符串地址。通过比较(stcmp)字符串,查找指定的函数名称(此时数组的索引称为name_index) [索引到是第0、1、2、3。。。。个] 3. 利用AddressOfNameOrdinals成员,转到Ordinal数组 4.在Ordinal数组中通过name_index [索引到的0、1、2、3。。。个] 查找相应ordinal值 [2个字节组成的数组][00 00 01 00 02 00 03 00 04 00这样排序的数组] 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=Get ProcAddress(5);