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