2.4 PE結構:節表詳細解析

2023-09-05 12:01:27

節表(Section Table)是Windows PE/COFF格式的可執行檔案中一個非常重要的資料結構,它記錄了各個程式碼段、資料段、資源段、重定向表等在檔案中的位置和大小資訊,是作業系統載入檔案時根據節表來進行各個段的對映和初始化的重要依據。節表中的每個記錄則被稱為IMAGE_SECTION_HEADER,它記錄了一個段的各種屬性資訊和在檔案中的位置和大小等資訊,一個檔案可以由多個IMAGE_SECTION_HEADER構成。

在執行PE檔案的時候,Windows 並不在一開始就將整個檔案讀入記憶體,PE裝載器在裝載的時候僅僅建立好虛擬地址和PE檔案之間的對映關係,只有真正執行到某個記憶體頁中的指令或者存取頁中的資料時,這個頁面才會被從磁碟提交到記憶體中,這種機制極大的節約了記憶體資源,使檔案的裝入速度和檔案的大小沒有太多的關係。

Windows 裝載器在裝載DOS部分PE檔案頭部分和節表部分時不進行任何處理,而在裝載節區的時候會根據節的不同屬性做不同的處理,一般需要處理以下幾個方面的內容:

節區的屬性: 節是相同屬性的資料的組合,當節被裝入記憶體的時候,同一個節對應的記憶體頁面將被賦予相同的頁屬性,Windows系統對記憶體屬性的設定是以頁為單位進行的,所以節在記憶體中的對其單位必須至少是一個頁的大小,對於X86來說這個值是4KB(1000h),而對於X64來說這個值是8KB(2000h),磁碟中儲存的程式並不會對齊4KB,而只有被PE載入器載入記憶體的時候,PE裝載器才會自動的補齊4KB對其的零頭資料。

節區的偏移: 節的起始地址在磁碟檔案中是按照IMAGE_OPTIONAL_HEADER結構的FileAhgnment欄位的值對齊的,而被載入到記憶體中時是按照同一結構中的SectionAlignment欄位的值對齊的,兩者的值可能不同,所以一個節被裝入記憶體後相對於檔案頭的偏移和在磁碟檔案中的偏移可能是不同的。

節區的尺寸: 由於磁碟映像和記憶體映像的對齊單位不同,磁碟中的映像在裝入記憶體後會自動的進行長度擴充套件,而對於未初始化的資料段(.data?)來說,則沒有必要為它在磁碟檔案中預留空間,只要可執行檔案裝入記憶體後動態的為其分配空間即可,所以包含未初始化資料的節在磁碟中長度被定義為0,只有在執行後PE載入器才會動態的為他們開闢空間。

不進行對映的節: 有些節中包含的資料僅僅是在裝入的時候用到,當檔案裝載完畢時,他們不會被遞交到實體記憶體中,例如重定位節,該節的資料對於檔案的執行程式碼來說是透明的,他只供Windows裝載器使用,可執行程式碼根本不會存取他們,所以這些節存在於磁碟檔案中,不會被對映到記憶體中。

一般來說,當一個PE檔案被編譯生成時則預設會存在.text,.data等基本節表,而每一個節表都是由一個IMAGE_SECTION_HEADER結構排列而成,每個結構都用來描述一個節,節表總被存放在緊接在PE檔案頭的地方,也即是從PE檔案頭開始偏移為00f8h的位置,針對每一個節中的定義可檢視節表結構體的定義;

typedef struct _IMAGE_SECTION_HEADER
{
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];
    union {
            DWORD   PhysicalAddress;
            DWORD   VirtualSize;           // 節區尺寸
    } Misc;
    DWORD   VirtualAddress;                // 節區RVA
    DWORD   SizeOfRawData;                 // 在檔案中對齊後的尺寸
    DWORD   PointerToRawData;              // 在檔案中的偏移
    DWORD   PointerToRelocations;          // 在OBJ檔案中使用
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;               // 節區屬性欄位
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

針對IMAGE_SECTION_HEADER中各個欄位的詳細解析:

  • Name:段名,是一個8位元組的ASCII字串,不足8位元組用0補齊。

  • VirtualSize:虛擬大小,標識在記憶體中佔用的大小,請勿與PhysicalSize(物理大小)混淆。

  • VirtualAddress:虛擬地址,標識在記憶體中對應段頭的地址,與實際載入的位置有關。

  • SizeOfRawData:物理大小,標識在PE檔案中該段的佔用大小,不足以檔案對齊單位則會進行填充。

  • PointerToRawData:實體地址,標識該段在檔案中的偏移位置。

  • PointerToRelocations:重定向表的偏移位置。

  • PointerToLinenumbers:行號表的偏移位置。

  • NumberOfRelocations:重定向表數量。

  • NumberOfLinenumbers:行號表數量。

  • Characteristics:標識該段的各種屬性資訊,包括下列常用屬性:

    • IMAGE_SCN_MEM_READ:可讀;
    • IMAGE_SCN_MEM_WRITE:可寫;
    • IMAGE_SCN_MEM_EXECUTE:可執行;
    • IMAGE_SCN_CNT_CODE:程式碼段;
    • IMAGE_SCN_CNT_INITIALIZED_DATA:已初始化資料段;
    • IMAGE_SCN_CNT_UNINITIALIZED_DATA:未初始化資料段;
    • IMAGE_SCN_LNK_INFO:包含附加資訊。

與資料目錄表的列舉方式基本一致,資料目錄表的列舉也不會太難,讀者只需要通過NtHeader->FileHeader.NumberOfSections獲取到當前有多少個節,並通過迴圈的方式依次得到這些節中的指標,並將該指標轉換為PIMAGE_SECTION_HEADER結構,依次迴圈輸出即可得到;

int main(int argc, char * argv[])
{
    BOOL PE = IsPeFile(OpenPeFile("c://pe/x86.exe"), 0);

    if (PE == TRUE)
    {
        printf("編號\t 節區名稱\t虛擬偏移\t虛擬大小\t實際偏移\t實際大小\t節區屬性\n");

        for (DWORD each = 0; each < NtHeader->FileHeader.NumberOfSections; each++, pSection++)
        {
            printf("%d\t %-9s\t 0x%.8X \t 0x%.8X \t 0x%.8X \t 0x%.8X \t 0x%.8X \n",
                each + 1, pSection->Name, pSection->VirtualAddress, pSection->Misc.VirtualSize,
                pSection->PointerToRawData, pSection->SizeOfRawData, pSection->Characteristics);
        }
    }
    else
    {
        printf("非標準程式 \n");
    }

    system("pause");
    return 0;
}

執行上述程式,即可輸出當前程式中存在的節表資訊,輸出效果如下圖所示;