Portable Executable
Das Portable Executable-Dateiformat (kurz. PE) wurde von Microsoft für ihr Bestriebssystem Windows entwickelt. Ziel dieses Dateiformats ist es, Programme zwischen unterschiedlichen Versionen von Windows und unterschiedlichen Rechenarchitekturen zu portieren.
Inhaltsverzeichnis |
Aufbau
Image Header
Das Format besteht aus einer vielzahl von Strukturen (genannt Image Header). Diese Strukturen geben Auskunft über den genauen Aufbau der Datei, wenn diese auf der Festplatte liegt, als auch im virtuellen Speicher.
IMAGE_DOS_HEADER
Der IMAGE_DOS_HEADER steht immer am Anfang einer PE-Datei. Nach ihm folgt der sogenannte Dos-Stub. Beide sind nur zur Abwärtskompatibilität zu MS-Dos vorhanden und können fast komplett ignoriert werden. Lediglich zwei Member der Struktur sind für uns wichtig. Zum einen "e_lmagic", da dieser zum Überprüfen der Datei genutzt werden kann (Wert muss immer 0x5A4D bzw. 0x4D5A sein), zum anderen "e_lfanew" dessen Wert als Offset zu den weiteren Strukturen verstanden werden kann.
typedef struct _IMAGE_DOS_HEADER { WORD e_magic; WORD e_cblp; WORD e_cp; WORD e_crlc; WORD e_cparhdr; WORD e_minalloc; WORD e_maxalloc; WORD e_ss; WORD e_sp; WORD e_csum; WORD e_ip; WORD e_cs; WORD e_lfarlc; WORD e_ovno; WORD e_res[4]; WORD e_oemid; WORD e_oeminfo; WORD e_res2[10]; LONG e_lfanew; } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
IMAGE_NT_HEADERS
Man unterscheidet hierbei zwischen IMAGE_NT_HEADERS32 und IMAGE_NT_HEADERS64. Wie die Namen schon vermuten lassen, findet man den IMAGE_NT_HEADERS32 bei einer PE32-Datei und IMAGE_NT_HEADERS64 bei einer Pe64-Datei. Diese Struktur besteht aus zwei weiteren Strukturen und einem DWORD, welches (wie e_magic) zur Überprüfung dienen kann.
typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER OptionalHeader; } IMAGE_NT_HEADERS, *PIMAGE_NT_HEADERS; typedef struct _IMAGE_NT_HEADERS64 { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER64 OptionalHeader; } IMAGE_NT_HEADERS, *PIMAGE_NT_HEADERS;
Anders als der Name vermuten lässt, ist der IMAGE_OPTIONAL_HEADER nicht optional. Diese Struktur liefert uns unter anderem Informationen über die Alignments (siehe Padding), die Addresse, an der wir unser Image finden und vieles mehr. IMAGE_FILE_HEADER liefert widerum Informationen über den Aufbau der Datei, zum Beispiel wieviele Sektionen in der Datei vorhanden sind. Für eine detailierte Beschreibung der beiden Strukturen IMAGE_FILE_HEADER und IMAGE_OPTIONAL_HEADER empfehle ich euch das MSDN. Es würde einfach den Rahmen dieses Wiki-Artikels sprengen jeden einzelnen Member zu erklären, trotzdem lasse ich es mir nicht nehmen einige zu erklären, allerdings innerhalb eines anderen Kontextes (z.B. Padding).
IMAGE_SECTION_HEADER
Die letzte Struktur / der letzte Header in diesem Abschnitt ist der IMAGE_SECTION_HEADER. Nach dem IMAGE_NT_HEADERS kommt ein Array vom Typ IMAGE_SECTION_HEADER, wobei die Größe an der Anzahl der Sektionen gebunden ist. Wenn wir also bei dem IMAGE_FILE_HEADER.NumberOfSections einen Wert von 8 vorliegen haben, dann folgt nach dem IMAGE_NT_HEADERS 8 IMAGE_SECTION_HEADER.
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;
Name ist ein 8-Byte-Array der einen Cstring enthält. Viele Compiler/Linker geben den Sektionen typische Namen. Zum Beispiel bestizt eine Sektion, die nur Code, also die Anweisungen für die CPU, enthält die bezeichnung ".CODE" oder ".text". Sektionen die zum Beispiel nur aus Ressourcen bestehen (später mehr dazu) haben typischerweise den Namen ".rsrc".
VirtualSize gibt die Größe der Sektion im virtuellen Speicher an, nicht zu verwechseln mit SizeOfRawData, denn VirtualSize kann kleiner sein (siehe Padding).
VirtualAddress ist die Addresse im virtuellen Speicher, an der die Sektion vom Windwos-Loader geladen werden soll. Der Wert muss immer ein vielfaches von IMAGE_OPTIONAL_HEADER.SectionAlignment sein.
SizeOfRawData gibt die Größe der Sektion innerhalb der Datei an. Der Wert muss immer ein vielfaches von IMAGE_OPTIONAL_HEADER.FileAlignment sein.
PointerToRawData ist ein Offset zum Beginn Sektion.
PointerToRelocations/PointerToLinenumbers, NumberOfrelocations/NumberOfLinenumbers liefern Informationen über die Relocations oder Linenumbers der Sektion. Die Pointer sind Offsets zu den jeweiligen Einträgen und die NumberOf (wie der Name schon sagt) liefern die Anzahl der Einträge.
Characteristics legt fest, wie auf die Sektion im Speicher zugegriffen werden kann. Man kann eine Sektion als READ/WRITE/EXECUTE-Able markieren, somit kann der Prozess ohne probleme vorhandene Daten in der Sektion überschreiben, lesen oder sogar ausführen. Für eine genaue Liste aller möglichen Werte bietet sich das MSDN an.
Padding
Auf diesen Abschnitt wurde schon zuvor hingewiesen. Im Grunde ist Padding so zu verstehen, dass eine bestimmte Dateimenge bei besonderen Kriterien vergrößert wird. Die Kriterien die im PE-Format entscheident sind heißen Alignment. Zu finden sind die Alignments (ja es sind 2.) im IMAGE_OPTIONAL_HEADER. Im IMAGE_SECTION_HEADER gibt es bestimmte Werte, die durch das Padding/Alignment angepasst werden müssen. Am besten ist dies mit einem einfachen Beispiel zu erklären. Nehmen wir an wir hätten eine Sektion mit einer Größe von 100, wobei in der Sektion auch wirklich nur benötigte Daten stehen und noch kein Padding zum Einsatz gekommen ist. Wenn wir jetzt im IMAGE_OPTIONAL_HEADER.FileAlignment einen Wert von 1000 haben, dann muss die Größe der Sektion innerhalb der Datei angepasst werden. Im Klartext heißt dass, dass wir am PointerToRawData unsere Sektion finden. Danach kommen allerdings 900 NULLen (Zero Padding). Werte die unter anderem durch das Padding/Alignment beeinflusst werden sind: IMAGE_OPTIONAL_HEADER.SizeOfImage, SizeOfRawData, PointerToRawData, VirtualAddress, VirtualSize (nicht unbedingt, aber ich würde es euch empfehlen). Dabei gilt eine einfache Regel, für alle Werte die etwas mit der Datei auf der Festplatte zutun haben wird das FileAlignment verwendet, für alle Werte die etwas mit dem virtuellen Speicher zutun haben dass SectionAlignment.
Die Werte kann man mit einer einfachen Formel berechnen: Value = ((Value%Alignment == 0) ? (Value) : Value + (Alignment - Value%Alignment));
Festplatte
Wie ihr schon im Kapitel "Padding" erfahren habt, gibt es einen großen Unterschied zwischen einer PE-Format die von der Festplatte eingelesen wird und einer die im virtuellen Speicher vorliegt. Zum einen ist das Padding/Alignment entscheident. So kann eine Sektion im virtuellen Speicher viel kleiner/größer sein als die gleiche Sektion, wenn sie von der Festplatte eingelesen wird. Zum anderen stehen die Informationen auf der Festplatte "direkt" hintereinander. Direkt nach unseren Images (IMAGE_OPTIONAL_HEADER.SizeOfHeaders) finden wir die Sektionen ohne weitere angaben. Das heißt, dass die Daten der Sektionen, inklusive der Null-Bytes, direkt hintereinander stehen. Falls ihr also mal eine PE-Datei mit einem Hex-Editor öffent, so dürfte es euch sehr schwer fallen die Sektionen auseinander zu halten (wenn man die ganzen NULL-Bytes außen vor lässt).
virtueller Speicher
Im virtuellen Speicher liegt das Image etwas anders vor. Zum einen werden die gesamten Header, inklusive des IMAGE_DOS_HEADER und Dos-Stub an der IMAGE_OPTIONAL_HEADER.ImageBase geladen. Anschließend werden die einzelnen Sektionen an die jeweilige VirtualAddress geladen, wobei diese abhängig von der ImageBase ist. Wird unsere Datei an die Addresse 0x40000 geladen und die VirtualAddress von unserer Sektion betrüge 0x10000, so würden wir unsere Sektion bei der Addresse 0x41000 finden. Es gibt mehrere Addressen/Zeiger die als relative virtual Address (RVA vorliegen). Macht also nicht den Fehler und versucht zum Beispiel per ReadProcessMemory eine Sektion an der VirtualAddress auszulesen sondern addiert diese immer mit der ImageBase. Fast alle (oder sogar alle) Werte die etwas mit Addressen im virtuellen Speicher zutun haben sind RVAs. Doch warum macht man sich überhaupt die Mühe solch eine Technik einzubinden? Auch DLL-Dateien (Dynamic Link Library) liegen im PE-Format vor und alle ausführbare Dateien brauchen mindestens eine DLL-Datei. Das Problem ist jetzt, dass die ImageBase bei den meisten DLL-Dateien gleich gesetzt ist. Jetzt stellt sich das Problem, dass die Addresse also schon von einer anderen Datei belegt worden sein kann. Somit wird die nächste DLL-Datei einfach ein paar Addressen höher geladen und ImageBase angepasst. Da alle Addressen RVAs sind spielt es also keine Rolle ob jetzt die ImageBase 0x40000 oder 0x50000 beträgt, sie sind immernoch gültig.