逆核系列No.16--PE32+

PE格式在64位系统下的格式为PE32+,相比之下做了一些变动,主要是为了向下兼容PE32格式,以及适应内存空间的变动所做出的的修正。本节内容在于学习PE32+格式。

PE32+是64位windows os 使用的可执行文件格式。

Windows OS 64bit中进程的虚拟内存为16TB,其中低8TB分配给用户模式,高8TB分给内核模式(因此可以根据指令地址判断程序所位于的区域),为了适应改变后的虚拟内存,原PE文件格式(PE32)做了修改,称为PE32+


IMAGE_NT_HEADERS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//32bit
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

//64bit
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_HEADERS64
typedef PIMAGE_NT_HEADERS64
#else
typedef IMAGE_NT_HEADERS32
typedef 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_HEADERS64
typedef PIMAGE_NT_HEADERS64
#else
typedef IMAGE_NT_HEADERS32
typedef PIMAGE_NT_HEADERS32
#endif

上述代码是与处理过程,根据系统的类型,将重定义为合适的IMAGE_NT_HEADERS结构体

IMAGE_FILE_HEADER

IMAGE_FILE_HEADER.Machine

该字段标识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_HEADER64
typedef PIMAGE_OPTIONAL_HEADER64
#else
typedef IMAGE_OPTIONAL_HEADER32
typedef PIMAGE_OPTIONAL_HEADER32
#endif

首先是代码末端根据系统表示重定义optionalheader的类型,分别指向IMAGE_OPTIONAL_HEADER64以及IMAGE_OPTIONAL_HEADER32,相应的结构体指针也发生变化。



IMAGE_OPTIONAL_HEADER

IMAGE_OPTIONAL_HEADER.Magic

该字段值由PE32的0x010B,变为0x020B。Windows PE装载器根据该字段来区分IMAGE_OPTIONAL_HEADER结构体是32位还是64位的。


IMAGE_OPTIONAL_HEADER.BaseOfData

PE32文件中该字段用于指示数据节的起始地址(RVA),而PE32+则删除了该字段


IMAGE_OPTIONAL_HEADER.ImageBase

该字段的数据类型由原来的双字(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中的堆和栈

在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; // PBYTE
DWORD Function; // PDWORD
DWORD Ordinal;
DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;

typedef struct _IMAGE_THUNK_DATA64 {
union {
ULONGLONG ForwarderString; // PBYTE
ULONGLONG Function; // PDWORD
ULONGLONG Ordinal;
ULONGLONG AddressOfData; // PIMAGE_IMPORT_BY_NAME
} 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; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound INT (PIMAGE_THUNK_DATA) 存着INT表地址
} DUMMYUNIONNAME;
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; //导入库的名字
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses) 存着IAT表地址
} 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; // PDWORD
ULONGLONG AddressOfCallBacks; // PIMAGE_TLS_CALLBACK *;
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; // PDWORD
DWORD AddressOfCallBacks; // PIMAGE_TLS_CALLBACK *
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

有DOS_HEADER.elfanew字段索引到IMAGE_NT_HEADERS位置

IMAGE_NT_HEADERS

第一个成员为Signature,不做赘述

IMAGE_NT_HEADERS的第二个成员—-IMAGE_FILE_HEADER:

可以看出Machine字段值为:0x8664

程序的节区数目为0x0006,也就是6个节区

并且SizeOfOptionalHeader为0x00F0,用于指示OptionalHeader的大小,默认32位程序是0xE0,64位程序则是0xF0。

IMAGE_OPTIONAL_HEADER

  • 变化一: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,继续跟进:

发现导入的相应函数名。

Author: Victory+
Link: https://cvjark.github.io/2022/05/11/逆核系列No-16-PE32/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.