PE檔案結構解析2
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個這樣的結構。
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就停止迴圈。
程式執行結果
個人對資料目錄表的理解:就是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
節的屬性
節表示例
前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進位制時用的
程式執行結果
0x5結語
主要是介紹了Rva與Foa的轉換和資料目錄表,節表中一些比較重要的成員,和如何使用程式碼打印出它們,涉及到指標和結構體相關的知識。
由於作者水平有限,文章如有錯誤歡迎指出。