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ới5A 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ỗiMZ
1, chính là signature (magic number) của file PE. - Trường
e_lfanew
là địa chỉ bắt đầu củaIMAGE_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.
- PointerToSymbolTable và NumberOfSymbols: 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.
- BaseOfCode và BaseOfData: 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
.exe
là0x00400000
. 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 OriginalFirstThunk và FirstThunk đượ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:
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
, GetModuleHandleA
và LoadLibraryA
để 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 SizeOfRawData và VirtualSize.
- Ít sử dụng hàm từ thư viện ngoài.
Related
Resources
- TryHackMe | Dissecting PE Headers
- IMAGE_NT_HEADERS32 (winnt.h) - Win32 apps | Microsoft Learn
- Portable Executable - Wikipedia
- A dive into the PE file format - Introduction - 0xRick’s Blog
Footnotes
-
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. ↩
-
xem thêm Dynamic Link Libraries (DLL) ↩