在上一篇文章里,已经讲解了加载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 / NAME) -> 资源 ID / NAME -> 语言 ID -> DataEntry (叶子节点) -----(指向)----> (二进制)资源数据)。
我建立了一个MFC对话框程序,里面添加了一个CTreeCtrl m_tree 变量。主要代码如下:
BOOL CPERcViewDlg::LoadPeFile(LPCTSTR lpszFileName, LPTSTR errormsg)
{
int i, j;
HANDLE hFile = CreateFile(
lpszFileName,
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if(hFile == INVALID_HANDLE_VALUE)
{
_tcscpy(errormsg, _T("Create File Failed.\n"));
return FALSE;
}
HANDLE hFileMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
CloseHandle(hFile);
if (hFileMapping == NULL || hFileMapping == INVALID_HANDLE_VALUE)
{
_stprintf(errormsg, _T("Could not create file mapping object (%d).\n"), GetLastError());
return FALSE;
}
LPBYTE lpBaseAddress = (LPBYTE)MapViewOfFile(hFileMapping, // handle to map object
FILE_MAP_READ, 0, 0, 0);
CloseHandle(hFileMapping);
if (lpBaseAddress == NULL)
{
_stprintf(errormsg, _T("Could not map view of file (%d).\n"), GetLastError());
return FALSE;
}
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddress;
PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)(lpBaseAddress + pDosHeader->e_lfanew);
//资源表的rva
DWORD rva_resTable = pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE].VirtualAddress;
if(rva_resTable > 0)
{
this->LoadResTable(lpBaseAddress, pNtHeaders, rva_resTable);
}
//关闭文件,句柄。。
UnmapViewOfFile(lpBaseAddress);
if(rva_resTable == 0)
{
_tcscpy(errormsg, _T("这个文件没什么可加载的。"));
return FALSE;
}
return TRUE;
}
//加载资源表
void CPERcViewDlg::LoadResTable(LPBYTE lpBaseAddress, PIMAGE_NT_HEADERS pNtHeaders, DWORD rva)
{
int i;
TCHAR nodeText[128];
HTREEITEM hItem_Res = NULL;
HTREEITEM hChild = NULL;
PIMAGE_RESOURCE_DIRECTORY pResTable = (PIMAGE_RESOURCE_DIRECTORY)ImageRvaToVa(
pNtHeaders, lpBaseAddress,
rva,
NULL);
_stprintf(nodeText, _T("ResourceTable(FileAddress: %08X)"), (DWORD)pResTable - (DWORD)lpBaseAddress);
hItem_Res = m_tree.InsertItem(nodeText, TVI_ROOT, TVI_LAST);
PIMAGE_RESOURCE_DIRECTORY_ENTRY pEntries = (PIMAGE_RESOURCE_DIRECTORY_ENTRY)((DWORD)pResTable + sizeof(IMAGE_RESOURCE_DIRECTORY));
for(i=0; i<(pResTable->NumberOfNamedEntries + pResTable->NumberOfIdEntries); i++)
{
this->AddChildNode(hItem_Res, lpBaseAddress, pNtHeaders, (DWORD)pResTable, pEntries + i, 1);
}
}
//递归函数,为资源树递归添加所有节点
//tableAddress: 志愿表起始地址(VA),
//pEntry:当前的entry
//depth: 深度,只有在depth = 1时,才把id为Bitmap等字符串
HTREEITEM CPERcViewDlg::AddChildNode(HTREEITEM hParent, LPVOID lpBaseAddress,
PIMAGE_NT_HEADERS pNtHeaders, DWORD tableAddress,
PIMAGE_RESOURCE_DIRECTORY_ENTRY pEntry, int depth)
{
int i;
TCHAR nodeText[256];
HTREEITEM hItem = NULL;
//先确定节点文本
if(pEntry->NameIsString) //检测最高位是不是1
{
PIMAGE_RESOURCE_DIR_STRING_U pString = (PIMAGE_RESOURCE_DIR_STRING_U)(tableAddress + pEntry->NameOffset);
_tcsncpy(nodeText, pString->NameString, pString->Length);
nodeText[pString->Length] = 0;
hItem = m_tree.InsertItem(nodeText, hParent, TVI_LAST);
}
else
{
if(depth == 1)
{
switch(pEntry->Id)
{
case 1: _tcscpy(nodeText, _T("Cursor")); break;
case 2: _tcscpy(nodeText, _T("Bitmap")); break;
case 3: _tcscpy(nodeText, _T("Icon")); break;
case 4: _tcscpy(nodeText, _T("Menu")); break;
case 5: _tcscpy(nodeText, _T("Dialog")); break;
case 6: _tcscpy(nodeText, _T("String")); break;
case 7: _tcscpy(nodeText, _T("FontDir")); break;
case 8: _tcscpy(nodeText, _T("Font")); break;
case 9: _tcscpy(nodeText, _T("Accelerator")); break;
case 10: _tcscpy(nodeText, _T("RCDATA")); break;
case 11: _tcscpy(nodeText, _T("MessageTable")); break;
case 12: _tcscpy(nodeText, _T("GroupCursor")); break;
case 14: _tcscpy(nodeText, _T("GroupIcon")); break;
case 16: _tcscpy(nodeText, _T("Version")); break;
case 17: _tcscpy(nodeText, _T("DlgInclude")); break;
case 19: _tcscpy(nodeText, _T("PlugPlay")); break;
case 20: _tcscpy(nodeText, _T("VXD")); break;
case 21: _tcscpy(nodeText, _T("ANICursor")); break;
case 22: _tcscpy(nodeText, _T("ANIIcon")); break;
case 23: _tcscpy(nodeText, _T("HTML")); break;
default: _stprintf(nodeText, _T("ID: %ld"), pEntry->Id); break;
}
}
else
{
_stprintf(nodeText, _T("ID: %ld"), pEntry->Id);
}
hItem = m_tree.InsertItem(nodeText, hParent, TVI_LAST);
}
//再确定节点类型(目录还是叶子)
if(pEntry->DataIsDirectory)
{
PIMAGE_RESOURCE_DIRECTORY pDir = (PIMAGE_RESOURCE_DIRECTORY)(tableAddress + pEntry->OffsetToDirectory);
PIMAGE_RESOURCE_DIRECTORY_ENTRY pEntries = (PIMAGE_RESOURCE_DIRECTORY_ENTRY)((DWORD)pDir + sizeof(IMAGE_RESOURCE_DIRECTORY));
for(i=0; i<(pDir->NumberOfNamedEntries + pDir->NumberOfIdEntries); i++)
{
AddChildNode(hItem, lpBaseAddress, pNtHeaders, tableAddress, pEntries + i, depth+1);
}
}
else
{
//叶子
PIMAGE_RESOURCE_DATA_ENTRY pDataEntry = (PIMAGE_RESOURCE_DATA_ENTRY)(tableAddress + pEntry->OffsetToData);
//具体的资源属于位于:pData->OffsetToData,这是一个RVA(不是相对于资源表头部的偏移!)
//去定位到实际的资源数据
DWORD pData = (DWORD)ImageRvaToVa(pNtHeaders, lpBaseAddress, pDataEntry->OffsetToData, NULL);
_stprintf(nodeText, _T("FileAddr: %08X; RVA: %08X; Size = %ld Bytes; "),
pData - (DWORD)lpBaseAddress,
pDataEntry->OffsetToData,
pDataEntry->Size);
m_tree.InsertItem(nodeText, hItem, TVI_LAST);
}
return hItem;
}
在这里有一点古怪,资源类型名称是用unicode存储在PE文件中的,而导入表的dll,函数名称是用ANSI存储的。所以我们在编码的时候不管你的项目用什么字符编码,如果要同时解析导入表和资源树,你重要做一次多字节和宽字符之间的转换。在同一个文件中同时使用两种编码,两种定义的字符串,这是有点让人感觉怪异的地方。
2024-09-25 补充:这里需要注意的是,资源表中的 NAME 是用 UNICODE (UTF16)编码存储的字符串,这意味着你的资源是可以取“中文”名称的,也可以,当然绝大多数情况下,资源的 NAME 还是英文。
这个小程序的运行效果如下,在打开的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 个节点。
2.1 Icon
这里就是一个图标的图像数据部分(BitmapInfo + XOR MASK + AND MASK),和ICO文件中的数据一致。
2.2 GroupIcon
这里就是 ICO 文件头和 ico dir entries 部分,但是需要注意的是,这里的 ICONDIRENTRY 和 ICO 文件中的 ICONDIRENTRY 的定义有细微不同(此处我参考了 ICONPRO 的代码,才弄清楚是怎么回事),其最后一个字段不同(数据类型和含义都不同),在文件中,最后一个字段是 DWORD dwImageOffset,指向图像数据(BITMAPINFO+XOR MASK + AND MASK )在 ICO 文件中的文件地址。而在 PE 文件资源数据中,最后一个字段是 WORD nID,表示的是 Icon 资源的 ID。两者的区别如下:
在PE文件中,GroupIcon 的资源数据如下:
#pragma pack( push )
#pragma pack( 2 )
typedef struct
{
BYTE bWidth; // Width of the image
BYTE bHeight; // Height of the image (times 2)
BYTE bColorCount; // Number of colors in image (0 if >=8bpp)
BYTE bReserved; // Reserved
WORD wPlanes; // Color Planes
WORD wBitCount; // Bits per pixel
DWORD dwBytesInRes; // how many bytes in this resource?
WORD nID; // the ID,注意这个成员和ICO文件中定义不同。
} MEMICONDIRENTRY, *LPMEMICONDIRENTRY;
typedef struct
{
WORD idReserved; // Reserved
WORD idType; // resource type (1 for icons)
WORD idCount; // how many images?
MEMICONDIRENTRY idEntries[1]; // the entries for each image
} MEMICONDIR, *LPMEMICONDIR;
#pragma pack( pop )
(3)对话框:
定位到一个 Dialog 资源,发现有点一头雾水,或许这里是经过序列化的二进制数据了。这里我没有进一步研究,如果要研究,可能可以参考《The Old New Things》(中文名:Windows 编程启示录)中关于对话框模板的介绍。
好了,这篇文章到这里差不多了。看起来很简单,也许这样去做的意义很有限,因为PE文件格式通常不是开发者关心的层面(除了搞破解和编写病毒),但现在我们大概可以想象下 MS 自己是如何解析 PE 文件的资源的。而 ResHack 之类的工具有可能是借助于 MS 提供的 API 来解析资源的。
【补充】在本文和上一篇文章中,我们主要通过 ImageRvaToVa 在 RVA 和文件地址中进行转换,哪么如果我们没有把文件映射到内存呢,当然我们也可以直接通过节表来自己做RVA 到文件地址或反向的转换。这里我们需要强调的是,在一个节内部,RVA 和 文件地址之间是一个常数差值(这是显然的,因为都是一个节的有效数据在文件中和内存中是相同的尺寸)。但是在不同节之间,这个差值是不同的。所以我们通常需要首先确定一个RVA位于那一个节中,然后根据节表中的信息换算出文件地址。为什么这个偏差和所在节有关呢,这是因为FileAlignment小于等于SectionAlignment,在小于的情况下,虚拟内存中的节间距比在文件中要大,所以不同节的两个地址偏差不同,越往高地址这个偏差会越大,如下图所示。
如果 FileAlignment 等于 SectionAlignment,哪么很可能 RVA 就是文件地址,换句话说,你在文件中看到的那些节数据基本是可以按照文件中的原样映射到进程空间的,这种是最简单的情况了。当然我们必须考虑所有情况,因此如果你从文件中读取了节表,可以用下面的代码来把RVA换算成文件地址:
//两者之间的偏差和段有关,随着段的不同而不同
//所以必须先确定rva位于那个段,再从该段的信息中获取文件地址
DWORD CPeFile::RvaToFileAddress(DWORD rva)
{
int i, iSection = -1;
if(this->m_pSectionHeaders == NULL)
return 0;
//查找该Rva位于那个段中
for(i=0; i<this->m_ntHeaders.FileHeader.NumberOfSections; i++)
{
if(rva >= this->m_pSectionHeaders[i].VirtualAddress
&& (rva <= this->m_pSectionHeaders[i].VirtualAddress + this->m_pSectionHeaders[i].Misc.VirtualSize))
{
//该rva位于该段
iSection = i;
break;
}
}
//未找到?
if(iSection < 0) return 0;
//换算
return this->m_pSectionHeaders[iSection].PointerToRawData +
(rva - this->m_pSectionHeaders[iSection].VirtualAddress);
}
【参考资料】
(1)看雪论坛的精华合集中关于PE知识的一些文章;
(2)Platform SDK 中 winnt.h 文件中的代码和注释。
(3)ICONPRO 源代码(MSDN中的例子)。
【修订历史】
(1)补充和修改对 GroupIcon 和 Icon 资源数据的准确描述。2010-12-27 18:42。