Trong phần này, ta sẽ tập trung vào việc truy xuất các thành phần bên trong file PE thay vì giải thích ý nghĩa của các thành phần đó như trong MalDev - Portable Executable Format.

Relative Virtual Addresses (RVAs)

Là các địa chỉ tương đối được dùng để tham chiếu đến các cấu trúc dữ liệu và các section khác nhau, chẳng hạn như code, data và resources ở trong file PE.

Về bản chất, RVA là một giá trị 32-bit lưu offset của một cấu trúc dữ liệu hoặc section tính từ đầu của file PE. Vì thế, để chuyển một RVA thành địa chỉ vùng nhớ tuyệt đối, hệ điều hành sẽ cộng RVA với địa chỉ cơ sở của file PE ở trong vùng nhớ.

DOS Header (IMAGE_DOS_HEADER)

Nằm ở đầu file PE và chứa thông tin về file chẳng hạn như kích thước và các thuộc tính. Thông tin quan trọng nhất ở trong DOS header là RVA (offset) đến NT header.

Đoạn code sau minh họa cho việc truy xuất DOS header:

// Pointer to the structure 
PIMAGE_DOS_HEADER pImgDosHdr = (PIMAGE_DOS_HEADER)pPE;		
if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE){
	return -1;
}

Có thể thấy, do nằm ở đầu file PE, việc truy xuất DOS header được thực hiện đơn giản bằng cách ép kiểu biến pPE thành kiểu PIMAGE_DOS_HEADER. Điều này giúp chúng ta có được con trỏ của DOS header. Sau đó, thực hiện kiểm tra giá trị của e_magic để chắc chắn rằng DOS header là hợp lệ.

NT Header (IMAGE_NT_HEADERS)

Thành phần e_lfanew của DOS header chứa offset đến NT header1. Cụ thể hơn, nó là một RVA đến vùng nhớ của cấu trúc IMAGE_NT_HEADERS.

Để có được địa chỉ tuyệt đối của NT header. Ta chỉ việc cộng địa chỉ cơ sở của file PE ở trong vùng nhớ với giá trị e_lfanew:

// Pointer to the structure
PIMAGE_NT_HEADERS pImgNtHdrs = (PIMAGE_NT_HEADERS)(pPE + pImgDosHdr->e_lfanew);
if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE) {
	return -1;
}

Câu lệnh if là để kiểm tra tính hợp lệ của NT header.

File Header (IMAGE_FILE_HEADER)

Do file header là một thành phần của NT header2 nên ta có thể truy xuất thông qua cấu trúc IMAGE_NT_HEADERS như sau:

IMAGE_FILE_HEADER		ImgFileHdr	= pImgNtHdrs->FileHeader;

Xem thêm [[MalDev - Portable Executable Format#dos-header-image_file_header|DOS Header (IMAGE_FILE_HEADER)]] để biết ý nghĩa của các thành phần bên trong file header.

Optional Header (IMAGE_OPTIONAL_HEADER)

Do optional header là một thành phần của NT header như file header nên ta có thể truy xuất thông qua cấu trúc IMAGE_NT_HEADERS như sau:

IMAGE_OPTIONAL_HEADER	ImgOptHdr = pImgNtHdrs->OptionalHeader;
if (ImgOptHdr.Magic != IMAGE_NT_OPTIONAL_HDR_MAGIC) {
	return -1;
}

Câu lệnh if là để kiểm tra tính hợp lệ của optional header với giá trị của IMAGE_NT_OPTIONAL_HDR_MAGIC sẽ được trình biên dịch thay thế thành IMAGE_NT_OPTIONAL_HDR32_MAGIC hoặc IMAGE_NT_OPTIONAL_HDR64_MAGIC tùy theo kiến trúc của file PE.

Một số thành phần là RVA ở trong optional header3:

  • AddressOfEntryPoint
  • BaseOfCodeBaseOfData

DataDirectory (IMAGE_DATA_DIRECTORY)

Là thành phần cuối cùng trong optional header. Nó là một mảng các cấu trúc IMAGE_DATA_DIRECTORY, mỗi cấu trúc chứa thông tin về một data directory cụ thể.

Hai thành phần của cấu trúc IMAGE_DATA_DIRECTORY3 là:

  • VirtualAddress: RVA của data directory ở trong file PE.
  • Size: kích thước của data directory.

Như đã đề cập ở [[MalDev - Portable Executable Format#optional-header-image_optional_header|Optional Header (IMAGE_OPTIONAL_HEADER)]], một data directory có thể được truy xuất từ mảng DataDirectory thông qua chỉ số của nó:

IMAGE_DATA_DIRECTORY ExpDataDir = ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];

Export Table (IMAGE_EXPORT_DIRECTORY)

Cấu trúc này hiện không được Microsoft mô tả nên ta cần tham khảo tài liệu của bên khác.

Định nghĩa của cấu trúc này:

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;
    WORD    MajorVersion;
    WORD    MinorVersion;
    DWORD   Name;
    DWORD   Base;
    DWORD   NumberOfFunctions;
    DWORD   NumberOfNames;
    DWORD   AddressOfFunctions;     // RVA from base of image
    DWORD   AddressOfNames;         // RVA from base of image
    DWORD   AddressOfNameOrdinals;  // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

Các thành phần quan trọng của export table:

  • NumberOfFunctions - Chỉ định số lượng hàm mà file PE sẽ export.
  • NumberOfNames - Chỉ định số lượng tên (bao gồm biến và hàm) mà file PE sẽ export. Lưu ý rằng file PE còn có thể export các số thứ tự (ordinal numbers).
  • AddressOfFunctions - Con trỏ của mảng chứa địa chỉ của các hàm sẽ được export.
  • AddressOfNames - Con trỏ của mảng chứa tên của các hàm sẽ được export.
  • AddressOfNameOrdinals - Con trỏ của mảng chứa các số thứ tự của các hàm sẽ được export.

Để truy xuất export table, ta cần lấy ra offset của nó thông qua DataDirectory và cộng thêm địa chỉ cơ sở của file PE:

ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;

Có thể thấy, ta ép kiểu về PIMAGE_EXPORT_DIRECTORY để có con trỏ đến cấu trúc IMAGE_EXPORT_DIRECTORY.

Import Address Table (IMAGE_IMPORT_DESCRIPTOR)

Cấu trúc này cũng không được mô tả chính thức bởi Microsoft mặc dù được định nghĩa bên trong file Winnt.h.

Định nghĩa của cấu trúc này:

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;
        DWORD   OriginalFirstThunk;
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp;
    DWORD   ForwarderChain;
    DWORD   Name;
    DWORD   FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR;

Truy xuất như sau:

IMAGE_IMPORT_DESCRIPTOR* pImgImpDesc = (PIMAGE_IMPORT_DESCRIPTOR)(pPE + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);

Với pPE là địa chỉ cơ sở của file PE trong vùng nhớ, ImgOptHdr là optional header và DataDirectory là mảng chứa các data directory.

Additional Undocumented Structures

Một vài ví dụ về các data directory không được mô tả bởi Microsoft:

  • IMAGE_TLS_DIRECTORY: dùng để lưu thông tin về TLS (Thread Local Storage)
  • IMAGE_RUNTIME_FUNCTION_ENTRY: chứa thông tin liên quan đến một runtime function. Đây là hàm sẽ được gọi bởi cơ chế xử lý ngoại lệ của hệ điều hành Windows để thực thi đoạn mã xử lý ngoại lệ.
  • IMAGE_BASE_RELOCATION: chứa thông tin về các base relocation của file PE. Cụ thể hơn, các base relocation cho biết cách chỉnh sửa địa chỉ của các hàm và biến được import vào file PE khi nó được nạp lên một vùng nhớ khác với lúc nó được liên kết, đặc biệt là khi có sự xuất hiện của kỹ thuật ASLR.

Cách truy xuất các data directory trên đều giống nhau: đều có địa chỉ tương đối truy xuất từ mảng DataDirectory thông qua index rồi được cộng với địa chỉ tuyệt đối của file PE.

PE Sections

Như đã biết, mỗi section trong file sẽ có một cấu trúc IMAGE_SECTION_HEADER tương ứng4.

Cấu trúc IMAGE_SECTION_HEADER được lưu ở trong một mảng sau cấu trúc IMAGE_NT_HEADERS:

PIMAGE_SECTION_HEADER pImgSectionHdr = (PIMAGE_SECTION_HEADER)(((PBYTE)pImgNtHdrs) + sizeof(IMAGE_NT_HEADERS));

Với pImgNtHdrs là con trỏ chứa địa chỉ tuyệt đối của cấu trúc IMAGE_NT_HEADERS.

Sau khi có con trỏ pImgSectionHdr, ta có thể dùng vòng lặp để truy xuất từng phần tử:

for (size_t i = 0; i < pImgNtHdrs->FileHeader.NumberOfSections; i++) {
	// pImgSectionHdr is a pointer to section 1
	pImgSectionHdr = (PIMAGE_SECTION_HEADER)((PBYTE)pImgSectionHdr + (DWORD)sizeof(IMAGE_SECTION_HEADER));
	// pImgSectionHdr is a pointer to section 2
}

Với FileHeader.NumberOfSections cho biết số lượng các section có trong file PE.

Resources

Footnotes

  1. xem thêm [[MalDev - Portable Executable Format#file-header-image_file_header|File Header (IMAGE_FILE_HEADER)]]

  2. xem thêm [[MalDev - Portable Executable Format#nt-header-image_nt_headers|NT Header (IMAGE_NT_HEADERS)]]

  3. xem thêm [[MalDev - Portable Executable Format#optional-header-image_optional_header|Optional Header (IMAGE_OPTIONAL_HEADER)]] 2

  4. xem thêm PE Sections