iT邦幫忙

2025 iThome 鐵人賽

DAY 8
1
Security

Zig 世代惡意程式戰記:暗影綠鬣蜥 の 獠牙與劇毒!系列 第 8

Day08 - 惡意程式前哨戰:全面分解 PE 文件格式

  • 分享至 

  • xImage
  •  

走在時代前沿的前言

欸完蛋了啦現在已經晚上六點了,我一個字都還沒動。

嗨大家好我 CX330,昨天帶大家深入的看了一下窗戶的 API,今天我們就要來詳細聊聊前幾天提到的 PE 文件格式。今天沒什麼時間聊天了,就直接開始吧!

疊甲

中華民國刑法第 362 條:「製作專供犯本章之罪之電腦程式,而供自己或他人犯本章之罪,致生損害於公眾或他人者,處五年以下有期徒刑、拘役或科或併科六十萬元以下罰金。」

本系列文章涉及多種惡意程式的技術,旨在提升個人技術能力與資安意識。本人在此強烈呼籲讀者,切勿使用所學到的知識與技術從事任何違法行為!

Zig 版本

本系列文章中使用的 Zig 版本號為 0.14.1。

PE 檔案簡介

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。更詳細來說,是這樣:

  • DOS Header
  • DOS Stub
  • PE File Header
  • Image Optional Header
  • Section Table
  • Data Dictionaries
  • Sections

PE Structure - https://tech-zealots.com/malware-analysis/pe-portable-executable-structure-malware-analysis-part-2/

我們來用 Binary Ninja 來解剖一隻程式,來看一下這些資料結構,這邊我們用 calc.exe 來作為小白鼠。我們先把這個小算盤的執行檔丟進 Binary Ninja 並且切換到 Hex view 來觀察觀察。

calc.exe in Binary Ninja Hex View

DOS Header (IMAGE_DOS_HEADER)

這是整個檔案最最最上面一開始的地方,是由十六進制的 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 標頭的位址

DOS Stub 程式

一個 Stub 是一個小型的程式,會在應用程式開始執行時會自動運行起來。當這個 Stub 不兼容 Windows 的時候,則會打印出:「This program cannot be run in DOS mode」的錯誤訊息。像是我們在沒有支援 Win32 的環境中執行 Win32 的程式,就會看到這個錯誤訊息。

在這種情況下,當執行檔被載入時候,MS-DOS 會執行這個 Stub 程式。當 Windows 載入器把 PE 檔案映射到記憶體的時候,被映射的第一個位元對應到 MS-DOS Stub 的第一個位元。

DOS Stub in Binary Ninja

還記得剛剛我們看到的 e_lfanew 的值為 0xE8,這代表我們的 DOS Stub 是從偏移量 0x40 開始到偏移量為 0xe8

NT Header (IMAGE_NT_HEADERS)

從這邊開始,就是真正的檔案內容了。這個由 ASCII 字串 PE (十六進制的 55 45 )開始的地方就是 NT Header,又名 PE Header。

這個的偏移量就是在剛剛提到的 MS-DOS Header 中的 e_lfanew 的地方,它是一個 IMAGE_NT_HEADERS 的結構體,主要包含 SignatureFileHeaderOptionalHeader,它長這樣。

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 in Binary Ninja

File Header (IMAGE_FILE_HEADER)

它會是在距離 NT Header 的 Signature 後面的 20 個位元組,也就是偏移量 4 的地方(第 5 個位元組)開始。它包含了檔案佈局的基本資訊。

如果用 Binary Ninja 的 Linear view 來看,可以看到它把 Signature 跟 File Header 合併統稱為 COFF Header 了,如下。

COFF Header in Binary Ninja

如果我們驗證一下,就可以確認 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 Header (IMAGE_OPTIONAL_HEADER)

儘管名稱中有 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
    • 告訴我們是 32 位元還是 64 位元
  • AddressOfEntryPoint
    • 這是 Windows 載入器將開始執行的地址。這裡有模組的進入點(Entry Point, EP)的相對虛擬地址(Relative Virtual Address, RVA),通常會在 .text 標籤裡面找到。對於執行檔,這是入口點;對於 DLL,則入口點函數則是可選的,不一定存在(可能為 0)
  • BaseOfCode / BaseOfData
    • 紀錄程式碼和資料的 RVA
  • ImageBase
    • 他是執行檔在記憶體中映射到特定位置的地址。在 Windows NT 中,執行檔的默認 Image 基址為 0x10000,而 DLL 的則默認在 0x400000。不過在 Windows 95 後,因 0x10000 不能再用來載入 32 位元執行檔(因其落在所有進程的共享線性地址區域),所以微軟將執行檔的基址改為 0x400000,而 DLL 改為 0x10000000
  • SectionAlignment / FileAlignment
    • Sections 的對齊和文件的對齊。當執行檔映射到記憶體中時,執行黨的每個 Section 都從一個虛擬地址開始,該地址會是此值的倍數
  • SizeOfImage
    • 表示 PE 文件在運行時佔用的記憶體大小,必須是 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 結構的指針。這個東西很直觀,就是一個資料的目錄,紀錄了在檔案中的哪裡可以找到其他重要的執行檔資料。

其中幾個重要的包括但不限於:

  • Export Directory
    • 包含此 PE 文件匯出的函數和資料(通常是 DLL)
  • Import Directory
    • 指向匯入地址表(Import Address Table)並包含此 PE 文件會數的 DLL 和函數的資訊
  • Resource Directory
    • 資源目錄,指向 PE 文件的資源部分
  • TLS Table
    • 指向執行緒局部儲存(Thread Local Storage)目錄。
  • Load Configuration Table
    • 包含跟安全性相關的設定,像是 DEP 和 ASLR 的標誌(Flag)
  • Import Address Table
    • 包含從其他執行檔匯入的函數地址,可以被用來存取其他執行檔的函數和資料

如果想要了解更多,推薦閱讀這篇 Exciting Journey Towards Import Address Table of an Executable

Section Header Table

我們在前幾天有介紹過 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
    • 一個 8 位元組的陣列,包含 Section 的名稱,例如 .text
  • VirtualSize
    • 當這個 Section 被載入記憶體時的大小
  • VirtualAddress
    • 當這個 Section 被載入記憶體時,第一個位元組的地址(相對於 ImageBase
  • SizeOfRawData
    • 這個 Section 在硬碟上的資料大小
    • 如果 VirtualSizeSizeOfRawData 之間有很明顯的差異,則代表該 Binary 可能是被加殼過的(Packed)
  • PointerToRawData
    • 原始資料 Section 在檔案中的偏移量
    • 所以如果檔案的對齊屬性是預設設置,我們則可以把這個值加上 SizeOfRawData 的值,則可以得到下一個 Section 在檔案中的偏移量
  • Characteristics

PE Sections

Section 是 PE 文件中很重要的部分,是程式碼、資料、資源等等實際存在的位置,且每個不同的 Section 都會有它各自的功能。這邊我會講一些最主要且幾乎會存在於每個 PE 文件中的 Sections。

  • .text
    • 包含了所有的程式碼,還有程式的進入點
  • .data
    • 這裡包含的是已初始化的資料(如已初始化的全域及區域變數),通常在檔案中有實體資料且是可讀可寫的。
  • .rdata
    • 包含唯讀的資料
  • .rsrc
    • 包含資源,像是圖片、svg、ico 文件等
  • .bss
    • 包含未初始化資料
  • .tls
    • 包含執行緒局部儲存(Thread Local Storage)資料

值得注意的是,惡意程式作者可以修改這些 Sections 的名稱!

References

鐵人賽期 PoPoo,你今天轉 Po 了嗎?

好耶我終於寫完了!今天超級忙但終於還是在我的努力不懈之下趕工完成啦!今天我們把整個 PE 文件格式都掃描過了一遍,然後大家如果有興趣的話也可以把 Binary Ninja 下載下來(或是你習慣的 Hex editor),跟著上面的講解試著做一次,會對於整個 PE 結構更加的了解!那就明天見囉!

如果對惡意程式開發或是惡意程式分析有興趣的話,這個系列會很適合你!最後也感謝大家的閱讀,歡迎順手按讚留言訂閱轉發(轉發可以讓朋友們知道你都在讀這種很技術的文章,他們會覺得你好帥好強好電,然後開始裝弱互相吹捧)~明天見!


上一篇
Day07 - 一窺窗戶中的風景:深入淺出 Windows API
下一篇
Day09 - 神出鬼沒的幻影綠鬣蜥:Payload 的加密與混淆(上)
系列文
Zig 世代惡意程式戰記:暗影綠鬣蜥 の 獠牙與劇毒!9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
tt27
iT邦新手 5 級 ‧ 2025-09-23 10:40:22

又学到了新东西 谢谢C老师

我要留言

立即登入留言