PE文件格式

打算复习下以前的笔记,选择性的整理迁移后关闭旧博客。要深入学习逆向,了解PE格式是最基础的,于是有了本篇

典型PE文件格式

PE32(portable executable)是32位Windows下可执行文件的格式,可执行文件要被加载器加载解析才能运行,就像之前学的图像格式,PE也有格式规范,装载器按照预定义的方式加载解析它,总的来看它长这样:

当然本篇学习的是PE格式头部分,这里先放上《黑客免杀攻防》里附的典型PE文件格式,即数据在文件中的排列方式,会在随后依次介绍它们各自的含义和它们之间的逻辑:

说明:这是典型格式,并不是每一个PE文件都这样,只要符合规范即可,表中大致按照各个结构体为一个整体作色,可以看到各个部分的大小,偏移,意义(详细的请看下面的介绍),特别需要关注加粗部分。
注:下文的代码注释会以本表作为实例来说明以便于理解,实际并不能保证是这么多。

基本概念

名词 含义
映像 泛指映射在内存中的可执行文件
ImageBase PE文件在内存中优先装载的地址
RVA relative virtual address,相对虚拟地址,它是相对imagebase的偏移位置
对齐粒度 对齐大小
小端序 Intel的CPU是小端序的,小端序即低位存低地址,高位存高地址,如数字0x1a335e(越往后位数越低)的存储是0x5e331a(越往后地址越高)
变量大小 BYTE->8位WORD->16位,LONG->32位,DWORD->32位(仅限于本文)

DOS

这是每个PE开始部分,为兼容而设计,在现代操作系统几乎没用了,主要看最后一个字段,它指示PE头的位置。
DOS头:

IMAGE_DOS_HEADER结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
WORD e_magic; // 魔数->MZ(4D5A)
WORD e_cblp; // 可执行页最后一页的代码数
WORD e_cp; // 可执行文件页数
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // 最小附加内存段需求
WORD e_maxalloc; // 最大附加内存段需求
WORD e_ss; // 初始堆栈段相对值
WORD e_sp; // 初始堆栈指针
WORD e_csum; // 校验和
WORD e_ip; // 同上-<Initial IP value
WORD e_cs; // 同上-<Initial (relative) CS value
WORD e_lfarlc; // 重定位表偏移,指向dos stub->0X0040
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM 厂商的ID
WORD e_oeminfo; // OEM 厂商的信息
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // PE头的偏移->0x0080
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

(注:本文的所有结构体来自VS2015的winnt.h头)

DOS存根(stub)为可选,在dos下会运行并结束程序,大小也任意

尽管它几乎没用但是它有显著的标志可以用来识别文件格式,例如魔数MZ,与存根显示”The program cant run ….”

PE头

这才是最重要的部分,主要学习此部分

文件头

文件头包含了PE标识和下面接着的部分,注意观察

IMAGE_NT_HEADERS32结构体:

1
2
3
4
5
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; //PE标识->PE00
IMAGE_FILE_HEADER FileHeader; //文件头
IMAGE_OPTIONAL_HEADER32 OptionalHeader; //扩展头
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

IMAGE_FILE_HEADER结构体:

1
2
3
4
5
6
7
8
9
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; //CPU类型
WORD NumberOfSections; //区段数量
DWORD TimeDateStamp; //创建时间
DWORD PointerToSymbolTable; //指向符号表偏移的指针
DWORD NumberOfSymbols; //符号表中符号的数量
WORD SizeOfOptionalHeader; //扩展头大小->例本文为E0
WORD Characteristics; //PE的属性(如下)
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

PE属性可为下面的值(看长相即可知这类宏能进行位运算):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define IMAGE_FILE_RELOCS_STRIPPED           0x0001  // 文件中不包含重定位信息
#define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 // 文件可执行
#define IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 // 不包含行号
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 // 不包含符号
#define IMAGE_FILE_AGGRESIVE_WS_TRIM 0x0010 // 强制缩减工作组
#define IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 // 能处理超过2GB范围的地址
#define IMAGE_FILE_BYTES_REVERSED_LO 0x0080 // 字节序颠倒的
#define IMAGE_FILE_32BIT_MACHINE 0x0100 // 运行于32位平台
#define IMAGE_FILE_DEBUG_STRIPPED 0x0200 // 不包含调试
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400 // 如果文件在移动介质中则在交换曲运行
#define IMAGE_FILE_NET_RUN_FROM_SWAP 0x0800 // 如果映像在网络则复制到交换曲运行
#define IMAGE_FILE_SYSTEM 0x1000 // 是系统文件.
#define IMAGE_FILE_DLL 0x2000 // 是DLL文件.
#define IMAGE_FILE_UP_SYSTEM_ONLY 0x4000 // 只能在单处理器上运行
#define IMAGE_FILE_BYTES_REVERSED_HI 0x8000 // 字节时颠倒的

扩展头:


IMAGE_OPTIONAL_HEADER32结构体格式:

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
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//

WORD Magic; //文件类型:普通可执行,ROM镜像,PE32+等
BYTE MajorLinkerVersion; //链接器主版本号
BYTE MinorLinkerVersion; //链接器子版本号
DWORD SizeOfCode; //含code属性(见区段部分)区段总大小(对齐后大小)
DWORD SizeOfInitializedData; //已初始化数据大小
DWORD SizeOfUninitializedData; //未初始化数据大小
DWORD AddressOfEntryPoint; //入口点RVA
DWORD BaseOfCode; //代码段起始RVA
DWORD BaseOfData; //数据段起始RVA

//
// NT additional fields.
//

DWORD ImageBase; //首选载入地址
DWORD SectionAlignment; //文件在内存中的对齐单位
DWORD FileAlignment; //文件在磁盘上的对齐单位
WORD MajorOperatingSystemVersion; //要求操作系统的主版本号
WORD MinorOperatingSystemVersion; //要求操作系统的子版本号
WORD MajorImageVersion; //可执行文件的主版本号
WORD MinorImageVersion; //可执行文件的子版本号
WORD MajorSubsystemVersion; //要求最低子系统的主版本号
WORD MinorSubsystemVersion; //要求最低子系统的子版本号
DWORD Win32VersionValue; //保留值,必须为0X00000000
DWORD SizeOfImage; //载入内存后的总大小(最后一个区段末尾地址-ImageBase)
DWORD SizeOfHeaders; //MS-DOS,PE头,区块表的尺寸之和
DWORD CheckSum; //校验和(一般文件可以为0X00000000,系统/内核文件需为有效值)
WORD Subsystem; //此可执行文件期待的运行系统(只对EXE重要),例如GUI,CUI等
WORD DllCharacteristics; //dllmain()何时被调用,默认为0
DWORD SizeOfStackReserve; //为线程保留的堆栈大小
DWORD SizeOfStackCommit; //栈初始内存大小
DWORD SizeOfHeapReserve; //为进程默认堆保留的内存
DWORD SizeOfHeapCommit; //每次指派给堆的内存大小
DWORD LoaderFlags; //与测试有关,默认为0
DWORD NumberOfRvaAndSizes; //下面这个结构体的个数,默认为0X10个
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];//这个结构体在下一段记录
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

目录表:

存储了15个表的大小与起始地址,这里面有一些十分重要的表,例如IAT与EAT,将在后面介绍,先看看长相:

可以看到最后一项全为0,表示保留,这是C语法,结构体数组以空结构体作为结束

IMAGE_DATA_DIRECTORY结构体:

1
2
3
4
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; //起始RVA
DWORD Size; //大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

从上一节中看到,它是一个数组,每一个元素就是这么个结构体,他们代表的含义是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // 导出表目录
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // 导入表目录
#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 // 基址重定位表
#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 // 导入地址表
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor

区段表:


区段表像上面的目录表,是一种结构体的数组,结构体如下:
IMAGE_SECTION_HEADER结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //区段名,如.bss
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc; //区段大小(未对齐的大小)
DWORD VirtualAddress; //区段载入内存后的RVA
DWORD SizeOfRawData; //区段在磁盘上占的空间(对齐后)
DWORD PointerToRawData; //此区段在文件中的偏移
DWORD PointerToRelocations; //此区段重定位表的偏移地址
DWORD PointerToLinenumbers; //行号表在文件中的偏移
WORD NumberOfRelocations; //此区段重定位表的数量
WORD NumberOfLinenumbers; //行号表数量
DWORD Characteristics; //区段属性值
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_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
#define IMAGE_SCN_TYPE_NO_PAD                0x00000008  // Reserved.
// IMAGE_SCN_TYPE_COPY 0x00000010 // Reserved.

#define IMAGE_SCN_CNT_CODE 0x00000020 // 包含code
#define IMAGE_SCN_CNT_INITIALIZED_DATA 0x00000040 // 包含已初始化数据
#define IMAGE_SCN_CNT_UNINITIALIZED_DATA 0x00000080 // 包含未初始化数据

#define IMAGE_SCN_LNK_OTHER 0x00000100 // Reserved.
#define IMAGE_SCN_LNK_INFO 0x00000200 // 包含注释或其他类型数据
// IMAGE_SCN_TYPE_OVER 0x00000400 // Reserved.
#define IMAGE_SCN_LNK_REMOVE 0x00000800 // 将不会成为映像的一部分
#define IMAGE_SCN_LNK_COMDAT 0x00001000 // 包含COM数据
// 0x00002000 // Reserved.
// IMAGE_SCN_MEM_PROTECTED - Obsolete 0x00004000
#define IMAGE_SCN_NO_DEFER_SPEC_EXC 0x00004000 // 此段TLB项中重置异常控制位
#define IMAGE_SCN_GPREL 0x00008000 // 能访问GP相关内容
#define IMAGE_SCN_MEM_FARDATA 0x00008000
// IMAGE_SCN_MEM_SYSHEAP - Obsolete 0x00010000
#define IMAGE_SCN_MEM_PURGEABLE 0x00020000
#define IMAGE_SCN_MEM_16BIT 0x00020000
#define IMAGE_SCN_MEM_LOCKED 0x00040000
#define IMAGE_SCN_MEM_PRELOAD 0x00080000

#define IMAGE_SCN_ALIGN_1BYTES 0x00100000 //
#define IMAGE_SCN_ALIGN_2BYTES 0x00200000 //
#define IMAGE_SCN_ALIGN_4BYTES 0x00300000 //
#define IMAGE_SCN_ALIGN_8BYTES 0x00400000 //
#define IMAGE_SCN_ALIGN_16BYTES 0x00500000 // 如果没有其他对齐方式,默认16字节,上下的同理.
#define IMAGE_SCN_ALIGN_32BYTES 0x00600000 //
#define IMAGE_SCN_ALIGN_64BYTES 0x00700000 //
#define IMAGE_SCN_ALIGN_128BYTES 0x00800000 //
#define IMAGE_SCN_ALIGN_256BYTES 0x00900000 //
#define IMAGE_SCN_ALIGN_512BYTES 0x00A00000 //
#define IMAGE_SCN_ALIGN_1024BYTES 0x00B00000 //
#define IMAGE_SCN_ALIGN_2048BYTES 0x00C00000 //
#define IMAGE_SCN_ALIGN_4096BYTES 0x00D00000 //
#define IMAGE_SCN_ALIGN_8192BYTES 0x00E00000 //
// Unused 0x00F00000
#define IMAGE_SCN_ALIGN_MASK 0x00F00000

#define IMAGE_SCN_LNK_NRELOC_OVFL 0x01000000 // 包含扩展重定位信息
#define IMAGE_SCN_MEM_DISCARDABLE 0x02000000 // 可以被丢弃
#define IMAGE_SCN_MEM_NOT_CACHED 0x04000000 // 不可缓存
#define IMAGE_SCN_MEM_NOT_PAGED 0x08000000 // 不可分页
#define IMAGE_SCN_MEM_SHARED 0x10000000 // 可共享
#define IMAGE_SCN_MEM_EXECUTE 0x20000000 // 可执行
#define IMAGE_SCN_MEM_READ 0x40000000 // 可读
#define IMAGE_SCN_MEM_WRITE 0x80000000 // 可写

IAT

这里不介绍导入地址表的作用,但是它与EAT在二进制安全中的作用是很大的,需要重点学习:

这里先来看看三个结构体,接着再说明他们之间的联系:
IMAGE_IMPORT_DESCRIPTOR结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // (PIMAGE_THUNK_DATA)指向导入名称表INT的RVA
} 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
} IMAGE_IMPORT_DESCRIPTOR;

IMAGE_THUNK_DATA32结构体:

1
2
3
4
5
6
7
8
9
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // 导入表导入函数的实际内存地址
DWORD Ordinal; // 导入表导入函数的导出序号
DWORD AddressOfData; // 指向IMAGE_IMPORT_BY_NAME结构
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;

IMAGE_IMPORT_BY_NAME结构体:

1
2
3
4
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; //函数序号
CHAR Name[1]; //函数名字符串
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

逻辑关系

  1. 从这里的表开始,指的都是数组【如一般一个PE文件不只导入一个库,要导入多少库就存在库数+1个IMAGE_IMPORT_DESCRIPTOR结构体(即为一个IMAGE_IMPORT_DESCRIPTOR[库数+1]的数组),且最后一个为空。】,导入表首地址RVA值由目录表数组的第二个元素指出IMAGE_DATA_DIRECTORY[1]——–
    1
    2
    IMAGE_DATA_DIRECTORY[1]-------- 
    #define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // 导入表目录
  2. 上面说了,导入表是IMAGE_IMPORT_DESCRIPTOR结构体数组,每一个元素代表导入的一个库,又知一般情况下一个库里面有很多导出的函数,函数可以通过函数名使用,也可以通过地址直接使用,或是无名函数通过编号使用,故IMAGE_IMPORT_DESCRIPTOR含有这个库导出函数的名称表和地址表首地址指针(如下图)。
  3. 通过IMAGE_IMPORT_DESCRIPTOR.OriginalFirstThunk可以找到IMAGE_THUNK_DATA32数组首地址,这个数组记录的就是有名函数的函数信息指针,再通过这个指针可以找到IMAGE_IMPORT_BY_NAME数据,这个结构体记录着单个函数的编号(ordinal)和函数名。
  4. 另一边,通过IMAGE_IMPORT_DESCRIPTOR.FirstThunk可以找到IMAGE_THUNK_DATA32数组首地址(此时这个数组用作IAT,即FirstThunk在初始时存储的数据可能和INT一样,但是初始化以后里面存放的是函数真实的地址,而FirstThunk与目录表里面的IAT关系是,所有DLL的FirstThunk合在一起为IAT,会有一个DLL的FirstThunk值与IAT的值一样),里面存的是所有函数的信息的指针,再通过这个指针可以找到IMAGE_IMPORT_BY_NAME数据,这个结构体记录着单个函数的编号(ordinal)和函数名。

PE装载器把导入函数输入至IAT的顺序

  1. 读取IMAGE_IMPORT_DESCRIPTOR[i].Name获取库名xxlib

  2. 装载相应库LoadLibrary(xxlib)

  3. 读取IMAGE_IMPORT_DESCRIPTOR[i]. OriginalFirstThunk获取INT

  4. 遍历INT获取相应的IMAGE_IMPORT_BY_NAME的地址addressp

  5. 使用GetProcAddress(addressp->Hint|Name)获取函数的起始地址

  6. 读取IMAGE_IMPORT_DESCRIPTOR[i].FirstThunk获取IAT首地址

  7. 将上面获得的函数起始地址输入到相应的IAT数组中

  8. 重复4~7直到INT结束

注意到,上面图中比较特别,INT和IAT指向内容一样,即他们两者相同,但是有时可以不一样,如存在无名函数,这时只能通过序号来获取地址,这时IAT数组就会大于INT。

EAT

吃。。。。导出地址表,和导入相对应,不过一个文件最多就一个

IMAGE_EXPORT_DIRECTORY结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
IMAGE_typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics; //保留,恒为0X00000000
DWORD TimeDateStamp; //时间戳
WORD MajorVersion;
WORD MinorVersion;
DWORD Name; // 库名称
DWORD Base; // 函数索引值基数
DWORD NumberOfFunctions; // 实际导出函数的个数
DWORD NumberOfNames; // 有名导出函数的个数
DWORD AddressOfFunctions; // 导出函数地址数组
DWORD AddressOfNames; // 函数名称地址数组
DWORD AddressOfNameOrdinals; // 序号(Ordinal)地址数组
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY; EXPORT_DIRECTORY

导出表相对简单,通过如上图即可清楚其中的逻辑,再附上GetProcAddress()函数原理,应该能够很好的理解它:

  1. 利用AddressOfNames成员转向函数名称数组

  2. 函数名称数组中存储着字符串地址,通过比较字符串查找匹配名

  3. 利用 AddressOfNameOrdinals找到ordinal数组

  4. 在ordinal中根据匹配名称的数组下标获取对应下标元素值

  5. 利用AddressOfFunctions元素找到EAT表

  6. 按4找到的索引获取EAT数组元素的值,即那个函数的地址(这里用的索引为ordinal值-base)

注意:在存在无名导出函数时,可以通过ordinal直接从第4步开始获取地址,即使不存在无名导出函数,AddressOfNames的index和ordinal也不一定对应,所以需要从1开始才能保证正确。

基址重定位:


由于imagebase指定的位置可能已经被占用(这一般是发生在DLL中的,因为EXE文件首先被加载,此时拥有一个独占的虚拟空间),PE装载器讲程序装载到了其他地址,于是PE里硬编码的VA就需要被修改,方法为:

  1. 找到硬编码的地址

  2. 将 对应值 - OriginalImageBase+NewImageBase

第二步很容易实现,于是主要关注怎么找到硬编码地址,其实他们都在编译时就被记录在了基址重定位表中,此表有基址重定位目录偏移指向,结构为:

1
2
3
4
5
6
7
8
9
10
11
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress; // 段落基址
DWORD SizeOfBlock; // 段落大小
/*
struct {
WORD Offset:12; // 偏移
WORD Type:4; // 重定位类型
}TypeOffset[1];
*/
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;

可以将地址相近的组成一个段落,在这里可以看到后面的TypeOffset是注释,说明它并不属于IMAGE_BASE_RELOCATION,可以将IMAGE_BASE_RELOCATION看做段首,它说明段落基址和段落大小,后面接TypeOffset数组表示段落主体,而TypeOffset中高4位指定重定位类型(如下宏定义),低12位表示偏移,于是实际的VA为Offset+VirtualAddress+ImageBase。

1
2
3
4
5
6
7
8
9
10
11
#define IMAGE_REL_BASED_ABSOLUTE              0
#define IMAGE_REL_BASED_HIGH 1
#define IMAGE_REL_BASED_LOW 2
#define IMAGE_REL_BASED_HIGHLOW 3 //最常见,4字节全部更新
#define IMAGE_REL_BASED_HIGHADJ 4
#define IMAGE_REL_BASED_MACHINE_SPECIFIC_5 5
#define IMAGE_REL_BASED_RESERVED 6
#define IMAGE_REL_BASED_MACHINE_SPECIFIC_7 7
#define IMAGE_REL_BASED_MACHINE_SPECIFIC_8 8
#define IMAGE_REL_BASED_MACHINE_SPECIFIC_9 9
#define IMAGE_REL_BASED_DIR64 10

内存地址转换

现在再来看看相对内存地址与文件偏移之间的转换,,在这之前要先了解下虚拟内存与物理内存,学汇编时,我们能写代码让CPU直接访问内存,但是实际上用户层是不能直接访问物理内存的,只有内核级权限才能访问它,实际上的内存管理如下:

二级页表

为了更充分利用内存,且兼顾速度,X86采用二级页表的方式,对于每一个进程,系统分配4GB的虚拟内存(不管物理内存实际有多大),其中0X00000000-0X7FFFFFFF为用户进程空间,0X80000000-0XFFFFFFFF为系统空间,每一个进程都拥有这么个4G的虚拟地址地址空间。

用户区是每个进程真正独立的可用内存空间,进程中的绝大部分数据都保存在这一区域。

  1. 主要包括:应用程序代码、全局变量、所有线程的线程栈以及 加载的DLL代码等

  2. 每个进程的用户区的虚拟内存空间相互独立,一般不可以直接跨进程访问,这使得一个程序直接破坏另一个程序的可能性非常小。

内核区中的所有数据是所用进程共享的,是操作系统代码的驻地。

  1. 其中包括: 操作系统内核代码, 以及与线程调度、 内存管理、文件系统支持、网络支持、 设备驱动程序相关的代码。

  2. 该分区中所有代码和数据都被操作系统保护。用户模式代码无法直接访问和操作:如果应用程序直接对该内存空间内的地址访问,将会发生地址访问违规。

(于是,这可以解释为什么同时用OD调试两个不同的程序,它们的内存地址却相同,因为每一个进程都独享这么一个虚拟空间)

但是虚拟的毕竟是假的,实际操作还是要作用在真实的内存上,即CPU在对内存进行操作时,内存管理单元MMU会将虚拟内存映射到物理内存上,所以出现的情况是虚拟内存的连续空间在实际的物理内存上并不一定连续,那么就需要一张表来记录虚拟内存与物理内存之间的映射关系(又因为用的页式存储,就把这个东东叫做页表,至于后面提到的页,认为是汇编那种段吧,只是比段还小,,,那把它比作磁盘扇区吧)。

由于内存太大,页表可能会很大,为便于查找就把它再细分下,相邻的为一组,再加个组索引即有了两张页表了,第一张表来查属于哪一组的,再到那一组去查找更具体的,下面是图例:

页表中就记录着实际的物理空间,还有一些状态信息(这里大致有个概念,到操作系统笔记详细记录)于是MMU就能实现虚拟地址与物理地址之间的映射了。

对齐

为了方便寻址等,一个PE文件在磁盘中存储与在内存中存储都不是一个结构后就直接紧接着存储下一个结构,而是根据对齐粒度,若是一个部分未达到粒度大小,就是用0填充,而他们的对齐粒度在PE文件里面有记录:

一般来说,在磁盘中存储时是200字节,而映射到内存中时为1000字节。

例如一个.text段在文件中的起始位置是0X00001200,在内存中的起始位置是0X00002000,实际大小是315字节,其后为.data,那么.data在文件中的起始位置为0X00001600,在内存中的起始位置是0X00002000。

转换

已经注意到了,前两篇在查看PE文件时里面存的很多都是VA(Virtual Address虚拟绝对地址)和RVA(Relative Virtual Address虚拟相对地址,相对ImageBase的地址),即在文件中存的很多都是加载到内存中的地址,而加载到内存中的实际为PE文件,我们想要直接在文件中找到那个地址的值,但是不能把地址直接拿来用(由于对齐粒度不同,一般内存中的PE都要比文件中的占的空间大),需要将RVA/VA转换为磁盘上的偏移FO公式如下:

文件偏移地址=所在区段文件起始偏移地址+相对区段偏移地址

相对区段偏移地址=虚拟地址-映像基址-所在区段虚拟起始偏移地址

<==>

文件偏移地址=相对虚拟地址-相对区段偏移地址+所在区段文件起始偏移地址

参考

《逆向工程核心原理》
《黑客免杀攻防》
《逆向工程权威指南》
《恶意软件防护机理》