PE 文件的資源區 (.rsrc 區段) 採用三層樹狀結構組織:**類型 -> 名稱/ID -> 語言 -> 數據**。本文基於 Microsoft PE/COFF 規格與實戰逆向經驗,詳細解析 IMAGE_RESOURCE_DIRECTORY、IMAGE_RESOURCE_DIRECTORY_ENTRY、IMAGE_RESOURCE_DIR_STRING_U、IMAGE_RESOURCE_DATA_ENTRY 四大核心結構,重點闡述「高位標誌位」編碼邏輯、Unicode 字串存儲、RVA 定位數據的完整路徑,並提供 Delphi/C++ 對應聲明與遍歷偽代碼。
.rsrc 區段 (PointerToRawData 開始)
+--------------------------+
| 根資源目錄 | (IMAGE_RESOURCE_DIRECTORY)
| NumberOfNamedEntries |
| NumberOfIdEntries |
+--------------------------+
| 目錄項陣列 | (IMAGE_RESOURCE_DIRECTORY_ENTRY[]) 排序: 名稱優先、升序
| [0] Type: RT_ICON (3) | -> 指向 子目錄 (Type 節點)
| [1] Type: RT_BITMAP(2) | -> 指向 子目錄
| [2] Name: "MY_RES" | -> 指向 子目錄 (Name 節點, NameIsString=1)
+--------------------------+
| 子目錄: Type=RT_ICON | (IMAGE_RESOURCE_DIRECTORY)
| ... | 項目: ID=101 (圖標組 ID) -> 指向語言子目錄
+--------------------------+
| 子目錄: Language=0x0409 | (IMAGE_RESOURCE_DIRECTORY)
| 項目: DataIsDirectory=0| -> 指向 資源數據項
+--------------------------+
| 資源數據項 | (IMAGE_RESOURCE_DATA_ENTRY)
| OffsetToData (RVA) | -> 實際資源數據 (圖標位圖數據...)
| Size |
+--------------------------+
| 實際資源數據區塊 | (原始位元組)
+--------------------------+
typedef struct _IMAGE_RESOURCE_DIRECTORY {
DWORD Characteristics; // 保留,通常 0
DWORD TimeDateStamp; // 資源編譯時間戳
WORD MajorVersion; // 版本號
WORD MinorVersion;
WORD NumberOfNamedEntries; // 使用字串名稱的項目數 (NameIsString=1)
WORD NumberOfIdEntries; // 使用整數 ID 的項目數 (NameIsString=0)
// 緊接著: IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[];
} IMAGE_RESOURCE_DIRECTORY;
NumberOfNamedEntries + NumberOfIdEntries。
typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
union {
struct { DWORD NameOffset:31; DWORD NameIsString:1; }; // 位域
DWORD Name; // 整體視圖
WORD Id; // 別名 (低 16 位)
};
union {
DWORD OffsetToData; // 整體視圖
struct { DWORD OffsetToDirectory:31; DWORD DataIsDirectory:1; };
};
} IMAGE_RESOURCE_DIRECTORY_ENTRY;
| 字段 | 位置 | 含義 | 處理方式 |
|---|---|---|---|
NameIsString | 最高位 (0x80000000) | 1 = 名稱是字串 (RVA 指向 IMAGE_RESOURCE_DIR_STRING_U)0 = 名稱是整數 ID (低 16/32 位) | if (entry.Name & 0x80000000) { // 字串 } else { // ID } |
DataIsDirectory | 最高位 (0x80000000) | 1 = 指向子目錄 (IMAGE_RESOURCE_DIRECTORY)0 = 指向資源數據項 ( IMAGE_RESOURCE_DATA_ENTRY) | if (entry.OffsetToData & 0x80000000) { // 子目錄 } else { // 數據 } |
NameOffset / OffsetToDirectory | 低 31 位 (0x7FFFFFFF) | 相對於**資源目錄區塊起始**的偏移量 (非 RVA,非檔案偏移) | DWORD offset = entry.OffsetToData & 0x7FFFFFFF; |
typedef struct _IMAGE_RESOURCE_DIR_STRING_U {
WORD Length; // 字元數 (不含結尾 NULL)
WCHAR NameString[1]; // Unicode 字串內容 (變長)
} IMAGE_RESOURCE_DIR_STRING_U;
NameOffset 定位。
typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
DWORD OffsetToData; // **RVA** (相對 ImageBase),指向實際資源數據
DWORD Size; // 數據大小 (位元組)
DWORD CodePage; // 代碼頁 (0=默認, 1200=Unicode, 65001=UTF-8)
DWORD Reserved; // 0
} IMAGE_RESOURCE_DATA_ENTRY;
OffsetToDirectory 是**相對資源目錄起始的偏移**;數據項的 OffsetToData 是**相對 ImageBase 的 RVA**。FileOffset = RVAToFileOffset(OffsetToData) (需區段表協助)。
// Delphi 風格
type
PIMAGE_RESOURCE_DIRECTORY = ^IMAGE_RESOURCE_DIRECTORY;
IMAGE_RESOURCE_DIRECTORY = packed record
Characteristics: DWORD;
TimeDateStamp: DWORD;
MajorVersion: WORD;
MinorVersion: WORD;
NumberOfNamedEntries: WORD;
NumberOfIdEntries: WORD;
end;
PIMAGE_RESOURCE_DIRECTORY_ENTRY = ^IMAGE_RESOURCE_DIRECTORY_ENTRY;
IMAGE_RESOURCE_DIRECTORY_ENTRY = packed record
case Integer of
0: (Name: DWORD); // 或 NameOffset/NameIsString
1: (Id: WORD);
case Integer of
0: (OffsetToData: DWORD);
1: (OffsetToDirectory: DWORD); // 或 DataIsDirectory
end;
PIMAGE_RESOURCE_DIR_STRING_U = ^IMAGE_RESOURCE_DIR_STRING_U;
IMAGE_RESOURCE_DIR_STRING_U = packed record
Length: WORD;
NameString: array[0..0] of WideChar;
end;
PIMAGE_RESOURCE_DATA_ENTRY = ^IMAGE_RESOURCE_DATA_ENTRY;
IMAGE_RESOURCE_DATA_ENTRY = packed record
OffsetToData: DWORD;
Size: DWORD;
CodePage: DWORD;
Reserved: DWORD;
end;
function EnumResources(ModuleBase: Pointer; ResDirRVA: DWORD; Level: Integer = 0):
var
ResDir: PIMAGE_RESOURCE_DIRECTORY;
Entry: PIMAGE_RESOURCE_DIRECTORY_ENTRY;
i: Integer;
NameStr: string;
DataEntry: PIMAGE_RESOURCE_DATA_ENTRY;
DataRVA, DataSize: DWORD;
DataPtr: Pointer;
begin
// 1. 定位當前目錄 (RVA -> VA)
ResDir := PIMAGE_RESOURCE_DIRECTORY(DWORD(ModuleBase) + ResDirRVA);
Entry := PIMAGE_RESOURCE_DIRECTORY_ENTRY(ResDir + 1); // 緊接頭部之後
// 2. 遍歷所有項目
for i := 0 to ResDir.NumberOfNamedEntries + ResDir.NumberOfIdEntries - 1 do
begin
// 3. 解析名稱/ID
if (Entry[i].Name and $80000000) <> 0 then begin // 字串名
NameStr := GetResourceString(ModuleBase, Entry[i].Name and $7FFFFFFF);
end else begin // 整數 ID
NameStr := Format('#%d', [Entry[i].Id]);
end;
// 4. 判斷子目錄還是數據
if (Entry[i].OffsetToData and $80000000) <> 0 then begin // 子目錄
EnumResources(ModuleBase,
Entry[i].OffsetToData and $7FFFFFFF, // 相對資源目錄起始偏移
Level + 1);
end else begin // 葉子節點: 數據項
DataEntry := PIMAGE_RESOURCE_DATA_ENTRY(
DWORD(ModuleBase) + (Entry[i].OffsetToData and $7FFFFFFF) // 注意: 這裡 OffsetToData 實為數據項 RVA?
// 修正: 目錄項 OffsetToData 低31位指向 IMAGE_RESOURCE_DATA_ENTRY 結構
// 該結構位於資源區塊內,其 RVA = 資源目錄起始 RVA + OffsetToData
);
DataRVA := DataEntry.OffsetToData; // 實際數據 RVA
DataSize := DataEntry.Size;
DataPtr := Pointer(DWORD(ModuleBase) + RVAToFileOffset(DataRVA)); // 實際指標
// 此處處理資源數據: 圖標、位圖、版本資訊、自訂數據...
ProcessResourceData(NameStr, DataPtr, DataSize, DataEntry.CodePage);
end;
end;
end;
OffsetToDirectory (低 31 位) = **相對資源目錄根節點起始的偏移** (非 RVA)。OffsetToData = **實際資源數據 RVA** (相對 ImageBase)。NameOffset 指向 IMAGE_RESOURCE_DIR_STRING_U,需讀取 Length 後複製 Length 個 WCHAR。LANGIDFROMLCID 宏可提取。PE 資源目錄樹雖結構精巧,但「雙高位標誌位 + 兩種不同基準偏移」極易導致解析錯誤。牢記:
NameIsString 決定名稱解析方式。DataIsDirectory 決定遞歸還是讀數據。掌握上述核心邏輯,即可編寫健壯的 PE 資源解析器、圖標提取器、版本資訊讀取器或資源修改工具。