0%

PE文件格式解析(3)

前言

在PE文件结构中,数据目录表对应的信息很重要,这里对其中几个常用的进行介绍。

资源表

资源表用来存储程序的各种界面数据,比如菜单、图标、版本信息等,其结构体如下

1
2
3
4
5
6
7
8
typedef struct _IMAGE_RESOURCE_DIRECTORY {
DWORD Characteristics; // 资源属性标识,通常为0x00000000
DWORD TimeDateStamp; // 资源建立的时间
WORD MajorVersion; // 资源主版本,通常为0x0004
WORD MinorVersion; // 资源子版本,通常为0x0000
WORD NumberOfNamedEntries; // 资源名称条目个数
WORD NumberOfIdEntries; // 资源ID条目个数
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;

资源在PE文件中是以目录结构的形式存在的,一般情况下这个目录分3层,从根目录开始分别为资源类型、
目录资源ID、资源代码页。每层的头部是一个 IMAGE_RESOURCE_DIRECTORY 结构,并且在其后面跟着一个
IMAGE_RESOURCE_DIRECTORY_ENTRY 结构数组,然后结构数组的每个成员则分别指向下一层目录结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
union {
struct {
DWORD NameOffset : 31; // 资源名偏移
DWORD NameIsString : 1; // 资源名为字符串
};
DWORD Name; // 资源/语言类型
WORD Id; // 资源数字ID
};
union {
DWORD OffsetToData; // 数据偏移地址
struct {
DWORD OffsetToDirectory : 31; // 子目录偏移地址
DWORD DataIsDirectory : 1; // 数据为目录
};
};
} IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;

第一个联合体的信息,是根据当前结构体所处的目录层次来决定的,位于 第1层 目录时 Name 有效,保存的
信息是 资源类型。位于 第2层 目录时 Id结构体 有效,取决于此资源的 索引方式,如果用的是 编号索引
就是 Id 有效,否则 结构体 有效。位于 第3层 目录时 Name 有效,保存的信息是 语言类型

第二个联合体的信息,理论上是根据具体情况而定的,如果下级是一个 子目录 的话,那么就是 结构体 生效,
如果下级是 资源数据 则是字段 OffsetToData 生效。

NameIsString1 时,NameOffset 指向一个 IMAGE_RESOURCE_DIR_STRING_U 结构体

1
2
3
4
typedef struct _IMAGE_RESOURCE_DIR_STRING_U {
WORD Length; // 字符串的字节数
WCHAR NameString[1]; // 字符串的内容信息
} IMAGE_RESOURCE_DIR_STRING_U, *PIMAGE_RESOURCE_DIR_STRING_U;

Name 位于 第1层 目录时表示的 资源类型 如下表所示

类型值 资源类型 类型值 资源类型
0x00000001 鼠标指针(Cursor) 0x00000008 字体(Font)
0x00000002 位图(Bitmap) 0x00000009 快捷键(Accelerators)
0x00000003 图标(Icon) 0x0000000A 非格式化资源(Unformatted)
0x00000004 菜单(Menu) 0x0000000B 消息列表(Message Table)
0x00000005 对话框(Dialog) 0x0000000C 鼠标指针组(Group Cursor)
0x00000006 字符串列表(String Table) 0x0000000E 图标组(Group Icon)
0x00000007 字体目录(Font Directory) 0x00000010 版本信息(Version Information)

在经过3层目录的索引后,最后是一个 IMAGE_RESOURCE_DATA_ENTRY 结构体,定义如下

1
2
3
4
5
6
typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
DWORD OffsetToData; // 资源数据的RVA
DWORD Size; // 资源数据的大小
DWORD CodePage; // 代码页
DWORD Reserved; // 保留字段,通常为0x00000000
} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;

资源表的3层目录关系,如下图所示

关系图

重定位表

系统在加载DLL文件时,并不是每次都能加载到预期的 ImageBase 基址上,所以DLL都存在 基址重定位表
用来修正相关的地址信息,另外EXE的 动态基址 技术,也是用 基址重定位表 实现的。

PE文件中的重定位信息是由多个 IMAGE_BASE_RELOCATION 结构体组成的,每个结构体只描述一个 4KB 大小
的分页内重定位信息,也就是 0x1000 字节,因此结构体中 VirtualAddress 的值总是为 0x1000 的倍数。

1
2
3
4
5
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress; // 需要重定位数据的起始RVA
DWORD SizeOfBlock; // 本结构与TypeOffset总大小
// WORD TypeOffset[1]; // 原则上不属于本结构
} IMAGE_BASE_RELOCATION, *PIMAGE_BASE_RELOCATION;

重定位的本质非常简单,就是比较实际加载地址与 ImageBase 的值,如果相等则不需要做任何操作,如果
不相等就需要把重定位表中指定的地址处加上这个差值。

TypeOffset 由两部分数据组成,高4位 表示 类型低12位 表示 偏移。类型定义如下

信息 宏定义
0 无重定位操作,填0后用于4字节对齐 IMAGE_REL_BASED_ABSOLUTE
1 重定位偏移指向位置的高2个字节需要被修正 IMAGE_REL_BASED_HIGH
2 重定位偏移指向位置的高2个字节需要被修正 IMAGE_REL_BASED_LOW
3 重定位偏移指向的4个字节的地址需要被修正 IMAGE_REL_BASED_HIGHLOW
4 需要使用两项TypeOffset才能完成索引操作 IMAGE_REL_BASED_HIGHADJ
5 基址重定位应用于MIPS jump指令 IMAGE_REL_BASED_MIPS_JMPADDR
6 保留 IMAGE_REL_BASED_RESERVED
9 基址重定位应用于MIPS16 jump指令 IMAGE_REL_BASED_IA64_IMM64
10 重定位偏移指向的8个字节(64位)地址需要被修正 IMAGE_REL_BASED_DIR64

重定位表结构如下图所示

结构图