haker.info | Etyczny hacking |

 Format plików PE/PE32+ dla początkujących

11 lipca 2019 godz. 03:06    Dawid Farbaniec    1520 słów

1. Wprowadzenie

liki wykonywalne, których tak często się używa w systemie Windows mogą być nieco owiane tajemnicą dla osób niezajmujących się programowaniem niskopoziomowym i inżynierią odwrotną kodu (ang. RCE). Pliki wykonywalne z rozszerzeniem *.exe, które tak często uruchamiasz jako użytkownik, a może i budujesz jako programista posiadają określony format nazywany PE (z ang. przenośny wykonywalny) oraz zmodyfikowaną wersję PE32+. Określenie Portable Executable (z ang. przenośny wykonywalny) informuje, że format ten jest niezależny od architektury. Warto jednak wspomnieć, że 32-bitowe wersje Windows korzystają z formatu PE, natomiast wersje 64-bitowe Windows obsługują dodatkowo format PE32+. Format PE obejmuje nie tylko pliki wykonywalne, ale także obiektowe oraz biblioteki DLL. Jako, że format PE bazuje na Unixowym formacie plików COFF, to często używa się też określenia PE/COFF.

1.1. Podstawowe pojęcia

Przesunięcie w pliku (ang. file offset) — liczba wyznaczająca położenie określonych danych od ustalonego miejsca w pliku. Bardzo często miejscem startowym jest początek pliku, który ma offset równy zero.

Adres wirtualny (ang. Virtual Address, VA) — jest to adres do danych po załadowaniu ich do pamięci. Prawie zawsze określa się go po prostu adresem. Można spotkać też określenia: liniowy lub efektywny. Adres wirtualny zawiera w sobie tzw. ImageBase, czyli miejsce w pamięci pod które ładowany jest plik.

Relatywny adres wirtualny (ang. Relative Virtual Address, RVA) — ten adres można otrzymać odejmując ImageBase danego modułu od adresu wirtualnego (VA) w tym module.

1.2. Schemat ogólny formatu PE

Ogólny schemat budowy pliku PE przedstawiono na rysunku 1.1. Bardziej szczegółowy opis określonych elementów znajdziesz w dalszej części wpisu.

Portable Executable File Format
Rysunek 1.1. Format pliku PE/COFF — schemat ogólny

2. Struktura pliku PE

2.1. Nagłówek MS-DOS (MS-DOS Stub)

Na samym początku pliku PE znajduje się nagłówek MS-DOS. Dwa pierwsze bajty to 0x4D i 0x5A, czyli znaki ASCII "MZ". Te dwie litery to inicjały Marka Zbikowskiego (Mark Joseph Zbikowski) jednego z ważniejszych programistów systemu MS-DOS. Sygnatura "MZ" technicznie określa, że nagłówek MS-DOS jest programem typu EXE dla tego podsystemu. Kod aplikacji nazywanej MS-DOS Stub został wstawiony na samym początku pliku PE w celach informacyjnych. Podczas próby uruchomienia w systemie MS-DOS pliku PE dla systemu Windows program MS-DOS Stub wyświetli informację This program cannot be run in DOS mode. Istnieje możliwość zmiany programu MS-DOS Stub za pomocą opcji konsolidatora (ang. linker).

Nagłówek MS-DOS w postaci struktury można znaleźć w pliku winnt.h, który zawiera środowisko Visual Studio (Visual C++).

Sygnatura MZ oraz nagłówek MS-DOS (Visual C++, winnt.h):

#define IMAGE_DOS_SIGNATURE 0x5A4D // MZ 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 LONG e_lfanew; // File address of new exe header } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

Warto zaznaczyć, że w polu e_lfanew (adres w pliku: 0x3C) znajduje się wartość przesunięcia (ang. offset), która określa gdzie znajduje się sygnatura "PE", czyli początek nagłówka PE.

Na rysunku 2.1 przedstawiono przykładowy plik PE otwarty w edytorze szesnastkowym XVI32. Pokolorowane obszary oznaczają poszczególne elementy pliku PE. Na rysunku 2.1 zmieścił się nagłówek MS-DOS, program MS-DOS Stub oraz początek nagłówka PE.

MS-DOS Header
Rysunek 2.1. Przykładowy plik PE w hex edytorze z objaśnieniem jego struktury

2.2. Sygnatura PE (PE Signature)

Sygnatura PE (wartości: 0x50 i 0x45, czyli znaki ASCII "PE") określa początek nagłówka PE. W pliku winnt.h sygnaturę definiuje stała:

#define IMAGE_NT_SIGNATURE 0x00004550 // PE00

2.3. Nagłówek COFF (COFF Header)

Po sygnaturze PE znajduje się nagłówek COFF, który posiada następujące pola:

Nazwa pola Przesunięcie (ang. offset) Rozmiar Opis
Machine 0 2 Numer określający typ maszyny docelowej. Dla architektury x64 będzie to stała IMAGE_FILE_MACHINE_AMD64 (wartość: 0x8664). Szczegóły w specyfikacji.
NumberOfSections 2 2 Ilość sekcji w pliku PE.
TimeDateStamp 4 4 Znacznik czasowy określający kiedy plik został utworzony (typ wartości time_t z języka C).
PointerToSymbolTable 8 4 Adres w pliku do tablicy symboli debugowania. Dla plików wykonywalnych powinno być tu zero.
NumberOfSymbols 12 4 Ilość wpisów w tablicy symboli debugowania. Dla plików wykonywalnych powinno być tu zero.
SizeOfOptionalHeader 16 2 Rozmiar opcjonalnego nagłówka PE (Optional Header). Wymagane tylko dla plików wykonywalnych.
Characteristics 18 2 Flagi określające atrybuty pliku. Szczegóły w specyfikacji.

Definicja nagłówka COFF w pliku winnt.h prezentuje się następująco:

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;

2.4. Opcjonalny nagłówek PE (PE Optional Header)

Kolejnym elementem, który można wyróżnić w formacie PE jest nagłówek opcjonalny. Jego nazwa informuje, że nie występuje on zawsze. Pliki wykonywalne muszą go posiadać, natomiast pliki obiektowe nie.

Nagłówek ten różni się dla 32-bitowych i 64-bitowych plików wykonywalnych. Struktura IMAGE_OPTIONAL_HEADER32 odpowiada plikom PE (32-bit), natomiast struktura IMAGE_OPTIONAL_HEADER64 jest dla plików PE32+ (64-bit).

Definicja nagłówka opcjonalnego dla plików PE32+ w nagłówku winnt.h prezentuje się następująco:

typedef struct _IMAGE_OPTIONAL_HEADER64 { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; ULONGLONG 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; ULONGLONG SizeOfStackReserve; ULONGLONG SizeOfStackCommit; ULONGLONG SizeOfHeapReserve; ULONGLONG SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;

Warto zaznaczyć, że sygnatura PE, nagłówek COFF oraz nagłówek opcjonalny zostały objęte w struktury o nazwach _IMAGE_NT_HEADERS oraz _IMAGE_NT_HEADERS64. Definicja struktury _IMAGE_NT_HEADERS z pliku winnt.h prezentuje się następująco:

typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

Szczegóły i opis pól nagłówka opcjonalnego w specyfikacji.

2.5. Nagłówki sekcji (Section Headers)

Nagłówki sekcji znajdują się zaraz po nagłówku opcjonalnym. Zawierają one informacje o sekcjach, jakie zawiera plik PE. W celu otrzymania adresu do nagłówków sekcji należy obliczyć przesunięcie sumując rozmiar nagłówków występujących wcześniej.

Rozmiar nagłówka sekcji to 40 bajtów (#define IMAGE_SIZEOF_SECTION_HEADER 40).

Każdy nagłówek sekcji posiada następujące pola:

Nazwa pola Przesunięcie (ang. offset) Rozmiar Opis
Name 0 8 Nazwa sekcji jako napis UTF-8 o maksymalnej długości 8 bajtów (dla plików wykonywalnych). W przypadku krótszej nazwy puste miejsce jest wypełniane zerami.
VirtualSize 8 4 Rozmiar wirtualny sekcji, czyli po załadowaniu do pamięci.
VirtualAddress 12 4 Dla plików wykonywalnych jest to adres początku sekcji relatywnie do adresu ImageBase (RVA).
SizeOfRawData 16 4 Dla plików wykonywalnych jest to rozmiar sekcji w pliku na dysku.
PointerToRawData 20 4 Adres w pliku do danych sekcji.
PointerToRelocations 24 4 Dla plików wykonywalnych tutaj zero.
PointerToLinenumbers 28 4 Dla plików wykonywalnych tutaj zero.
NumberOfRelocations 32 2 Dla plików wykonywalnych tutaj zero.
NumberOfLinenumbers 34 2 Dla plików wykonywalnych tutaj zero.
Characteristics 36 4 Flagi określające atrybuty sekcji. Szczegóły w specyfikacji.

Definicja nagłówka sekcji w pliku winnt.h prezentuje się następująco:

typedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_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;

2.6. Dane sekcji (Section Data)

Dane sekcji znajdują się po nagłówkach sekcji. Adres do danych określonej sekcji można uzyskać poprzez odczytanie wartości pola PointerToRawData z nagłówka sekcji. Rozmiar sekcji określa natomiast wartość pola SizeOfRawData.

2.6.1. Szczególne typy sekcji

Warte uwagi są zarezerwowane sekcje specjalne dotyczące plików wykonywalnych.

Nazwa sekcji Opis
.bss Dane niezainicjalizowane w wolnym formacie.
.data Dane zainicjalizowane w wolnym formacie.
.edata Tabele eksportu. Zawierają symbole, które pozwalają innym plikom wykonywalnym korzystać z funkcji, które są eksportowane przez ten moduł.
.idata Tabele importu. Zawierają symbole importowanych funkcji przez plik wykonywalny.
.rdata Dane zainicjalizowane tylko do odczytu.
.text Kod do wykonania w wolnym formacie.
... Szczegóły w specyfikacji

3. Trochę kodu dla przykładu

Na listingu 3.1 poniżej przedstawiono program, który czyta określony plik PE, a następnie odczytuje wartość pola e_magic z nagłówka MS-DOS.

Listing 3.1. Odczytanie wartości pola e_magic z nagłówka MS-DOS pliku PE (Visual C++)
#include <Windows.h> #include <strsafe.h> int wmain(int argc, TCHAR* argv[]) { HANDLE hFile = NULL; DWORD dwSize = NULL; DWORD dwRead = NULL; LPVOID fileBytes = NULL; PIMAGE_DOS_HEADER dosHeader = { }; TCHAR infoText[64] = TEXT(""); // otwórz plik PE hFile = CreateFile(TEXT("D:\\masm64\\prog1.exe"), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) return EXIT_FAILURE; // pobierz rozmiar pliku i zaalokuj pamięć dwSize = GetFileSize(hFile, NULL); fileBytes = new BYTE[dwSize]; // czytaj plik if (ReadFile(hFile, fileBytes, dwSize, &dwRead, NULL) == FALSE) return EXIT_FAILURE; // odczytaj nagłówek MS-DOS z pliku PE dosHeader = (PIMAGE_DOS_HEADER)fileBytes; // wklej magiczną liczbę do napisu StringCchPrintf(infoText, 64, TEXT("dosHeader->e_magic = 0x%x"), dosHeader->e_magic); // wyświetl komunikat z wartością pola e_magic MessageBox(0, infoText, TEXT("haker.info"), 0); return EXIT_SUCCESS; }

Działanie programu z listingu 3.1 przedstawiono na rysunku 3.1.

Odczytanie nagłówka pliku PE
Rysunek 3.1. Program odczytuje i wyświetla magiczną liczbę z nagłówka pliku PE (Visual C++)

4. Różnice pomiędzy PE, a PE32+

Różnice w strukturze IMAGE_OPTIONAL_HEADER32, a IMAGE_OPTIONAL_HEADER64 przedstawiono w tabeli poniżej.

Nazwa pola Format PE Format PE32+
BaseOfData ULONG Usunięto
ImageBase ULONG ULONGLONG
SizeOfStackReserve ULONG ULONGLONG
SizeOfStackCommit ULONG ULONGLONG
SizeOfHeapReserve ULONG ULONGLONG
SizeOfHeapCommit ULONG ULONGLONG

5. Zakończenie

Mam nadzieję, że artykuł ten przybliżył początkującym format PE/COFF. Nie chciałem tego wprowadzenia za bardzo obciążać dużą ilością szczegółowych opisów. Z tego powodu jako kolejny krok polecam dokument Microsoft Portable Executable and Common Object File Format Specification firmy Microsoft.
Dziękuję za przeczytanie!

Dawid Farbaniec


Tagi:  reverse-engineering  pe-file-format 
Wszystkie treści umieszczone na tej witrynie są chronione prawem autorskim. Surowo zabronione jest kopiowanie i rozpowszechnianie zawartości tej witryny bez zgody autora. Wszelkie opublikowane tutaj treści (w tym kody źródłowe i inne) służą wyłącznie celom informacyjnym oraz edukacyjnym. Właściciele tej witryny nie ponoszą odpowiedzialności za ewentualne niezgodne z prawem wykorzystanie zasobów dostępnych w witrynie. Użytkownik tej witryny oświadcza, że z zamieszczonych tutaj danych korzysta na własną odpowiedzialność. Wszelkie znaki towarowe i nazwy zastrzeżone zostały użyte jedynie w celach informacyjnych i należą wyłącznie do ich prawnych właścicieli. Korzystając z zasobów witryny haker.info oświadczasz, że akceptujesz powyższe warunki.