读取PE文件的资源表

简介:     在上一篇文章里,已经讲解了加载PE文件的导入表。本篇简要介绍PE文件的资源表的结构和定位方式。 所谓资源表(resource table),就是通常在IDE的资源视图中所看到的那个Tree视图,因此资源表在PE文件中同样是这样的一种类似资源管理器一样的树状逻辑结构。

    在上一篇文章里,已经讲解了加载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
//  加载PE文件
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存储的。所以我们在编码的时候不管你的项目用什么字符编码,如果要同时解析导入表和资源树,你重要做一次多字节和宽字符之间的转换。在同一个文件中同时使用两种编码,两种定义的字符串,这是有点让人感觉怪异的地方。

    这个小程序的运行效果如下,在打开的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 的资源数据如下:

 

 
 
// 备注:此处代码引用自 ICONPRO

#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换算成文件地址:

 

code_RvaToFileAddress
// 注意sections在文件中以fileAlignment对齐,在进程中以sectionAlignment对齐
// 两者之间的偏差和段有关,随着段的不同而不同
// 所以必须先确定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。

目录
相关文章
|
7月前
|
存储 运维 监控
“df -i” 以inode模式来显示磁盘使用情况--这是什么意思?
“df -i” 以inode模式来显示磁盘使用情况--这是什么意思?
150 0
|
5月前
EMCpower PowerPath 修改磁盘名称一致
EMCpower PowerPath 修改磁盘名称一致
46 1
|
安全 编译器 API
2.5 PE结构:导入表详细解析
导入表(Import Table)是Windows可执行文件中的一部分,它记录了程序所需调用的外部函数(或API)的名称,以及这些函数在哪些动态链接库(DLL)中可以找到。在Win32编程中我们会经常用到导入函数,导入函数就是程序调用其执行代码又不在程序中的函数,这些函数通常是系统提供给我们的API,在调用者程序中只保留一些函数信息,包括函数名机器所在DLL路径。
205 1
|
存储 Windows
2.6 PE结构:导出表详细解析
导出表(Export Table)是Windows可执行文件中的一个结构,记录了可执行文件中某些函数或变量的名称和地址,这些名称和地址可以供其他程序调用或使用。当PE文件执行时Windows装载器将文件装入内存并将导入表中登记的DLL文件一并装入,再根据DLL文件中函数的导出信息对可执行文件的导入表(IAT)进行修正。
244 1
|
存储 算法 编译器
2.7 PE结构:重定位表详细解析
重定位表(Relocation Table)是Windows PE可执行文件中的一部分,主要记录了与地址相关的信息,它在程序加载和运行时被用来修改程序代码中的地址的值,因为程序在不同的内存地址中加载时,程序中使用到的地址也会受到影响,因此需要重定位表这个数据结构来完成这些地址值的修正。当程序需要被加载到不同的内存地址时,相关的地址值需要进行修正,否则程序运行会出现异常。而重定位表就是记录了在程序加载时需要修正的地址值的相关信息,包括修正地址的位置、需要修正的字节数、需要修正的地址的类型等。重定位表中的每个记录都称为一项(entry),每个entry包含了需要修正的地址值的详细信息,通常是以可变
212 0
2.7 PE结构:重定位表详细解析
|
存储 安全 API
2.1 PE结构:文件映射进内存
PE结构是`Windows`系统下最常用的可执行文件格式,理解PE文件格式不仅可以理解操作系统的加载流程,还可以更好的理解操作系统对进程和内存相关的管理知识,在任何一款操作系统中,可执行程序在被装入内存之前都是以文件的形式存放在磁盘中的,在早期DOS操作系统中,是以COM文件的格式存储的,该文件格式限制了只能使用代码段,堆栈寻址也被限制在了64KB的段中,由于PC芯片的快速发展这种文件格式极大的制约了软件的发展。
900 0
|
存储 缓存 固态存储
【Linux】基础IO --- 内核级和用户级缓冲区、磁盘结构、磁盘的分治管理、block group块组剖析…
【Linux】基础IO --- 内核级和用户级缓冲区、磁盘结构、磁盘的分治管理、block group块组剖析…
|
存储 API 数据安全/隐私保护
PE格式:导入表与IAT内存修正
关于Dump内存原理,我们可以使用调试API启动调试事件,然后再程序的OEP位置写入CC断点让其暂停在OEP位置,此时程序已经在内存解码,同时也可以获取到程序的OEP位置,转储就是将程序原封不动的读取出来并放入临时空间中,然后对空间中的节表和OEP以及内存对齐进行修正,最后将此文件在内存保存出来即可。
322 0
PE格式:导入表与IAT内存修正
|
安全 小程序 Shell
PE格式:新建节并插入代码
PE格式是 Windows下最常用的可执行文件格式,理解PE文件格式不仅可以了解操作系统的加载流程,还可以更好的理解操作系统对进程和内存相关的管理知识,而有些技术必须建立在了解PE文件格式的基础上,如文件加密与解密,病毒分析,外挂技术等。
256 0
PE格式:新建节并插入代码
|
存储 安全 数据安全/隐私保护
PE格式:新建节并插入DLL
PE格式是 Windows下最常用的可执行文件格式,理解PE文件格式不仅可以了解操作系统的加载流程,还可以更好的理解操作系统对进程和内存相关的管理知识,而有些技术必须建立在了解PE文件格式的基础上,如文件加密与解密,病毒分析,外挂技术等。
237 0
PE格式:新建节并插入DLL