.NET Manifest Resources






3.86/5 (8投票s)
一篇关于 .NET Manifest Resources 内部格式的文章。
引言
本文讨论的是 .NET Manifest Resources 的内部格式(或者更确切地说,是其中包含的 ".resources" 文件)。我不知道这里的代码是否对你有用(很可能没用),但我喜欢撰写关于未公开主题的文章。事实上,这篇文章没什么了不起的,我写它只是因为我在网上找不到任何关于这个主题的文档,甚至在 .NET 元数据规范(Partition II MetaData.doc)中也没有。
前段时间,我(因为需要)编写了一个名为 CFF Explorer 的 PE 编辑器,并支持 .NET 元数据,因为当时没有这样的工具。我能找到的唯一工具是 Asmex(你可以在 CSDN 上找到它),但这个工具的问题在于你无法修改元数据字段,而且它仍然依赖于 .NET Framework。我并不是想批评 Asmex,它肯定很有用,只是因为我需要一些不同的东西。总之,我为 PE 编辑器编写了一个资源查看器,并想展示元数据资源。所以,为了做到这一点,并且避免使用外部 .NET 程序集,我不得不分析 Manifest Resource 格式。
让我们来看看 .NET 程序集中的 Manifest Resources
正如你所见,可以有各种类型的文件。例如,读取一个位图非常简单:每个 Manifest Resource 都以一个 DWORD
开头,它告诉我们实际嵌入资源的大小……仅此而已……之后,就是我们的位图。好的,但那些 ".resources" 文件呢?这篇文章就全是关于它们的。在这个截图中,.NET 程序集中的每个对话框都有一个 ".resources" 文件,这意味着对话框的所有资源都包含在对话框自己的 ".resources" 文件中。
使用 .NET Framework 来处理这些已编译的资源文件非常容易。你可以通过一个名为 Resgen.exe 的实用工具(可在 MSDN 上下载)将它们转换为 XML 文件(".resx" 文件),或者直接使用 System.Resources
命名空间下的成员(它们也能处理 ".resx" 文件)来做任何你想做的事情。例如,你可以创建一个(代码来自 MSDN)
using System;
using System.Resources;
public class WriteResources {
public static void Main(string[] args) {
// Creates a resource writer.
IResourceWriter writer = new ResourceWriter("myResources.resources");
// Adds resources to the resource writer.
writer.AddResource("String 1", "First String");
writer.AddResource("String 2", "Second String");
writer.AddResource("String 3", "Third String");
// Writes the resources to the file or stream, and closes it.
writer.Close();
}
}
通过使用 ResourceWriter
,创建资源文件是一项非常容易的任务。那么如何读取它们呢?
using System;
using System.Resources;
using System.Collections;
public class ReadResources {
public static void Main(string[] args) {
// Opens a resource reader and gets an enumerator from it.
IResourceReader reader = new ResourceReader("myResources.resources");
IDictionaryEnumerator en = reader.GetEnumerator();
// Goes through the enumerator, printing out the key and value pairs.
while (en.MoveNext()) {
Console.WriteLine();
Console.WriteLine("Name: {0}", en.Key);
Console.WriteLine("Value: {0}", en.Value);
}
reader.Close();
}
}
确实非常容易。通过 IResourceReader
接口,我们可以请求一个枚举器,它会给我们每一个资源的名称和值。也可以直接从文件中加载特定资源等等。所以,正如你所见,Framework 提供了我们处理资源文件所需的一切。无论如何,对于那些仍然对了解内部格式感兴趣的人,请继续阅读。
资源文件格式
一个非常简短的描述。第一个 DWORD
是一个签名,必须是 0xBEEFCACE,否则资源文件将被视为无效。第二个 DWORD
包含此资源文件的读取器数量,别担心,这不是我们需要讨论的……这是 Framework 的东西。第三个 DWORD
是读取器类型的大小。这个数字仅对我们跳过后面的字符串(或字符串)有好处,字符串类似:"System.Resources.ResourceReader, mscorlibsSystem.Resources.RuntimeResourceSet, mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"。它告诉 Framework 使用哪个读取器来处理这个资源文件。
好了,现在我们进入有趣的部分。下一个 DWORD
告诉我们资源文件的版本(现有版本是 1 和 2)。在版本之后,另一个 DWORD
给出了文件中实际资源的数量。再紧随其后的是另一个 DWORD
,它给出资源类型的数量。
为了收集我们所需的附加信息,我们需要跳过资源类型。对于每种类型,都有一个 7 位编码的整数,它给出后面字符串的大小。要解码这类整数,你需要读取每个字节,直到找到一个最高位未设置的字节,并进行一些额外的操作来获得最终值……目前,我们先坚持格式。在跳过类型之后,我们需要将我们的位置对齐到 8 字节基准。然后我们有 DWORD
* NumberOfResources
,每个 DWORD
包含一个资源的哈希值。然后我们有相同数量的 DWORD
,这次是资源名称的偏移量。紧接着一个重要的 DWORD
:数据部分偏移量。我们需要这个偏移量来检索资源偏移量。在这个 DWORD
之后,就是资源名称。好吧,实际上不仅仅是名称(我只是这样称呼它),每个名称(7 位编码整数 + Unicode 字符串)后面都有一个 DWORD
,这是一个偏移量,你可以将其加到数据部分偏移量来检索资源偏移量。给定资源偏移量后,我们找到的第一项是一个 7 位编码的整数,它是当前资源的类型索引。
源代码
我把所有代码都放在一个简单的类里了
class CResourcesFile
{
public:
CResourcesFile();
~CResourcesFile();
BYTE *pBaseAddress;
UINT Size;
DWORD Version;
DWORD NumberOfResources;
DWORD NumberOfTypes;
BYTE *pTypes;
BYTE *pNamesOffsets;
BYTE *pDataSection;
BYTE *pNames;
BOOL ProcessResourcesFile(BYTE *pAddress, UINT uSize);
BOOL ReadName(UINT nResource, WCHAR *Str, UINT Len);
BOOL GetResourceInfo(UINT nResource, WCHAR *Str, UINT Len,
DWORD *Offset, INT *TypeIndex);
private:
BOOL DecodeInt(BYTE *pAddress, INT *Value, UINT *uSize);
};
这个类的使用非常简单,你只需将类及其所有成员复制/粘贴到你的项目中,并以某种方式像这样使用它
void main()
{
TCHAR FileName[MAX_PATH];
_tprintf(_T("Resources File to open:\n"));
_tscanf(_T("%s"), FileName);
//
// Open and read file
//
// ....
CResourcesFile ResFile;
if (ResFile.ProcessResourcesFile(BaseAddress, FileSize) == FALSE)
{
VirtualFree(BaseAddress, 0, MEM_RELEASE);
return;
}
_tprintf(_T("\n\nFile: %s\n"), FileName);
_tprintf(_T("Version: %d\n"), ResFile.Version);
_tprintf(_T("Number of resources: %d\n"), ResFile.NumberOfResources);
_tprintf(_T("Number of types: %d\n"), ResFile.NumberOfTypes);
_tprintf(_T("\nList resources:\n\n"));
WCHAR ResName[1024];
for (UINT x = 0; x < ResFile.NumberOfResources; x++)
{
DWORD Offset;
INT TypeIndex = 0;
if (ResFile.GetResourceInfo(x, ResName, 1024, &Offset, &TypeIndex))
{
_tprintf(_T("Name: %S - Offset: %08X - TypeIndex: %d\n"),
ResName, Offset, TypeIndex);
}
}
VirtualFree(BaseAddress, 0, MEM_RELEASE);
getch();
}
首先需要做的是使用 ProcessResourcesFile
函数处理资源文件
BOOL CResourcesFile::ProcessResourcesFile(BYTE *pAddress, UINT uSize)
{
BYTE *ptr = pAddress;
//
// Collect basic information: pointer and size of the file
//
pBaseAddress = ptr;
Size = uSize;
//
// Read the magic number, its value has to be: 0xBEEFCACE
//
DWORD MagicNumber;
MagicNumber = *(DWORD *) ptr;
ptr += sizeof (DWORD);
if (MagicNumber != RESOURCES_MAGIC_NUMBER)
return FALSE;
DWORD NumberOfReaderTypes;
NumberOfReaderTypes = *(DWORD *) ptr;
ptr += sizeof (DWORD);
DWORD SizeOfReaderTypes;
SizeOfReaderTypes = *(DWORD *) ptr;
ptr += sizeof (DWORD);
//
// Skip ReaderTypes
//
ptr += SizeOfReaderTypes;
//
Version = *(DWORD *) ptr;
ptr += sizeof (DWORD);
//
// Read number of resources
//
NumberOfResources = *(DWORD *) ptr;
ptr += sizeof (DWORD);
//
// Read number of types
//
NumberOfTypes = *(DWORD *) ptr;
ptr += sizeof (DWORD);
//
// Skip Types: (CHAR *Type;) * NumOfTypes
// (Save position)
//
pTypes = ptr;
for (UINT x = 0; x < NumberOfTypes; x++)
{
INT StringSize = 0;
UINT ValueSize = 0;
if (!DecodeInt(ptr, &StringSize, &ValueSize))
return FALSE;
ptr += ValueSize;
ptr += StringSize;
}
//
// Alignes position
//
DWORD Position = (DWORD) (((ULONG_PTR) ptr) - ((ULONG_PTR) pBaseAddress));
DWORD Aligned = Position & 7;
if (Aligned != 0)
{
ptr += (8 - Aligned);
}
//
// Skip name hashes
//
ptr += (sizeof (DWORD) * NumberOfResources);
//
// Skip name positions (first save location)
//
pNamesOffsets = ptr;
ptr += (sizeof (DWORD) * NumberOfResources);
//
// Read Data Section Offset
//
DWORD DataSectionOffset;
DataSectionOffset = *(DWORD *) ptr;
ptr += sizeof (DWORD);
pDataSection = (BYTE *) (DataSectionOffset + ((ULONG_PTR) pBaseAddress));
//
// Save names position
//
pNames = ptr;
return TRUE;
}
ReadName
只是 GetResourceInfo
的一个简短版本,所以我们只看 GetResourceInfo
,跳过其余部分
//
// Collect Resource Info
//
BOOL CResourcesFile::GetResourceInfo(UINT nResource, WCHAR *Str, UINT Len,
DWORD *Offset, INT *TypeIndex)
{
//
// Read name
//
DWORD NameOffset = *(DWORD *) ((nResource * sizeof (DWORD)) +
((ULONG_PTR) pNamesOffsets));
if (NameOffset > (DWORD) (((ULONG_PTR) pNames) - ((ULONG_PTR) pDataSection)))
return FALSE;
ZeroMemory(Str, Len * sizeof (WCHAR));
BYTE *ptr = (BYTE *) (NameOffset + ((ULONG_PTR) pNames));
INT NameSize = 0;
UINT ValueSize = 0;
if (!DecodeInt(ptr, &NameSize, &ValueSize))
return FALSE;
ptr += ValueSize;
memcpy(Str, ptr, NameSize);
ptr += NameSize;
//
// After reading the name
//
DWORD DataOffset = *(DWORD *) ptr;
BYTE *pData = (BYTE *) (DataOffset + ((ULONG_PTR) pDataSection));
//
// Collect info
//
if (Offset) *Offset = (DWORD) (((ULONG_PTR) pData) -
((ULONG_PTR) pBaseAddress));
if (TypeIndex)
{
*TypeIndex = 0;
ValueSize = 0;
if (!DecodeInt(pData, TypeIndex, &ValueSize))
return FALSE;
}
return TRUE;
}
就这样,希望它有所帮助。
后记
当然,这还不是全部,如果你想处理资源,你需要根据它们的类型来处理。这意味着你需要从 TypeIndex
中获取它们的类型。检索类型字符串非常简单;与其跳过所有类型,不如只跳过位于 TypeIndex
之前的那些类型,然后读取后面的字符串。在我的 CFF Explorer 中,我支持某些类型的资源(并且只显示那些),例如位图、图标和 PNG。
玩得开心!