PE Format Note
Baes PE文件不是作为单一内存映射文件被载入内存的,Windows加载器(PE加载器)遍历 PE 文件决定文件的那一部分被映射
PE文件被加载到内存后,内存中的版本被称为模块
映射文件的起始地址称为模块句柄 ,可以过模块句柄访问内存中的其他数据结构 这个初始内存地址也称为 基地址(ImageBase)
在 32位 Windows系统中可以直接调用 GetModuleHandle
以取得指向 DLL 的指针,通过指针访问该 DLL Module 的内容
HMOULDE GetModuleHandle (LPCTSTR lpModuleName) ;
调用该函数时会传递一个可执行文件或 DLL 文件名字符串,如果系统找到文件,则返回该可执行文件或 DLL 文件映像所加载的基地址,也可以调用 GetModuleHandle来传递 NULL参数,此时将返回调用者的可执行文件的基地址
基地址的值是由 PE文件本身设定的,默认 EXE文件的基地址是 400000h
、DLL 文件的基地址是 10000000h
PE文件被映射到内存时,每个程序都有自己的虚拟空间,这个虚拟空间的内存地址称为虚拟地址(VA)
为了避免在 PE文件中出现绝对内存地址引入了 相对虚拟地址(RVA) 的概念
RVA 只是内存中的一个简单的、相对于 PE文件载入地址(基地址)的偏移位置,它是一个相对的地址(或称偏移量)
假设一个 EXE文件从 400000h处载入,而它的代码区块 开始于 401000h处,代码区块的 RVA 计算如下:
目标地址 401000 h - 载入地址 400000 h = RVA 1000 h
将一个 RVA 转换成真实的地址,用实际的载入地址加 RVA,得到实际的内存地址:
虚拟地址VA = 基地址 + 相对虚拟地址400000 h + 1000 h = 401000 h
当 PE文件存储在磁盘中时,某个数据的位置与文件头的偏移量称为 文件偏移地址(File Offset)**或 **物理地址(Raw Offset) ; 文件偏移地址从 PE文件的第一个字符开始计数,起始值为 0; 用十六进制工具打开文件时,所显示的地址值就是文件偏移地址
Dos 头 40 bytes
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 typedef struct _IMAGE_DOS_HEADER { WORD e_magic; WORD e_cblp; WORD e_cp; WORD e_crlc; WORD e_cparhdr; WORD e_minalloc; WORD e_maxalloc; WORD e_ss; WORD e_sp; WORD e_csum; WORD e_ip; WORD e_cs; WORD e_lfarlc; WORD e_ovno; WORD e_res[4 ]; WORD e_oemid; WORD e_oeminfo; WORD e_res2[10 ]; DWORD e_lfanew; } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
xxe -l 0x40 calc.dll
xxe -l 64 calc.dll
00000000: 4d5a 9000 0300 0000 0400 0000 ffff 0000 MZ.............. 00000010: b800 0000 0000 0000 4000 0000 0000 0000 ........@....... 00000020: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000030: 0000 0000 0000 0000 0000 0000 d000 0000 ................
0x20 填充为 0,偏移从 0x30 开始
00000020: 0000 0000 0000 0000 0000 0000 0000 0000 ................
PE文件头偏移量
xxd -s 0x3c -l 4 calc.dll
e_lfanew
值为: 000000d0h
NT 头/ PE 文件头 使用 DOS头的 e_lfanew
字段得到 PE文件头的起始偏移量,用其加上基址,得到 PE 文件头的指针
typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
NT 文件头 / PE 文件头 包含 PE 文件的一些基本信息,偏移量基于 PE文件头(IMAGE_NT_HEADERS
)
typedef struct _IMAGE_FILE_HEADER { WORD Machine; WORD NumberOfSections; DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORD SizeOfOptionalHeader; WORD Characteristics; } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
000000d0
PE文件头起始偏移量
xxd -s 0xd0 -l 128 calc.dll
Out:
000000d0 : 5045 0000 4 c01 0400 a00 b 0 d53 0000 0000 PE..L......S....000000e0 : 0000 0000 e000 0221 0 b01 0 c00 0002 0000 .......!........000000f0 : 000 e 0000 0000 0000 3011 0000 0010 0000 ........0 .......00000100 : 0020 0000 0000 0010 0010 0000 0002 0000 . ..............00000110 : 0600 0000 0000 0000 0600 0000 0000 0000 ................00000120 : 0050 0000 0004 0000 0000 0000 0200 4005 .P............@.00000130 : 0000 1000 0010 0000 0000 1000 0010 0000 ................00000140 : 0000 0000 1000 0000 0000 0000 0000 0000 ................
WORD(2) × 9 = 18 BYTE(1) × 2 = 2 DWORD(4) × 19 = 76= 96
可选头 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 typedef struct _IMAGE_OPTIONAL_HEADER { 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;
IMAGE_DATA_DIRECTORY
结构:
typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size; } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
Directory Entries
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #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 #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
IMAGE_OPTIONAL_HEADER
比较重要的几个字段:
AddressOfEntryPoint: 持有 EP的 RVA值,指出程序最先执行的代码起始地址
ImageBase: 指出文件的优先装入地址(32位进程虚拟内存范围为:0~7FFFFFFF)
SectionAlignment: 内存中区块的对齐值
FileAlignment: 磁盘区块的对齐值
SizeOfImage: 指定了PE Image在虚拟内存中所占空间的大小
SizeOfHeaders: 指出整个PE头的大小
Subsystem: 区分系统驱动文件和普通可执行文件
NumberOfRvaAndSizes: 指定 DataDirectory数组的个数
DataDirectory: 由 IMAGE_DATA_DIRECTORY
结构体组成的数组
节区头 / 区块表 typedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; union { DWORD PhysicalAddress; DWORD VirtualSize; } Misc; DWORD VirtualAddress; DWORD SizeOfRawData; DWORD PointerToRawData; DWORD PointerToRelocations; DWORD PointerToLinenumbers; WORD NumberOfRelocations; WORD NumberOfLinenumbers; DWORD Characteristics; } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
块名:.text
、.rdata
、.data
、.reloc
, .
不是必须的
000001c0 : 0000 0000 0000 0000 2 e74 6578 7400 0000 .........text...000001d0 : 5201 0000 0010 0000 0002 0000 0004 0000 R...............000001e0 : 0000 0000 0000 0000 0000 0000 2000 0060 ............ ..`000001f0 : 2 e72 6461 7461 0000 0 c01 0000 0020 0000 .rdata....... ..00000200 : 0002 0000 0006 0000 0000 0000 0000 0000 ................00000210 : 0000 0000 4000 0040 2 e64 6174 6100 0000 ....@..@.data...00000220 : 0 d08 0000 0030 0000 000 a 0000 0008 0000 .....0 ..........00000230 : 0000 0000 0000 0000 0000 0000 4000 00 c0 ............@...00000240 : 2 e72 656 c 6 f63 0000 2000 0000 0040 0000 .reloc.. ....@..00000250 : 0002 0000 0012 0000 0000 0000 0000 0000 ................00000260 : 0000 0000 4000 0042 0000 0000 0000 0000 ....@..B........00000270 : 0000 0000 0000 0000 0000 0000 0000 0000 ................
常见区块:
.text : 默认代码区块
.data : 默认读/写数据区块,全局变量、静态变量一般放在这里
.rdata : 默认只读数据区块,程序很少用到该块中的数据
.idata : 包含其他外来 DLL 的函数及数据信息,及输入表,经常合并到另一个区块,如: .rdata 区块
.edata : 输出表
.bss : 未初始化数据,很少使用
.tls : 线程局部存储器
.reloc : 可执行文件的基址重定位,一般只是 DLL 需要
创建和重命名自己的区块
#pragma data_seg("MY_DATA" )
此时,所以被 Visual C++处理的数据都将放在一个叫 MY_DATA
的区块内,而不是默认的 .data
区块
objdump -M intel -h calc.dll
Sections :Idx Name Size VMA LMA File off Algn 0 .text 00000152 10001000 10001000 00000400 2 **2 CONTENTS , ALLOC, LOAD, READONLY, CODE 1 .rdata 0000010 c 10002000 10002000 00000600 2 **2 CONTENTS , ALLOC, LOAD, READONLY, DATA 2 .data 0000080 d 10003000 10003000 00000800 2 **2 CONTENTS , ALLOC, LOAD, DATA 3 .reloc 00000020 10004000 10004000 00001200 2 **2 CONTENTS , ALLOC, LOAD, READONLY, DATA
NOTE: 为加载到内存中的 PE 文件为 PE 文件,加载到内存中的形态被称为 映像(image)
区块对齐值 有两种对其值,一种用于磁盘文件内,另一种用于内存中
在 PE文件头里,FileAlignment
定义了磁盘区块的对齐值。每个区块从对齐值的倍数 的偏移位置开始,区块的实际代码或数据不一定刚好这么多,所以在不足的地方一般以 00h
来填充,这就是区块的 间隙
例如,一个 PE文件的对其值是 200h,这样每个区块从 200h的倍数的文件偏移位置开始。
假如区块的第一个节在 400h 处,长度为 90h,那么 400h~490h为这一区块的内容,而文件对其值为 200h,那么 490~600h会被 0 填充,这段空间称为区块间隙,下一个区块的开始地址为 600h
objdump -M intel -x calc.dll
SectionAlignment 00001000 FileAlignment 00000200
在 PE文件头里,SectionAlignment
定义了内存中区块的对齐值,当 PE文件被映射到内存中时,区块总是至少从一个页边界处开始。也就是说,当一个 PE文件被映射到内存中时,每个区块的第 1 个字节对应于某个内存页
在 x86 系列CPU中,内存页是按 4KB(1000h)排列的 在 x64 中,内存页是按照 8KB(2000h)排列的
所以,在 x86系统中,PE文件区块的内存对齐值一般为 1000h,每个区块从 1000h的倍数的内存偏移开始
例如,.text区块在磁盘文件中的偏移为 400h,在内存中将其载入 1000h字节处;.rdata 区块在磁盘文件偏移的 600h处,在内存中将被载入地址之上的 2000h字节处
文件偏移与虚拟地址转换 在同一区块中,各地址偏移量是相等的,可用下面的公式对区块中的任意 File Offset 与 VA 进行转换。注意,不同区块在磁盘与内存中的差值不同
文件被映射到内存时,MS-DOS头、PE文件头和块表的偏移位置与大小均不会变,而各地区块被映射到内存后,其偏移位置会发生变化
Offset = RVA - ∆k FIle Offset = VA - ImageBase - ∆k FIle Offset = VA - ImageBase - (RVA - Section.Raw Offset )
区块
虚拟偏移量(RVA)
文件偏移量(Raw Offset)
差值(∆k)
.text
1000h
400h
0C00h
.rdata
2000h
600h
1A00h
.data
3000h
800h
2800h
∆k = 虚拟偏移量 - 文件偏移量
∆k = RVA - Section .Raw Offset
例如,某一虚拟地址(VA) 为 401112h(基地址: 400000h), 要计算它的文件偏移地址。
401112h 在 .text块中,此时 ∆k = 0C00h,故
File Offset = VA - ImageBase - ∆k = 401112h - 40000h - C00h = 521h
若虚拟内存地址为 4020D2h
File Offset = VA -ImageBase - ∆k = 4020D2h - 40000h - 1A00h = 6D2h
输入表/IAT 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 There is an import table in .rdata at 0 x10002024 The Import Tables (interpreted .rdata section contents) vma : Hint Time Forward DLL First Table Stamp Chain Name Thunk 00002024 0000204 c 00000000 00000000 000020 fe 00002000 DLL Name: KERNEL32 .dll vma : Hint/Ord Member-Name Bound-To 2070 127 CloseHandle 207e 338 ExitThread 208c 1191 ResumeThread 209c 215 CreateProcessA 20ae 740 GetThreadContext 20c2 1322 SetThreadContext 20d6 1434 VirtualAllocEx 20e8 1512 WriteProcessMemory 00002038 00000000 00000000 00000000 00000000 00000000
可执行文件使用来自其他 DLL的代码或数据的动作称为输入
输入函数:被程序调用但其执行代码不再程序中的函数
在 PE文件内有一组数据结构,他们分别对应于被输入的 DLL。每一个这样的结构都给出了被输入的 DLL 的名称并指向一组函数指针。这组函数指针被称为 输入地址表 。每一个被引入的 API 在 IAT里有都保留的位置,在哪里它将被 Windows加载器写入输入函数的地址。一旦模块被载入,IAT中将包含所要调用输入函数的地址
导出函数修饰符
__declspec(dllimport) void func (void ) ;
输入表的结构 PE文件头的可选映像头,数据目录表的第二个成员指向输入表,以一个 IMAGE_IMPORT_DESCRIPTOR
(IID)数组开始。每个被 PE文件隐式链接的 DLL都有一个 IID
IID用来描述被引入的 DLL文件
typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; DWORD OriginalFirstThunk; }; DWORD TimeDateStamp; DWORD ForwarderChain; DWORD Name; DWORD FirstThunk; } IMAGE_IMPORT_DESCRIPTOR;
OriginalFirstThunk: 包含指向 **输入名称表(INT)**的地址(RVA),以 0/NULL 结束
Name: 库名称字符串的地址(RVA),该字符串包含输入 DLL 的名称(例如:USER32.DLL)
FirstThunk: 包含指向 **输入地址表(IAT)**的地址(RVA),是一个 IMAGE_THUNK_DATA
结构的数组,以 0/NULL 结束
INT是一个指向 IMAGE_THUNK_DATA
结构的数组,数组中的每个 IMAGE_THUNK_DATA
结构都指向 IMAGE_IMPORT_BY_NAME
结构
INT IMAGE_IMPORT_BY_NAME IAT -> IMAGE_THUNK_DATA -> 01 [函数1 ] <- IMAGE_THUNK_DATA <-| |-> OriginalFirstThunk -> IMAGE_THUNK_DATA -> n[函数n] <- IMAGE_THUNK_DATA <-| | -> 0 (结束) |IMAGE_IMPORT_DESCRIPTOR |-> TimeDateStamp | |-> ForwarderChain | |-> Name -> "USER32.DLL" | |-> FirstThunk
IMAGE_THUNK_DATA
结构:
IMAGE_THUNK_DATA STRUCT union u1 ForwarderString DWORD // 指向一个转向者字符串的RVA Function DWORD // 被输入的函数的内存地址 Ordinal DWORD // 被输入的 API 的序数值 AddressOdData DWORD // 指向 IMAGE_IMPORT_BY_NAME ends IMAGE_THUNK_DATA ENDS
IMAGE_IMPORT_BY_NAME
结构:
IMAGE_IMPORT_BY_NAME STRUCT Hint WORD Name BYTE IMAGE_IMPORT_BY_NAME ENDS
输入地址表 Q: 为什么会有两个并行的指针数组指向 IMAGE_IMPORT_BY_NAME
结构?
A: 因为 OriginalFirstThunk
是单独的一项,不可改写,称为 INT;FirstThunk
是有 PE装载器 重写的。PE装载器先搜索 OriginalFirstThunk ,如果找到,加载器就迭代搜索数组中的每个指针,找出每个 IMAGE_IMPORT_BY_NAME
结构所指向的输入函数的地址(Function)。然后,加载器用函数真正的入口地址来替代由 FirstThunk 指向的 IMAGE_THUNK_DATA
数组里元素的值
INT IMAGE_IMPORT_BY_NAME IAT -> IMAGE_THUNK_DATA -> 01 [函数1 ] <- [函数 1 的地址] <-| |-> OriginalFirstThunk -> IMAGE_THUNK_DATA -> n[函数n] <- [函数 n 的地址] <-| | -> 0 (结束) |IMAGE_IMPORT_DESCRIPTOR |-> TimeDateStamp | |-> ForwarderChain | |-> Name -> "USER32.DLL" | |-> FirstThunk
在某些情况下,一些函数仅由序号引出
确定数据目录表 (DataDirectory) 的第二个成员的位置(指向输入表)
该指针在 PE文件头的 80h 偏移处,需要找到 PE文件头的起始位置
PE文件头的起始位置在 DOS 头的 e_lfanew
处得到
例:
SectionAlignment 00001000 FileAlignment 00000200 Sections :Idx Name Size VMA LMA File off Algn 0 .text 00000152 10001000 10001000 00000400 2 **2 CONTENTS , ALLOC, LOAD, READONLY, CODE 1 .rdata 0000010 c 10002000 10002000 00000600 2 **2 CONTENTS , ALLOC, LOAD, READONLY, DATA 2 .data 0000080 d 10003000 10003000 00000800 2 **2 CONTENTS , ALLOC, LOAD, DATA 3 .reloc 00000020 10004000 10004000 00001200 2 **2 CONTENTS , ALLOC, LOAD, READONLY, DATA
PE头起始位置: e_lfanew: 000000d0
数据目录表的第二个成员位置(0x01): D0h + 80h = 150h
输入表的 RVA: 00000150: 2420 0000 : 0x2024 (RVA)
磁盘文件偏移: 2024h 位于 .rdata ∆k = 2000h - 600h = 1A00h
File Offset = RVA - ∆k = 2024h - 1A00h = 624h
输入表(IID)位置:624h
xxd -s 0x624 -l 0x20 calc.dll
00000624 : 4 c20 0000 0000 0000 0000 0000 fe20 0000 L ........... ..00000634 : 0020 0000 0000 0000 0000 0000 0000 0000 . ..............
objdump -x calc.dll
vma : Hint Time Forward DLL First Table Stamp Chain Name Thunk00002024 0000204 c 00000000 00000000 000020 fe 00002000
第四个字段(DLL Name) 是 20FEh(RVA),将它减去 1A00h = 6FE 查看此处数据:
xxd -s 0x6fe -l 0x10 calc.dll
000006fe : 4 b45 524 e 454 c 3332 2 e64 6 c6 c 0000 0000 KERNEL32 .dll....
查看 IMAGE_THUNK_DATA
: OriginalFirstThunk (204C RVA) 处的数据 204Ch - 1A00h = 64C
xxd -s 0x64C -l 0x20 calc.dll
0000064c : 7020 0000 7 e20 0000 8 c20 0000 9 c20 0000 p ..~ ... ... ..0000065c : ae20 0000 c220 0000 d620 0000 e820 0000 . ... ... ... ..
一共有 8 个 IMAGE_THUNK_DATA 结构,也就是 8 个导入函数 第一个函数: 2070h - 1A00h = 670h
00000670 : 7 f00 436 c 6 f73 6548 616 e 646 c 6500 5201 ..CloseHandle.R.
第二个函数: 207Eh - 1A00 = 67Eh
0000067e : 5201 4578 6974 5468 7265 6164 0000 a704 R.ExitThread....
第三个函数: 208Ch - 1A00h = 68Ch
0000068c : a704 5265 7375 6 d65 5468 7265 6164 0000 ..ResumeThread..
前面两字节的空缺是函数名引用(Hint)
查看导入函数: = (IMAGE_THUNK_DATA)DWORD * 4 + (IMAGE_IMPORT_BY_NAME[Hint])4 = 20
到达 IMAGE_IMPORT_BY_NAME 的 Name 字段 64Ch + 20h(0ffset) = 66Ch
查看数据(导入函数名): xxd -s 0x66C -l 0x90 calc.dll
0000066c : 0000 0000 7 f00 436 c 6 f73 6548 616 e 646 c ......CloseHandl0000067c : 6500 5201 4578 6974 5468 7265 6164 0000 e.R.ExitThread..0000068c : a704 5265 7375 6 d65 5468 7265 6164 0000 ..ResumeThread..0000069c : d700 4372 6561 7465 5072 6 f63 6573 7341 ..CreateProcessA000006ac : 0000 e402 4765 7454 6872 6561 6443 6 f6 e ....GetThreadCon000006bc : 7465 7874 0000 2 a05 5365 7454 6872 6561 text..*.SetThrea000006cc : 6443 6 f6 e 7465 7874 0000 9 a05 5669 7274 dContext....Virt000006dc : 7561 6 c41 6 c6 c 6 f63 4578 0000 e805 5772 ualAllocEx....Wr000006ec : 6974 6550 726 f 6365 7373 4 d65 6 d6 f 7279 iteProcessMemory
然后再看看 FirstThunk(2000h),减去 1A00h = 600h 同样指向 IMAGE_THUNK_DATA
结构
00000600 : 7020 0000 7 e20 0000 8 c20 0000 9 c20 0000 p ..~ ... ... ..00000610 : ae20 0000 c220 0000 d620 0000 e820 0000 . ... ... ... ..00000620 : 0000 0000 4 c20 0000 0000 0000 0000 0000 ....L ..........00000630 : fe20 0000 0020 0000 0000 0000 0000 0000 . ... ..........00000640 : 0000 0000 0000 0000 0000 0000 7020 0000 ............p ..00000650 : 7 e20 0000 8 c20 0000 9 c20 0000 ae20 0000 ~ ... ... ... ..00000660 : c220 0000 d620 0000 e820 0000 0000 0000 . ... ... ......00000670 : 7 f00 436 c 6 f73 6548 616 e 646 c 6500 5201 ..CloseHandle.R.00000680 : 4578 6974 5468 7265 6164 0000 a704 5265 ExitThread....Re
FirstThunk 在程序运行时被初始化。
FirstThunk 字段值指向的地址与 INT重复,系统在程序初始化的时候根据 OriginalFirstThunk找到函数名,调用 GetProcAddress
函数(或功能类似的函数)并根据函数名取得函数的入口地址,然后用函数入口地址取代 FirstThunk 指向地址中对应的值(IAT)
call 00401164 00401164 jmp dword ptr [00402010] ; 这个 IAT 地址内的数据指向导入函数真实地址
输出表 输出表时数据目录表(DataDirectory)的第一个成员(0x00),指向 IMAGE_EXPORT_DIRECTORY
(IED)结构 DLL 文件提供输出表向系统提供输出函数名、序号和入口地址等信息
typedef struct _IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; DWORD TimeDateStamp; WORD MajorVersion; WORD MinorVersion; DWORD Name; DWORD Base; DWORD NumberOfFunctions; DWORD NumberOfNames; DWORD AddressOfFunctions; DWORD AddressOfNames; DWORD AddressOfNameOrdinals; } IMAGE_EXPORT_DIRECTORY,*PIMAGE_EXPORT_DIRECTORY;
AddressOfFunctions : 指向保存所有输出函数的地址数组
NumberOfFunctions : 保存数组元素数目
AddressOfNames : 指向保存通过名字引出的函数的地址数组 RVA
NumberOfNames : 名字数目
此时有两个模块,分别是地址数组和名字数组,两个数组以 AddressOfNameOrdinals 作为连接枢纽;使用指向地址数组的索引最为链接,PE装载器在名字数组中找到匹配名字的同时,也获取了指向地址指标中对应元素的索引
|-> ... AddressOfFunctions |-> NumberOfFunctions=3 0x40042 0x400197 |-> NumberOfNames=2 |-> [Func1][0x40085 ][Func3] (其中 2 个以名称输出) IMAGE_EXPORT_DIRECTORY |-> AddressOfFunctions --------| | |-> AddressOfNames ------------|-> RVA of Name 1 <---> [Index of Name 1 ] <-| | |-> RVA of Name 3 <---> [Index of Name 2 ] <-| |-> AddressOfNameOrdinals ---------------------------------------------------| AddressOfNames AddressOfNameOrdinals
例子:
PE 头起始:f8h 输出表:f8h + 78h(offset) = 170h
00000030 : 0000 0000 0000 0000 0000 0000 f800 0000 ................
输出表: 18E50h
计算文件偏移
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Sections :Idx Name Size VMA LMA File off Algn 0 .textbss 00010000 10001000 10001000 00000000 2 **2 ALLOC , LOAD, CODE 1 .text 000051 a2 10011000 10011000 00000400 2 **2 CONTENTS , ALLOC, LOAD, READONLY, CODE 2 .rdata 00001 f9 c 10017000 10017000 00005600 2 **2 CONTENTS , ALLOC, LOAD, READONLY, DATA 3 .data 00000200 10019000 10019000 00007600 2 **2 CONTENTS , ALLOC, LOAD, DATA 4 .idata 00000973 1001 a000 1001 a000 00007800 2 **2 CONTENTS , ALLOC, LOAD, READONLY, DATA 5 .msvcjmc 0000010 f 1001 b000 1001 b000 00008200 2 **2 CONTENTS , ALLOC, LOAD, DATA 6 .00 cfg 00000109 1001 c000 1001 c000 00008400 2 **2 CONTENTS , ALLOC, LOAD, READONLY, DATA 7 .rsrc 00000326 1001 d000 1001 d000 00008600 2 **2 CONTENTS , ALLOC, LOAD, READONLY, DATA 8 .reloc 0000057 c 1001 e000 1001 e000 00008 a00 2 **2 CONTENTS , ALLOC, LOAD, READONLY, DATA
18E50h 位于 .rdata 段
∆k = 17000h - 5600h = 11A00h
File Offset = RVA - ∆k = 18E50h - 11A00 = 7450h
文件偏移到 2450h 就是输出表内容:
00007450 : 0000 0000 ffff ffff 0000 0000 828 e 0100 ................00007460 : 0100 0000 0100 0000 0100 0000 788 e 0100 ............x...00007470 : 7 c8 e 0100 808 e 0100 0 e11 0100 8 c8 e 0100 |...............00007480 : 0000 6 d79 646 c 6 c2 e 646 c 6 c00 4 d73 6700 ..mydll.dll.Msg.00007490 : 0000 0000 0000 0000 0000 0000 0000 0000 ................
这个 DLL 只有一个函数 Msg
DLL Name : 18E82h - 11A00h = 7482h
AddressOfFunctions : 18E78h - 11A00h = 7478h
AddressOfNames : 18E7Ch - 11A00h = 747Ch
AddressOfNameOrdinals : 18E80h - 11A00h = 7480h
00007482 : 6 d79 646 c 6 c2 e 646 c 6 c00 4 d73 6700 0000 mydll.dll.Msg...00007478 : 0 e11 0100 8 c8 e 0100 0000 6 d79 646 c 6 c2 e ..........mydll.00007488 : 646 c 6 c00 4 d73 6700 0000 0000 0000 0000 dll.Msg.........0000747c : 8 c8 e 0100 0000 6 d79 646 c 6 c2 e 646 c 6 c00 ......mydll.dll.0000748c : 4 d73 6700 0000 0000 0000 0000 0000 0000 Msg.............00007480 : 0000 6 d79 646 c 6 c2 e 646 c 6 c00 4 d73 6700 ..mydll.dll.Msg.
PE 装载器调用 GetProcAddress
来查找 mydll.dll 里的 API 函数 Msg ,系统通过定位 mydll.dll 的 IMAGE_EXPORT_DIRECTORY
结构开始工作
PE 装载器获得输出函数名表(ENT)的起始地址,知道这个数组只有一个条目,然后对名字进行二进制查找,直到发现字符串 “Msg” 为止。PE 装载器发现 Msg是数组的第一个条目后,加载器从输出序数表中读取相应的第一个值,这个值是 Msg的输出序数。使用输出序数作为进入 ETA 的索引(也要考虑 Base域值),得到 Msg的 RVA,用这个 RVA地址加 mydll.dll 的载入地址,得到 Msg的实际地址
基址重定位 在 PE 文件中,重定位表往往单独作为一块,用 .reloc
表示
将文件中的所有可能需要修改的地址放在一个数组里,如果 PE 文件不在首选的地址(基地址)载入,那么文件中的每一个定位都需要被修正
EXE文件总是使用独立的虚拟地址空间,所以 EXE 文件不需要重定位信息
基址重定位表结构 基址重定位表通过数据目录表(0x05)的 IMAGE_DIRECTORY_ENTRY_BASERELOC
条目查找;基址重定位数据采用类似按页分割的方法组织,是由许多重定位块串接成的,每个块中存放 4KB(1000h) 的重定位信息,每个重定位数据块的大小必须以 **DWORD(4字节)**对齐
typedef struct _IMAGE_BASE_RELOCATION { DWORD VirtualAddress; DWORD SizeOfBlock; } IMAGE_BASE_RELOCATION,*PIMAGE_BASE_RELOCATION;
VirtualAddress : 这组重定位数据的开始 RVA地址。各重定向的地址加上这个值才是该重定位项的完整 RVA 地址
SizeOfBlock : 当前重定位结构的大小。因为 VirtualAddress 和 SizeOfBlock 的大小都是固定的 4 字节,所以这个值减去 8 就是 TypeOffset 数组的大小
TypeOffset : 一个数组,数据每项大小为 2字节,共 16位。这 16位分为高 4 位和低 12位。高 4 位代表重定向类型;低 12位是重定向地址,它与 VirtualAddress 相加就是指向 PE映像中需要修改的地址数据的指针
重定位类型:
0 : IMAGE_REL_BSAED_ABSOLUTE
: 没有具体含义,只是为了让每个段 4字节对齐
3 : IMAGE_REL_BSAED_HIGHLOW
: 重定位指向整个地址都需要修正,实际上大部分情况下都是这样的
10 : IMAGE_REL_BSAED_DIR64
: 出现在 64位 PE文件中,对指向的整个地址进行修正
在 x86可执行文件中,所有基址重定向类型都是 3 ,在一组重定位结束的地方会出现 0,这些重定位什么都不做,只是用于填充,以便下一个 IMAGE_BASE_RELOCATION
按 4 字节对齐
对于 IA-64 可执行文件,重定向类型似乎总是 10,尽管 IA-64 的 EXE页大小是 8KB,但基址重定位仍是 4KB的块
例子:
xxd -s 0x198 -l 0x10 mydll.dll
00000198 : 00 e0 0100 9403 0000 b082 0100 3800 0000 ............8 ...
重定位表: 1E000h,位于 .reloc , ∆k = 1E000h - 8A00h = 15600h
重定位表一直位于 .reloc
段,可以直接找 .reloc
段的地址
File Offset = RVA - ∆k = 1E000h - 15600h = 8A00h
Sections:
7 .rsrc 00000326 1001 d000 1001 d000 00008600 2 **2 CONTENTS , ALLOC, LOAD, READONLY, DATA8 .reloc 0000057 c 1001 e000 1001 e000 00008 a00 2 **2 CONTENTS , ALLOC, LOAD, READONLY, DATA
IMAGE_BASE_RELOCATION 结构:
00008a00 : 0010 0100 8400 0000 0f36 3736 4036 4536 .........676@6E6 00008a00 : 0010 0100 8400 0000 0f36 3736 4036 4536 .........676@6E6 00008a10 : 4d36 5f36 6436 6c36 7e36 8336 8b36 9d36 M6_6d6l6 ~6.6.6.6 00008a20 : a236 aa36 d036 d436 d836 dc36 3f37 4d37 .6.6.6.6 .6.6?7M7 00008a30 : 5237 5a37 a737 cb37 d437 da37 013 a 113 a R7Z7.7.7 .7 .7 .:.:00008a40 : 223 a 353 a 983 a ec3a f03a f43a f83a 263 b ":5 :.:.:.:.:.:&00008a50 : 2 b3b 3d 3b 7d 3b 8d 3b b33b b83b d93b de3b +00008a60 : ec3b 603 c 693 c 723 c f63c fb3c 0d 3d 223d .00008a70 : 313d 393d 5 c3d 7 b3d 363 e 3 b3e 4d 3e 6 b3e 1 =9 =\={=6 >00008a80 : 763 f 0000 0020 0100 a800 0000 2830 3730 v?... ......(070
VirtualAddress : 11000h
SizeOfBlock : 84h (84h - 8h) / 2h(WORD) = 3Eh (62)
重定位数据1 : 360f
重定位数据2 : 3637
重定位数据61 : 763f
结束 : 0000
(重定位数据)61 + 1(结束) = 62
查看重定位数据: 原始数据 : 0F36h TypeOffset : 360Fh TypeOffset 高4 : 3h TypeOffset 低12: 60Fh 低 12加 VirtualAddress : 11000h + 60Fh = 1160Fh 转换为文件偏移地址: 1160Fh ,
∆k = 11000h - 400h = 10C00h
File Offset = 1160Fh - 10C00h = A0Fh
重定位所需要的数据1 指向的数据 xxd -s 0xA0F -l 0x4 mydll.dll
执行 PE 文件前,加载程序在进行重定位的时候,会用 PE 文件在内存中的实际映像地址减 PE 文件所要求的映像地址,根据重定位类型的不同将差值添加到相应的地址数据中
Easy Code 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 #include <stdio.h> #include <Windows.h> IMAGE_DOS_HEADER image_dos; IMAGE_FILE_HEADER image_file; IMAGE_OPTIONAL_HEADER image_option;void read_pe (void ) { FILE* read_pe; errno_t * err; if ((err = fopen_s(&read_pe, "C:\\Users\\0x20C\\Desktop\\dll hijacking\\Invoke-Dll.exe" , "r" )) != 0 ) { printf ("File Open Fail!\n" ); exit (0 ); } printf ("File Open Done!\n" ); fread(&image_dos, sizeof (IMAGE_DOS_HEADER), 1 , read_pe); printf ("e_magic : %04X\n" , image_dos.e_magic); printf ("PE Header Start(e_lfanew) : %08X\n" , image_dos.e_lfanew); DWORD pe_header = image_dos.e_lfanew; DWORD file_offset = pe_header + sizeof (DWORD); fseek(read_pe, file_offset, SEEK_SET); fread(&image_file, sizeof (IMAGE_FILE_HEADER), 1 , read_pe); printf ("SizeOfOptionalHeader : %04X\n" , image_file.SizeOfOptionalHeader); DWORD option_offset = file_offset + sizeof (IMAGE_FILE_HEADER); fseek(read_pe, option_offset, SEEK_SET); fread(&image_option, sizeof (IMAGE_OPTIONAL_HEADER), 1 , read_pe); printf ("SectionAlignment : %08X\n" , image_option.SectionAlignment); printf ("FileAlignment : %08X\n" , image_option.FileAlignment); printf ("SizeOfImage : %08X\n" , image_option.SizeOfImage); printf ("IMPORT : %08X\n" , image_option.DataDirectory[1 ]); printf ("BASERELOC : %08X\n" , image_option.DataDirectory[5 ]); fclose(read_pe); }int main (int argc, char * argv[]) { read_pe(); return 0 ; }
Out
File Open Done!e_magic : 5 A4 DPE Header Start(e_lfanew) : 000000 E8 SizeOfOptionalHeader : 00 E0 SectionAlignment : 00001000 FileAlignment : 00000200 SizeOfImage : 00020000 IMPORT : 0001 B1 D0 BASERELOC : 0001 F000
Links & Resources