iT邦幫忙

1

學習成為人體 PE Parser

EN 2021-05-29 17:26:391999 瀏覽

筆者最近在閱讀 aaaddress1 的大作: Windows APT Warfare:惡意程式前線戰術指南,因為書中圍繞著 PE File 進行,腦容量太小的我又一直忘記 PE 的檔案結構,最後決定還是認真的把它研究一遍並寫成筆記。
PE (Portable Executable) 是一種用於可執行文件、目標文件和動態連結庫的文件格式,主要使用在 32 位和 64 位的 Windows 作業系統上。

有點像是 Linux 作業系統中的 elf 檔。

DOS Header

在一串連續的記憶體中, DOS Header 一定會是記憶體中的首段內容, DOS Header 中的幾項資訊會是比較重要的:

  • e_magic
    e_magic 幫助我們辨認該 PE 檔案是否合法,一般來說,它應該永遠等於 MZ 字串。
    如果以 C/C++ 檢查 PE File ,可以這樣做:

    #include <windows.h>
    
    // ...
    void parser(char* filePtr){
        IMAGE_DOS_HEADER* dosHdr = (IMAGE_DOS_HEADER *)filePtr;
        if(dosHdr->e_magic != IMAGE_DOS_SIGNATURE){
            return;
        }
        // ...
    }
    // ...
    
  • e_lfanew
    觀察 e_lfanew 之前,必須先了解什麼是 RVA (Relative Virtual Address), RVA 是程式入口點的參考位址,舉例來說:
    如果程式被放入虛擬地址(Virtual address, VA)的 0x01000000 處,且 RVA 位於 0x102D6C 處,那麼程式在記憶體中的實際入口就會是 VA + RVA:

      0x01000000
    + 0x00102D6C
    = 0x01102D6C
    

    e_lfanew 其實就是指向了 NT Headers 的 RVA ,換個角度思考,我們將前面的 dosHdr 加上偏移量 (在這邊指 RVA),就可以獲得 NT Headers 的起始位址:

    IMAGE_NT_HEADERS* ntHdrs = (IMAGE_NT_HEADERS *)((size_t)dosHdr + dosHdr->e_lfanew);
    

NT Headers

透過讀取 DOS Header 獲得 NT Headers 的起始位址以後,我們就可以對 PE 檔案做更進一步的檢驗。
NT Headers 共包含了兩大結構,分別是 File Header 以及 Optional Header 。

File Header


參考上圖,在 File Header 結構中有多個屬性,每個屬性代表的資訊如下:

  • Machine
    紀錄 PE 檔案所存放的機械碼屬於哪一種指令集架構:

    • x86
    • ARM
    • x64
  • NumberOfSections
    一個 PE File 通常會有好幾段塊狀區域, NumberofSections 紀錄了 PE 檔案的區段數量。

    這個參數對我們撰寫程式解析 PE File 非常有幫助,至於那些塊狀區段存了什麼,晚點會提到。

  • TimeDateStamp
    紀錄程式編譯時間的時間戳。

  • PointerToSymbolTable
    符號表地址,用於除錯,一般為 0 。

  • NumberOfSymbols
    如果符號表存在,這邊會記錄符號數量。

  • Characteristics
    紀錄了整個 PE 的屬性,包含:

    • Executable
    • Info of redirection
    • 32-bit or not
    • DLL modules

Optional Header

Source

Optional Header 的中文稱可選段,實際上,如果要讓 PE 能夠順利地被執行程式裝載器使用, Optional Header 為必備的。

補充:
Optional Header 不存在於 Object File (COFF),而是在編譯的連結階段才會由連結器補上。

參考上圖, Optional Header 包含了很多參數,下面針對重要的參數作介紹:

  • Address of entry point
    程式碼編譯後,程式的入口點,也就代表當 Program 被作業系統載入時, Process 會從這邊開始執行。

    一般來說,入口點會指向 .text section 的函式開頭。

  • ImageBase
    記錄了 PE 檔案 mapping 到記憶體上的預設位址,通常為 0x400000 或是 0x800000

  • SizeOfImage
    記錄了當程式處於動態執行階段需要多少空間才能存放整個 Image 。

  • Section alignment
    動態的區域對齊, 32-bit 的環境下預設大小為 0x1000 bytes 。

  • File alignment
    靜態的區域對齊, 32-bit 的環境下預設大小為 0x200 bytes 。

    假設有不足 0x200 bytes 的資料要放進塊狀區段,塊狀區段的大小為 0x200 bytes ,如果資料多於預設大小,塊狀區段的大小則為 0x400 bytes 。

  • Size of headers
    DOS Header + NT Headers + Section Headers 的大小。

  • Data directory

    • Export table
    • Import table
    • Ressource table
    • Exception table
    • Import Address table

Section Headers

Section Headers 的位址緊隨在 NT Headers 的後方,使用 C/C++ 可以輕鬆的獲得其位址:

IMAGE_SECTION_HEADER* sectHdr = (IMAGE_SECTION_HEADER *)((size_t)ntHdrs + sizeof(*ntHdrs));

至於 Section Headers 的本體到底是什麼呢?它其實就是一個存放塊狀區段資訊的陣列:

for (size_t i = 0; i < ntHdrs->FileHeader.NumberOfSections; i++){
		printf("\t#%.2x - %8s - %.8x - %.8x \n", i, sectHdr[i].Name, sectHdr[i].PointerToRawData, sectHdr[i].SizeOfRawData);
        }

每一塊存放塊狀區段資訊的空間都會有以下屬性:

  • PointerToRawData
    該區段處存在靜態檔案的偏移量。

  • SizeofRawData
    該區段的實際大小。

  • VirtualAddress
    相較於映像基址的相對偏移量。

  • VirtualSize
    顯示該區段需要被分配多少動態空間。

  • Characteristics
    紀錄該區段是否可讀、可寫、可執行。

常見區段

  • .text
    用於存放程式碼。

  • .data
    用來宣告已初始化的資料與常數。

  • .bss
    存放已宣告但尚未初始化的變數。

  • .rdata
    存放唯讀資料。

  • .idata
    存放引入的函式與資料,這些資料會在 Process 建立時,由執行程式裝載器負責填充。

  • .edata
    存放用來導出給其他程式使用的函式與資料。

  • .rsrc
    用於記錄程式使用了哪些資源。

  • reloc
    重定位,當 PE 程式載入失敗,會以此段作為參考進行調整。

總結

了解 PE File 的基本結構後,我們就可以將惡意 shellcode 添加至目標檔案再將 Entry point 指向惡意程式區段的 Virtual Address 做些壞壞的事(?)

本文章介紹的內容大概只有 Windows APT Warfare:惡意程式前線戰術指南整本書的皮毛,如果想學更多就去下單買一本 Windows APT Warfare:惡意程式前線戰術指南吧!

References


圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言