PE文件解析(四)

资源表

资源表的结构相对复杂,采用了类似磁盘目录结构的方式保存,通常目录有3层,第1层目录类似于一个文件系统的根目录,指出了整个资源表中有多少种不同的类型(如光标、菜单、快捷键等);第2层目录指明了当前类型的资源中,有多少个资源;而第3层目录被称为资源代码页

为了便于理解,先给出一张资源表的树形结构图

资源目录结构

1
2
3
4
5
6
7
8
typedef struct _IMAGE_RESOURCE_DIRECTORY {
DWORD Characteristics; //资源属性,但通常为0
DWORD TimeDateStamp; // 资源建立时间
WORD MajorVersion; // 资源的版本,但通常为0
WORD MinorVersion;
WORD NumberOfNamedEntries; // 使用名字的资源条目的个数
WORD NumberOfIdEntries; // 使用ID的资源条目个数
} IMAGE_RESOURCE_DIRECTORY

其中最关键的属性是NumberOfNamedEntries和NumberOfIdEntries,他们指出了本目录种目录项的总和(即资源类型的总个数)

资源目录入口结构

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;
} DUMMYSTRUCTNAME;
DWORD Name;
WORD Id;
} DUMMYUNIONNAME;
union {
DWORD OffsetToData;
struct {
DWORD OffsetToDirectory:31;
DWORD DataIsDirectory:1;
} DUMMYSTRUCTNAME2;
} DUMMYUNIONNAME2;
} IMAGE_RESOURCE_DIRECTORY_ENTRY

可以看到其中有两个union结构,我们可以简单的理解为,资源目录入口结构包含两个属性Name(ID)、OffsetToData,根据情况的不同,他们有不同的含义

  • Name(ID)属性:定义了目录项的名称或ID,当最高位(NameIsString)为0时,表示该属性通过ID使用;为1时,表示该属性通过Name使用,且资源名称使用Unicode编码,低31位为Name的Offset,但NameOffset并不直接指向字符串,而是指向一个IMAGE_RESOURCE_DIR_STRING_U结构,该结构如下所示
    • 当用于第一层目录时,定义的是资源的类型
    • 当用于第二层目录时,定义的是资源的名称
    • 当用于第三层目录时,定义的时代码页的编号
1
2
3
4
typedef struct _IMAGE_RESOURCE_DIR_STRING_U {
WORD Length; // 字符串的长度(不一定以0结尾,所以该属性很重要)
WCHAR NameString[ 1 ]; // 具体的Unicode字符串
} IMAGE_RESOURCE_DIR_STRING_U
  • OffsetToData属性:一个指针,当最高位为1时,低31位指向下一层IMAGE_RESOURCE_DIRECTORY的起始地址;当最高位为0时,指向IMAGE_RESOURCE_DATA_ENTRY结构

当NameOffset和OffsetToData作为Offset使用时,该指针从资源区块的开始处计算偏移,即它们并不是RVA

当IMAGE_RESOURCE_DIRECTORY_ENTRY用在第一层的目录时,其Name(ID)属性用于标记资源类型,而Windows有14种预定义的类型,且用ID标识,ID的数值在1到16之间

ID值 资源类型
01h 光标(Cursor)
02h 位图(Bitmap)
03h 图标(Icon)
04h 菜单(Menu)
05h 对话框(Dialog)
06h 字符串(String)
07h 字体目录(Front Directory)
08h 字体(Front)
09h 快捷键(Accelerators)
0Ah 未格式化资源(Unformatted)
0Bh 消息表(Message Table)
0Ch 光标组(Group Cursor)
0Eh 图标组(Group Icon)
10h 版本信息(Version Information)

资源数据入口结构

1
2
3
4
5
6
typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
DWORD OffsetToData; // 资源数据的RVA
DWORD Size; // 资源数据的长度
DWORD CodePage; // 代码页,一般为0
DWORD Reserved; // 保留字段
} IMAGE_RESOURCE_DATA_ENTRY

该结构就是真正的资源数据的结构了,其中OffsetToData是RVA,而不是相对于资源块起始位置的Offset

实例

我这里用一个Win32程序为实例,先用PE Tools查看其资源表内容

根目录

可以看到其中共有7种不同的资源类(24好像是清单类型,暂时每找到相关文档信息),首先在16进制编辑器中观察其根目录

  • 4字节Characteristics,此时为0
  • 4字节TimeDateStamp,为0
  • 2字节MajorVersion,为0
  • 2字节MinorVersion,为0
  • 2字节NumberOfNamedEntries,为0
  • 2字节NumberOfIdEntries,为7

根目录下的IMAGE_RESOURCE_DIRECTORY_ENTRY

我们以Menu作为分析对象,它是第二个IMAGE_RESOURCE_DIRECTORY_ENTRY结构,因为该结构大小位8字节,所以它的位置应该是2400h + 8 = 2408h,我们在16进制编辑器中可以看到如下内容

可以看到其Name(ID)属性值为:0x00000004,其OffsetToData属性值为0x800000E8,我们进一步解析

  • Name(ID)属性的最高位为0,表示它通过ID标识,由于是第一层目录中,它代表一个预定义的资源,查表我们发现04h是Menu
  • OffsetToData属性的最高位为1,表示它指向下一层目录,其Offset是E8h,那么下一层目录的地址为2400h + E8h = 24E8h

第二层目录的IMAGE_RESOURCE_DIRECTORY

第二层目录如下所示

可以知道其NumberOfIdEntries为1,表示Menu资源下只有一个该类资源

紧跟其后的8字节就是IMAGE_RESOURCE_DIRECTORY_ENTRY结构,可以看到其Name(ID)属性值为:0x0000006D,其OffsetToData属性值为0x80003030

  • Name(ID)属性的最高位为0,表示它通过ID标识,ID为6Dh,即十进制的109
  • OffsetToData属性的最高位为1,表示它指向下一层目录,其Offset是330h,那么下一层目录的地址为2400h + 330h = 2730h

第三层目录的IMAGE_RESOURCE_DIRECTORY

可以知道其NumberOfIdEntries为1,表示该资源代码页只有一项

紧跟其后就是IMAGE_RESOURCE_DIRECTORY_ENTRY结构

可以看到其Name(ID)属性值为:0x00000804,其OffsetToData属性值为0x000004F8

  • Name(ID)属性的最高位为0,表示它通过ID标识,ID为804h
  • OffsetToData属性的最高位为0,表示它指向IMAGE_RESOURCE_DATA_ENTRY结构,其Offset是4F8h,那么该结构的地址为2400h + 4F8h = 28F8h

IMAGE_RESOURCE_DATA_ENTRY

  • OffsetToData为01AE90h
  • Size为50h
  • CodePage为0
  • Reserved为0

IMAGE_RESOURCE_DIR_STRING_U

因为示例程序比较简单,没有用到这个结构,所以这里单独找一个比较复杂的程序用来展示

可以看到根目录下第一类资源是用Name标识的,其名字为”AFX_DIALOG_LAYOUT”,我们在16进制中观察其IMAGE_RESOURCE_DIRECTORY_ENTRY

可以看到其Name(ID)属性值为:0x8000CB2E,其OffsetToData属性值为0x800000B0

  • Name(ID)属性的最高位为1,表示它通过Name标识,且Name的Offset为CB2Eh,那么IMAGE_RESOURCE_DIR_STRING_U结构的地址为630000h + CB2Eh = 63CB2Eh
  • OffsetToData属性的最高位为1,表示它指向下一层目录

可以看到IMAGE_RESOURCE_DIR_STRING_U结构中Length为11h,而NameString为Unicode表示的”AFX_DIALOG_LAYOUT”(可以注意到字符串并没有以0结尾,所以Length属性很重要)