PE格式在64位系统下的格式为PE32+,相比之下做了一些变动,主要是为了向下兼容PE32格式,以及适应内存空间的变动所做出的的修正。本节内容在于学习PE32+格式。
PE32+是64位windows os 使用的可执行文件格式。
Windows OS 64bit中进程的虚拟内存为16TB,其中低8TB分配给用户模式,高8TB分给内核模式(因此可以根据指令地址判断程序所位于的区域),为了适应改变后的虚拟内存,原PE文件格式(PE32)做了修改,称为PE32+
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32; typedef struct _IMAGE_NT_HEADERS64 { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER64 OptionalHeader; } IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64; #ifdef _WIN64 typedef IMAGE_NT_HEADERS64typedef PIMAGE_NT_HEADERS64#else typedef IMAGE_NT_HEADERS32typedef PIMAGE_NT_HEADERS32#endif
PE32+使用IMAGE_NT_HEADERS64结构体,而PE32使用的是IMAGE_NT_HEADERS32结构体,二者的区别在于第三个成员OptionalHeader的结构体类型是IMAGE_OPTIONAL_HEADER32还是IMAGE_OPTIONAL_HEADER64。
1 2 3 4 5 6 7 #ifdef _WIN64 typedef IMAGE_NT_HEADERS64typedef PIMAGE_NT_HEADERS64#else typedef IMAGE_NT_HEADERS32typedef PIMAGE_NT_HEADERS32#endif
上述代码是与处理过程,根据系统的类型,将重定义为合适的IMAGE_NT_HEADERS结构体
该字段标识CPU类型,还有更多的字段值
IMAGE_OPTIONAL_HEADER
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 typedef struct _IMAGE_OPTIONAL_HEADER32 { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; DWORD ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; DWORD SizeOfStackReserve; DWORD SizeOfStackCommit; DWORD SizeOfHeapReserve; DWORD SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32; typedef struct _IMAGE_OPTIONAL_HEADER64 { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; ULONGLONG ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; ULONGLONG SizeOfStackReserve; ULONGLONG SizeOfStackCommit; ULONGLONG SizeOfHeapReserve; ULONGLONG SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64; #define IMAGE_NT_OPTIONAL_HDR32_MAGIC 0x10b #define IMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20b #ifdef _WIN64 typedef IMAGE_OPTIONAL_HEADER64typedef PIMAGE_OPTIONAL_HEADER64 #else typedef IMAGE_OPTIONAL_HEADER32typedef PIMAGE_OPTIONAL_HEADER32#endif
首先是代码末端根据系统表示重定义optionalheader的类型,分别指向IMAGE_OPTIONAL_HEADER64以及IMAGE_OPTIONAL_HEADER32,相应的结构体指针也发生变化。
该字段值由PE32的0x010B,变为0x020B。Windows PE装载器根据该字段来区分IMAGE_OPTIONAL_HEADER结构体是32位还是64位的。
PE32文件中该字段用于指示数据节的起始地址(RVA),而PE32+则删除了该字段
该字段的数据类型由原来的双字(DWORD)变为ULONGLONG类型(8个字节)这是为了适配增大的进程虚拟内存。借助此字段设置的大小,PE32+文件能够加载到64bit进程的虚拟内存空间(16TB)的任何位置(EXE/DLL文件被加载到低位的8TB用户区域,SYS文件被加载到高位的8TB内核区域)
PS:
AddressOfEntryPoint、SizeOfImage等字段的大小与原PE32位是一样的,都是DWORD大小,这些字段的数据类型都是DOWRD,意味着PE32+格式文件的占用实际虚拟内存中,各映像的大小最大为4GB(32位)。但是由于ImageBase的大小为8个字节,程序文件能够载入到进程虚拟内存的任意地址位置。
在IMAGE_OPTIONAL_HEADER中关于堆和栈的相关字段(SizeOfStackReserve、SizeOfStackCommit、SizeOfHeapReserve、SizeOfHeapCommit)的数据类型变为ULONGLONG类型,这也是适配增大的虚拟内存空间作出的修正。
IMAGE_THUNK_DATA IMAGE_THUNK_DATA位于OptionalHeader.DataDirectory[1]中的存放有导入表存放的位置(RVA)以及导入表大小(Size)的结构体IMAGE_DIRECTORY_ENTRY_IMPORT其中VirtualAddress指向的导入表信息结构体IMAGE_IMPORT_DESCRIPTOR,其中OriginalFirstThunk(IAT)存放的导入具体信息。不是很清楚这段的可以看看开头PO出来的结构图
IMAGE_THUNK_DATA结构体大小由原来的4字节变为8个字节
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 typedef struct _IMAGE_THUNK_DATA32 { union { DWORD ForwarderString; DWORD Function; DWORD Ordinal; DWORD AddressOfData; } u1; } IMAGE_THUNK_DATA32; typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;typedef struct _IMAGE_THUNK_DATA64 { union { ULONGLONG ForwarderString; ULONGLONG Function; ULONGLONG Ordinal; ULONGLONG AddressOfData; } u1; } IMAGE_THUNK_DATA64; typedef IMAGE_THUNK_DATA64 * PIMAGE_THUNK_DATA64;#ifdef _WIN64 typedef IMAGE_THUNK_DATA64 IMAGE_THUNK_DATA;typedef PIMAGE_THUNK_DATA64 PIMAGE_THUNK_DATA;#else typedef IMAGE_THUNK_DATA32 IMAGE_THUNK_DATA;typedef PIMAGE_THUNK_DATA32 PIMAGE_THUNK_DATA;#endif
检查标识根据结构重定义结构体。可以看到结构体的字段类型由DOWRD变为ULONGLONG,这也是适配增大后的进程虚拟空间作出的修正。
此外,需要指出的一点是存放导入具体信息的结构体IMAGE_THUNK_DATA是由结构体IMAGE_IMPORT_DESCRIPTOR的OriginalFirstThunk(IAT)或者FirstThunk(INT)以RVA方式指出,结构体IMAGE_IMPORT_DESCRIPTOR在PE32和PE32+中没有差别:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; DWORD OriginalFirstThunk; } DUMMYUNIONNAME; DWORD TimeDateStamp; DWORD ForwarderChain; DWORD Name; DWORD FirstThunk; } IMAGE_IMPORT_DESCRIPTOR; typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
这两个字段都是DWORD类型,跟踪这两个字段会根据系统不同检索到不一样的内容,PE32检索到IMAGE_THUNK_DATA32(大小4个字节),而PE32+会检索到IMAGE_THUNK_DATA64(大小为8个字节),因此跟踪IAT是要注意数组元素的大小。
IMAGE_TLS_DIRECTORY
关于TLS,概念不是很深,查找了相关资料:https://0xnope.top/2021/03/13/TLS回调函数/
TLS (Thread Local Storage 线程本地存储 )回调函数(Callback Function)。
TLS是各线程的独立的数据存储空间,对象的存储在线程开始时分配,线程结束时回收,每个线程有该对象自己的实例。使用TLS技术可在线程 内部独立使用或修改进程 的全局数据 或静态数据 。
TLS 回调函数的==调用运行要先于 EP 代码的执行==。==该特性使它可以作为一种反调试技术使用,许多病毒或壳会利用这一点执行一些特殊的操作。==
创建或者终止某线程时,TLS回调函数会自动执行(前后共两次)。
在PE头中的位置IMAGE_NT_HEADERS -> IMAGE_OPTIONAL_HEADER -> IMAGE_DATA_DIRECTORY[9]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 typedef struct _IMAGE_TLS_DIRECTORY64 { ULONGLONG StartAddressOfRawData; ULONGLONG EndAddressOfRawData; ULONGLONG AddressOfIndex; ULONGLONG AddressOfCallBacks; DWORD SizeOfZeroFill; union { DWORD Characteristics; struct { DWORD Reserved0 : 20 ; DWORD Alignment : 4 ; DWORD Reserved1 : 8 ; } DUMMYSTRUCTNAME; } DUMMYUNIONNAME; } IMAGE_TLS_DIRECTORY64; typedef IMAGE_TLS_DIRECTORY64 * PIMAGE_TLS_DIRECTORY64;typedef struct _IMAGE_TLS_DIRECTORY32 { DWORD StartAddressOfRawData; DWORD EndAddressOfRawData; DWORD AddressOfIndex; DWORD AddressOfCallBacks; DWORD SizeOfZeroFill; union { DWORD Characteristics; struct { DWORD Reserved0 : 20 ; DWORD Alignment : 4 ; DWORD Reserved1 : 8 ; } DUMMYSTRUCTNAME; } DUMMYUNIONNAME; } IMAGE_TLS_DIRECTORY32; typedef IMAGE_TLS_DIRECTORY32 * PIMAGE_TLS_DIRECTORY32;#ifdef _WIN64 typedef IMAGE_TLS_DIRECTORY64 IMAGE_TLS_DIRECTORY;typedef PIMAGE_TLS_DIRECTORY64 PIMAGE_TLS_DIRECTORY;#else typedef IMAGE_TLS_DIRECTORY32 IMAGE_TLS_DIRECTORY;typedef PIMAGE_TLS_DIRECTORY32 PIMAGE_TLS_DIRECTORY;#endif
其中的字段StartAddressOfRawData、EndAddressOfRawData、AddressOfIndex、AddressOfCallBacks四个字段的大小由PE32中的DWORD(占4字节大小)变为PE32+中的ULONGLONG类型(占8字节),这几个字节存储的是RVA值,其中比较关键的是AddressOfCallBacks,指向TLS回调函数的地址数组(程序启动之前,系统会调用数组当中的函数,数组以NULL结束),这说明一个程序可以有多个TLS回调函数。
至此,PE32+修正部分结束,根据实际例子进行查找比对,这里以windbg.exe(x64)为例
示例演示 有DOS_HEADER.elfanew字段索引到IMAGE_NT_HEADERS位置
第一个成员为Signature,不做赘述
IMAGE_NT_HEADERS的第二个成员—-IMAGE_FILE_HEADER:
可以看出Machine字段值为:0x8664
程序的节区数目为0x0006,也就是6个节区
并且SizeOfOptionalHeader为0x00F0,用于指示OptionalHeader的大小,默认32位程序是0xE0,64位程序则是0xF0。
变化一:Magic字段值 = 0x020B,标识是64位下的PE文件
变化二:在PE32+中删除了BaseOfData字段
变化三:ImageBase的类型由DWORD(4字节)变为ULONGLONG(8字节),例子中的值 = 0x0000000140000000
重要的字段
AddressOfEntryPoint = 0x0004C7F0(RVA)
SectionAlignment = 0x00001000,内存中节区对齐的大小
FileAlignment = 0x00001000,文件中节区对齐的大小
SizeofImage = 0x000A5000,内存中整个PE文件的映射尺寸,可以比实际的大,但必须是SectionAlignment的整数倍,对齐的效果
IAT相关 DataDirectory[1] -> IMAGE_DIRECTORY_ENTRY_IMPORT.VirtualAddress = 0x00065F34, IMAGE_DIRECTORY_ENTRY_IMPORT.Size = 0x00000154.
RVA = 0x00065F34位于节区.rdata
找到该节区,利用字段进行RVA -> RAW的转换
RAW = 0x00065F34 - 0x00050000 + 0x0005000 = 0x00065F34 (RAW)
第一个IMAGE_IMPORT_DESCRIPTOR结构体:
OriginalFirstThunk = 0x00066088 (RVA) -> RAW = 0x00066088
name = 0x00066BF0(RVA)-> RAW = 0x00066BF0
第一个导入的dll名称:
第一个DLL导入的函数地址
PE32+中IMAGE_THUNK_DATA的大小由4字节变为8字节,因此该DLL导入的第一项函数的位置是:0x0000000000066B68
换成RAW,继续跟进:
发现导入的相应函数名。