欸完蛋了啦現在已經晚上六點了,我一個字都還沒動。
嗨大家好我 CX330,昨天帶大家深入的看了一下窗戶的 API,今天我們就要來詳細聊聊前幾天提到的 PE 文件格式。今天沒什麼時間聊天了,就直接開始吧!
中華民國刑法第 362 條:「製作專供犯本章之罪之電腦程式,而供自己或他人犯本章之罪,致生損害於公眾或他人者,處五年以下有期徒刑、拘役或科或併科六十萬元以下罰金。」
本系列文章涉及多種惡意程式的技術,旨在提升個人技術能力與資安意識。本人在此強烈呼籲讀者,切勿使用所學到的知識與技術從事任何違法行為!
本系列文章中使用的 Zig 版本號為 0.14.1。
PE 格式(Portable Executable)是專屬於 Microsoft Windows 的一個文件格式,它會告訴系統如何載入和運行這個檔案等等。每個可執行檔都共享一個叫做 COFF(Common Object File Format)的通用格式,而 PE 格式就是這種 COFF 格式之一。常見的後綴名稱包括但不限於以下:
.exe
.dll
.scr
.sys
PE 文件格式就是一種資料結構,告訴 Windows 系統載入器需要哪些資訊來執行和管理程式,包括動態連結的函式庫、API 匯出匯入表、資源管理資料和 TLS(Thread Local Storage)資料等等。
硬碟上的資料結構和記憶體中的資料結構是相同的,所以如果你確定可以在 PE 檔案中找到某樣東西,那麼在檔案被載入記憶體後,我們幾乎可以確定能在記憶體中找到完全相同的內容。值得注意的是,PE 檔案並不是作為單一的記憶映射文件映射到記憶體裡面,而是由 Win32 載入器(Loader)去解析 PE 文件並決定要映射檔案的哪些部分。
一個 PE 文件可以被我們切成很多的塊,包含 Headers、Tables 跟 Sections。更詳細來說,是這樣:
我們來用 Binary Ninja 來解剖一隻程式,來看一下這些資料結構,這邊我們用 calc.exe
來作為小白鼠。我們先把這個小算盤的執行檔丟進 Binary Ninja 並且切換到 Hex view 來觀察觀察。
這是整個檔案最最最上面一開始的地方,是由十六進制的 4D 5A
開始的,在 ASCII 中會是 MZ
開頭的。DOS Header 佔了檔案最一開始的 64 個位元組,也就是剛剛那個圖片中的前 4 行(每行有 16 個位元組)。
這個 MZ
開頭的特徵確定了這個檔案是一個 PE 檔案,而整個 IMAGE_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
<strong>LONG e_lfanew; // Offset to the NT header</strong>
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
MZ
是 Mark Zbikowski 的縮寫,他是 MS-DOS 的開發人員之一。這個字段稱為 e_magic
或是魔法數字(Magic number),是用來識別 MS-DOS 相容文件類型的關鍵字。所有和 MS-DOS 相容的可執行文件都會把這個數字設置為 0x5A4D,也就是 MZ
。
而最後一個欄位是 e_lfanew
,是一個 4 個字節的偏移量(本例中為 e8 00 00 00
),用來指示 PE 標頭的位址
一個 Stub 是一個小型的程式,會在應用程式開始執行時會自動運行起來。當這個 Stub 不兼容 Windows 的時候,則會打印出:「This program cannot be run in DOS mode」的錯誤訊息。像是我們在沒有支援 Win32 的環境中執行 Win32 的程式,就會看到這個錯誤訊息。
在這種情況下,當執行檔被載入時候,MS-DOS 會執行這個 Stub 程式。當 Windows 載入器把 PE 檔案映射到記憶體的時候,被映射的第一個位元對應到 MS-DOS Stub 的第一個位元。
還記得剛剛我們看到的 e_lfanew
的值為 0xE8
,這代表我們的 DOS Stub 是從偏移量 0x40
開始到偏移量為 0xe8
。
從這邊開始,就是真正的檔案內容了。這個由 ASCII 字串 PE
(十六進制的 55 45
)開始的地方就是 NT Header,又名 PE Header。
這個的偏移量就是在剛剛提到的 MS-DOS Header 中的 e_lfanew
的地方,它是一個 IMAGE_NT_HEADERS
的結構體,主要包含 Signature
、FileHeader
和 OptionalHeader
,它長這樣。
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
而這個 Signature
就是剛提到的這個 PE
的開頭,由於是一個 DWORD
(有 4 位元組),所以準確來說是 50 45 00 00
,也就是 PE\0\0
。
它會是在距離 NT Header 的 Signature 後面的 20 個位元組,也就是偏移量 4 的地方(第 5 個位元組)開始。它包含了檔案佈局的基本資訊。
如果用 Binary Ninja 的 Linear view 來看,可以看到它把 Signature 跟 File Header 合併統稱為 COFF Header 了,如下。
如果我們驗證一下,就可以確認 File Header 是 20 個位元組。還記得剛剛說的 NT Header 的 Signature 位於 DOS Header 的 e_lfanew
所標註的偏移量,也就是 0xe8
,而這邊整個 COFF Header 是從 0xe8 到 0x100,相減一下是 0x18,也就是十進位的 24,剛好是 4 bytes 的 Signature 加上 20 bytes 的 File Header。
File Header 主要包含了以下部分:
starting from | type | Variable name | information |
---|---|---|---|
1 | WORD |
Machine |
CPU 架構 |
2 | WORD |
NumberOfSections |
Sections 數量 |
5 | DWORD |
TimeDateStamp |
編譯時間(Unix epoch) |
9 | DWORD |
PointerToSymbolTable |
符號表的檔案偏移 |
13 | DWORD |
NumberOfSymbols |
符號數量 |
17 | WORD |
SizeOfOptionalHeader |
Optional Header 大小 |
19 | WORD |
Characteristics |
檔案屬性 Flags |
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;
比較重要的是,有很多資訊都是儲存在 Characteristics
裡面的,像是是否為 DLL 等資訊,這都可以透過 DiE 去查看到。
儘管名稱中有 Optional 這個字,但其他並非可選的,它充滿了很多的關鍵資訊,像是程式開始運行的位置和將會使用多少記憶體等等。之所以會被稱作「Optional」只是因為某些文件類型不會有這個 Header。
這裡面包含的資訊會比 IMAGE_FILE_HEADER
還要來得更多。
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
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;
我們也可以透過 Binary Ninja 的 Linear view 查看這些詳細的值,像這以下的程式碼(由 Binary Ninja Decompiler 所產生)。
140000100 struct PE64_Optional_Header __pe64_optional_header =
140000100 {
140000100 enum pe_magic_1 magic = PE_64BIT
140000102 uint8_t majorLinkerVersion = 0xe
140000103 uint8_t minorLinkerVersion = 0x14
140000104 uint32_t sizeOfCode = 0xc00
140000108 uint32_t sizeOfInitializedData = 0x6200
14000010c uint32_t sizeOfUninitializedData = 0x0
140000110 uint32_t addressOfEntryPoint = 0x1870
140000114 uint32_t baseOfCode = 0x1000
140000118 uint64_t imageBase = 0x140000000
140000120 uint32_t sectionAlignment = 0x1000
140000124 uint32_t fileAlignment = 0x200
140000128 uint16_t majorOperatingSystemVersion = 0xa
14000012a uint16_t minorOperatingSystemVersion = 0x0
14000012c uint16_t majorImageVersion = 0xa
14000012e uint16_t minorImageVersion = 0x0
140000130 uint16_t majorSubsystemVersion = 0xa
140000132 uint16_t minorSubsystemVersion = 0x0
140000134 uint32_t win32VersionValue = 0x0
140000138 uint32_t sizeOfImage = 0xb000
14000013c uint32_t sizeOfHeaders = 0x400
140000140 uint32_t checkSum = 0x14163
140000144 enum pe_subsystem_1 subsystem = IMAGE_SUBSYSTEM_WINDOWS_GUI
140000146 enum pe_dll_characteristics_1 dllCharacteristics = IMAGE_DLLCHARACTERISTICS_HIGH_ENTROPY_VA | IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE | IMAGE_DLLCHARACTERISTICS_NX_COMPAT | IMAGE_DLLCHARACTERISTICS_GUARD_CF | IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE
140000148 uint64_t sizeOfStackReserve = 0x80000
140000150 uint64_t sizeOfStackCommit = 0x2000
140000158 uint64_t sizeOfHeapReserve = 0x100000
140000160 uint64_t sizeOfHeapCommit = 0x1000
140000168 uint32_t loaderFlags = 0x0
14000016c uint32_t numberOfRvaAndSizes = 0x10
140000170 struct PE_Data_Directory_Entry_1 exportTableEntry =
140000170 {
140000170 uint32_t virtualAddress = 0x0
140000174 uint32_t size = 0x0
140000178 }
140000178 struct PE_Data_Directory_Entry_1 importTableEntry =
140000178 {
140000178 uint32_t virtualAddress = 0x2794
14000017c uint32_t size = 0xa0
140000180 }
140000180 struct PE_Data_Directory_Entry_1 resourceTableEntry =
140000180 {
140000180 uint32_t virtualAddress = 0x5000
140000184 uint32_t size = 0x4710
140000188 }
140000188 struct PE_Data_Directory_Entry_1 exceptionTableEntry =
140000188 {
140000188 uint32_t virtualAddress = 0x4000
14000018c uint32_t size = 0xf0
140000190 }
140000190 struct PE_Data_Directory_Entry_1 certificateTableEntry =
140000190 {
140000190 uint32_t virtualAddress = 0x0
140000194 uint32_t size = 0x0
140000198 }
140000198 struct PE_Data_Directory_Entry_1 baseRelocationTableEntry =
140000198 {
140000198 uint32_t virtualAddress = 0xa000
14000019c uint32_t size = 0x2c
1400001a0 }
1400001a0 struct PE_Data_Directory_Entry_1 debugEntry =
1400001a0 {
1400001a0 uint32_t virtualAddress = 0x2320
1400001a4 uint32_t size = 0x54
1400001a8 }
1400001a8 struct PE_Data_Directory_Entry_1 architectureEntry =
1400001a8 {
1400001a8 uint32_t virtualAddress = 0x0
1400001ac uint32_t size = 0x0
1400001b0 }
1400001b0 struct PE_Data_Directory_Entry_1 globalPtrEntry =
1400001b0 {
1400001b0 uint32_t virtualAddress = 0x0
1400001b4 uint32_t size = 0x0
1400001b8 }
1400001b8 struct PE_Data_Directory_Entry_1 tlsTableEntry =
1400001b8 {
1400001b8 uint32_t virtualAddress = 0x0
1400001bc uint32_t size = 0x0
1400001c0 }
1400001c0 struct PE_Data_Directory_Entry_1 loadConfigTableEntry =
1400001c0 {
1400001c0 uint32_t virtualAddress = 0x2010
1400001c4 uint32_t size = 0x118
1400001c8 }
1400001c8 struct PE_Data_Directory_Entry_1 boundImportEntry =
1400001c8 {
1400001c8 uint32_t virtualAddress = 0x0
1400001cc uint32_t size = 0x0
1400001d0 }
1400001d0 struct PE_Data_Directory_Entry_1 iatEntry =
1400001d0 {
1400001d0 uint32_t virtualAddress = 0x2128
1400001d4 uint32_t size = 0x140
1400001d8 }
1400001d8 struct PE_Data_Directory_Entry_1 delayImportDescriptorEntry =
1400001d8 {
1400001d8 uint32_t virtualAddress = 0x0
1400001dc uint32_t size = 0x0
1400001e0 }
1400001e0 struct PE_Data_Directory_Entry_1 clrRuntimeHeaderEntry =
1400001e0 {
1400001e0 uint32_t virtualAddress = 0x0
1400001e4 uint32_t size = 0x0
1400001e8 }
1400001e8 struct PE_Data_Directory_Entry_1 reservedEntry =
1400001e8 {
1400001e8 uint32_t virtualAddress = 0x0
1400001ec uint32_t size = 0x0
1400001f0 }
1400001f0 }
總之,爆炸多,以下是重要的一些欄位:
Magic
AddressOfEntryPoint
.text
標籤裡面找到。對於執行檔,這是入口點;對於 DLL,則入口點函數則是可選的,不一定存在(可能為 0)BaseOfCode
/ BaseOfData
ImageBase
SectionAlignment
/ FileAlignment
SizeOfImage
SectionAlignment
的值的倍數Subsystem
WINDOWS_GUI
或是 WINDOWS_CLI
最後,在 IMAGE_OPTIONAL_HEADER
的最尾端,會有 Data Directory,每一個的型別都是 IMAGE_DATA_DIRECTORY
,如下。
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
Data Directory 的成員是指向第一個 IMAGE_DATA_DIRECTORY
結構的指針。這個東西很直觀,就是一個資料的目錄,紀錄了在檔案中的哪裡可以找到其他重要的執行檔資料。
其中幾個重要的包括但不限於:
如果想要了解更多,推薦閱讀這篇 Exciting Journey Towards Import Address Table of an Executable。
我們在前幾天有介紹過 Sections 了,而 Section Header Table 就是一個專門用來存放關於每個 Section 的資料的表,他的資料型別 IMAGE_SECTION_HEADER
如下:
typedef struct _IMAGE_SECTION_HEADER {
BYTE 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
.text
VirtualSize
VirtualAddress
ImageBase
)SizeOfRawData
VirtualSize
和 SizeOfRawData
之間有很明顯的差異,則代表該 Binary 可能是被加殼過的(Packed)PointerToRawData
SizeOfRawData
的值,則可以得到下一個 Section 在檔案中的偏移量Characteristics
Section 是 PE 文件中很重要的部分,是程式碼、資料、資源等等實際存在的位置,且每個不同的 Section 都會有它各自的功能。這邊我會講一些最主要且幾乎會存在於每個 PE 文件中的 Sections。
.text
.data
.rdata
.rsrc
.bss
.tls
值得注意的是,惡意程式作者可以修改這些 Sections 的名稱!
好耶我終於寫完了!今天超級忙但終於還是在我的努力不懈之下趕工完成啦!今天我們把整個 PE 文件格式都掃描過了一遍,然後大家如果有興趣的話也可以把 Binary Ninja 下載下來(或是你習慣的 Hex editor),跟著上面的講解試著做一次,會對於整個 PE 結構更加的了解!那就明天見囉!
如果對惡意程式開發或是惡意程式分析有興趣的話,這個系列會很適合你!最後也感謝大家的閱讀,歡迎順手按讚留言訂閱轉發(轉發可以讓朋友們知道你都在讀這種很技術的文章,他們會覺得你好帥好強好電,然後開始裝弱互相吹捧)~明天見!