工具如 ExeScope、Resource Hacker、Visual Studio 均能瀏覽/修改 PE (Portable Executable) 檔案內嵌資源 (圖示、位圖、對話框、字串表、版本資訊等)。本文深入剖析 PE 資源目錄結構,示範如何以純程式碼解析並導出位圖資源,無需依賴第三方庫。
+------------------------+
| DOS MZ Header | 傳統 DOS 執行檔頭 (offset 0)
| DOS Stub | "This program cannot be run in DOS mode"
+------------------------+
| PE Signature (PE\0\0) | offset 0x3C 指向此處
+------------------------+
| IMAGE_FILE_HEADER | COFF 檔案頭 (機器類型、段數、時間戳...)
+------------------------+
| IMAGE_OPTIONAL_HEADER32| 邏輯分佈資訊 (入口點、映像基址、段對齊、子系統...)
+------------------------+
| Section Table | 區段表陣列 (每區段 40 bytes)
| .text | 程式碼
| .data | 初始化資料
| .rsrc | ★ 資源區段 (重點)
| .reloc | 重定位表
+------------------------+
| Section Data... | 實際區段內容
winnt.h)// 區段表項目
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[8]; // 區段名 (如 ".rsrc\0\0\0")
union { DWORD PhysicalAddress; DWORD VirtualSize; } Misc;
DWORD VirtualAddress; // RVA (Relative Virtual Address)
DWORD SizeOfRawData; // 檔案中實際大小 (對齊後)
DWORD PointerToRawData; // 檔案偏移 (關鍵!)
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics; // 屬性 (可讀/可寫/可執行/含資料...)
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
// 資源目錄結構 (樹狀,三層:Type → Name/ID → Language)
typedef struct _IMAGE_RESOURCE_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
WORD NumberOfNamedEntries; // 以名稱索引
WORD NumberOfIdEntries; // 以 ID 索引
// 緊接 IMAGE_RESOURCE_DIRECTORY_ENTRY 陣列
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;
typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
union {
DWORD NameOffset; // 高位元=1 表示字串名稱 (offset to STRING)
DWORD Id; // 高位元=0 表示數值 ID
};
union {
DWORD OffsetToData; // 高位元=0: 資料項目 (leaf)
DWORD OffsetToDirectory; // 高位元=1: 子目錄 (branch)
};
} IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;
// 資料項目 (葉節點)
typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
DWORD OffsetToData; // RVA to actual resource data
DWORD Size; // 資料大小
DWORD CodePage; // 代碼頁
DWORD Reserved;
} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;
// 位圖檔案結構 (資源中的原始資料為 BITMAPINFO + pixels,缺少 FILEHEADER)
typedef struct _BITMAPFILEHEADER {
WORD bfType; // 'BM' (0x4D42)
DWORD bfSize; // 檔案總大小
WORD bfReserved1;
WORD bfReserved2;
DWORD bfOffBits; // 像素資料偏移
} BITMAPFILEHEADER;
typedef struct _BITMAPINFOHEADER {
DWORD biSize; // 40
LONG biWidth;
LONG biHeight;
WORD biPlanes; // 1
WORD biBitCount; // 1/4/8/16/24/32
DWORD biCompression; // BI_RGB=0
DWORD biSizeImage;
LONG biXPelsPerMeter;
LONG biYPelsPerMeter;
DWORD biClrUsed;
DWORD biClrImportant;
} BITMAPINFOHEADER;
bool IsValidPE(const BYTE* fileBase)
{
// 1. DOS 簽名 'MZ'
if (*(WORD*)fileBase != 0x5A4D) return false; // 'MZ'
// 2. 取得 PE Header 偏移 (offset 0x3C)
DWORD peOffset = *(DWORD*)(fileBase + 0x3C);
if (peOffset >= fileSize) return false;
// 3. PE 簽名 'PE\0\0'
if (*(DWORD*)(fileBase + peOffset) != 0x00004550) return false; // 'PE\0\0'
return true;
}
PIMAGE_SECTION_HEADER FindResourceSection(const BYTE* fileBase)
{
DWORD peOffset = *(DWORD*)(fileBase + 0x3C);
PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)(fileBase + peOffset);
PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNtHeaders);
for (int i = 0; i < pNtHeaders->FileHeader.NumberOfSections; ++i, ++pSection)
{
if (strncmp((char*)pSection->Name, ".rsrc", 5) == 0)
return pSection;
}
return NULL;
}
void EnumResources(const BYTE* fileBase, PIMAGE_SECTION_HEADER pRsrcSection,
PIMAGE_RESOURCE_DIRECTORY pDir, int level, std::vector& results)
{
// 計算目錄在檔案中的實際位置
// RVA → File Offset: RVA - Section.VirtualAddress + Section.PointerToRawData
DWORD dirRVA = (DWORD)((BYTE*)pDir - fileBase); // 簡化:假設映射完整映像
// 實際需轉換:dirRVA = pDirRVA - pRsrcSection->VirtualAddress + pRsrcSection->PointerToRawData
WORD namedCount = pDir->NumberOfNamedEntries;
WORD idCount = pDir->NumberOfIdEntries;
WORD totalEntries = namedCount + idCount;
PIMAGE_RESOURCE_DIRECTORY_ENTRY pEntry = (PIMAGE_RESOURCE_DIRECTORY_ENTRY)(pDir + 1);
for (int i = 0; i < totalEntries; ++i, ++pEntry)
{
bool isNamed = (pEntry->NameOffset & 0x80000000) != 0;
DWORD nameOrId = isNamed ? pEntry->NameOffset & 0x7FFFFFFF : pEntry->Id;
bool isSubDir = (pEntry->OffsetToDirectory & 0x80000000) != 0;
DWORD offset = isSubDir ? (pEntry->OffsetToDirectory & 0x7FFFFFFF) : pEntry->OffsetToData;
if (isSubDir)
{
// 遞迴進入子目錄
PIMAGE_RESOURCE_DIRECTORY pSubDir = (PIMAGE_RESOURCE_DIRECTORY)(fileBase + offset);
EnumResources(fileBase, pRsrcSection, pSubDir, level + 1, results);
}
else
{
// 葉節點:資料項目
PIMAGE_RESOURCE_DATA_ENTRY pDataEntry = (PIMAGE_RESOURCE_DATA_ENTRY)(fileBase + offset);
results.push_back({
level, nameOrId, isNamed,
pDataEntry->OffsetToData, // RVA
pDataEntry->Size,
pDataEntry->CodePage
});
}
}
}
DWORD RvaToFileOffset(PIMAGE_SECTION_HEADER pSection, DWORD rva)
{
if (rva >= pSection->VirtualAddress &&
rva < pSection->VirtualAddress + pSection->Misc.VirtualSize)
{
return rva - pSection->VirtualAddress + pSection->PointerToRawData;
}
return 0xFFFFFFFF; // 不在該區段
}
// 導出位圖資源
bool ExportBitmapResource(const BYTE* fileBase, PIMAGE_SECTION_HEADER pRsrcSection,
const ResourceInfo& res, const char* outputPath)
{
DWORD fileOffset = RvaToFileOffset(pRsrcSection, res.DataRVA);
if (fileOffset == 0xFFFFFFFF) return false;
const BYTE* pData = fileBase + fileOffset;
// 資源中的位圖資料 = BITMAPINFOHEADER + RGBQUAD[] + pixels
// 需自行構造 BITMAPFILEHEADER
BITMAPINFOHEADER* pInfo = (BITMAPINFOHEADER*)pData;
if (pInfo->biSize != sizeof(BITMAPINFOHEADER)) return false;
// 計算調色板大小
DWORD paletteSize = 0;
if (pInfo->biBitCount <= 8)
paletteSize = (pInfo->biClrUsed ? pInfo->biClrUsed : (1 << pInfo->biBitCount)) * sizeof(RGBQUAD);
DWORD pixelOffset = sizeof(BITMAPINFOHEADER) + paletteSize;
DWORD pixelSize = pInfo->biSizeImage ? pInfo->biSizeImage : (res.DataSize - pixelOffset);
BITMAPFILEHEADER fh = {0};
fh.bfType = 0x4D42; // 'BM'
fh.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + paletteSize;
fh.bfSize = fh.bfOffBits + pixelSize;
fh.bfReserved1 = fh.bfReserved2 = 0;
FILE* fp = fopen(outputPath, "wb");
if (!fp) return false;
fwrite(&fh, sizeof(fh), 1, fp);
fwrite(pData, res.DataSize, 1, fp); // 寫入完整 BITMAPINFO + pixels
fclose(fp);
return true;
}
PE File Mapping
│
▼
Verify DOS 'MZ' @ 0x00
│
▼
Read PE Offset @ 0x3C → PE Header
│
▼
Verify 'PE\0\0' Signature
│
▼
Parse Section Table → Find ".rsrc" Section
│
▼
Locate Resource Directory Root (Section.VirtualAddress → RVA)
│
▼
Recursive Walk: Type → Name/ID → Language → Data Entry
│
▼
For Each Data Entry:
│
├─► RVA → File Offset (using Section Header)
│
├─► Read Raw Data (Size bytes)
│
├─► Identify Type (RT_BITMAP=2, RT_ICON=3, RT_STRING=6, etc.)
│
└─► Process Accordingly:
BITMAP → Add FILEHEADER → Save .bmp
ICON → Parse ICONDIR → Save .ico
STRING → Decode Unicode → Save .txt
...
| 常數 | 值 | 說明 |
|---|---|---|
| RT_CURSOR | 1 | 游標 |
| RT_BITMAP | 2 | 位圖 |
| RT_ICON | 3 | 圖示 |
| RT_MENU | 4 | 選單 |
| RT_DIALOG | 5 | 對話框 |
| RT_STRING | 6 | 字串表 |
| RT_FONTDIR | 7 | 字型目錄 |
| RT_FONT | 8 | 字型 |
| RT_ACCELERATOR | 9 | 快捷鍵 |
| RT_RCDATA | 10 | 原始資料 |
| RT_MESSAGETABLE | 11 | 訊息表 |
| RT_VERSION | 16 | 版本資訊 |
| RT_MANIFEST | 24 | 啟動清單 |
資源目錄項目中的 OffsetToData 為 RVA (Relative Virtual Address),必須透過區段表轉換為檔案實際偏移:FileOffset = RVA - Section.VirtualAddress + Section.PointerToRawData。
PE 資源中的位圖資料僅包含 BITMAPINFOHEADER + 調色板 + 像素資料,缺少 BITMAPFILEHEADER。導出 .bmp 時必須自行構造檔案頭。
biClrUsed 為 0 時表示使用最大調色板 (1 << biBitCount)。位元深度 > 8 (真彩色) 無調色板。
RT_STRING 資料為 UTF-16LE,每字串前 2 位元組為長度,需正確解碼。
新版 Windows 可能使用 LZMS 壓縮資源,需檢查 IMAGE_RESOURCE_DATA_ENTRY.CodePage 或區段特徵判斷並解壓。
pe-parse, libpeconv, PeLib。PeNet, Mono.Cecil (側重 IL, 亦支援資源), System.Reflection.Assembly.GetManifestResourceStream (僅限 .NET 資源)。pefile (最熱門), lief (跨平台)。goblin, pelite。wrestool (icoutils), ResourceHacker (CLI 模式)。PE 資源提取看似神秘,實則是結構化二進位解析的標準練習:驗證簽名 → 定位區段 → 遍歷目錄樹 → RVA 轉換 → 型別處理。掌握 IMAGE_RESOURCE_DIRECTORY 三層樹狀結構與 BITMAPFILEHEADER 補全技巧,即可開發屬於自己的資源檢視/修改工具。現代專案建議優先使用成熟庫 (pefile, PeNet) 避免重複造輪子,但在嵌入式、無依賴、安全分析等場景,手寫解析器仍具不可替代價值。