Im folgenden möchte ich den ersten Teil meines hoffentlich mehrteiligen Tutorials darstellen:
Den Stack.
Ich selber benutze primär Linux, habe jedoch zu Testzwecken immer mehrere Win-VMs. Ich benutze für dieses einfache
Tutorial gdb als Debugger, und würde bei komplizierteren Sachen zu Immunity wechseln, damit der gedoofte Win-User auch etwas praktisch ausprobieren kann.
ANFORDERUNGEN:
Basics von x86 Assembly in Intel-Syntax verstehen
Eigentlich enthält vorige Anforderung dies: Grundlagen der Prozess- und Speicherorganisation
Syntax von C sollte bekannt sein
DER STACK:
Der Stack ist ein Segment innerhalb des Speichers, welcher zur Aufbewahrung lokaler, nicht-statischer Variablen (und Argumente) dient.
Er funktioniert nach dem LIFO-Prinzip(Last-In-First-Out):
-Mithilfe der PUSH-Instruktion wird ein DWORD (= 4 bytes) auf dem Stack abgelegt (ESP - 4)
-Mithilfe der POP-Instruktion wird ein DWORD aus dem Stack in ein Register, oder ein Speicherbereich kopiert (ESP + 4)
-Es wird immer der zuletzt gepushte Wert mittels POP entnommen
Der Stack ist unterteilt in sogenannte Stack-Frames, wobei jeder Stack-Frame für eine Funktion innerhalb eines C(++)-Programms zuständig ist. Das EBP-Register (Extended Base Pointer) zeigt dabei auf den Anfang des momentanen Stack-Frames (wird deswegen auch Frame Pointer genannt), und das ESP-Register zeigt auf das Ende des Stack-Frames (Extended Stack Pointer). Eine Besonderheit des Stacks ist seine Ausrichtung:
Mittels eines PUSH wird ESP um ein DWORD verringert, nicht erhöht! Umgekehrt wird mittels einer POP-Instruktion, ESP um ein DWORD erhöht!
Dies liegt daran, dass der Anfang des Stacks (also der erste Stack-Frame, in C der Frame der main()-Funktion) auf einer hohen Speicheradresse im Prozesspeicher beginnt, und alle nachfolgenden gepushten Werte bzw. Stack-Frames sich in niedrigeren Adressen befinden.
Der Stack wächst also von oben nach unten!
Warum der Stack so aufgebaut ist, wie er es ist, wird dir im Abschnitt Funktionsepilog klar.
Folgender Beispielcode:
void funcA(int arg1, int arg2) { int local = 2; } int main() { funcA(1, 2); return 0; }
Kompilat:
# Function main() 0x080483da <+0>: push ebp # FUNKTIONS- 0x080483db <+1>: mov ebp,esp# PROLOG 0x080483dd <+3>: push 0x2 0x080483df <+5>: push 0x1 0x080483e1 <+7>: call 0x80483cb <funcA> 0x080483e6 <+12>: add esp,0x8 0x080483e9 <+15>: mov eax,0x0 0x080483ee <+20>: leave # FUNKTIONS- 0x080483ef <+21>: ret # EPILOG # Function funcA(..) 0x080483cb <+0>: push ebp # FUNKTIONS- 0x080483cc <+1>: mov ebp,esp# PROLOG 0x080483ce <+3>: sub esp,0x10 0x080483d1 <+6>: mov DWORD PTR [ebp-0x4],0x2 0x080483d8 <+13>: leave # FUNKTIONS- 0x080483d9 <+14>: ret # EPILOG
Der Funktionsprolog:
push ebp mov ebp, esp
Das ist der sogenannte Funktionsprolog und der ist bei allen C-Funktionen gleich. Zuerst wird der alte Frame-Pointer mittels push ebp auf dem Stack abgelegt:
und anschließend wird das EBP-Register mit dem ESP-Register überschrieben. Das ESP-Register zeigt momentan auf den zuvor gepushten Wert, den alten Frame-Pointer (Saved Frame Pointer, SFP). Der aktuelle Frame-Pointer (das EBP-Register) zeigt nun, genauso wie ESP auf diese Speicheradresse im RAM, die sich im Stack befindet und den SFP beinhaltet. Ein neuer Stack-Frame beginnt.
Funktionsoperationen/Funktionsaufruf von funcA(...):
0x080483dd <+3>: push 0x2 0x080483df <+5>: push 0x1 0x080483e1 <+7>: call 0x80483cb <funcA>
Anschließend werden die Hex-Werte 0x2 und 0x1 (dezimal: 2*16â°=2*1=2, 1*16â°=1*1=1) auf den Stack gepusht.
Und zufällig sind das genau die Werte, die wir der Function funcA(1,2) als Parameter übergeben haben, in umgekehrter Reihenfolge!
Danach wird die Funktion funcA(...) mittels call <Addresse von funcA(...)> aufgerufen.
Die Call-Instruktion spielt beim Exploiting von Buffer-Oveflows eine große Rolle:
Es wird nämlich nicht *nur* die Adresse, an der sich die Assembly-Instruktionen der funcA(...) im .text-Segment befindet, angesprungen (EIP zeigt drauf). Nein, es wird zusätzlich die nächste Instruktion von main() auf dem Stack gepusht, quasi als Rücksprungadresse, wenn von funcA(...) zurück in die main()-Funktion verzweigt wird. Call ist folgendem äquivalent:
push 0x080483e6 # Adresse der Instruktion, die auf call folgt (push EIP+2, da eine Instruktion i.d.R. 2 bytes einnimmt) JMP <Addresse von funcA(...)> # funcA(...) aufrufen
Funktionsepilog von funcA(...):
0x080483ce <+3>: sub esp,0x10 0x080483d1 <+6>: mov DWORD PTR [ebp-0x4],0x2 0x080483d8 <+13>: leave # FUNKTIONS- 0x080483d9 <+14>: ret # EPILOG
Wir sind nun in funcA(...), der Prolog wurde schon durchgeführt und die Funktionsoperationen dieser Funktions sind relativ unspannend, denn der Stack wird bei <+3> um 16 bytes vergrößert, und anschließend wird bei <+6> der Wert 2, der lokalen Variable local auf dem Stack geschrieben (nicht direkt gepusht, wie man sieht). Das einzig nennenswerte hierbei ist die Tatsache, dass die ebp-relative Adressierung deutlich wird. Denn die 2 wird in eine Adresse, die als Offset zum ebp dargestellt wird geschrieben (in die Adresse 4 bytes "nach" dem EBP, da Variablen etc... von niederen zu hohen Speicher ausgedehnt sind).
Die interessanten Instruktionen sind die leave- und die RET-Instruktion, die den sogenannten Funktionsepilog darstellen, also das Ende einer Funktion einleiten, und den Zustand der vorigen "Oberfunktion" einleiten (hier: main()).
Die leave-Instruktion ist generell äquivalent zu folgendem Ausdruck:
mov esp, ebp pop ebp
Zuerst wird der Wert des EBP, die Adresse des SFP (dem Frame-Pointer von main()), in ESP kopiert. Der Stack-Frame ist also praktisch nicht mehr existent:
Beide zeigen nun auf den SFP von main(), und mittels pop ebp wird genau dieser SFP nun zum aktuellen Frame-Pointer, da dieser in EBP geladen wurde.
Der Stack sieht momentan wiefolgt aus:
Nun findet die RET-Instruktion statt, die folgendem Befehl gleicht:
pop eip
Da der SFP schon gepoppt wurde, und somit ESP um ein DWORD erhöht wurde, zeigt der ESP nun auf den RET, die Return-Address der Instruktion innerhalb von main(), die nach dem call <funcA(...)> ist. Dieser Wert wird nun in den EIP geladen, und die Funktion funcA(...) wurde vollständig abgearbeitet, und es wurde zur Funktion main() gewechselt, samt Frame-Pointer von main():
Der entscheidene Punkt hierbei ist, dass durch die RET-Instruktion, der Inhalt, der sich an diesen Zeitpunkt am ESP befindet, in den EIP geladen wurde. Der EIP enthält ja bekannterweise die Adresse der auszuführenden Instruktion. So steuert man den Programmfluss, und so könnte man auch die Ausführung willkürlichen, schädlichen Codes herbeiführen. Dazu aber im nächsten Tutorial mehr.
Im hoffentlich nächsten Teil wird der Programmfluss eines einfachen Programms gesteuert. Bei Fehlern, konstruktiver Krititk/Tipps, nicht zögern, und per PN oder hier im Thread schreiben. Die Bilder musste ich leider als Thumbs einfügen, Vollbild ist anscheinend zu groß.