Chúng ta sẽ nghiên cứu các loại header sau:

  • IMAGE_DOS_HEADER
  • IMAGE_NT_HEADERS
    • FILE_HEADER
    • OPTIONAL_HEADER
    • IMAGE_SECTION_HEADER
    • IMAGE_IMPORT_DESCRIPTOR

Các header này đều được tổ chức bằng các Struct.

IMAGE_DOS_HEADER

Phần IMAGE_DOS_HEADER bao gồm 64 byte đầu tiên của file PE:

Các thông tin bên trong IMAGE_DOS_HEADER:

Phân tích:

  • Hai byte đầu tiên (trường e_magic) có giá trị là 4D 5A, tương ứng với 5A 4D do kiến trúc x86 của Intel sử dụng định dạng little-endian. Giá trị này là ASCII encoding của chuỗi MZ1, chính là signature (magic number) của file PE.
  • Trường e_lfanew là địa chỉ bắt đầu của IMAGE_NT_HEADERS và có giá trị là 0x000000d8.

DOS_STUB

Phần DOS_STUB:

Chi tiết:

Phần DOS_STUB là một đoạn code nhỏ chỉ chạy khi file PE không tương thích với hệ thống. Nó sẽ in ra thông điệp !This program cannot be run in DOS mode.

Chú ý rằng các thông tin về size, hashes và entropy là được suy ra từ một header cụ thể chứ không tồn tại ở trong DOS_STUB:

  • Size là kích thước của section với đơn vị là byte.
  • Hashes là giá trị băm của section.
  • Entropy là độ ngẫu nhiên của dữ liệu trong section.

IMAGE_NT_HEADERS

Phần IMAGE_NT_HEADERS bao gồm:

NT_HEADERS

Phần NT_HEADERS bao gồm:

  • Signature
  • FILE_HEADER
  • OPTIONAL_HEADER

Signature

Bốn byte đầu tiên là signature 50 45 00 00 tương ứng với PE của phần NT_HEADERS:

File Header

Phần FILE_HEADER có dạng như sau:

Giải thích ý nghĩa các trường:

  • Machine: loại kiến trúc mà file PE được tạo ra. Trong hình trên thì kiến trúc là i386 có nghĩa là file PE này tương thích với kiến trúc 32-bit Intel.
  • NumberOfSections: cho biết số lượng các section có trong file PE. File PE trong hình trên có 5 section.
  • TimeDateStamp: ngày và giờ biên dịch.
  • PointerToSymbolTableNumberOfSymbols: hai trường này không liên quan đến file PE.
  • SizeOfOptionalHeader: kích thước của phần optional header. Trong hình trên thì kích thước là 224 bytes.
  • Characteristics: đề cập đến các thuộc tính khác nhau của file PE.

OPTIONAL_HEADER

Phần OPTIONAL_HEADER có dạng như sau:

Phần này nằm sau phần FILE_HEADER.

Bên dưới là vị trí bắt đầu của OPTIONAL_HEADER:

Giải thích ý nghĩa của một số trường:

  • Magic: cho biết file PE là ứng dụng 32-bit hay 64-bit. Nếu giá trị là 0x010B thì là 32-bit còn nếu giá trị là 0x020B thì là 64-bit. Ví dụ trên cho ta biết rằng file PE là ứng dụng 32-bit.
  • AddressOfEntryPoint: là một field quan trọng đối với việc phân tích mã độc hay dịch ngược mã nguồn. Nó cho biết địa chỉ mà Windows sẽ bắt đầu thực thi. Nói cách khác, chỉ thị đầu tiên được thực thi sẽ nằm tại địa chỉ này. Đây là một địa chỉ ảo tương đối (Relative Virtual Address - RVA). Tức là, nó nằm ở một ví trí tương đối so với base address của image (ImageBase) khi được nạp vào bộ nhớ chính.
  • BaseOfCodeBaseOfData: là các địa chỉ tương đối so với ImageBase của code và data section.
  • ImageBase: là địa chỉ nạp ưu tiên của PE trong bộ nhớ. Một cách tổng quát, ImageBase của file .exe0x00400000. Do Windows không thể nạp nhiều PE file tại địa chỉ này cùng lúc nên nó sẽ tiến hành di dời (relocation).
  • Subsystem: cho biết subsystem cần để chạy image. Subsystem có thể là Windows Native, GUI, CUI (Command User Interface) hoặc các loại subsystem khác. Ví dụ trên cho biết subsystem là 0x0002 tương ứng với Windows GUI.
  • DataDirectory: là một cấu trúc chứa các thông tin import và export của file PE (còn được gọi là Import Address Table và Export Address Table)

IMAGE_SECTION_HEADER

Dữ liệu mà file PE cần dùng để chạy chẳng hạn như code, icon, hình ảnh, các thành phần của giao diện người dùng, … được lưu ở trong các section khác nhau. Thông tin về các section đó có thể được tìm thấy trong phần IMAGE_SECTION_HEADER, có dạng như sau:

Các section thường thấy trong file PE:

  • .text: chứa executable code của ứng dụng. Characteristics của section này bao gồm CODE, EXECUTE và READ. Điều này cho biết section này chứa executable code có thể được đọc nhưng không thể được ghi.
  • .data: chứa cac dữ liệu khởi tạo của ứng dụng. Nó có thể có quyền READ/WRITE nhưng không thể có quyền EXECUTE.
  • .rdata/.idata: thường chứa các thông tin import của file PE. Các thông tin này giúp file PE có thể sử dụng các hàm từ các file khác hoặc từ Windows API.
  • .ndata: chứa các dữ liệu chưa được khởi tạo.
  • .reloc: chứa các thông tin relocation của file PE.
  • .rsrc: chứa các icon, hình ảnh và các tài nguyên khác cần cho UI.

Các trường quan trọng trong từng section:

  • VirtualAddress: cho biết địa chỉ RVA trong bộ nhớ của section.
  • VirtualSize: cho biết kích thước của section khi được nạp lên bộ nhớ chính.
  • SizeOfRawData: cho biết kích thước của section ở trong ổ đĩa trước khi được nạp lên bộ nhớ chính.
  • Characteristics: chứa các quyền hạn của section.

IMAGE_IMPORT_DESCRIPTOR

Các file PE không chứa nhiều code để chạy mà thường dùng Windows API của hệ điều hành Windows. Phần IMAGE_IMPORT_DESCRIPTOR chứa thông tin về các Window API mà file PE sẽ sử dụng khi được thực thi. Các thông tin này quan trọng trong việc xác định các hành động của file PE.

Ví dụ, nếu file PE sử dụng CreateFile API thì ta biết rằng nó có thực hiện tạo file khi được thực thi. Một ví dụ khác, nếu file PE sử dụng các hàm CreateProcessW, CreateDirectoryW, và WriteFile từ kernel32.dll thì ta biết rằng file có ý định tạo process, tạo thư mục và ghi file.

Phần IMAGE_IMPORT_DESCRIPTOR có dạng như sau:

Trong hình trên, ta thấy file PE sử dụng một vài DLL2 để thực hiện một vài thao tác đăng ký (registry).

Trường OriginalFirstThunkFirstThunk được hệ điều hành sử dụng để xây dựng Import Address Table (IAT) cho file PE.

Packing and Identifying Packed Executables

Do các thông tin về file PE có thể được truy xuất dễ dàng bằng cách công cụ chẳng hạn như hex editor hoặc pe-tree nên người ta thường dùng các packer để ngăn quá trình dịch ngược. Packer là một công cụ dùng để xáo trộn (obfuscate) dữ liệu trong file PE để nó không thể được đọc trừ khi unpacking. Khi file PE thực thi, nó sẽ chạy unpacking routine để trích xuất mã nguồn gốc và thực thi. Các lập trình viên phần mềm thường dùng packer để ngăn việc crack phần mềm còn người tạo ra malware thường dùng nó để tránh bị phát hiện.

Để phát hiện ra các packer thì có hai cách:

From Section Headers

Sử dụng tool pecheck để phân tích file. Output có thể có dạng như sau:

user@machine$ pecheck zmsuz3pinwl
PE check for 'zmsuz3pinwl':
Entropy: 7.978052 (Min=0.0, Max=8.0)
MD5     hash: 1ebb1e268a462d56a389e8e1d06b4945
SHA-1   hash: 1ecc0b9f380896373e81ed166c34a89bded873b5
SHA-256 hash: 98c6cf0b129438ec62a628e8431e790b114ba0d82b76e625885ceedef286d6f5
SHA-512 hash: 6921532b4b5ed9514660eb408dfa5d28998f52aa206013546f9eb66e26861565f852ec7f04c85ae9be89e7721c4f1a5c31d2fae49b0e7fdfd20451191146614a
 entropy: 7.999788 (Min=0.0, Max=8.0)
 entropy: 7.961048 (Min=0.0, Max=8.0)
 entropy: 7.554513 (Min=0.0, Max=8.0)
.rsrc entropy: 6.938747 (Min=0.0, Max=8.0)
 entropy: 0.000000 (Min=0.0, Max=8.0)
.data entropy: 7.866646 (Min=0.0, Max=8.0)
.adata entropy: 0.000000 (Min=0.0, Max=8.0)
.
.
.
.
.
.
----------PE Sections----------
 
[IMAGE_SECTION_HEADER]
0x1F0      0x0   Name:                          
0x1F8      0x8   Misc:                          0x3F4000  
0x1F8      0x8   Misc_PhysicalAddress:          0x3F4000  
0x1F8      0x8   Misc_VirtualSize:              0x3F4000  
0x1FC      0xC   VirtualAddress:                0x1000    
0x200      0x10  SizeOfRawData:                 0xD3400   
0x204      0x14  PointerToRawData:              0x400     
0x208      0x18  PointerToRelocations:          0x0       
0x20C      0x1C  PointerToLinenumbers:          0x0       
0x210      0x20  NumberOfRelocations:           0x0       
0x212      0x22  NumberOfLinenumbers:           0x0       
0x214      0x24  Characteristics:               0xE0000040
Flags: IMAGE_SCN_CNT_INITIALIZED_DATA, IMAGE_SCN_MEM_EXECUTE, IMAGE_SCN_MEM_READ, IMAGE_SCN_MEM_WRITE
Entropy: 7.999788 (Min=0.0, Max=8.0)
MD5     hash: fa9814d3aeb1fbfaa1557bac61136ba7
SHA-1   hash: 8db955c622c5bea3ec63bd917db9d41ce038c3f7
SHA-256 hash: 24f922c1cd45811eb5f3ab6f29872cda11db7d2251b7a3f44713627ad3659ac9
SHA-512 hash: e122e4600ea201058352c97bb7549163a0a5bcfb079630b197fe135ae732e64f5a6daff328f789e7b2285c5f975bce69414e55adba7d59006a1f0280bf64971c
.
.
.
.
.

Trong section trên, ta thấy trường Name không có giá trị. Ngoài ra, ta thấy entropy của 4 section có giá trị tiệm cận 8. Mà entropy càng cao thì dữ liệu càng ngẫu nhiên. Từ hai thông tin này ta biết rằng file PE có sử dụng packer.

Một dấu hiệu khác để phát hiện packer là tập quyền của section. Trong ví dụ trên, ta thấy section có dữ liệu khởi tạo, quyền đọc, ghi và thực thi. Ngoài ra, nếu có nhiều section có quyền EXECUTE thì ta biết rằng file PE có sử dụng packer do chỉ có section .text mới có quyền này.

Trường SizeOfRawData và trường VirtualSize trong IMAGE_SECTION_HEADER cũng có thể được dùng để phát hiện packer. Cụ thể, nếu file có sử dụng packer thì giá trị của SizeOfRawData sẽ nhỏ hơn rất nhiều so với VirtualSize đối với các section có quyền ghi và thực thi. Lý do là vì khi file PE unpack để thực thi, nó sẽ thực hiện ghi dữ liệu và làm tăng kích thước ở trên bộ nhớ chính so với kích thước ở trên ổ đĩa.

From Import Functions

Đối với các file PE có sử dụng packer thì thường sẽ sử dụng các hàm GetProcAddress, GetModuleHandleALoadLibraryA để unpack trong quá trình thực thi:

Summary

Tổng hợp các dấu hiệu của packer:

  • Tên section không theo quy ước.
  • Quyền EXECUTE ở nhiều section.
  • Có một vài section có entropy cao (tiệm cận 8).
  • Khác biệt lớn giữa SizeOfRawDataVirtualSize.
  • Ít sử dụng hàm từ thư viện ngoài.
list
from outgoing([[TryHackMe - Dissecting PE Headers]])
sort file.ctime asc

Resources

Footnotes

  1. MZ này là viết tắt của Mark Zbikowski, một kiến trúc sư của Microsoft đã tạo ra định dạng file MS-DOS.

  2. xem thêm Dynamic Link Libraries (DLL)