Code Analyse

Transcrição

Code Analyse
Code Analyse
Eine Einführung in die Code Analyse.
Wintersemester 2006
Professor:
Prof. Dr. Thomas Risse
Studenten:
Christian Schleif
I7I (127624)
Holger Wolf
I7I (122143)
Inhaltsverzeichnis
1.
Einleitung ........................................................................................................................................ 3
2.
Befehle und Register....................................................................................................................... 4
2.1.
P4 Register ............................................................................................................................... 4
2.1.1.
3.
Einführung Programmierung .......................................................................................................... 5
3.1.
Programmtypen ....................................................................................................................... 5
3.2.
Befehlsformat ........................................................................................................................ 13
3.3.
Manuelles Disassemblieren ................................................................................................... 17
3.4.
Struktur von ausführbaren Dateien ....................................................................................... 18
3.5.
Debuggen und Disassemblieren ............................................................................................ 22
3.5.1.
4.
5.
General-Purpose Register .............................................................................................. 4
Versteckte Sprünge...................................................................................................... 27
Paradigmen für die Analyse von ausführbaren Dateien............................................................... 29
4.1.
Daten erkennen ..................................................................................................................... 29
4.2.
Programmstrukturen erkennen ............................................................................................. 51
Verwendete Software ................................................................................................................... 54
5.1.
Disassembler und Debugger .................................................................................................. 54
5.1.1.
Dumpbin.exe (.Net) ..................................................................................................... 54
5.1.2.
IDA Pro Disassembler................................................................................................... 54
5.1.3.
Windasm ...................................................................................................................... 54
5.1.4.
The OllyDbg Debugger ................................................................................................. 54
5.1.5.
Programme für bestimmte Programmiersprachen ..................................................... 54
5.2.
Hexeditors .............................................................................................................................. 55
5.2.1.
5.3.
PS Pad .......................................................................................................................... 55
Andere Tools .......................................................................................................................... 55
5.3.1.
Microsoft Visual Studio 2005 ....................................................................................... 55
5.3.2.
MASM32 ...................................................................................................................... 55
6.
Literatur ........................................................................................................................................ 56
7.
Abbildungsverzeichnis .................................................................................................................. 58
8.
Quelltextverzeichnis ..................................................................................................................... 59
2
1. Einleitung
Diese Ausarbeitung soll sich mit der Code Analyse und ihrer verschiedene Schwierigkeiten,
Ansatzpunkte und Ausführen beschäftigen. Hierzu gehen wir zu Beginn auf die Befehle und Register
der gewählten Architektur ein. Anschließend werden wir eine Einführung in die Programmierung
unter Windows geben, wo wir vorzugsweise die Programmiersprache C verwenden. Für C haben wir
uns entschlossen, da diese Sprache eine der am weit verbreitetsten Programmiersprachen ist.
Danach gehen wir auf die eigentliche Code Analyse ein: was ist zu beachten? Wie werden Strukturen
wie Schleife, Verzweigungen usw. dargestellt.
Am Ende geben wir eine kurze Einführung in die von uns verwendete Software.
Alle Informationen dieser Arbeit basieren auf den in der Literaturliste angegebenen Quellen. In den
meisten Fällen werden am Ende eines Kapitel oder eines Absatzes Verweise auf die zu Grunde
liegende Literatur gegeben. In einigen Fällen ist es jedoch so, dass das Wissen auf mehr Quellen
basiert als direkt angegeben. Das liegt daran, dass bei uns dieses Wissen schon vor dem Verfassen
der Arbeit vorhanden war. In den meisten Fällen haben wir uns jedoch bemüht, Literaturhinweise zu
geben, die das jeweilige Thema abdecken.
Da uns für diese Ausarbeitung nur Intel-Prozessoren zu Verfügung stehen, beschränken wir uns auf
die Verwendung des Intel Pentium 4.
3
2. Befehle und Register
Um Code zu analysieren, müssen bestimmte Informationen über die Befehle und Register bekannt
sein. Da im folgenden der Pentium 4 als Architektur genutzt wurde, geben wir an dieser Stelle einen
Überblick über Befehle und Register des Pentium 4 gegeben, die wir für wichtig erachten.
2.1.
P4 Register
Dieser Abschnitt beschreibt die Register, die von uns benutzt wurden. Der Pentium 4 verspricht
folgende Register: flags-register, segment-register, control-register, system-address-register and das
debug-register. Des Weiteren gibt es noch das EIP Register, welches gleichbedeutend mit dem
„Instruction Pointer“ ist. Für diese Ausarbeitung interessieren uns nur die im nächsten Abschnitt
beschriebenen Register.
2.1.1. General-Purpose Register
Da der Pentium 4 auf der x86 Architektur basiert und die Abwärtskompatibilität gewährt sein muss,
wenn diese Abwärtskompatibilität nicht vorhanden wäre, würden ältere Programme nicht auf den
aktuellen Prozessoren laufen. Man kann die Register wie folgt auszählen:
EAX = (16 + AX = (AH + AL))
EBX = ---------- ‚’ ---------ECX = ---------- ‚’ ---------EDX = ---------- ‚’ ---------ESI = (16 + SE)
EDI = (16 + DI)
EBP = --- ‚’ --ESP = --- ‚’ --EAX, EBX, ECX und EDX nennen sich Arbeitsregister und sind in weitere Register unterteilt.
Beispielsweise sind die unteren 16Bit des EAX Registers AX. AX ist wiederum in die Register AH und
AL unterteilt. EDI und ESI sind Index-Register. Das EBP Register wird für die Adressierung von lokalen
Variablen des Stacks verwendet. Das ESP Register ist der Stackpointer, der automatisch durch
Befehle wie PUSH, POP, RET oder CALL verändert wird. ESI, EDI, ESP und EBP sind ebenfalls in weitere
Register unterteilt.
[vgl. [1],[28],[29]]
4
3. Einführung Programmierung
Bevor wir uns mit Disassemblierung, Code Analyse und IDA Pro beschäftigen, ist es notwendig, einige
Informationen zu Programmierung und Windows, Assembler und Befehlsdarstellung zu geben.
3.1.
Programmtypen
Heutzutage basieren die meisten Programme auf einer API (Application programm interface). Über
die API kann ein Programm direkt mit dem Betriebssystem kommunizieren bzw. bei der Nutzung
einer API Funktion ist nicht bekannt, ob es eine Funktion im Systemkernel oder in einer Bibliothek
(DLL dynamic link library) ist. Normalerweise werden API Funktionen in DLL gehalten, die in einem
Systemverzeichnis zu finden sind (Bsp. unter Windows: „Windows\system32“).
DLLs werden vom Compiler erst sehr spät gelinkt, das nennt sich (so-called late implicit binding).
Derzeit existieren weit über 3000 API Funktionen und mit jeder neuen Version des jeweiligen
Betriebssystems kommen weitere hinzu. Beispiele oft verwendeter Bibliotheken sind:

Kernel32.dll: Diese Bibliothek beinhaltet Funktionen zur Kontrolle und zum Management des
Systems, zum Beispiel Speicher-, Resourcen- oder Dateiverwaltung

User32.dll: Diese Bibliothek enthält Funktionen für das Userinterface wie Windowmessages,
Timer oder Menus

Gdi32.dll: GDI steht für Grafics device interface (GDI) und enthält diese Bibliothek Funktionen
für grafische Operationen zum Zeichnen

Comctl32.dll: Diese Bibliothek ist für den neuen grafischen Control-Style bei Windows
zuständig (sie kam hauptsächlich mit Windows XP)
Für jede API Funktion, die einen String entgegen nimmt gibt es zwei Varianten, die sich durch ihren
Namen unterscheiden. Funktionen, die am Ende ein A haben, stellen ihre Funktion für ANSI
Codierung dar. Enden Funktionen mit einen W, so steht das für Unicode. Nach außen wird jeweils nur
eine Funktion bereitgestellt. Betrachtet man MessageBox als Beispiel, dann wird man MessageBox
aufrufen, aber intern wird entweder auf MessageBoxA oder MessageBoxW verwiesen. Welche der
möglichen Funktionen genommen werden soll, wird vom Compiler bestimmt, es sei denn, man
definiert es einmalig per Hand. Schreibt man direkt Assembler, ist es notwendig, bei jeder Funktion
die richtige auszuwählen, um sie zu benutzen.
Bei Windows Programmierung gibt es zwei grundsätzliche Arten von Programmen. Zum einen gibt es
Konsolenanwendungen und zum anderen Anwendungen, die eine grafische Benutzeroberfläche
haben.
Neben diesen zwei Arten von Programmen gibt es noch weitere, wie Dienste und Treiber, die wir
aber im Rahmen dieser Arbeit nicht behandeln.
5
In sehr vielen Programmen wird nicht direkt auf die API zugegriffen, sondern es wird eine Schicht
(Layer) dazwischen gelegt. Dieser Layer wird durch Bibliotheken realisiert. Dadurch wird das
Analysieren des Programms schwieriger, da man heraus finden muss, welche Bibliotheken verwendet
werden.
Prinzipiell ist es möglich, dass eine Konsolenanwendung eine GUI (Dialoge usw.) hat bzw. zeigt oder
eine GUI Anwendung eine Konsole öffnet.
Die meisten Konsolenprogramme verfolgen einen sequentiellen Ablauf, das bedeutet, dass sie
beispielsweise zu Beginn etwas ausgeben in der Regel Textmeldungen, anschließend etwas einlesen
(zum Beispiel Eingaben vom Benutzer) und zuletzt wieder etwas ausgeben. Ein anderer möglicher
Ablauf könnte sein, dass das Programm eine Datei irgendwo hinkopiert und anschließend einen
Eintrag in die Windowsregistrierung schreibt, wie es bei einer Installation der Fall sein könnte.
6
Folgender Quelltext verdeutlicht einen solchen Ablauf:
#include <windows.h>
void main() {
char *string = "Bitte eine Zeichenkette eingeben\n\0";
char *hw = "Hello User!\n\0";
char buf[100];
DWORD d;
// Gibt die Konsole frei in der das Programm gestartet wurde
FreeConsole();
// Erzeugt eine neue Konsole
AllocConsole();
// Fuer Ausgaben ein Output-Handle besorgen
HANDLE ho = GetStdHandle(STD_OUTPUT_HANDLE);
// Um von der Konsole zu lesen ein Input-Handle besorgen
HANDLE hi = GetStdHandle(STD_INPUT_HANDLE);
// Die beiden Strings ausgeben
WriteConsole(ho, hw, lstrlen(hw),&d, NULL);
WriteConsole(ho, string, lstrlen(string),&d, NULL);
// Von der Kommando-Zeile lesen
ReadConsole(hi,(void*)buf,100,&d,NULL);
// Handles wieder schließen
CloseHandle(ho);
CloseHandle(hi);
// Konsole freigeben
FreeConsole();
}
Listing 1: Typisches Konsolenprogramm
Dieses Trivialprogramm gibt zwei Strings aus und wartet dann auf eine Eingabe. FreeConsole() und
AllocConsole() sorgen dafür, dass die aktuelle Konsole, in der das Programm gestartet wird, freigeben
wird und eine neue erstellt wird. Dieses Programm nutzt ausschließlich API Funktionen.
Normalerweise arbeiten Programmierer überwiegend mit Bibliotheken, um sich Schreibarbeit zu
ersparen. Das gleiche Programm würde unter der Verwendung einer Bibliothek wie folgt aussehen:
#include <stdio.h>
void main() {
char *string = "Bitte eine Zeichenkette eingeben\n\0";
char *hw = "Hello User!\n\0";
char buf[100];
// Gibt die Konsole frei, in der das Programm gestartet wurde
FreeConsole();
// Erzeugt eine neue Konsole
AllocConsole();
// Die beiden String ausgeben
puts(hw);
puts(string);
// Von der Kcommando-Zeile lesen
gets(buf);
// Konsole freigeben
FreeConsole();
}
Listing 2: Typisches Konsolenprogramm, das eine Bibliothek nutzt
7
Wie zu sehen ist, sind für das Einlesen bzw. Ausgeben von Daten statt jeweils drei Zeilen nur eine
Zeile nötig. Im späteren Verlauf zeigen wir, dass ein guter Disassembler Standardbibliotheken
automatisch erkennt. Das ist auch ein Grund, warum Programmierer oftmals keine
Standardbibliotheken nutzen, um so ihr Programm zu verschleiern, damit andere Unbefugte den
Code nicht analysieren können.
Wenn man diese Programme disassemblieren würde, könnte man sie einfacher nachvollziehen, da
sie eben sequentiell sind. Wenn aber komplexere Benutzersteuerungen vorhanden sein sollen, wird
das Programm um einiges komplizierter. Bei solchen Programmen braucht man eine Schleife, die
Ereignisse des Betriebssystems verarbeitet. Folgender Quelltext ermöglicht eine Interaktion mit dem
Benutzer:
#include <windows.h>
//Funktionen deklarieren
BOOL WINAPI handler(DWORD);
void inputcons();
void print(char *);
//Variablen deklarieren
HANDLE h1,h2;
char *sError = "Error input!\n";
char buf[35];
char *sCTRLC = "CTRL+C\n";
char *sCTRLBREAK = "CTRL+BREAK\n";
char *sCLOSE = "CLOSE\n";
char *sCTRL = "CTRL\n";
char *sChar="Character '%c' \n";
void main() {
// Gibt die Konsole frei in der das Programm gestartet wurde
FreeConsole();
// Erzeugt eine neue Konsole
AllocConsole();
// Fuer Ausgaben ein Output-Handle besorgen
h1=GetStdHandle(STD_OUTPUT_HANDLE);
// Ein Input-Handle besorgen
h2=GetStdHandle(STD_INPUT_HANDLE);
// Handler fuer Konsolen Events auf die Funktion "handler" setzen
SetConsoleCtrlHandler(handler,TRUE);
// "message-processing loop" ausführen
inputcons();
// Handler fuer Konsolen Events auf die Funktion "handler" entfernen
SetConsoleCtrlHandler(handler,FALSE);
// Handles freigeben
CloseHandle(h1);
CloseHandle(h2);
// Konsole wieder freigeben
FreeConsole();
// Beenden des Programms
ExitProcess(0);
};
// Handler Funktion, die vom Betriebssystem aufgerufen wird und
// ueber Ereignisse informiert
BOOL WINAPI handler(DWORD ct) {
// Ausgeben, dass Steuerung + C gedrueckt wurde
if(ct == CTRL_C_EVENT) {
print(sCTRLC);
8
}
// Wenn der "X"-Button gedrueckt wird, "Close" ausgeben und beenden
if(ct == CTRL_CLOSE_EVENT) {
print(sCLOSE);
Sleep(1000);
ExitProcess(0);
};
return TRUE;
};
// Diese Funktion stellt den "message-processing loop" dar
void inputcons() {
DWORD n;
INPUT_RECORD ir;
//Quasi Endlosschleife für das Einlesen von Tastendrücken
while(ReadConsoleInput(h2, &ir, 1, &n)) {
if(ir.EventType == KEY_EVENT) {
// Ausgeben, dass links Steuerung gedrückt wurde
if(ir.Event.KeyEvent.dwControlKeyState ==
LEFT_CTRL_PRESSED) {
print(sCTRL);
}
// Ausgeben, dass rechts Steuerung gedrückt wurde
if(ir.Event.KeyEvent.dwControlKeyState ==
RIGHT_CTRL_PRESSED) {
print(sCTRL);
}
// Gedrueckte Taste ausgeben (abc...)
if(ir.Event.KeyEvent.uChar.AsciiChar >= 32)
{
// char Array buf mit sChar + das gedrückte Zeichen füllen
// Wenn z.B. a gedrückt wurde ist buf = Character a \n
wsprintf(buf,sChar,ir.Event.KeyEvent.uChar.AsciiChar);
print(buf);
}
};
};
// Wenn bei ReadConsoleInput ein Fehler (Rueckgabe 0) auftrat,
// Meldung machen und 5 sek. warten
print(sError);
Sleep(5000);
};
// Diese Funktion schreibt einen String auf die Konsole
void print(char *sString) {
DWORD n;
WriteConsole(h1,sString,lstrlen(sString),&n,NULL);
};
Listing 3: Interaktives Programm
Wie dieses Beispiel verdeutlicht, steigt der Implementierungsaufwand und somit wird das
Disassemblieren auch schwieriger, da es sich bei der Funktion „handler“ um eine WINAPI Funktion
handelt, die vom Betriebssystem aufgerufen wird und in einem eigenen Thread läuft. Inputcons()
liest von der Konsole einfach alles ein und verarbeitet es. Diese Funktion nennt man auch „messageprocessing loop“. Dieses Vorgehen findet man unter anderem bei grafischen Anwendungen. Eine GUI
Anwendung kann mehrere Fenster haben, aber nur einen „message-processing loop“. Um eine
Anwendung, wie in Abbildung 1 dargestellt, zu schreiben, braucht man folgenden Quellcode.
9
Abbildung 1: Einfache HalloWelt GUI
#include <windows.h>
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR
lpCmdLine,
int
nCmdShow)
{
char cname[] = "Class";
char title[] = "Hallo Welt GUI";
MSG msg;
// Windows Klasse definieren
WNDCLASS wc;
wc.style
= 0;
wc.lpfnWndProc
= (WNDPROC)WndProc;
wc.cbClsExtra
= 0;
wc.cbWndExtra
= 0;
wc.hInstance
= hInstance;
wc.hIcon
= LoadIcon(hInstance, (LPCTSTR)IDI_APPLICATION);
wc.hCursor
= LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
wc.lpszMenuName
= 0;
wc.lpszClassName =cname;
// Klasse registrieren, wenn es nicht klappt beenden
if(!RegisterClass(&wc))
{
return 0;
}
// Hauptfenster erstellen
HWND hWnd = CreateWindow(
cname,
title,
WS_OVERLAPPEDWINDOW,
0,
0,
500,
300,
NULL,
NULL,
hInstance,
NULL);
10
// Überprüfung, ob das Fenster erstellt wurde
if (!hWnd)
{
return 0;
}
// Fenster anzeigen
ShowWindow(hWnd, nCmdShow);
// Fenster aktualisieren
UpdateWindow(hWnd);
// Message-processing loop
while (GetMessage(&msg, NULL, 0, 0))
{
// Virtual Keys in ASCII umwandeln
TranslateMessage(&msg);
// Nachricht an die window procedure
DispatchMessage(&msg);
}
return 0;
};
// Window procedure
LRESULT CALLBACK WndProc(HWND hWnd,
UINT message,
WPARAM wParam,
LPARAM lParam)
{
switch(message)
{
// Nachricht ist eine Create Nachricht
case WM_CREATE:
break;
// Programm soll beendet werden
case WM_DESTROY:
// Nachricht schicken zum Beenden des Programms
PostQuitMessage(0);
break;
// Fenster soll neu gezeichnet werden
case WM_PAINT:
break;
// Alle anderen Nachrichten (zum Beispiel WM_SIZE) von der
// Default Funktion bearbeiten lassen
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
Listing 4: Einfaches Windowsprogramm
11
Im Grunde funktioniert hier das gleiche Prinzip wie bei der Konsolenanwendung. Als erstes wird eine
Fensterklasse (WNDClass) definiert und gegenüber dem Betriebsystem registriert, danach wird ein
„message-processing loop“ durchlaufen, der die Nachrichten vom System verarbeitet. Die
eigentliche Funktionalität wird in der WndProc beschrieben.
Nachdem die Fensterklasse registriert ist, werden Nachrichten geholt (GetMessage) und übersetzt
(TranslateMessage) und durch den Aufruf der DispatchMessage-Methode dafür gesorgt, dass das
Betriebssystem WndProc aufruft. Die Nachricht (msg) enthält das Handle für das Fenster (definiert
durch WNDCLASS), einen Code für die Nachricht, zwei optionale Parameter, die Zeit des Auftretens
und die Koordinaten des Mauszeigers.
Um so ein Programm später zu disassemblieren, ist die Adresse von der WndProc nötig, da diese
Funktion normalerweise die gesamte Funktionalität des Programms enthält und nur vom System
aufgerufen wird. Um die Adresse herauszufinden muss man den Inhalt von lpfnWndProc (siehe
WNDCLASS) anschauen. lpfnWndProc ist ein Zeiger auf die aufzurufende Funktion.
Mit dem eben erworbenen Wissen sowie dem Wissen, dass eigene Nachrichten größer gleich
WM_USER sein müssen, lässt sich bereits erahnen, dass man eigene Nachrichten an das Programm
schicken kann. Das einzige Problem ist, dass häufig diverse Fenster nur eine WndProc haben, weil
man anhand des Handles auf das Fenster schließen kann. In solchen Fällen braucht man die
Unterstützung von Debuggern wie zum Beispiel OllyDbg oder ähnlich.
[vgl. [18], [12], [11]]
12
3.2.
Befehlsformat
In diesem Kapitel erklären wir das Befehlsformat. Betrachtet man den Dump der Befehle eines
Programms, das eine MessageBox anzeigt (siehe Listing 5), lassen sich drei Bereiche erkennen.
Hierbei interessiert uns zunächst nur der erste Block. Was es mit den anderen Blöcken auf sich hat,
beschreiben wir in Kapitel 3.4.
Abbildung 2: Dump eines Programms
.586P
.MODEL FLAT,STDCALL
includelib c:\masm32\lib\user32.lib
EXTERN
MessageBoxA@16:NEAR
; Data segment
_DATA SEGMENT
TEXT1 DB 'Kein Problem!',0
TEXT2 DB 'Message',0
_DATA ENDS
; Code segment
_TEXT SEGMENT
START:
PUSH OFFSET 0
PUSH OFFSET TEXT2
PUSH OFFSET TEXT1
PUSH 0
CALL MessageBoxA@16
RETN
_TEXT ENDS
END START
Listing 5: MessageBox Assembler Quellcode
13
Im ersten Block sind die Maschinencodebefehle als hexadezimale Zahlen dargestellt. Um diese lesen
zu können, müssen wir sie in lesbaren Code, nämlich Assembler, umwandeln. Es ist notwendig, zu
wissen, wie der Code aufgebaut ist, um das bewerkstelligen zu können.
Hier sei erwähnt, dass Befehle zwischen 1 und 10 Byte oder mehr lang sind. Abbildung 3 verdeutlicht
den Aufbau von den Befehlen.
Quelle: Intel® 64 and IA-32 Architectures Software Developer's Manual Volume 2A:
Abbildung 3: Befehlsformat Intel
Die ersten 4 Bytes eines Befehls können Präfixe sein. Es gibt vier Arten von Präfixen:
‘lock and repeat’, ‘segment override’ und ‘branch hints’, ‘operand-size override’ und ‘address-size
override’ (siehe [1] in Volume 2A). Die ‚lock and repeat‘ Präfixe werden dafür genutzt,
sicherzustellen, dass sie exklusiven Zugriff auf den Speicher haben (üblicherweise in
Mehrprozessorsystemen). ’Repeat ’ Präfixe werden für String Operationen gebraucht, so dass ein
Befehl für jedes Element ausgeführt wird. ’branch hints’ Präfixe können genutzt werden, um dem
Prozessor einen Hinweis für bedingte Sprünge zu geben. Das ’operand-size override’ und das
’address-size override’ Präfix werden für Operationen, die mit 16Bit arbeiten, gebraucht (z.B. mov
eax, bx). Dadurch lässt sich erkennen, dass die Nutzung von 16Bit Operationen schlechter ist, da
Register 32 Bit groß sind und für die Konvertierung von 16 zu 32 extra Operationen (Prozessorintern)
ausgeführt werden müssen.
Wenn kein Präfix gegeben ist, fängt der Befehl mit dem Befehlscode an. Folgender Code sichert drei
Register auf den Stack und danach holt er sie wieder runter.
PUSH EAX
PUSH EBX
PUSH ECX
POP ECX
POP EBX
POP EAX
RET
Listing 6: Beispiel für Intel Befehle
Der Maschinencode dieser Befehle ist:
14
50 53 51 59 5B 58 C3
Man könnte nun annehmen, dass jeder Befehl 1 Byte groß ist. Schaut man sich jedoch die
Binärdarstellung der Befehle an (10010000 = PUSH EAX, 10010011 = PUSH EBX, 10010001 = PUSH
ECX ) lässt sich erkennen, dass alle PUSH Befehle mit 0x5 anfangen. Ähnlich ist dies bei den POP
Befehlen: 10011001 = POP ECX, 10011011 = POP EBX, 10011000 = POP EAX. Der Befehl POP hat
immer 10011 und Push hat immer 10010. Die „General Porpose“ Register wie folgt codiert:

EAX = 000

EBX = 011

ECX = 001

EDX = 010

EDI = 111

ESI = 110

ESP = 100

EBP = 101
Eine Frage, die sich jetzt stellt, ist was mit den 16Bit Registern, wie AX ist. Wie anfangs schon
erwähnt, gibt es Präfixe, die sich auf die Befehle auswirken und in diesem Fall wird das ‚operand-size
override‘ verwendet. Wenn man sich diese Befehle anschaut, sehen sie noch recht homogen aus.
Betrachtet man außerdem noch PUSH und POP für die Segment-Register, dann sieht es nicht mehr
ganz so homogen aus:

PUSH FS = 0x0FA0

PUSH DS = 0x1E

PUSH GS = 0x0FA8

PUSH SS = 0x16

PUSH ES = 0x06

POP ES = 0x07

POP SS = 0x17

POP GS = 0x0FA9

POP DS = 0x1F

POP FS = 0x0FA1
Hier sind einige Befehle zwei Byte lang, was daher kommt, dass sie erst mit späteren Architekturen
hinzugekommen sind. Des Weiteren fangen diese Befehle nicht immer mit 0x5 an.
Ähnlich wie es bei den „normalen“ PUSH und POP Befehlen ist, ist es auch bei den bedingten
Sprüngen. Sie haben den Opcode 0x7 und die letzten 3 Bits definieren die Bedingung. Zwar ist ein Bit
15
noch übrig, es reichen jedoch 3 Bit aus, um 8 Bedingungen zu definieren. Hierbei sei erwähnt, dass es
sich um den ’short jump’ handelt, also Sprünge innerhalb von 256 Byte.
Bei einem weiten Sprung sieht der Befehlscode so aus: 0F 83 1E 10 00 00. Der Befehlscode an sich ist
0F 8 und wieder stehen die letzten 3 Bits für die Bedingung. 1E 10 00 00 steht für die Adresse und
bedeutet nichts anderes als Adresse 0x 00 00 10 1E.
Als nächstes optionales Byte eines Befehls kommt das MOD R/M Byte. Dieses Byte wird von vielen
Befehlen genutzt, die auf Operanden im Speicher zugreifen. Es enthält drei Informationen:

Das MOD Feld kombiniert mit dem R/M Feld nutzt man dafür, acht Register und 24
Adressierungsmodi auszuwählen.

Das Reg/Opcode Feld gibt entweder ein Register an oder erweitert den Opcode.

Wie oben schon beschrieben verwendet man es für die Adressmodi.
Einige Befehle brauchen neben den MOD R/M Byte noch ein weiteres Adressierungsbyte, das
sogenannte SIB Byte. SIB steht für Scale*Index+Base und ist in drei Felder aufgeteilt. Diese halten
folgende Informationen:

Das zwei Bit Scale Feld spezifiziert den Skalierungsfaktor.

Das drei Bit Index Feld spezifiziert die Index Register Nummer.

Das drei Bit Base Feld spezifiziert die Nummer des Base Registers.
Die Berechung der Adresse mit dem SIB Byte ist so darstellt (Index * 2^Scale) + Base. Das bedeutet,
dass dem Wert des Base Registers das Zweifache vom Index addiert wird. [ECX + EDX*2 +
406080A0h] würde folgenden Aufbau bewirken:






ModR/M.mod = 10 für „Register + Word“ kodiert (siehe *1+).
ModR/M.Reg/Opcode = Hier wird das Ziel kodiert sein.
ModR/M.R/M = 100 für ESP, dieser wird aber ignoriert und so interpretiert, dass das
SIB Byte folgt.
SIB.Scale = Steht auf 1, weil der Faktor mit 2^1 = 2 dargestellt wird.
SIB.Index = EDX
SIB.Base = ECX
Der Befehl OR EAX, [ECX + EDX*2 + 406080A0h] sieht also so aus:
Opcode
ModR/M
00001011 10 000 100
SIB
Displacement 406080A0
01 010 001
10100000 10000000 01100000 01000000
So wird der Inhalt von edx mit 2 multipliziert, der Wert von ecx addiert und darauf dann 0x406080A0
addiert. Weitere Informationen dazu können in [1] und [26] gefunden werden.
[vgl. [1], [26], [28],[29]]
16
3.3.
Manuelles Disassemblieren
Mit dem Wissen aus den vorherigen Abschnitt und einer Befehlsliste, wie sie unter [1] und [17] zu
erhalten ist, kann man schon den Dump von Abbildung 2 per Hand disassemblieren.
Der erste Befehl ist
0x6A
welcher mit “PUSH imm8“ übersetzt wird. Darauf folgt
0x00,
womit der
erste Befehl schon disassembliert ist: “PUSH 0“. Das nächste Byte ist 68, was ebenfalls ein PUSH ist,
diesmal jedoch mit einem 32Bit Parameter, der hier “0E 30 40 00“ ist. Also lautet der Befehl
“PUSH 00 40 30 0E“. Anschließend kommt wieder die 68, womit der Befehl “PUSH 00 40 30 00“
lautet. Jetzt kommt wieder der erste Befehl “PUSH 0“. Der nächste Befehl lautet “E8“ und steht für
“Call Near 32“. Die “01 00 00 00“ stehen für einen Branch von 3, dessen Bedeutung später
ausgeführt wird. Der nächste Befehl ist C3 und steht für RET und beendet das Programm. Wenn wir
uns jetzt noch einmal den Call anschauen, sehen wir, dass der Parameter “00 00 00 01“ ist, was
bedeutet, dass ein Sprung um 3 Befehle gemacht wird. An dieser Stelle steht der Befehl FF, der für
JUMP steht und das ModR/M Byte nutzt um die Addressierungsart zu definieren. Hier bedeutet es,
dass das Sprungziel an der Adresse 0x00402000 ist Somit haben wir folgenden Code disassembliert:
PUSH
PUSH
PUSH
PUSH
CALL
RET
JMP
0
40300Eh
403000h
0
0000001
dword ptr ds:[00402000h]
Listing 7: Manuel disassemblierter Code
Für weitere Informationen zu Assemblerprogrammierung sei an dieser Stelle an die entsprechende
Literatur verwiesen. Wenn man sich diesen Code anschaut, kann man noch nicht direkt erkennen,
was genau aufgerufen wird. Damit der Disassembler dies erfährt, sind mehr Informationen über die
Struktur von ausführbaren Dateien notwendig, die wir im nächsten Abschnitt behandeln. Um das
Geheimnis des mysteriösen Calls zu erfahren, sei auf das Listing 5 verwiesen.
Bei diesem Quelltext wird die MessageBox API-Funktion genutzt. Letztendlich gibt das Programm nur
eine Meldung aus mit dem Test „Kein Problem!“ und dem Titel „Message“. Durch die PUSH Befehle
werden die Parameter der Funktion auf den Stack gelegt.
[vgl. [1]]
17
3.4.
Struktur von ausführbaren Dateien
Wie das Beispiel aus dem vorherigen Abschnitt zeigt, wurde aus dem Assembler eine API Funktion
aufgerufen. Intelligente Disassembler wie OllyDBG, IDA Pro oder WinDasm erkennen, welche APIoder Bibliotheksfunktionen verwendet werden. Die Information, welche Bibliotheken verwendet
werden, wird in der Exe-Datei gespeichert, die wir hier näher erklären werden. Der prinzipielle
Aufbau einer ausführbaren Datei ist in Abbildung 4: Aufbau einer EXE-Datei. dargestellt. Jede
ausführbare Datei fängt mit dem ASCII Bytepaar MZ an. Diese beiden Buchstaben sind die Initialen
des Microsoft Programmierers Mark Zbikowski, der das Grundformat spezifiziert hat.
Quelle: http://msdn.microsoft.com/msdnmag/issues/02/02/PE/
Abbildung 4: Aufbau einer EXE-Datei
Nach diesen Bytes kommt das so genannte „stub“ Programm, das, wenn die Datei unter MS DOS
ausgeführt wird, „This program cannot be run in DOS mode“ ausgibt. Der Dos Header kann am
besten anhand folgender Struktur aus der Winnt.h erklärt werden:
18
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;
Listing 8: IMAGE_DOS_HEADER
An dieser Datei interessieren hauptsächlich e_magic, e_lfarlc und e_lfanew. e_magic representiert
die MZ Signatur, e_lfarlc enthält die Adresse der relocation Tabelle, die unter DOS für relative
Adressen gebraucht wurde und e_lfanew zeigt auf den Portable Executable (PE) Header, der
sozusagen der Header für Windowsprogramme ist. Weitere Informationen zu dieser Struktur können
im Microsoft Developer Network (siehe [12]) gefunden werden.
Der PE Header hat folgende Struktur, die ebenfalls in der Winnt.h zu finden ist.
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
Listing 9: IMAGE_NT_HEADER
Wie sich erkennen lässt, besteht diese Struktur aus zwei Strukturen: IMAGE_FILE_HEADER und
IMAGE_OPTIONAL_HEADER32. IMAGE_FILE_HEADER sieht wie folgt aus:
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;
Listing 10: IMAGE_FILE_HEADER
19
IMAGE_OPTIONAL_HEADER32 sieht so aus:
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD
Magic;
BYTE
MajorLinkerVersion;
BYTE
MinorLinkerVersion;
DWORD
SizeOfCode;
DWORD
SizeOfInitializedData;
DWORD
SizeOfUninitializedData;
DWORD
AddressOfEntryPoint;
DWORD
BaseOfCode;
DWORD
BaseOfData;
DWORD
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;
DWORD
SizeOfStackReserve;
DWORD
SizeOfStackCommit;
DWORD
SizeOfHeapReserve;
DWORD
SizeOfHeapCommit;
DWORD
LoaderFlags;
DWORD
NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
Listing 11: IMAGE_OPTIONAL_HEADER
Detaillierte Informationen zu den Strukturen können in [7], [12],[19] und [21] gefunden werden.
An dieser Stelle möchten wir nur IMAGE_DATA_DIRECTORY erwähnen. Hierbei handelt es sich um
ein Array der Größe 16 (IMAGE_NUMBEROF_DIRECTORY_ENTRIES ist 16) und enthält die Struktur
wie sie in beschrieben ist. Zwar ist die Größe auf 16 Felder gesetzt, es werden jedoch nur 12
gebraucht:
1. Tabelle mit den importierten Funktionen
2. Tabelle mit den exportierten Funktionen
3. Ressourcen Tabelle
4. Tabelle mit den Ausnahmen
5.
Sicherheitstabelle
6. „Sections“ Tabelle
7. Debug Tabelle
8. Beschreibungen
9. Computergeschwindigkeit in MIPS
20
10. Thread local storage (TLS)
11. Konfigurations-Tabelle
12. Tabelle mit den importierten Adressen
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD
VirtualAddress;
DWORD
Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
Listing 12: IMAGE_DATA_DIRECTORY
Die IMAGE_DATA_DIRECTORY Struktur hat 2 Felder. Das erste definiert die virtuelle Adresse und das
zweite gibt die Größe der dahinter liegende Datei. Die Struktur, die sich an der Adresse befindet ist
der IMAGE_IMPORT_DESCRIPTOR (siehe Listing 13). Die Felder sind wie folgt definiert:

Characeristics – Adresse einer weiteren Struktur mit einer importierten Funktion.

TimeDataStamp – Zeitpunkt als die DLL erstellt wurde.

ForwarderChain – Normalerweise -1.

Name – Adresse des ASCII String, der den Namen der DLL beschreibt.

FirstThunk – Adresse einer Struktur, die die Namen der importierten Funktionen enthält.
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD
Characteristics;
DWORD
OriginalFirstThunk;
};
DWORD
TimeDateStamp
DWORD
ForwarderChain;
DWORD
Name;
DWORD
FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR;
Listing 13: IMAGE_IMPORT_DESCRIPTOR
Characteristics und FirstThunk sind zwar unterschiedliche Felder werde aber durch die Struktur
IMAGE_THUNK_DATA32 dargestellt.
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString;
DWORD Function;
DWORD Ordinal;
DWORD AddressOfData;
} u1;
} IMAGE_THUNK_DATA32;
Listing 14: IMAGE_THUNK_DATA32
Durch das Schlüsselwort „union“ wird definiert, dass unterschiedliche Datentypen an einer
Speicherstelle sind. Mit anderen Worten besteht diese Struktur nur aus einem Feld. Dieses Feld
definiert die relative virtuelle Adresse des importierten Funktionsnamen. Wenn auf diese Struktur
vom FirstThunk Feld verwiesen wird, dann schreibt der Loader beim Ausführen die Adresse der
Funktion in diese Struktur.
[vgl. [7], [12],[19],[21]]
21
3.5.
Debuggen und Disassemblieren
In diesem Abschnitt gehen wir näher auf das Disassemblieren von Programmen ein. In Kapitel 4 geht
es konkret um die Analyse des Assembler Codes, während wir an dieser Stelle vorerst nur das
Disassemblieren näher beschreiben. Kommen wir an dieser Stelle noch einmal auf das Programm von
3.3 zurück (siehe Listing 5). Für die Assemblierung haben wir MASM32 (Microsoft Macro Assembler)
(siehe [14] und [15]) benutzt. Mit dem Befehl ML.exe /c /coff msgbox.asm (/c bedeutet,
dass es nicht automatisch gelinkt werden soll und /coff heißt, dass das COFF Format verwendet
werden soll) wird das Programm assembliert und mit LINK /subsystem:console
msgbox.obj wird es gelinkt. Um dieses Programm zu disassemblieren, lassen sich verschiedene
Programme nutzen. Für den in Abbildung 5 gezeigten Dump haben wir dumpbin.exe des Visual
Studio genutzt, welches ebenfalls in MASM32 enthalten ist.
Abbildung 5: MessageBox Beispiel Disassembliert
Der zweite und dritte PUSH überträgt die Adressen der Texte der MessageBox auf den Stack. Die
Adressen 0x40300E und 0x4030000 beziehen sich auf die Datasection, wo sich alle initialisierten
Daten befinden. Die Datasection ist der dritte Block in Abbildung 2. Was dumpbin.exe aber nicht
automatisch erkennt, ist dass der JMP sich auf die MessageBox bezieht.
Um den JMP zu verstehen, muss man sich die Import Tabelle der ausführbaren Datei genauer
anschauen. Da die Tabelle für jede verwendete DLL einen IMAGE_IMPORT_DESCRIPTOR hat, der
Informationen über die importierten Funktionen hat. Die Adresse ist 0x2000, da aber die virtuelle
Adresse 0x400000 ist, ergibt sich 0x402000. Wenn man die EXE-Datei in einen HEX Editor lädt, kann
man bei Adresse 0x600 die Bytefolge 0x3820 sehen, was für die Adresse 0x2038 steht. 0x2038 + 2
ergibt 0x203A und an dieser Stelle muss MessageBoxA stehen, dies ist der Name der importierten
22
DLL Funktion. Damit man das nicht alles per Hand rausfinden muss wird einem diese Arbeit von
besseren Disassemblern wie beispielsweise IDA Pro oder Windasm abgenommen. In Abbildung 6
kann man diesen Vorteil anhand von Windasm sehen.
Abbildung 6: Windasm beim MessageBox Beispiel
Bis jetzt sieht es so aus, als wenn das Disassemblieren einfach wäre. Fügt man jedoch beispielsweise
beim Quelltext nach dem RET ein DB 50 hinzu, dann wird das vom den meisten Disassemblern falsch
interpretiert. Denn der DB 50 wird durch MASM32 in 0x32CC gewandelt, was “xor al,bl“
darstellt. Das kommt daher, dass die Disassembler Datenfelder nicht erkennen und versuchen, diese
als Opcodes zu interpretieren.
Wenn man diesen Assemblerbefehl betrachtet, wird man bei der statischen Analyse (reines
Betrachten des Codes) sehr verwirrt. Die Verwirrung kommt daher, dass es normalerweise keinen
Sinn macht nach einem RET einen xor al,bl zu machen und man sich deshalb fragt, ob irgendwann
dahin gesprungen wird.
Man kann auch Disassembler aufs Glatteis führen, um so das Analysieren des Assemblers schweiriger
zu machen. Dazu kann man den Quelltext aus dem Listing 15 betrachten, der auf dem vorherigem
Beispiel basiert. Er wurde dahingehend erweitert, dass er am Anfang die Startadresse auf den Stack
packt. Nach dem Aufrufen der MessageBox, wird die Startadresse in EDX geladen und anschließend
die Differenz vom Start zu l1 darauf addiert, was die Adresse von l1 ergibt. An dieser Unterroutine
befindet sich nur ein RET.
23
.586P
. MODEL FLAT,STDCALL
includelib c:\masm32\lib\user32.lib
EXTERN
MessageBoxA@16:NEAR
; Data segment
_DATA SEGMENT
TEXT1 DB 'Kein Problem!',0
TEXT2 DB 'Message',0
_DATA ENDS
; Code segment
_TEXT SEGMENT
START:
PUSH OFFSET START
PUSH OFFSET 0
PUSH OFFSET TEXT2
PUSH OFFSET TEXT1
PUSH 0
CALL MessageBoxA@16
POP EDX
ADD EDX, l1 - Start
CALL EDX
RETN
DB 50
l1:
RETN
_TEXT ENDS
END START
Listing 15: Disassembler aufs Glatteis führen
Nachdem wir das Erzeugnis des Assemblers disassembliert haben, mussten wir enttäuscht
feststellen, dass Windasm, dumpbin.exe und auch IDA Pro es nicht richtig disassemblieren konnten.
Alle drei Programme lieferten folgende Ergebnisse:
Abbildung 7: dumpbin.exe aufs Glatteis geführt
24
Abbildung 8: Windasm aufs Glatteis geführt
Abbildung 9: IDA Pro aufs Glatteis geführt
25
Wie man auf allen Screenshots sehen kann, wird die Routine l1 nicht erkannt, sondern nur das “xor
al,bl“.
Ein merkwürdiges Verhalten zeigt sich, wenn man den Quelltext nochmals erweitert. Man braucht
nur vor PUSH OFFSET START die Zeile MOV EBX, OFFSET l1 einfügen und schon ändert sich das Bild.
Zumindest beim IDA Pro, die anderen beiden erkennen die Routine trotzdem nicht. IDA Pro geht
sogar so weit, dass IDA Pro erkennt, dass es sich um eine leere Routine handelt und sie deswegen als
nullsub_1 benennt. Wie man sieht, sind Disassembler nicht zu allem fähig und man muss zu
Debuggern greifen. Das letzte Beispiel stellt somit die Einleitung zum nächsten Thema „Versteckte
Sprünge“ dar.
26
3.5.1. Versteckte Sprünge
Um sein Programm sicherer vor dem Analysieren durch Angreifer zu machen, gibt es verschiedene
Möglichkeiten. Hier werden wir beschreiben, wie man JMP Befehle so verschleiert, dass sie nicht auf
dem ersten Blick erkannt werden. Eine Möglichkeit bietet sich, wenn man indirekte Sprünge
verwendet. Der Befehl JMP DWORD PTR [var] definiert einen Sprung an die Adresse, die in var
gespeichert ist. JMP EBX bedeutet, dass die Adresse in EBX ist. JMB DWORD PTR [EBX] bedeutet, dass
das EBX Register die Adresse enthält, wo sich eine Variable befindet, die die Adresse beinhaltet.
Wenn man JMP EAX in einem Code verwendet, dann ist es meistens dem Disassembler nicht möglich
zu wissen, wohin der Sprung führt. Es könnte sein, dass eine andere Routine erst das EAX füllt und
dann zu diesen JMP EAX springt oder dass das EAX Register ein paar 100 Zeilen vor dem Jump
definiert wurde. Wenn wir unser MessageBox Beispiel wieder aufgreifen und es wie in Listing 16
verändern, sehen wir, wie man PUSH und RET nutzen kann, um einen Sprung auszuführen.
.586P
.MODEL FLAT,STDCALL
includelib c:\masm32\lib\user32.lib
EXTERN
MessageBoxA@16:NEAR
; Data segment
_DATA SEGMENT
mem1 DD OFFSET la2 ; Adresse von la2 speichern
TEXT1 DB 'Kein Problem!',0
TEXT2 DB 'Message',0
_DATA ENDS
; Code segment
_TEXT SEGMENT
START:
MOV EAX, mem1
;Der geheimnisvolle Sprung
PUSH EAX
RETN
la1:
RETN
la2:
PUSH OFFSET 0
PUSH OFFSET TEXT2
PUSH OFFSET TEXT1
PUSH 0
CALL MessageBoxA@16
RETN
_TEXT ENDS
END START
Listing 16: PUSH/RET Sprung
27
Dies stellt nur eine Variante dar, einen Sprung zu verstecken. Ein richtiges Programm würde
selbstverständlich diverse Befehle zwischen dem PUSH und dem RET enthalten. Noch trickreicher
wird es, wenn man PUSH EAX durch SUB ESP, 4 und MOV DWORD PTR [ESP], EAX ersetzt. Durch diese
Befehle wird die Adresse auch auf den Stack gepackt und ebenfalls durch den RET Befehl zu la2
gesprungen. Die gleiche Technik kann man natürlich auch mit bedingten Sprüngen machen. Eine
weitere Sache, die bei bedingten Sprüngen hinzukommt, ist dass ein Vergleichsbefehl wie CMP EAX,
100 (Vergleich ob in EAX 100 ist)oder ähnlich nicht viel über den Inhalt von EAX aussagt. EAX hätte
ebenfalls viele Zeilen über dem eigentlichen Befehl befüllt werden können.
Eine viel effektivere Methode nennt sich Code Overlapping. Bei dieser Methode beinhaltet ein Befehl
einen anderen Befehl. Listing 17 gibt ein Beispiel für das Code Overlapping.
.586P
.MODEL FLAT,STDCALL
includelib c:\masm32\lib\user32.lib
EXTERN
MessageBoxA@16:NEAR
; Data segment
_DATA SEGMENT
mem1 DD OFFSET l2
TEXT1 DB 'Kein Problem!',0
TEXT2 DB 'Message',0
_DATA ENDS
; Code segment
_TEXT SEGMENT
START:
MOV EAX, mem1
SUB ESP, 4
MOV DWORD PTR [ESP], EAX
MOV AX, 015EBH
JMP $ - 2
;Springe 2 Byte zurück. Instructionpointer - 2
l2:
PUSH OFFSET 0
PUSH OFFSET TEXT2
PUSH OFFSET TEXT1
PUSH 0
CALL MessageBoxA@16
RETN
l1:
RETN
_TEXT ENDS
END START
Listing 17: Code Overlapping Beispiel
Das eigentliche Code Overlapping wird durch zwei Befehle gemacht. MOV AX, 015EBH und JMP $ - 2.
Der Parameter 015EB ist das gleiche wie JMP SHORT l1. Zu beachten ist, dass vorher der Stack so
verändert wurde, dass der RETN von l1 auf l2 springen lässt.
28
4. Paradigmen für die Analyse von ausführbaren Dateien
In den vorherigen Abschnitten haben wir gesehen, wie man Opcode disassembliert und was für
Tricks genutzt werden können, um Disassembler oder Codeanalysten aufs Glatteis zu führen. In
diesem Abschnitt beschäftigen wir uns mit der Analyse von Programmen. Als erstes gehen wir auf
das Erkennen von Daten ein und anschließend auf das Erkennen von Programmstrukturen.
Wie aus [28] und [29] bekannt, würde eine Compiler-Optimierung den erzeugten Code verändern
und somit würde die Struktur verändert werden und man würde die original Struktur nicht so einfach
wieder erkennen. Aus diesem Grunde ist, soweit nicht anders erwähnt, bei allen Kompilierungen die
Optimierung ausgeschaltet.
4.1.
Daten erkennen
In diesem Abschnitt geht es um das erkennen von Daten in einem Programm. Da die meisten
Programme heutzutage in einer Hochsprache wie C/C++ oder Delphi geschrieben sind haben wir
unsere Beispiele in C geschrieben. Als erstes werden wir uns um globale Variablen kümmern. Dafür
schauen wir uns folgenden Code an:
#include <stdio.h>
int a;
int b = 20;
int s = 0;
void main() {
a = 10;
s = a + b;
printf("%d",s);
}
Listing 18: Beispiel für globale Variablen
Wenn man das Programm ohne Optimierung kompilieren lässt und das Maschinenprogramm dann
mit IDA Pro öffnet, erhält man folgenden Assemblercode:
29
Abbildung 10: Listing 18 disassembliert
Wie man sehen kann, hat IDA automatisch die richtigen Variablen (a = dword_403020, b =
dword_403000 und s = dword_403024) erkannt. Abgesehen davon, dass IDA Pro die Variablen
erkannt hat, hat er auch die Größen der Variablen erkannt (dword = double word). Was einem beim
genaueren Betrachten auffällt ist, dass b eine andere Adresse hat als die anderen Variablen. Das
kommt daher, dass der Compiler a und s für uninitialisierte Variablen hält. s wird für eine
uninitialisierte Variable gehalten, sie ist zwar initialisiert, aber dieser Wert wird nicht verwendet. Des
Weiteren fällt auf, dass der Code redundant ist (a könnte beispielsweise durch eine Konstante ersetzt
werden), was aber auf das Kompilieren ohne Optimierung zurückzuführen ist. Wenn man es noch
einmal kompiliert und im Visual Studio die Optimierung auf schnellen Code stellt, bekommt man
folgendes Ergebnis von IDA.
30
Abbildung 11: Listing 18 disassembliert (Optimierung eingeschaltet)
Zu sehen ist, dass eine Gruppierung der Befehle stattgefunden hat. Das hat damit zu tun, dass die
Intel Prozessoren 2 Pipelines haben (U und V), womit unter bestimmten Umständen durch
gleichzeitiges Abarbeiten der Befehle die Geschwindigkeit verdoppelt wird. Da C die Nutzung von
Pointern bietet und diese Nutzung auch Gang und Gebe ist wollten wir das Listing 18 so abändern,
dass s ein Pointer ist. Das geschieht, indem wir int s durch int *s ersetzen. Pointer haben zur Folge,
dass Compiler die indirekte Adressierung verwenden. So wird aus MOV EAX, s MOV EAX, [EDX].
Durch das Nutzen eines Zeigers sieht der Quelltext so aus:
#include <stdio.h>
#include <stdlib.h>
int a;
int b = 20;
int *s;
void main() {
s = (int*)malloc(4);
a = 10;
*s = a + b;
printf("%d",*s);
free(s);
}
Listing 19: globale Variablen mit Zeiger
31
Abbildung 12: globale Variablen mit Zeigern
Abbildung 12 zeigt den disassemblierten Assembler, der nicht optimiert wurde. Durch die
Funktionsaufrufe malloc(4) und free(s) ist zusätzlicher Code hinzugekommen, der letztendlich
aber dem aus Abbildung 10 sehr ähnlich sieht. Der Hauptunterschied besteht darin, dass die
indirekte Adressierung verwendet wird.
Eine weitere interessante Sache ist, wenn man einer Variable einen Wert aus dem Adressraums des
Programms zuweist (siehe Listing 20).
#include <stdio.h>
#include <stdlib.h>
int a;
int b;
int c;
void main() {
a = 10;
b = 20;
c = 0x401034;
printf("%d %d %d", a,b,c);
}
Listing 20: Variable mit Adressraumwert
32
Wenn man dieses Programm mit dem IDA Pro disassembliert, dann nimmt IDA Pro an, dass es sich
um eine Adresse handelt. IDA Pro interpretiert das Setzen des Wertes so, als ob es sich um eine
Adresse handelt, auf die gesprungen wird (siehe Abbildung 13). Außerdem macht IDA aus dem
Programmablauf einen Graphen, da IDA Pro denkt, dass sich an 0x401034 eine Prozedur befindet.
Abbildung 13: Variable hat einen Wert aus dem Adressraum
Wenn man den Wert auf 0x401000 setzt, verweist IDA Pro auf die _main, was der Anfang des
Programms ist. Diese Missinterpretation des Codes kommt daher, dass Disassembler auch nur
Programme sind und sich an Algorithmen halten müssen und deswegen nicht unterscheiden können,
ob es sich wirklich um eine Adresse oder einen Wert handelt.
Ein weiterer Punkt ist das Herausfinden des Typs, die Größe und die Adresse einer Variablen.
Listing 21 und Listing 22 zeigen zwei unterschiedliche Deklarationen und Initialisierungen von jeweils
drei Variablen.
#include <windows.h>
BYTE bvar = 0xab;
WORD c1 = 0x1234;
DWORD d1 = 0x34567890;
void main() {
printf("%d %d %d",bvar,c1,d1);
}
Listing 21: Variablen BYTE, WORD, DWORD
33
#include <windows.h>
WORD c1 = 0x1234;
BYTE bvar = 0xab;
DWORD d1 = 0x34567890;
void main() {
printf("%d %d %d",bvar,c1,d1);
}
Listing 22: Variablen WORD, BYTE, DWORD
Wenn man diese beiden Quelltexts kompiliert und sich mit IDA die Datenorganisation anschaut
(siehe Abbildung 14 und Abbildung 15) lässt sich feststellen, dass beim ersten Listing jede Variable 4
Byte einnimmt und dass beim zweiten Listing das WORD und das BYTE sich 4 Byte teilen.
Abbildung 14: Assembler von BYTE, WORD und DWORD
34
Abbildung 15: Assembler von WORD, BYTE und DWORD
Das Ganze würde sich noch mehr ändern, wenn die Variablen nicht initialisiert wären. Dann wäre
zwischen den initialisierten und nicht initialisierten Variablen zwar ein etwas größerer Abstand, aber
sie wären im gleichen Datensegment. Es ist zwar kompliziert, ein Datensegment für initialisierte
Variablen (_data) und eins für nicht initialisierte (_bss) Variablen anzulegen, aber der Linker führt
diese beiden wieder zusammen. In Listing 23 werden zwei Segmente erstellt, eins mit dem Namen
_bss und eins namens _data. Wenn man es kompiliert, linkt und anschließend den dump und den
Disassemblierten Code aus Abbildung 16 anschaut, lässt sich erkennen, dass der Datenbereich von
0x402000 bis 0x402010 geht. Das Programm hat den einzigen Zweck, der nicht initialisierten Variable
den Wert 0x40 zuzuweisen. Dies geschieht durch den MOV Befehl.
35
.586P
.MODEL FLAT,STDCALL
; Data segment
_DATA SEGMENT
iniVar db 10H
_DATA ENDS
_BSS SEGMENT
uniniVar db ?
_BSS ENDS
; Code segment
_TEXT SEGMENT
START:
mov [uniniVar], 40H
RETN
_TEXT ENDS
END START
Listing 23: _data und _bss Segment
Abbildung 16: Initialisierte und unitialisierte Variablen
Jetzt haben wir gesehen, dass initialisierte und nicht initialisierte Variablen sich im selben Segment
befinden und dass die Anordnung unterschiedlich sein kann, daraus lässt sich schließen, dass der Typ
bzw. die Größe von Variablen nicht anhand des Datensegments zu erfahren ist. Deswegen müssen
36
Disassembler anhand der Assemblerbefehle die Größe der Typen erfahren, wenn wir an das
Befehlsformat aus Kapitel 3.2 denken. Wenn man folgendes Programm kompiliert und es dann
disassembliert sieht, man drei Befehle.
00401000: C605 6E3340 0014
mov byte ptr ds:[0040336Eh],14h
00401007: 66 C705 6C334000 0A00
mov word ptr ds:[0040336Ch],0Ah
00401010: C705 70334000 1E000000
mov dword ptr ds:[00403370h],1Eh
Listing 24: Zuweisungen auf unterschiedliche Datentypen
#include <windows.h>
BYTE b;
WORD w;
DWORD dw;
void main() {
b = 20;
w = 10;
dw = 30;
}
Listing 25: 3 Zuweisungen in C aus unterschiedliche Datentypen
Wie man in Listing 24 sehen kann, hat der erste den Opcode C6 und das MOD R/M Byte 05, wie bei
den anderen auch. Das MOD R/M Byte ist gleich, da alle dieselbe Adressierungsart verwenden. Der
zweite Befehl arbeitet auf 16Bit und hat deswegen das Präfix 0x66 und der letzte hat kein Präfix,
sondern nur ein größeres Argument. Dass der 8Bit Befehl einen anderen Opcode hat als die anderen,
ist historisch bedingt. So kann ein Disassembler, wie der Prozessor, anhand der Befehle erkennen
um, welche Größe es sich bei den verwendeten Daten handelt.
Genauso wie bei Ganzzahlen können Gleitkommazahlen auch auf diese weise Erkannt werden, bis
auf einige Compiler, die bei der Verwendung von 32Bit Variablen Initialisierungen nicht über
Coprozessorbefehle sondern über normale Befehle vornehmen. Ein solcher Befehl könnte so
aussehen MOV DWORD PTR dbl_40C2C4, 95810625H.
In solchen Fällen muss man lediglich das Format von Gleitkommazahlen kennen (nähere Information
können in der entsprechenden Literatur gefunden werden, zum Beispiel [25]).
Neben Ganzzahlen und Gleitkommazahlen nutzen Programme in fast allen Fällen auch immer
Zeichenketten (Strings). Strings sind so ähnlich wie Arrays, enthalten meistens jedoch weitere
Information wie beispielsweise die Länge. Einmal gibt es ASCII und Unicode Strings, derzeit geht der
Trend eher zu Unicode Strings. Bei ASCII wird jedes Zeichen in einem Byte dargestellt und bei im
Unicode zwei Byte, wobei das Ende einer Zeichenkette mit einer 0 bzw. bei Unicode mit zweimal 0
markiert wird. Bei dieser Methode muss für das Erkenne der Länge der ganze String betrachtet
37
werden. Eine andere Methode ist es, die Länge oder das Ende irgendwo anzugeben. So könnte zu die
Adresse vom Ende der Zeichenkette am Anfang stehen.
Man könnte auch beide Verfahren kombinieren, um so eine hohe Kompatibilität zu erreichen, Dann
stellt sich jedoch die Frage, was passiert, wenn die angegebene Länge bzw. das angegebene Ende
nicht stimmt. Das könnte dann beispielsweise zu einer Ausnahme („Exception“) führen.
Wenn man sich folgendes Beispiel (siehe Listing 26) anschaut und das Ergebnis in IDA Pro lädt
(Abbildung 17: Unicode Beispiel disassembliert), erkennt IDA automatisch, dass es sich um Unicode
handelt. Schaut man sich das Datensegment an (Doppelklick auf aHelloWorld) (Abbildung 18, das
„DATA XREF: _main+6R“ kann ignoriert werden, das bedeutet nur, dass es in der _main Prozedur
referenziert wird) und drückt anschließend die Taste <A>, wandelt IDA Pro die Werte automatisch in
ASCII um (siehe Abbildung 19). So steht jedes Zeichen als ASCII Code gefolgt von 00 dar.
38
#include <stdio.h>
wchar_t s[] = L"Hello World";
wchar_t f[] = L"%s\n";
void main(){
wprintf(f,s);
}
Listing 26: Unicode Beispiel
Abbildung 17: Unicode Beispiel disassembliert
39
Abbildung 18: Datensegment des Unicode Beispiel
Abbildung 19: Datensegement des Unicode Beispiels als ASCII dargestellt
40
Wie man sehen kann, werden die Buchstaben mit ASCII-Codes gefolgt von 0 dargestellt.
Schauen wir uns folgendes Beispiel an.
#include <stdio.h>
#include <string.h>
char s[] = "Hello";
void main() {
strcat(s, " World");
printf("%s\n", s);
}
Listing 27: strcat Beispiel1
Wenn man diesen Quelltext kompiliert und disassembliert erhält man einen gut analysierten
Assembler (siehe Abbildung 21). Hier hat IDA Pro die Strings erkannt und die Bibliotheksfunktion
strcat sowieso. Wenn man jetzt aber die Variablen Deklaration von s in die Methode verschiebt und
es dann noch mal disassembliert sieht die Sache ganz anders aus (siehe Abbildung 22). Zwar
funktioniert das Programm noch, aber IDA Pro schafft es nicht den „Hello“ String zu erkennen,
obwohl ihm die Methode strcat bekannt ist. Wenn wir nun diesen Quelltext analysieren, steht bei
Adresse 0x401003 die Reservierung von 8 Byte auf dem Stack. Anschließend wird in das EAX Register
„Hell“ (da das EAX nur 4 Byte groß ist)geladen und danach auf den Stack geschoben. Nun passiert das
gleiche für „o\0“. Anschließend wird die Adresse von „ World“ auf den Stack gepackt und strcat
aufgerufen. Der Compiler hat den String als Datenblock (dd) definiert, deswegen konnte der IDA Pro
den String nicht erkennen.
Abbildung 20: Datenbereich mit dem lokalen String
41
Abbildung 21: Disassembliertes strcat Beispiel
Abbildung 22: Disassemblierte geändertes strcat Beispiel
42
Wenn man an Strings denkt, fällt einem auf, dass diese sehr viel Ähnlichkeit mit Arrays haben. Der
Hauptunterschied ist, dass Arrays eine Größe und keine definierten Enden haben.
Abbildung 23 zeigt den Assembler zu Listing 28, wo IDA Pro die Schleife erkannt hat und sie als
Graphen darstellt. Dieser Graph verdeutlicht nur den Aufbau der Schleif in ihre einzelnen
Bestandtelie. Die wirkliche Anordnung der Befehle ist anders als der Graph andeutet. Die Abbildung
30 zeigt wie das Programm ohne Graphdarstellung aussieht.
#include <stdio.h>
int a[10];
void main() {
for (int i = 0; i < 10; i++) a[i] = i;
}
Listing 28: Array Beispiel
Abbildung 23: Assembler des Array Beispiels
43
Abbildung 24: Assember des Array Beispiels (Ohne Graph)
Um nun die Größe und den Typ des Arrays zu bestimmen, müssen wir den Code genauer betrachten.
Einem fällt sofort auf, dass die Variable var_4 i sein könnte, da sie auf 0 gesetzt wird und darauf mit
0xA (10) verglichen wird. Danach wird i in ecx und edx gespeichert, dann wird von dem Array, was
mit dword_403018 bezeichnet ist, das i-te Feld mit dem edx (i) belegt. ecx* 4 bedeutet nichts
anderes, als dass i mal 4 genommen wird, da wir 32 Bit Werte haben und deswegen das nächste
Element erst 4 Byte später beginnt. Durch das *4 erfahren wir auch, dass es sich um einen 4 Byte
großen Datentypen handelt. Jetzt wird nur noch der Zähler (i) um eins erhöht. Leider kann man
davon nicht ableiten, dass man bei einer Schleife nur nach dem Zähler suchen muss und man somit
die Größe des Arrays bekommt. Es kommt öfter vor, dass Programmierer nur Teile eines Arrays
abarbeiten und später den Rest. Bei solchen Fällen bleibt einem nichts anderes über, als Debugger zu
nehmen und die Größe selber zu ermitteln. Noch komplizierter wird es, wenn Funktionen aufgerufen
werden, die Funktionen aufrufen, die als Parameter Zeiger nutzen. Bei solchen Fällen hilft einem nur
der Debugger. Man muss dann einen Breakpoint setzen und schauen, von wo der Aufruf kam und
sich dann rückwärts durchhangeln.
Neben Feldern gibt es noch so etwas ähnliches, nämlich Strukturen. Bei einem Array sind alle
Elemente vom gleichen Typ, aber bei einer Struktur können es unterschiedliche Typen sein. Diese
Strukturen werden genauso angesprochen wie Arrays, nämlich mit der Startadresse. Das Problem ist,
44
dass man nicht immer weiß, ob die unterschiedlichen Variablen zu der Struktur gehören oder nicht.
Listing 29 verdeutlicht das.
#include <stdio.h>
#include <windows.h>
struct a {
char s[10];
BYTE b;
int i;
};
a a1;
void main() {
for (int j = 0; j < 10; j++) {
a1.s[j] = 'A';
}
a1.b = 10;
a1.i = 10000;
}
Listing 29: Strukturen Beispiel1
IDA Pro macht daraus den in Abbildung 25 gezeigten Assembler. In Assembler findet man die
Struktur quasi wieder, denn in diesem Beispiel ist es nicht nötig, dass man die Struktur kennt. Dies
ändert sich aber, wenn man eine Funktion hat, der die Struktur als Parameter übergeben wird.
Abbildung 25: Struktur Beispiel1 disassembliert
Listing 30 zeigt ein Beispiel wo eine Struktur an eine Funktion übergeben wird.
45
#include <stdio.h>
#include <windows.h>
struct a {
char s[10];
BYTE b;
int i;
};
a a1;
void init(a);
void main(){
init(a1);
}
void init(a c) {
for (int j = 0; j < 10; j++) {
a1.s[j] = 'A';
}
a1.b = 10;
a1.i = 10000;
}
Listing 30: Strukturen Beispiel 2
Wenn man sich den Assembler, den IDA Pro erzeugt, anschaut, dann stellt man fest, dass in der
Main-Methode (Abbildung 26) erst 16 (0x10) Bytes auf dem Stack reserviert werden, dann der Stack
Pointer in EAX geladen. Jetzt werden 4 mal 4 Bytes auf den Stack geschoben, was den Variablen
entspricht. Wobei die ersten beiden 4 Bytes dem Array entsprechen und die anderen für die
Variablen genutzt werden. Das Interessante kommt jedoch erst in init-Funktion, die von IDA Pro mit
sub_401040 bezeichnet wird. Sollte man denken, dass die Funktion auf die Variable über den Stack
zugreift, aber sie tut es nicht. Das lässt sich nur auf den Compiler zurückführen, deswegen sieht der
Assembler genauso aus, wie der aus dem vorherigen Beispiel.
46
Abbildung 26: Disassemblierte Mainmethode des 2. Struktur Beispiels
Abbildung 27: Disassemblierte Init-Methode des 2. Struktur Beispiels
47
Anhand dieses Beispiels kann man sehen, dass Disassembler nicht in der Lage sein können Strukturen
zu erkennen, da einfach Informationen über die Struktur fehlen. Das trifft jedoch nicht bei API
Strukturen zu. Wenn wir uns das Listing 4 noch mal anschauen und dies kompilieren und das
Programm in IDA Pro öffnen, sehen wir, dass IDA Pro automatisch erkennt, dass wir die WndClassStruktur verwenden (siehe Abbildung 28). Somit kann man auch herausfinden, wo die Window
Procedure ist. In Abbildung 28 ist der Zugriff auf die Struktur dargestellt. Für diesen Zugriff wird
immer das ebp Register (extended base pointer) genommen. Dieses Register wird für lokale
Variablen genutzt, die auf dem Stack verwaltet werden und nicht durch ein Register dargestellt.
Abbildung 28: IDA Pro erkennt WndClass
Wenn wir uns das Listing 31 anschauen sehen wir, dass die main-Methode drei3 Variablen hat und
die add-Methode aufruft, die drei Parameter hat und eine lokale Variable hat.
#include <stdio.h>
int add(int *, int, int);
void main() {
int i = 10, s,j;
s = 12;
j = 20;
printf("%d\n", add(&s,i,j));
}
int add(int *s1, int i1, int j1) {
int n;
*s1 = *s1 + 10;
n = *s1 + j1 + i1;
48
return n*n;
}
Listing 31: Variablen Beispiel
Und wenn wir uns jetzt den dazu gehörigen Assembler (siehe Abbildung 29 und Abbildung 30)
anschauen, sehen wir, dass zuerst mit dem sub esp, 0xC 12 Byte auf dem Stack reserviert werden
und dass danach die drei Variablen der Main initialisiert werden (mov [ebp+var_4],0AH usw.).
Anschließend werden die Daten auf den Stack geschoben und die Methode wird aufgerufen. IDA Pro
verwendet Bezeichner var_C, var_8 und var_4, die die relative Adresse auf dem Stack angibt. So
findet die Adressierung mit [ebp+var_4] statt. Dies bedeutet, dass die Adresse von ebp umd den
Wert von var_4 (-4) addiert wird. Die -4 kommt daher, weil der Stack nach unten „wächst“ und der
ebp noch die Adresse des Stackpoint (esp) vor der Speicherreservierung (sub esp, 0xC). Anschließend
werden die Variablen und die Adresse von s (lea edx, ebp+var_8; push edx) auf den Stack geschoben.
Der Befehl lea (load effective address) lädt in diesem Fall die Adresse, die epb+var_8 ergibt in edx.
Diese muss gemacht werden, da die Funktion add einen Zeiger als Parameter hat und somit die
Adresse von s übergeben wird.
Abbildung 29: Disassembliertes des Variablen Beispiels
49
IDA Pro hat arg_0, arg_4 und arg_8 angelegt, die als Offset für den Stack genutzt werden (siehe
Abbildung 30). Des Weiteren gibt es jetzt auch var_4, die für n verwendet wird. Die Zeile *s1 = *s1 +
10 wird durch die Befehle mov eax, [ebp+arg_0];mov ecx, [eax];add ecx, 0Ah;mov edx,
[ebp+arg_0];mov [edx], ecx dargestellt. Erst wird die Adresse von s in eax geschrieben, dann der
Inhalt von s in ecx, dann findet die Addition von 10 (0xA) statt, dann wird die Adressen zu s in edx
geschrieben und dann wir der Inhalt von ecx (22) in *s geschrieben. Das Ergebnis der Funktion wird in
eax geschrieben. Anschließend wird in der Main-Methode eax auf den Stack geschoben und dann
printf aufgerufen.
Abbildung 30: Disassemblierte Funktion Add des Variablem Beispiels
50
4.2.
Programmstrukturen erkennen
Nachdem wir uns bisher mit dem Erkennen von Daten und von Variablen beschäftigt haben, wollen
wir uns in diesem Abschnitt dem Erkennen der Programmstrukturen widmen. Wie wir bis jetzt
gesehen haben, werden normalerweise Parameter über den Stack an eine Funktion übergeben, bis
auf das Beispiel, wo eine Struktur übergeben wurde.
Prinzipiell gibt es vier Varianten der Parameterübergabe:
1. Übergabe über den Stack: Diese Methode haben wir in den vorherigen Beispielen schon öfter
gesehen. Sie ist die meist genutzte Methode und wird auf unterschiedliche Weise verwendet.
Entweder mit PUSH und POP Befehlen oder es wird direkt auf den Stack zugegriffen.
2. Übergabe mittels des Data Segments: Bei dieser Methode werden die Parameter für jede
Methode im Datensegment gespeichert.
3. Parameter im Programmcode: Diese Methode nutzt man eigentlich nur in Assembler
Programmen. Nach einem call könnte zum Beispiel ein String kommen und in der Funktion
würde man einfach die Rücksprung Adresse vom Stack holen und so hat man Sie Adresse des
Strings. Ein Beispiel ist in Listing 32 gegeben.
....
CALL PROC1
DB "Dieser Paremeter steht im Code",0
....
PROC1:
; Rücksprung Adresse vom Stack holen
; Parameter mit Adresse bestimmen
; Tue was
; Springe zurück
RETN
Listing 32: Variablen im Code
4. Parameter in Registern: Hierbei werden die Parameter in Registern gehalten, diese Methode
ist sehr schnell und wird auch genutzt. Sie ist aber begrenzt durch die Anzahl der Register.
Neben den Parameter haben sehr viele Funktionen auch einen Rückgabewert, dieser wird
überwiegend im EAX Register zurückgegeben. Bei einer Gleitkommazahl wird st(0) benutzt und für
einen 64Bit Wert werden jeweils 32 Bit über EDX und EAX zurückgegeben. Wenn der Rückgabetyp
ein Boolean ist, dann wird für true MOV EAX, 1 geschrieben und MOV EAX, 0 für false.
Funktionen kann man durch folgende Merkmale erkennen:

Ein Call deutet auf eine Funktion hin. Bei einem Call [EAX] kann der Code Analyzer nicht
erkennen, was für eine Methode aufgerufen wird und man muss den Debugger nutzen.
51

Wenn wie in Kapitel 3.5.1 Sprünge für das Aufrufen von Funktionen genutzt werden, dann
muss ebenfalls ein Debugger genutzt werden, um Funktionen zu finden.


Funktionen haben meistens einen Prolog oder Epilog. Der Prolog sieht meistens so aus:
PUSH EBP
MOV EBP, ESP
SUB ESP, N
Wobei N für die Anzahl der zu reservierenden Bytes auf dem Stack steht. Der Epilog sieht wie
folgt aus:
MOV ESP, EBP
POP EBP
Wenn mehrere try…catch Anweisungen verwendet werden, dann werden auch mehrere
Epiloge im Assembler auftauchen, die Disassembler verwirren.
Am Ende einer Funktion steht meistens der Befehl RET. Wenn eine Funktion mehre „returns“
hat, dann könnte eine Sequenz ähnlich der folgenden ausgeführt werden.
CMP EAX, 1
JNZ L1
RET
L1:
Neben diesen Merkmalen gibt es weitere, die wir an dieser Stelle aber nicht weiter erläutern. Wir
verweisen auf die entsprechende Literatur wie zum Beispiel [24].
Eine weitere wichtige Struktur in der Programmierung sind Verzweigungen. Listing 33 zeigt ein
Beispiel für eine einfache Verzweigung.
#include <stdio.h>
void main() {
int a,b;
scanf("%d", &a);
scanf("%d",&b);
if (a >= b) {
printf("a >= b\n");
} else {
printf("a < b\n");
}
}
Listing 33: Verzweigung
Folgender Assembler zeigt die Verzweigung für Listing 33.
cmp edx, [ebp + 4]
jl short loc_40103F
...
jmp short loc_40104C
loc_40103F:
...
loc_40104C:
...
retn
Listing 34: Assembler Verzweigung
Bei verschachtelten Verzweigungen erhöht sich die Anzahl der CMP und JMP Anweisungen. Genau
das gleiche passiert auch bei Switch-Anweisungen.
52
Eine weitere wichtige Eigentschaft sind Schleifen. Sie bestehen aus zwei Teilen. Ein Teil ist der Body,
in dem die eigentliche Funktion, beispielsweise eine Addition ausführen oder etwas ausgeben,
ausgeführt wird und der andere ist die Bedingungsprüfung. Ein weiterer dritter optionaler Teil ist in
der Regel für einen Schleifenzähler zuständig, wie es beispielsweise bei einer For-Schleife üblich ist.
Da diese meistens eine Variable vom Typ int haben, der nach jedem durchlauf um eins erhöht wird.
Heutige Programmiersprachen sind oft objektorientiert. Wenn man sich die Daten eines Objektes
anschaut, ähneln diese sehr den Strukturen. Da Objekte in fast allen Fällen Methoden haben, muss
eine Unterscheidung beim Aufrufen getroffen werden, welches Objekt diese Funktion aufruft. Diese
Unterscheidung wird durch Übergabe einer Referenz auf den Datenbereich erledigt.
[vgl. [1][2][11][12][15][16][17][24][28][29]]
53
5. Verwendete Software
Beim Erstellen dieser Ausarbeitung haben wir verschiedene Werkzeuge verwendet bzw. uns
angeschaut. Diese möchten wir in diesem Abschnitt kurz erwähnen.
5.1.
Disassembler und Debugger
Programme in Hochsprachen werden durch den Compiler in Maschinensprache übersetzt. Um aus
der Maschinensprache wieder etwas Lesbares zu machen, kann man Disassembler nutzen. Ein
Disassembler wandelt den Maschinencode in Assembler.
5.1.1. Dumpbin.exe (.Net)
Dumpbin.exe ist eine kleine Anwendung zum Disassemblieren von EXE-Dateien. Es wird bei Visual
Studio mitgeliefert, ist aber auch im MASM32 Paket enthalten. Die Funktionalitäten von
dumpbin.exe sind nicht sehr umfangreich. Beispielsweise erkennt das Programm nicht automatisch
das Verwenden von API-Funktionen, wie wir in Kapitel 3.5 gesehen haben. Des Weiteren hat das
Programm Probleme mit Dateisegementen (wie zum Beispiel das Datensegment .data), die nicht den
Standardname haben.
5.1.2. IDA Pro Disassembler
Der IDA Pro Disassembler ist unserer Meinung nach der beste Disassembler. Für diese Arbeit haben
wir die Version 5 genommen. Neben den Funktionen eines Disassemblers bietet dieses Programm
auch einen Debugger.
5.1.3. Windasm
Windasm ist ein Disassembler und Debugger, der schon länger nicht mehr vertrieben wird. Obwohl
er nicht mehr vertrieben wird, tauchen immer wieder neue Versionen von ihm auf. Die Version, die
wir verwendet haben, ist die Version 9.
5.1.4. The OllyDbg Debugger
Der OllyDbg ist eine frei verfügbarer Debugger und Disassembler.
5.1.5. Programme für bestimmte Programmiersprachen
Neben den vorgestellten Programmen gibt es auch Programme, die für bestimmte
Programmiersprachen gedacht sind. Hier wäre beispielsweise DeDe (Delphi Decompiler) zu nennen.
54
5.2.
Hexeditors
5.2.1. PS Pad
PS Pad ist eine Editor für Windows, der unter [28] bezogen werden kann.
5.3.
Andere Tools
5.3.1. Microsoft Visual Studio 2005
Alle in dieser Arbeit verwendeten C Quelltexte wurde mit Visual Microsoft C++ 2005 Express Edition
entwickelt und kompiliert. Diese Software ist frei verfügbar und ist unserer Meinung nach eines der
Besten Entwicklungstools für Microsoft Windows. Es kann unter [22] bezogen werden.
5.3.2. MASM32
MASM steht für Microsoft Macro Assembler und ist von Microsoft entwickelt wurden. Es ist frei
verfügbar und eignet sich für x86 Prozessoren. Ein Vorteil von MASM ist, dass man anhand von
Macros leicht API Funktionen von Windows nutzen kann.
siehe [14][15]
55
6. Literatur
[1] http://www.intel.com/products/processor/manuals/index.htm Intel® 64 and IA-32
Architectures Software Developer's Manual 1, 2A, 2B, 3A, 3B
[2] http://www.intel.com/products/processor/manuals/index.htm Intel® 64 and IA-32
Architectures Optimization Reference Manual
[3] http://reverse-engineering.net/: Forum über Reverse Engineering, das in Insiderkreisen auch
als die Bibel der Disassemblierung genannt wird.
[4] http://www.openrce.org/articles/: Community, die sich mit Reverse Engineering beschäftigt.
Neben einem Forum bietet diese Seite ein Reihe von Artikeln, die einem beim Reverse
Engineering helfen.
[5] http://reteam.org/: Seite von einer Gruppe, die sich mit Reverse Engineering beschäftigen.
Auf ihr sind verschiedene Papers und Konteste zu finden, die sich mit dem Thema
beschäftigen.
[6] http://phrack.org/: Archive von Artikeln, die hauptsächlich mit Stack-Overflow beschäftigen.
[7] http://www.acm.uiuc.edu/sigmil/RevEng/: EBook über Reverse Engineering, das aber noch
nicht vollständig ist.
[8] http://codebreakers-journal.com/: Journal zu Software-Entwicklung, Virusforschung,
Softwaresicherung.
[9] Compilerbau, 2 Tle., Tl.1 von Alfred V. Aho, Ravi Sethi, Jeffrey D. Ullman; Oldenbourg;
Auflage: 2., durchges. Aufl. (Dez. 1999); ISBN-10: 3486252941; Bibliotheks Signatur: kyb
432/4Ü-1(2)
[10]Compilerbau, 2 Tle., Tl.2 (Broschiert) von Alfred V. Aho, Ravi Sethi, Jeffrey D. Ullman;
Oldenbourg; Auflage: 2., durchges. Aufl. (Januar 1999); ISBN-10: 3486252666; Bibliotheks
Signatur: n 425/195-2(2)
[11]C++ (Gebundene Ausgabe) von Ulrich Breymann; Hanser Fachbuchverlag; Auflage: 8., Aufl.
(April 2005); ISBN-10: 3446402535; Bibliotheks Signatur: n 448/8(8)a
[12]http://msdn.microsoft.com: Das Micrsoft Developer Network ist eine Platform für
Entwickler. Sie enthält viele Tutorials, Artikel und eine API Referenz über alles was man für
die Programmierung unter Windows wissen muss.
[13]http://www.imn.htwkleipzig.de/~waldmann/edu/ws05/compiler/folien/compiler/compiler.html Skript zum
Compilerbau der HTWK Leipzig
[14]http://de.wikipedia.org/wiki/MASM: Wikipedia Seite zu MASM32.
[15]http://www.codingcrew.de/masm32/index.php: Deutscher Support von Masm32. Des
Weiteren gibt es ein deutsches Forum, das einem bei Problemen helfen kann.
56
[16]http://webster.cs.ucr.edu/ Diese Seite stellt verschiedene Onlinebooks bereit, die einem
beim Lernen von Assembler helfen.
[17]http://www.sandpile.org/ia32/index.htm Sandpile.org gibt eine Übersicht über verschiedene
Prozessor-Architekturen.
[18]Windows-Programmierung. Das Entwicklerhandbuch zur WIN32-API (Broschiert) von
Charles Petzold; Verlag: Microsoft Press Deutschland (März 2005): ISBN-10: 3860631888;
Signatur: n 450/604(5)
[19]http://www.deinmeister.de/wasmtut.htm : Tutorial zu Assemblerprogrammierung mit
MASM
[20]Windows 2000 Systemprogrammierung mit VC++ von Ron Nanko; Verlage: Düsseldorf :
Data-Becker, 2000, ISBN-10: 3815820944; Signatur: kyb 339/28
[21]http://www.microsoft.com/whdc/system/platform/firmware/PECOFFdwn.mspx:
Spezifikation des Microsoft Portable Execution and Common Objcect File Format
[22]http://msdn2.microsoft.com/en-us/vstudio/default.aspx: Startseite für das Visual Studio.
[23]http://andremueller.gmxhome.de/: Seite von Andre Müller, die ein Tutorial zu
Assemblerprogrammierung gibt.
[24]Disassembling Code: IDA Pro and SoftICE with CDROM: IDA Pro and SoftICE von Vlad
Pirogov; Verlag: A-List; Auflage: Bk&CD-Rom (23. Januar 2006); ISBN-10: 1931769516
[25]Rechnerorganisation und -entwurf (Gebundene Ausgabe)
von David A. Patterson, John L. Hennessy, Arndt Bode, Wolfgang Karl, Theo Ungerer; Verlag:
Spektrum Akademischer Verlag; Auflage: 3., Aufl. (September 2005); ISBN-10: 3827415950
[26]http://www.swansontec.com/sintel.htm: Seite von Will Swanson, die Informationen über
den Intelbefehlsatz enthält.
[27]Übersetzerbau; Theorie, Konstruktion, Generierung von R. Wilhelm, D. Maurer; Springer;
1997
[28]http://www.weblearn.hs-bremen.de/risse/RST/docs/RST.pdf: Skript zur RST Vorlesung.
[29]http://www.weblearn.hs-bremen.de/risse/RST/docs/RSTslide.pdf: Folien zur RST Vorlesung.
57
7. Abbildungsverzeichnis
Abbildung 1: Einfache HalloWelt GUI .................................................................................................... 10
Abbildung 2: Dump eines Programms ................................................................................................... 13
Abbildung 3: Befehlsformat Intel .......................................................................................................... 14
Abbildung 4: Aufbau einer EXE-Datei .................................................................................................... 18
Abbildung 5: MessageBox Beispiel Disassembliert ............................................................................... 22
Abbildung 6: Windasm beim MessageBox Beispiel............................................................................... 23
Abbildung 7: dumpbin.exe aufs Glatteis geführt .................................................................................. 24
Abbildung 8: Windasm aufs Glatteis geführt ........................................................................................ 25
Abbildung 9: IDA Pro aufs Glatteis geführt ........................................................................................... 25
Abbildung 10: Listing 18 disassembliert ................................................................................................ 30
Abbildung 11: Listing 18 disassembliert (Optimierung eingeschaltet) ................................................. 31
Abbildung 12: globale Variablen mit Zeigern ........................................................................................ 32
Abbildung 13: Variable hat einen Wert aus dem Adressraum .............................................................. 33
Abbildung 14: Assembler von BYTE, WORD und DWORD ..................................................................... 34
Abbildung 15: Assembler von WORD, BYTE und DWORD ..................................................................... 35
Abbildung 16: Initialisierte und unitialisierte Variablen........................................................................ 36
Abbildung 17: Unicode Beispiel disassembliert .................................................................................... 39
Abbildung 18: Datensegment des Unicode Beispiel ............................................................................. 40
Abbildung 19: Datensegement des Unicode Beispiels als ASCII dargestellt ......................................... 40
Abbildung 20: Datenbereich mit dem lokalen String ............................................................................ 41
Abbildung 21: Disassembliertes strcat Beispiel..................................................................................... 42
Abbildung 22: Disassemblierte geändertes strcat Beispiel ................................................................... 42
Abbildung 23: Assembler des Array Beispiels ...................................................................................... 43
Abbildung 24: Assember des Array Beispiels (Ohne Graph) ................................................................. 44
Abbildung 25: Struktur Beispiel1 disassembliert .................................................................................. 45
Abbildung 26: Disassemblierte Mainmethode des 2. Struktur Beispiels .............................................. 47
Abbildung 27: Disassemblierte Init-Methode des 2. Struktur Beispiels................................................ 47
Abbildung 28: IDA Pro erkennt WndClass ............................................................................................. 48
Abbildung 29: Disassembliertes des Variablen Beispiels ...................................................................... 49
Abbildung 30: Disassemblierte Funktion Add des Variablem Beispiels ................................................ 50
58
8. Quelltextverzeichnis
Listing 1: Typisches Konsolenprogramm ................................................................................................. 7
Listing 2: Typisches Konsolenprogramm, das eine Bibliothek nutzt ....................................................... 7
Listing 3: Interaktives Programm............................................................................................................. 9
Listing 4: Einfaches Windowsprogramm ............................................................................................... 11
Listing 5: MessageBox Assembler Quellcode ........................................................................................ 13
Listing 6: Beispiel für Intel Befehle ........................................................................................................ 14
Listing 7: Manuel disassemblierter Code .............................................................................................. 17
Listing 8: IMAGE_DOS_HEADER ............................................................................................................ 19
Listing 9: IMAGE_NT_HEADER............................................................................................................... 19
Listing 10: IMAGE_FILE_HEADER ........................................................................................................... 19
Listing 11: IMAGE_OPTIONAL_HEADER ................................................................................................ 20
Listing 12: IMAGE_DATA_DIRECTORY ................................................................................................... 21
Listing 13: IMAGE_IMPORT_DESCRIPTOR ............................................................................................. 21
Listing 14: IMAGE_THUNK_DATA32 ...................................................................................................... 21
Listing 15: Disassembler aufs Glatteis führen ....................................................................................... 24
Listing 16: PUSH/RET Sprung ................................................................................................................. 27
Listing 17: Code Overlapping Beispiel ................................................................................................... 28
Listing 18: Beispiel für globale Variablen .............................................................................................. 29
Listing 19: globale Variablen mit Zeiger ................................................................................................ 31
Listing 20: Variable mit Adressraumwert .............................................................................................. 32
Listing 21: Variablen BYTE, WORD, DWORD ......................................................................................... 33
Listing 22: Variablen WORD, BYTE, DWORD ......................................................................................... 34
Listing 23: _data und _bss Segment ...................................................................................................... 36
Listing 24: Zuweisungen auf unterschiedliche Datentypen .................................................................. 37
Listing 25: 3 Zuweisungen in C aus unterschiedliche Datentypen ........................................................ 37
Listing 26: Unicode Beispiel ................................................................................................................... 39
Listing 27: strcat Beispiel1 ..................................................................................................................... 41
Listing 28: Array Beispiel ....................................................................................................................... 43
Listing 29: Strukturen Beispiel1 ............................................................................................................. 45
Listing 30: Strukturen Beispiel 2 ............................................................................................................ 46
Listing 31: Variablen Beispiel ................................................................................................................. 49
Listing 32: Variablen im Code ................................................................................................................ 51
Listing 33: Verzweigung ......................................................................................................................... 52
59
Listing 34: Assembler Verzweigung ....................................................................................................... 52
60

Documentos relacionados