首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> VC >> VC++

PE 檔案資源提取核心技術:從 DOS Header 到位圖資源導出的完整解析

     編輯:VC++

前言:PE 格式是 Windows 執行檔的通用語言

工具如 ExeScope、Resource Hacker、Visual Studio 均能瀏覽/修改 PE (Portable Executable) 檔案內嵌資源 (圖示、位圖、對話框、字串表、版本資訊等)。本文深入剖析 PE 資源目錄結構,示範如何以純程式碼解析並導出位圖資源,無需依賴第三方庫。

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;

資源提取核心演算法

步驟 1:驗證 PE 檔案並定位區段表

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;
}

步驟 2:遞迴遍歷資源目錄樹


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
            });
        }
    }
}

步驟 3:RVA 轉檔案偏移並讀取資料


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_*)

常數值說明
RT_CURSOR1游標
RT_BITMAP2位圖
RT_ICON3圖示
RT_MENU4選單
RT_DIALOG5對話框
RT_STRING6字串表
RT_FONTDIR7字型目錄
RT_FONT8字型
RT_ACCELERATOR9快捷鍵
RT_RCDATA10原始資料
RT_MESSAGETABLE11訊息表
RT_VERSION16版本資訊
RT_MANIFEST24啟動清單

常見坑點與解決方案

⚠ 1. RVA ≠ File Offset

資源目錄項目中的 OffsetToData 為 RVA (Relative Virtual Address),必須透過區段表轉換為檔案實際偏移:FileOffset = RVA - Section.VirtualAddress + Section.PointerToRawData。

⚠ 2. 資源位圖缺少 FILEHEADER

PE 資源中的位圖資料僅包含 BITMAPINFOHEADER + 調色板 + 像素資料,缺少 BITMAPFILEHEADER。導出 .bmp 時必須自行構造檔案頭。

⚠ 3. 調色板大小動態計算

biClrUsed 為 0 時表示使用最大調色板 (1 << biBitCount)。位元深度 > 8 (真彩色) 無調色板。

⚠ 4. 字串資源編碼為 Unicode

RT_STRING 資料為 UTF-16LE,每字串前 2 位元組為長度,需正確解碼。

⚠ 5. 壓縮資源 (Windows 10+)

新版 Windows 可能使用 LZMS 壓縮資源,需檢查 IMAGE_RESOURCE_DATA_ENTRY.CodePage 或區段特徵判斷並解壓。

現代化工具鏈建議

  • C/C++: pe-parse, libpeconv, PeLib。
  • C#: PeNet, Mono.Cecil (側重 IL, 亦支援資源), System.Reflection.Assembly.GetManifestResourceStream (僅限 .NET 資源)。
  • Python: pefile (最熱門), lief (跨平台)。
  • Rust: goblin, pelite。
  • 命令列: wrestool (icoutils), ResourceHacker (CLI 模式)。

結語

PE 資源提取看似神秘,實則是結構化二進位解析的標準練習:驗證簽名 → 定位區段 → 遍歷目錄樹 → RVA 轉換 → 型別處理。掌握 IMAGE_RESOURCE_DIRECTORY 三層樹狀結構與 BITMAPFILEHEADER 補全技巧,即可開發屬於自己的資源檢視/修改工具。現代專案建議優先使用成熟庫 (pefile, PeNet) 避免重複造輪子,但在嵌入式、無依賴、安全分析等場景,手寫解析器仍具不可替代價值。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved