PE檔案結構解析1

SecIN發表於2022-05-20

0x0導讀

今天覆習一下pe檔案結構,本文主要講了pe檔案結構基本概念和Dos頭,Nt頭,可選頭的一些比較重要的成員,話不多說來看文章。

0x1環境

編譯器:VirsualStudio2022

16進位制檢視工具:winhex

0x2基本概念

我們先來看下百度給出的定義:

PE檔案的全稱是Portable Executable,意為可移植的可執行的檔案,常見的EXE、DLL、OCX、SYS、COM都是PE檔案。

我的理解:

pe檔案結構就像一張說明書,用來說明這個pe檔案的情況,而pe檔案結構是表示符合pe檔案結構的檔案如exe,sys(驅動檔案),com等檔案的資訊。舉個例子,買電腦的時候都會有一張配置單,來說明這個電腦的cpu是啥,記憶體條多大,顯示卡是哪個廠的等資訊。pe檔案結構就像是這一張配置單,只不過配置單裡面的資訊是表示這臺電腦的情況,而pe檔案結構表示的是這個exe(這裡就只用exe來舉例),相關的資訊,比如哪一部分是程式碼,哪一部分是資料,執行的時候載入到記憶體的哪裡,pe檔案的大小是多少等資訊。

下面是pe檔案結構圖,因為pe檔案結構內容太多了所以今天只講Dos頭,PE檔案頭(nt頭),標準pe頭(file頭),可選檔案頭(option頭),剩下的以後在講(因為清楚的圖片比較大上傳不上去只能湊活著看了)。

wKg0C2IzGqWABUtgAAE2L8me6cw390.png

0x3Dos頭解析

Dos頭定義,其實就是一個結構體,比較重要的有e_magic和e_lfanew,這兩個成員。

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;

e_magic是一個WORD型別,兩個位元組,用來判斷該pe檔案是不是可執行的,值是0x4D5A,用字串就是MZ所以它也叫mz標記,如果把它給改掉那麼程式就無法執行。

修改前

wKg0C2IzHPaAKzOsAAAb7CStWlQ552.png

修改後

wKg0C2IzHUqAKzW2AAAphWXOMqE674.png

wKg0C2IzHWyAOLBEAABANUCyZ1M652.png

e_lfanew是一個LONG型別大小是4位元組,可以把它理解為一個偏移,透過它加基址(程式碼開始的地方)來找到pe檔案頭(nt頭)。

wKg0C2IzHnAdEOHAADSS7KZtUQ243.png

程式碼解析

#include <stdio.h>
#include <Windows.h>
#define path "C:\\Users\\allen\\Desktop\\ipmsg.exe"

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;
printf("E_magic:%x\n", Dos->e_magic);
printf("E_lfanew:%x\n", Dos->e_lfanew);
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);定義一個指標指向malloc函式申請的記憶體,malloc函式第一個引數是申請記憶體的大小,PBYTE就是char*

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

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

PIMAGE_DOS_HEADER Dos = (PIMAGE_DOS_HEADER)ptr;定義一個結構體指標來指向那塊記憶體,也就是用這塊記憶體中的資料來填充這個結構體指標所指向的結構體。

printf("E_magic:%x\n", Dos->e_magic);//列印結構體成員e_magic的值,列印結構體成員要用->
printf("E_lfanew:%x\n", Dos->e_lfanew);//列印結構體成員e_lfanew的值
getchar();//暫停等待使用者輸入。

程式執行結果,因為我這裡開啟的是另一個程式所以e_lfanew的值不一樣。

wKg0C2IzKTWAWqyRAACvYJFGDgo926.png

0x3Nt頭解析

Nt頭定義。

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

Signature是一個DWORD裡面儲存著PE標記也就是0x4550,這個值代表此檔案是一個有效的pe檔案,如果被修改程式也會無法執行。

FileHeader一個結構體,是標準pe頭開始的地方,結構體定義如下,大小是20個位元組。因為主要講的是nt頭所以對此結構說的不是很詳細,下面會講到此結構。

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;

OptionalHeader也是一個結構體,是可選頭開始的地方,定義如下,因為主要講的是nt頭所以對此結構說的不是很詳細,下面會講到這個結構。

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;

程式碼解析

#include <stdio.h>
#include <Windows.h>
#define path "C:\\Users\\allen\\Desktop\\ipmsg.exe"

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;

printf("E_magic:%x\n", Dos->e_magic);
printf("E_lfanew:%x\n", Dos->e_lfanew);
PIMAGE_NT_HEADERS Nt = (PIMAGE_NT_HEADERS)(ptr + Dos->e_lfanew);
printf("Signature:%x\n", Nt->Signature);
printf("FileHeader:%x\n", Nt->FileHeader);
printf("OptionalHeader:%x\n", Nt->OptionalHeader);
getchar();
}

上面講過的程式碼就不在贅述了。

PIMAGE_NT_HEADERS Nt = (PIMAGE_NT_HEADERS)(ptr + Dos->e_lfanew);也是定義一個結構體指標然後指向基址加上Dos頭的e_lfanew成員,定位到nt頭,並填充這個結構體指標指向的結構體。實際上還是基址加偏移的方式定位的。

printf("Signature:%x\n", Nt->Signature);
printf("FileHeader:%x\n", Nt->FileHeader);
printf("OptionalHeader:%x\n", Nt->OptionalHeader);

上面這幾行程式碼就是列印結構體的資料了,printf函式就是列印資料。

程式執行結果

wKg0C2IzLyWAVi5cAADbr69IKYQ356.png

0x4File頭解析

File頭定義,大小20位元組。

typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;
    WORD    NumberOfSections;
    DWORD   TimeDateStamp;
    DWORD   PointerToSymbolTable;//COFF符號表格的偏移位置。此欄位只對COFF除錯資訊有用
    DWORD   NumberOfSymbols;//COFF符號表格中的符號個數。該值和上一個值在release版本的程式裡為0
    WORD    SizeOfOptionalHeader;
    WORD    Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

Machine大小兩位元組,表示當前pe檔案的目標CPU型別,0代表任何平臺,14c代表i386及後續處理器。

NumberOfSections大小兩位元組,代表節表(區塊)的數量。

TimeDateStamp大小四位元組,一個時間戳,表示當前pe檔案何時建立時間。

因為PointerToSymbolTableNumberOfSymbols這兩個成員很少被使用所以就不多介紹,直接看SizeOfOptionalHeader大小兩位元組,表示可選頭(Option頭)的大小。

Characteristics大小兩位元組,表示檔案的型別,010f代表可執行檔案。

程式碼解析

#include <stdio.h>
#include <Windows.h>
#define path "C:\\Users\\allen\\Desktop\\ipmsg.exe"

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;
printf("E_magic:%x\n", Dos->e_magic);
printf("E_lfanew:%x\n", Dos->e_lfanew);
PIMAGE_NT_HEADERS Nt = (PIMAGE_NT_HEADERS)(ptr + Dos->e_lfanew);
printf("Signature:%x\n", Nt->Signature);
printf("FileHeader:%x\n", Nt->FileHeader);
printf("OptionalHeader:%x\n", Nt->OptionalHeader);
PIMAGE_FILE_HEADER File = (PIMAGE_FILE_HEADER)(ptr + Dos->e_lfanew + 4);
printf("Machine:%x\n", File->Machine);
printf("NumberOfSections:%x\n", File->NumberOfSections);
printf("Characteristics:%x\n",File->Characteristics);
printf("TimeDateStamp:%x\n", File->TimeDateStamp);
printf("SizeOfOptionalHeader:%x\n", File->SizeOfOptionalHeader);
printf("TimeDateStamp:%x\n",File->TimeDateStamp);
getchar();
}

PIMAGE_FILE_HEADER File = (PIMAGE_FILE_HEADER)(ptr + Dos->e_lfanew + 4);先定義一個結構體指標,在用基址+Dos頭的e_lfanew成員定位到nt頭,跟據nt頭的結構體定義可知到Signature成員後面就是File頭而Signature的大小是四位元組,所以加4就定位到File頭,用File頭的資料填充上面定義的結構體指標指向的結構體即可。

程式執行結果

wKg0C2IzNQmAXgIOAAEyqPYqH0E767.png

0x5Option頭解析

Option頭結構體定義

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;

Magic大小兩位元組,用於表示是32位pe檔案還是64位pe檔案,如果是10B就代表32位檔案,20B代表64位檔案。

SizeOfCode大小四位元組,程式碼總大小,需要檔案對齊。

SizeOfInitializedData大小四位元組,代表已經初始化資料的大小,需要按照檔案對齊

SizeOfUninitializedData大小四位元組,代表未初始化資料大小,也是要按照檔案對齊。

AddressOfEntryPoint大小四位元組,是程式入口地址,也就是OEP(需要加上ImageBase才是真正的程式入口)。

BaseOfCode大小四位元組,程式碼節開始的地方。

BaseOfData大小四位元組,資料開始的地方。

ImageBase大小四位元組,記憶體映象基址也就是程式載入進記憶體中時的基址(一般是0x400000)。

FileAlignment大小四位元組,檔案對齊,如果檔案對齊是200那麼不足200的會在後面補0,如過一個數是188那麼它按照檔案對齊後就是200,主要作用是提高cpu工作效率。

SectionAlignment大小四位元組,記憶體對齊,和檔案對齊一樣,也是為了提高cpu工作效率。

SizeOfImage大小四位元組,PE檔案在記憶體中的總大小,按照記憶體對齊

SizeOfHeaders大小四位元組,所有頭的大小,Dos頭+Nt頭成員Signature+File頭+Option頭+節表的總大小,需要按照檔案對齊。

程式碼解析

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

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;
printf("E_magic:%x\n", Dos->e_magic);
printf("E_lfanew:%x\n", Dos->e_lfanew);
PIMAGE_NT_HEADERS Nt = (PIMAGE_NT_HEADERS)(ptr + Dos->e_lfanew);
printf("Signature:%x\n", Nt->Signature);
printf("FileHeader:%x\n", Nt->FileHeader);
printf("OptionalHeader:%x\n", Nt->OptionalHeader);
PIMAGE_FILE_HEADER File = (PIMAGE_FILE_HEADER)(ptr + Dos->e_lfanew + 4);
printf("Machine:%x\n", File->Machine);
printf("NumberOfSections:%x\n", File->NumberOfSections);
printf("Characteristics:%x\n",File->Characteristics);
printf("TimeDateStamp:%x\n", File->TimeDateStamp);
printf("SizeOfOptionalHeader:%x\n", File->SizeOfOptionalHeader);
printf("TimeDateStamp:%x\n",File->TimeDateStamp);
PIMAGE_OPTIONAL_HEADER32 Option=(PIMAGE_OPTIONAL_HEADER32)(ptr + Dos->e_lfanew + 20+4);
printf("AddressOfEntryPoint:%x\n",Option->AddressOfEntryPoint);
printf("BaseOfCode:%x\n",Option->BaseOfCode);
printf("BaseOfData:%x\n",Option->BaseOfData);
printf("FileAlignment:%x\n",Option->FileAlignment);
printf("SectionAlignment:%x\n",Option->SectionAlignment);
printf("ImageBase:%x\n",Option->ImageBase);
printf("Magic:%x\n",Option->Magic);
printf("SizeOfCode:%x\n",Option->SizeOfCode);
printf("SizeOfHeaders:%x\n",Option->SizeOfHeaders);
printf("SizeOfImage:%x\n",Option->SizeOfImage);
printf("SizeOfInitializedData:%x\n",Option->SizeOfInitializedData);
printf("SizeOfUninitializedData:%x\n",Option->SizeOfUninitializedData);
getchar();
}

PIMAGE_OPTIONAL_HEADER32 Option=(PIMAGE_OPTIONAL_HEADER32)(ptr + Dos->e_lfanew + 20+4);還是定義一個結構體指標,然後填充結構體指標指向的結構體,我們們主要看怎麼找到Option頭的,(ptr + Dos->e_lfanew + 20+4);還是先透過基址加Dos頭成員e_lfanew找到nt頭在加Nt頭成員Signature的大小找到File頭這裡加上File頭的大小就可以定位到Option頭。

程式執行結果

wKg0C2IzO2SAEE9AAGMeYT2eQc261.png

0x6結語

主要是介紹了Dos頭,Nt頭,File頭,Option頭的一些比較重要的成員,和如何定位到它們,涉及到指標與結構體相關操作。

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

相關文章