PE檔案結構解析2

SecIN發表於2022-05-23

0x0導讀

上一篇文章把Dos頭,Nt頭,可選頭裡的一些成員說過了(文章連結:[PE檔案結構解析1-SecIN (sec-in.com)]),今天主要講的內容是,Rva(記憶體偏移)轉換Foa(檔案偏移),資料目錄表,節表,話不多說來看文章。

0x1環境

編譯器:VirsualStudio2022

16進位制檢視工具:winhex

0x2Rva與Foa轉換

rva到foa的意義:因為pe檔案在檔案中和在記憶體中的大小是不一樣的,在記憶體中,pe檔案會被拉伸,所以同一個地址在記憶體中和檔案中所指向的值是不一樣的,所以要把記憶體中的偏移轉換成檔案中的偏移,下面我們來看一下轉換的函式。

函式定義

DWORD rtf(PBYTE buffer, DWORD rva)
{
PIMAGE_DOS_HEADER doshd = (PIMAGE_DOS_HEADER)buffer;
PIMAGE_NT_HEADERS nthd = (PIMAGE_NT_HEADERS)(buffer + doshd->e_lfanew);
PIMAGE_FILE_HEADER filehd = (PIMAGE_FILE_HEADER)(buffer + doshd->e_lfanew + 4);
PIMAGE_OPTIONAL_HEADER32 optionhd = (PIMAGE_OPTIONAL_HEADER32)(buffer + doshd->e_lfanew + 24);
PIMAGE_SECTION_HEADER sectionhd = IMAGE_FIRST_SECTION(nthd);
for (int i = 0; i < filehd->NumberOfSections; i++)
{
if (rva >= sectionhd[i].VirtualAddress && rva <= sectionhd[i].VirtualAddress + sectionhd[i].SizeOfRawData)
{
return rva - sectionhd[i].VirtualAddress + sectionhd[i].PointerToRawData;
}
}
}

轉換函式程式碼解析:首先為這個函式定義了兩個引數,一個引數是基址用於定位各種頭,和節表,另一個引數是要轉換的rva,PIMAGE開頭的程式碼主要是定位頭和節表,為下面迴圈判斷做準備,rva是在節中的,所以只需要迴圈判斷,如果rva>=當前節的VirtualAddress又小於VirtualAddress+SizeOfRawData就可以判斷出這個rva在當前節,只需要減去VirtualAddress再加上PointerToRawData即可。

0x3資料目錄解析

資料目錄是可選頭的一個成員(對可選頭有疑問的可以看上一篇文章),這個成員是一個結構體型別的,像這個樣的成員一共有16個,也就是說資料目錄表其實就是16個這樣的結構體,結構體定義如下。

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;//一個rva,指向真正的資料表
    DWORD   Size;//資料表大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

這16個結構體對應的表分別是匯出表,匯入表,資源表,異常處理表,安全表,重定位表,除錯表,版權表,指標目錄,TLS表,載入配置表,繫結輸入表,匯入地址表,延遲載入表,COM資訊,保留最後一個表是保留起來的用不到。

結構體中的VirtualAddress成員是一個rva,透過這個rva可以找到真正的表所在的地方,我們可以把VirtualAddress中的值看作一個"中間人",可以透過這個"中間人"來找到真正的資料表。這裡要注意時16個這樣的結構。

wKg0C2I9fU6AY6udAABThbXhqKw341.png

Size表示當前結構體的VirtualAddress所指向表的大小。

程式碼解析

#include <stdio.h>
#include <Windows.h>
#define path "C:\\Users\\blue\\Desktop\\Dll1.dll"

void main()
{
FILE* fp = fopen(path, "rb");
fseek(fp, 0, SEEK_END);
int size = ftell(fp);
rewind(fp);
PBYTE ptr = (PBYTE)malloc(size);
memset(ptr, 0, size);
fread(ptr, size, 1, fp);
PIMAGE_DOS_HEADER Dos = (PIMAGE_DOS_HEADER)ptr;
PIMAGE_NT_HEADERS Nt = (PIMAGE_NT_HEADERS)(ptr + Dos->e_lfanew);
PIMAGE_FILE_HEADER File = (PIMAGE_FILE_HEADER)(ptr + Dos->e_lfanew + 4);
PIMAGE_OPTIONAL_HEADER32 Option = (PIMAGE_OPTIONAL_HEADER32)(ptr + Dos->e_lfanew + 20 + 4);
PIMAGE_DATA_DIRECTORY DataDir = Option->DataDirectory;
for (int i = 0; i < 17; i++)
{
printf("VirtualAddress:%x\n", DataDir[i].VirtualAddress);
printf("Size:%x\n", DataDir[i].Size);
}
getchar();
}

前三行程式碼主要是包含標頭檔案,定義宏作為fopen函式的引數。

FILE* fp = fopen(path, "rb");定義一個檔案指標來接受fopen函式返回值,fopen第一個引數是要開啟檔案的路徑,第二個引數是以什麼方式開啟,這裡是rb也就是以二進位制方式開啟一個檔案,只能讀不可以寫。

fseek(fp, 0, SEEK_END);第一個引數是要設定檔案的檔案指標,第二個引數是一個相對於第三個引數是一個偏移量,第三個引數SEEK_END代表檔案的末尾,程式碼大致意思檔案流重定向到檔案末尾。

int size = ftell(fp);定義一個變數用來接收ftell函式的返回值,ftell函式作用是計算檔案的大小,第一個引數是要計算那個檔案的檔案指標。

rewind(fp);將檔案流重定向到檔案開頭,為下面讀取資料做準備。

PBYTE ptr = (PBYTE)malloc(size);定義一個PBYTE型別的指標指向malloc函式申請的記憶體,malloc函式第一個引數是申請多大的記憶體,PBYTE就是char*

memset(ptr, 0, size);用0填充剛才申請的記憶體塊,第一個引數記憶體塊的地址,第二個引數用什麼填充,第三個引數填充多大。

fread(ptr, size, 1, fp);用於讀取資料到記憶體,第一個引數是要讀到哪裡,第二個引數是讀多少位元組,第三個引數讀多少次,第四個引數要讀取檔案的檔案指標。

PIMAGE_DOS_HEADER結構體定義

typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
    WORD   e_magic;                     // Magic number
    WORD   e_cblp;                      // Bytes on last page of file
    WORD   e_cp;                        // Pages in file
    WORD   e_crlc;                      // Relocations
    WORD   e_cparhdr;                   // Size of header in paragraphs
    WORD   e_minalloc;                  // Minimum extra paragraphs needed
    WORD   e_maxalloc;                  // Maximum extra paragraphs needed
    WORD   e_ss;                        // Initial (relative) SS value
    WORD   e_sp;                        // Initial SP value
    WORD   e_csum;                      // Checksum
    WORD   e_ip;                        // Initial IP value
    WORD   e_cs;                        // Initial (relative) CS value
    WORD   e_lfarlc;                    // File address of relocation table
    WORD   e_ovno;                      // Overlay number
    WORD   e_res[4];                    // Reserved words
    WORD   e_oemid;                     // OEM identifier (for e_oeminfo)
    WORD   e_oeminfo;                   // OEM information; e_oemid specific
    WORD   e_res2[10];                  // Reserved words
    LONG   e_lfanew;                    // File address of new exe header
  } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

PIMAGE_DOS_HEADER Dos = (PIMAGE_DOS_HEADER)ptr;定義一個PIMAGE_DOS_HEADER型別的結構體指標來指向剛才申請的那塊記憶體,也可以說用這塊記憶體中的資料來填充這個結構體指標所指向的結構體,因為ptr是PBYTE型別和PIMAGE_DOS_HEADER型別不同所以要把ptr從PBYTE強轉成PIMAGE_DOS_HEADER型別。

typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

PIMAGE_NT_HEADERS Nt = (PIMAGE_NT_HEADERS)(ptr + Dos->e_lfanew);定義一個PIMAGE_NT_HEADERS32型別的結構體指標在·透過基址+偏移的方式定位到Nt頭,也就是ptr與Dos頭的e_lfanew成員相加得到一個地址,這個地址就是Nt頭開始的地方,也可理解為用這個地址中的資料填充這個結構體指標指向的結構體。

PIMAGE_FILE_HEADER結構體定義

typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;
    WORD    NumberOfSections;
    DWORD   TimeDateStamp;
    DWORD   PointerToSymbolTable;
    DWORD   NumberOfSymbols;
    WORD    SizeOfOptionalHeader;
    WORD    Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

PIMAGE_FILE_HEADER File = (PIMAGE_FILE_HEADER)(ptr + Dos->e_lfanew + 4);定義一個結構體指標(PIMAGE_NT_HEADERS32型別),在用基址+Dos頭的e_lfanew成員定位到nt頭,跟據nt頭的結構體定義可以知道Signature成員後面就是File頭而Signature成員大小是四位元組,所以加4定位到File頭,所以是ptr + Dos->e_lfanew + 4運算結果就是File頭開始的地址,再用這個結構體指標指向這個地址即可。

PIMAGE_OPTIONAL_HEADER32;結構體定義

typedef struct _IMAGE_OPTIONAL_HEADER {
    //
    // Standard fields.
    //

    WORD    Magic;
    BYTE    MajorLinkerVersion;
    BYTE    MinorLinkerVersion;
    DWORD   SizeOfCode;
    DWORD   SizeOfInitializedData;
    DWORD   SizeOfUninitializedData;
    DWORD   AddressOfEntryPoint;
    DWORD   BaseOfCode;
    DWORD   BaseOfData;

    //
    // NT additional fields.
    //

    DWORD   ImageBase;
    DWORD   SectionAlignment;
    DWORD   FileAlignment;
    WORD    MajorOperatingSystemVersion;
    WORD    MinorOperatingSystemVersion;
    WORD    MajorImageVersion;
    WORD    MinorImageVersion;
    WORD    MajorSubsystemVersion;
    WORD    MinorSubsystemVersion;
    DWORD   Win32VersionValue;
    DWORD   SizeOfImage;
    DWORD   SizeOfHeaders;
    DWORD   CheckSum;
    WORD    Subsystem;
    WORD    DllCharacteristics;
    DWORD   SizeOfStackReserve;
    DWORD   SizeOfStackCommit;
    DWORD   SizeOfHeapReserve;
    DWORD   SizeOfHeapCommit;
    DWORD   LoaderFlags;
    DWORD   NumberOfRvaAndSizes;
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

PIMAGE_OPTIONAL_HEADER32 Option = (PIMAGE_OPTIONAL_HEADER32)(ptr + Dos->e_lfanew + 20 + 4);定義一個PIMAGE_OPTIONAL_HEADER32型別的結構體指標,然後透過基址+Dos頭e_lfanew成員定位到nt頭再加上nt頭的Signature成員(4位元組)得到File頭地址,在加上File頭的大小(20位元組),它們相加結果是一個地址這個地址是可選頭開始的地方,用結構體指標指向這個地址即可。

PIMAGE_DATA_DIRECTORY結構體定義

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

PIMAGE_DATA_DIRECTORY DataDir = Option->DataDirectory;定義一個PIMAGE_DATA_DIRECTORY型別的結構體指標,透過可選頭的DataDirectory成員定位到資料目錄表。

for (int i = 0; i < 17; i++)
{
printf("VirtualAddress:%x\n", DataDir[i].VirtualAddress);
printf("Size:%x\n", DataDir[i].Size);
}

這裡使用了一個for迴圈來迴圈列印資料目錄表的資料。每迴圈一次i就加1知道i小於17就停止迴圈。

程式執行結果

wKg0C2I9kluAWy72AAEagIiKrI119.png

個人對資料目錄表的理解:就是16個結構體組成的表,透過結構體的VirtualAddress成員可以找到真正的表。

0x4節表解析

節表的大小是40個位元組(注意是一個節表大小是40),節表的數量有File頭的NumberOfSections成員決定,節表指向節,節是用來儲存資料的,如.txt節存放程式碼,.data節存放資料,但是並不是一成不變的,也可以把存放資料的節名字改成.txt並不會影響程式執行。

節表的定義

typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];
    union {
            DWORD   PhysicalAddress;
            DWORD   VirtualSize;
    } Misc;
    DWORD   VirtualAddress;
    DWORD   SizeOfRawData;
    DWORD   PointerToRawData;
    DWORD   PointerToRelocations;
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

Name[IMAGE_SIZEOF_SHORT_NAME]一個BYTE型別的陣列,用來存放當前節的名字,大小是8位元組。

Misc一個聯合體,通常會使用VirtualSizez成員,VirtualSize當前節記憶體中的大小。

VirtualAddress記憶體中節開始的地方,也就是記憶體中的偏移。

SizeOfRawData節在檔案中的大小,按照檔案對齊。

PointerToRawData檔案中節開始的地方,檔案中的偏移。

Characteristics節的屬性

節表示例

wKg0C2I9moWAT4rGAAAlXbjT2Ug017.png

前8位元組是節的名字,可以看出名字是.textbss,根據上面節定義可知名字後面是VirtualSize(記憶體中節大小),我們從73也就是名字結束的地方往後查4個位元組得到VirtualSize的值00010000去掉前面的0得到10000,再從VirtualSize結束的地方查4個位元組得到VirtualAddress的值00001000同樣去掉前面的0得到1000,剩下的成員怎麼找就不贅述了,也是和上面這幾個成員一樣都是查出來的,我們直接看程式碼,用程式碼解析節表。

程式碼解析

#include <stdio.h>
#include <Windows.h>
#define path "C:\\Users\\blue\\Desktop\\Dll1.dll"

void main()
{
FILE* fp = fopen(path, "rb");
fseek(fp, 0, SEEK_END);
int size = ftell(fp);
rewind(fp);
PBYTE ptr = (PBYTE)malloc(size);
memset(ptr, 0, size);
fread(ptr, size, 1, fp);
PIMAGE_DOS_HEADER Dos = (PIMAGE_DOS_HEADER)ptr;
PIMAGE_NT_HEADERS Nt = (PIMAGE_NT_HEADERS)(ptr + Dos->e_lfanew);
PIMAGE_FILE_HEADER File = (PIMAGE_FILE_HEADER)(ptr + Dos->e_lfanew + 4);
PIMAGE_OPTIONAL_HEADER32 Option = (PIMAGE_OPTIONAL_HEADER32)(ptr + Dos->e_lfanew + 20 + 4);
PIMAGE_DATA_DIRECTORY DataDir = Option->DataDirectory;

for (int i = 0; i < 17; i++)
{
printf("VirtualAddress:%x\n", DataDir[i].VirtualAddress);
printf("Size:%x\n", DataDir[i].Size);

}
PIMAGE_SECTION_HEADER Section = IMAGE_FIRST_SECTION(Nt);
printf("節表\n");
for (int i = 0; i < File->NumberOfSections; i++)
{

printf("Name:%s\n", Section[i].Name);
printf("VirtualSize:%x\n", Section[i].Misc.VirtualSize);
printf("VirtualAddress:%x\n", Section[i].VirtualAddress);
printf("SizeOfRawData:%x\n",Section[i].SizeOfRawData);
printf("PointerToRawData:%x\n", Section[i].PointerToRawData);
printf("Characteristics:%x\n", Section[i].Characteristics);
}
getchar();
}

上面說解析過的程式碼就不贅述了,直接看PIMAGE_SECTION_HEADER Section = IMAGE_FIRST_SECTION(Nt);這行程式碼還是定義一個PIMAGE_SECTION_HEADER型別的結構體指標,這裡使用IMAGE_FIRST_SECTION宏來定位節表這樣方便些,當然也可以透過,可選頭的地址+可選頭的大小來定位節表。

for (int i = 0; i < File->NumberOfSections; i++)
{

printf("Name:%s\n", Section[i].Name);
printf("VirtualSize:%x\n", Section[i].Misc.VirtualSize);
printf("VirtualAddress:%x\n", Section[i].VirtualAddress);
printf("SizeOfRawData:%x\n",Section[i].SizeOfRawData);
printf("PointerToRawData:%x\n", Section[i].PointerToRawData);
printf("Characteristics:%x\n", Section[i].Characteristics);
}

因為File頭裡NumberOfSections成員代表節表的數量,所以要用它作為判斷條件,迴圈列印即可,這裡列印名字是要用%s是列印字串時用的,%d是10進位制時用的,%x是列印16進位制時用的

程式執行結果

wKg0C2I9nSyARq6JAAF9ACzYN78868.png

0x5結語

主要是介紹了Rva與Foa的轉換和資料目錄表,節表中一些比較重要的成員,和如何使用程式碼列印出它們,涉及到指標和結構體相關的知識。

由於作者水平有限,文章如有錯誤歡迎指出。

相關文章