在上一篇文章里,已经讲解了加载PE文件的导入表。本篇简要介绍PE文件的资源表的结构和定位方式。 所谓资源表(resource table),就是通常在IDE的资源视图中所看到的那个Tree视图,因此资源表在PE文件中同样是这样的一种类似资源管理器一样的树状逻辑结构。
对树,我们不能想类似导入表那样当作线性表中的数组去比较简单直观的加载,而是要用递归函数去重建,这是因为树的定义就是用递归做的定义,所以对树的操作天生的就和递归函数分不开。看起来不可预判的复杂结构,递归函数的代码却非常简洁。
资源表在optionalHeader的DataDirectory数组中位于第三个元素,其索引为2,从这里的RVA我们可以定位到资源表的位置。我们先介绍资源表的几个重要数据结构:
(1)IMAGE_RESOURCE_DIRECTORY: (16 bytes)
之后我们把它简称为 dir,表示一个目录,之后跟了多个dir entry(每个dir entry可以理解为一个索引,它指向某个东西),这些 entry 我们可以认为是这个目录的一部分。而这个结构本身描述的是这个目录的一些信息,这里最重要的信息是,我们可以知道后面有多少个 dir entry。在16进制编辑器里,dir正好占据一个整行。
(1.1) WORD NumberOfNamedEntries; 这是用户自定义资源类型的个数。
(1.2) WORD NumberOfIdEntries; 这是典型资源例如位图,图标,对话框等资源类型的个数。
上面这两个值加在一起就是 dir 后面紧跟的 dir entry 的个数。
(2)IMAGE_RESOURCE_DIRECTORY_ENTRY:(8 bytes)
我们把它简称为 dir entry,它紧跟在dir的后面,它代表了资源树上一个节点,节点本身的信息来自它的第一个成员(指向一个名称字符串或者本身就是一个ID),它更重要的信息是包含了一个偏移量(它的第二个成员),指向一个data entry 或者 dir。因此它颇类似一个链表中的节点的作用。在16进制编辑器里,每一行是两个dir entry。
(2.1) DWORD name / Id; 第一个成员,表示是它是一个用户定义的名称还是资源类型的ID号。取决于最高位的值。如果是一个用户定义的名称,它是一个偏移,指向的是那个
IMAGE_RESOURCE_DIR_STRING_U(一个int16的字符串长度为前导的 unicode 字符串)。如果是一个 ID 号,那么它直接就是 ID 号本身。
(2.2) DWORD offsetToData / offsetToDirectory; 第二个成员,是一个偏移量,指向该name或者Id 节点的 data entry 或者下一级 dir。
这两个成员的具体含义都是由它们的最高位是 1 还是 0 而决定的。
(3)IMAGE_RESOURCE_DIR_STRING_U:(长度不固定)
表示的是一个Unicode字符串。
(3.1) WORD Length; 这个字符串的字符长度。
(3.2) WCHAR NameString[]; Unicode字符串的内容。
(4)IMAGE_RESOURCE_DATA_ENTRY :(16 bytes)
简称 data entry,它表示这里是叶子节点,不必再向下扩展。它指向一个资源的实际数据。
(4.1) DWORD OffsetToData; 注意这是资源数据的RVA。(而非偏移量) (4.2) DWORD Size; 资源数据的尺寸(bytes)。
(4.3) DWORD CodePage; 代码页,看起来没什么用,基本为0。 (4.4) DWORD Reserved; 保留。
好了,现在我们总结一下需要强调的两点:
(1)资源名称是以长度为前导的unicode字符串。
(2)只有 data entry 中的 offset 是RVA,其他成员中的offset 都是距离资源表的偏移。
下面我还是画一张图,来更直观的表达资源表的结构,注意 Image_resource_dir_string_u 下面的点是表示字符串长度不固定的意思,另外,本图是示意图,实际上到底有多少级子目录也是不确定的,请不要根据下图误解为资源树的深度一定是下面这样子的(实际上资源树的层次通常是:资源类型->资源ID->语言ID-> DataEntry->资源数据)。
我建立了一个MFC对话框程序,里面添加了一个CTreeCtrl m_tree 变量。主要代码如下: code_load_resTable 在这里有一点古怪,资源类型名称是用unicode存储在PE文件中的,而导入表的dll,函数名称是用ANSI存储的。所以我们在编码的时候不管你的项目用什么字符编码,如果要同时解析导入表和资源树,你重要做一次多字节和宽字符之间的转换。在同一个文件中同时使用两种编码,两种定义的字符串,这是有点让人感觉怪异的地方。 这个小程序的运行效果如下,在打开的PE文件中,我添加了一种用户自定义资源,名称是UserDefined,添加了16个字节:
可以根据节点信息,在PE文件中,找到这个自定义资源的数据。也可以找到这个资源类型名称的字符串的位置:
00030230h: 00 00 00 00 00 00 00 00 - 0B 00 55 00 53 00 45 00 ; ..........U.S.E. 00030240h: 52 00 44 00 45 00 46 00 - 49 00 4E 00 45 00 44 00 ; R.D.E.F.I.N.E.D.
可以看到这个字符串的文件地址是 0x00030238,它的长度是 0x000B(11个字符),11个字符占据的是22个字节(wide char)
(1)关于位图:
再定位到一个Bitmap,在那里我看到了熟悉的BitmapInfoHeader结构,注意,对于bmp文件需要的BitmapFileHeader在资源里是没有的也没有必要。这个应该很简单,我们很容易从这里的数据创建出位图对象。
(2)关于图标:
图标我注意到在资源表里分为两种,Icon 和 group icon。前者是应该是所有图标中的所有图像,后者是 IDE 的资源视图中看到的图标(可含有多个图像)。比如说我在IDE中添加了 4 个图标,每个图标中添加了 9 个图像。则在资源表中,Icon 具有 36 个节点, GroupIcon 具有 4 个节点。
Icon 文件由,ICO头和图标数据。ICO头用ICON_DIR结构体表示,其中又分为两个部分图标头和图标项。图标头用来描述ICO 文件中图标的个数,用ICON_DIR结构体表示。 {
word idreserved; // reserved (must be 0)保留,必须为0
word idtype; // resource type (1 for icons)资源类型,图标用1表示 word idcount; // how many images?图标个数 } icondir
紧跟在该结构体之后的是N个图标项,用来描述每个图标的详细信息,用ICON_DIR_ENTRY 结构体表示,每个图标对应有一个图标项 {
byte bwidth; // width, in pixels, of the image宽度,以像素为单位 byte bheight; // height, in pixels, of the image高度,以像素为单位 byte bcolorcount; // number of colors in image (0 if >=8bpp)颜色数 byte breserved; // reserved ( must be 0)保留,必须为0
word wplanes; // color planes为目标设备说明位面数,其值将总是被设为1
word wbitcount; // bits per pixel每像素所占的字节数
dword dwbytesinres; // how many bytes in this resource?该图标所占位数
dword dwimageoffset; // where in the file is this image?这个图标在文件中的起始位置 } icondirentry,