Stackmanipulation
Dieses Kapitel soll die Manipulation des Stacks in Windows- bzw. Linux-Systemen
beschreiben. Nun ist dies allerdings ein schwerwiegender Eingriff in ein laufendes
Programm, welcher auch die Funktionalität stark beeinträchtigt bzw.
verändert.
Somit dürfte auch klar sein, dass diese Thematik besonders interessant
für sogenannte "Hacker" ist. Nun soll dies keine Anleitung zum
hacken sein, sondern eher die Effekte einer versehentlichen oder sogar gewollten
Manipulation des Stack erläutern. Besonders deshalb, da nach diesem schon
seit langem bekannten Problem immer noch viele Angriffe auf Programme erfolgreich
durchgeführt werden.
Speichersegmentierung
Soll auf einer x86-CPU ein Programm oder Prozess gestartet werden, müssen
vor der eigentlichen Ausführung erst ein paar Vorbereitungen getroffen
werden. In enger Zusammenarbeit mit dem Betriebssystem werden für den auszuführenden
Prozess drei Bereiche im Hauptspeicher gesucht. Dabei spielt die Startadresse
eines Segmentes keine Rolle und kann bzw. wird bei einem erneuten Programmstart
auch variieren. Entscheidend ist hierbei nur, dass jedes Segment für sich
einen zusammenhängenden Adressbereich fixer Größe abdeckt.
Nun stellt sich aber die Frage, welchen Sinn die Unterteilung des Speichers
macht, bzw. welche Funktion jedes einzelne Segment hat. Wird ein fertig erstelltes
Programm, vorzugsweise eine .exe, gestartet und im ersten Schritt vom Betriebssystem
(OS) von einem Datenträger gelesen, besteht es primär nur aus ausführbarem
Code. Diese CPU-Anweisungen werden in einem eigens dafür vorgesehenen Bereich,
dem Codesegment (CS), abgelegt. Da ein Programm nicht anderes als ein vom OS
aufgerufenes Unterprogramm ist, werden häufig auch Übergabeparameter
an das Programm übergeben. Genauso wie es in der Regel auch einen Rückgabewert
an das OS liefert. Diese für den Prozess statischen Parameter, ebenfalls
die Strings, die in der Konsole mittels printf ausgegeben werden, werden im
sogenannten Datensegment (DS) gehalten. Das letzte Segment, das Stacksegment
(SS), das in unserem Falle näher betrachtet werden muss, beinhaltet, wie
sein Name schon sagt, den Stack. Nicht nur die Rücksprungadressen, sondern
auch lokale Variablen werden hier abgelegt.
Am Rande sei noch erwähnt, dass drei CPU-Register, die Segmentregister
CS, DS und SS, auf die Anfangsadressen der drei Segmente zeigen. So kann eine
dynamische Anordnung der Bereiche bei der Programmausführung realisiert
werden.
Die unten stehende Abbildung gibt eine denkbare Anordnung mit den Adressen der
drei Segmente im Hauptspeicher wieder. Nicht ganz korrekt ist die Darstellung,
dass die Segmentregister CS, DS und SS die Adressen direkt halten. Sie verweisen
lediglich auf einen Speicherbereich, in dem die Startadressen zu finden sind.
Gut zu erkennen ist jedoch die "räumliche" Trennung der einzelnen
Segmente im Speicher.

Code- und Datensegment
Da sie eine weniger entscheidende Rolle beim Bufferoverflow spielen, sollen
nur noch ein paar ergänzende Informationen zu den Segmenten Code und Daten
gegeben werden.
Wie schon erwähnt, stehen im Codesegment die Befehle des eigentlichen Programms.
Da sich die Firma Intel aus nicht nachvollziehbaren Gründen nicht auf eine
einheitliche Länge der Befehle einlassen wollte, gibt der aktuelle Befehl
auch seine eigentliche Länge wieder. Somit lassen sich die Positionen der
nächsten Befehle nicht im Voraus bestimmen. Bei Auszügen der Codesegmente
ist hierauf zu achten, sonst könnte es zur Verwirrung kommen.
Daten, die generell für den gesamten Ablauf dem gesamten Prozess zur Verfügung
stehen sollen, werden im Datensegment gespeichert. Hierzu zählen die schon
erwähnten Textstrings oder Kommandozeilenparameter, aber auch globale Variablen,
die vor dem eigentlichen Hauptprogramm deklariert und initialisiert werden.
Um ein Überschreiben der Codedaten zu verhindern, wacht das Betriebssystem
über das Codesegment. Aus diesem Bereich kann der Anwender bzw. der Prozess
die Daten nur lesen, ein Schreiben ist nicht möglich. Man sagt auch, der
Bereich ist read-only. Nur das OS hat das Recht, den Programmcode abzulegen,
also zu schreiben.
Anders sieht es im Datensegment aus. Es dürfte leicht nachvollziehbar sein,
dass eine Beschränkung auf "nur Lesen" nicht viel Sinn machen
würde, wenn hier die Variablen abgelegt werden sollen. Somit gilt in diesem
Segment auch für den Prozess read + write. Daten können so zur Laufzeit
geändert werden.
Für beide Segmente gilt, dass sie von den niedrigen zu den hohen Adressen
hin wachsen. D.h. der nächste Befehl oder die nächste Variable liegt
an der bzw. den nächst höheren Adressen.
Stacksegment
Ohne dieses Segment wäre die Ausführung von Unterprogrammen und die
Realisierung von globalen Variablen wohl um einiges schwieriger. Sobald ein
Prozess eine Funktion oder Prozedur aufruft, verzweigt der Programmablauf an
eine andere Stelle im Hauptspeicher. Damit der Befehl nach dem Aufruf auch wieder
gefunden werden kann, wird die Adresse nach dem aktuellen Instruction Pointer
(EIP) auf dem Stack abgelegt. Man bezeichnet diesen Wert als Rücksprungadresse.
Da sehr viele Berechnungen und Operationen in den CPU-Registern durchgeführt
werden und diese auch nur begrenzt zur Verfügung stehen, ist es notwendig,
diese gelegentlich freizugeben. Bei einem Sprung in eine Routine dürfen
die Registerinhalte allerdings nicht verloren gehen. Deshalb werden sie ebenfalls
auf dem Stack zwischengespeichert und nach dem Rücksprung in die Register
zurück geschrieben. So wird jedem Unterprogramm der Einsatz aller Register
ermöglicht, ohne dass Daten verloren gehen können.
Lokale Variablen lassen sich auch sehr gut auf dem Stack anlegen. Sie existieren
für eine Routine nur solange, bis ein Rücksprung erfolgt, was auch
dem Konzept der lokal angelegten Variablen entspricht.
Der Stack verhält sich sehr dynamisch. Er wächst innerhalb seines
Segmentes, je stärker die Ausführung von Routinen geschachtelt ist.
Gewöhnungsbedürftig ist, dass dieses Segment von den hohen zu den
niedrigen Adressen hin orientiert ist. Mit anderen Worten, er wächst zu
den niedrigen Adressen hin.
Die weiter oben erwähnte Eigenschaften der Speicherung von lokalen Variablen
hat zur Folge, dass ein Programm read + write Rechte auf dem Stack besitzt.
Was als solches schon ein gewisses Risiko birgt, da hier zwei Kategorien von
Daten abgelegt werden. Daten in den Variablen, die "nur" Ergebnisse
des Programms beeinflussen und die Daten, die den Programmfluss wie eben die
Rücksprungadressen steuern.
Noch viel gefährlicher ist die Tatsache, dass Daten auf dem Stack als Programmcode
interpretiert und ausgeführt werden kann.
Stackregister
Um den Stack richtig verwalten zu können, werden noch ein paar weitere
Register benötigt. Zum einen der eigentliche Stackpointer (ESP), der auf
das zuletzt gezeigte Element auf dem Stapel zeigt. Mit seiner Hilfe ist es möglich,
die Werte von z.b. Registern in umgekehrter Reihenfolge wieder vom Stack zu
nehmen. Außerdem zeigt es der CPU, wo der nächste zu sichernde Wert
abgelegt werden kann.
Nun ergeben sich aber ein paar kleine Schwierigkeiten beim Aufruf eines Unterprogramms.
Dieses speichert auch wieder eine Rücksprungadresse, sichert einige Register
und legt vielleicht auch einige lokale Variablen an. Da sämtliche zu sichernde
Daten einfach immer auf den Stack geschoben werden und bei geschachteltem Aufruf
von Routinen nicht mehr erkennbar ist, wo die letzte Rücksprungadresse
liegt, gibt es den sogenannten Basepointer (EBP).
Dass dieser auch notwendig bei dem Anlegen von lokalen Variablen ist, soll nun
geklärt werden. Der Basepointer als CPU-Register zeigt auf den Anfang des
Stacks. So lassen sich auch die lokalen Variablen, die sich im Adressbereich
darunter befinden, mit einem Offset plus dem Basepointer lokalisieren. Durch
diese relative Adressierung wird im Programmcode nur mit dem festen Offset gerechnet.
Will man jetzt die Rücksprungadresse ausmachen, kann man natürlich
darauf vertrauen, alle Daten wieder vom Stack zu nehmen bis der Stackpointer
auf die entsprechende Adresse zeigt. Oder man befragt den Basepointer, da dieser
immer auf die Adresse über der Rücksprungadresse zeigt.
In der unteren Abbildung zeigt der Basepointer auf eine Speicherzelle, die einen
alten EBP hält. Dies ist eine äußerst wichtige Sache, dass der
vorherige Basepointer beim Sprung in eine Routine gesichert wird.

Der Abbildung nach befindet sich der Prozess gerade in einem Unterprogramm und hat dabei lokale Variablen (Daten) deklariert und Register gesichert. Angenommen der Rücksprung soll erfolgen, dann werden die Daten in die Register zurückgeschrieben. Dabei rutscht der ESP nach unten. Als nächstes erreicht es das Feld alter EBP. Dieses Datum wird in das Register EBP geschrieben. Nun haben wir die Situation wie in der zweiten Abbildung. Nach dem ausgeführten Sprung in die aufrufende Prozedur, kann wieder mit ESP und EBP auf die lokalen Variablen dieser Routine zugegriffen werden oder bei einem erneuten Aufruf die Register gesichert werden.
Stackoperationen
Prinzipiell lässt sich jede Operation, die auf das Datensegment anwendbar
ist, auch auf dem Stack ausführen, schließlich besitzt ein Prozess
für beide Segmente read + write Zugriffsrechte. Üblich ist hier die
Verwendung des Befehles mov, mit dem Daten von einer Speicherzelle oder einem
Register zum anderen kopiert wird.
Da diese Form des Transfers nicht dem eigentlichen Konzept des Stacks entspricht,
existieren noch die zwei wesentlichen Befehle push und pop. Mit ihnen werden
die Register der CPU auf dem Stack gesichert und bei Bedarf wieder zurückgeschrieben.
Dabei wird die Position, an der das Datum des Registers geschrieben wird nur
durch den Stackpointer (ESP) bestimmt. Eine explizite Adressangabe ist mit push
und pop nicht möglich. Wird ein push ausgeführt, d.h. ein Register
auf dem Stack gerettet, wird der Stackpointer um eine Adresse verringert. Bzw.
um vier Adressen, da die Register nun mal 32 Bit breit sind. Das Verringern
lässt sich damit erklären, dass der Stack wie schon erwähnt zu
den kleinen Adressen hin wächst. Umgekehrt verhält es sich, wenn ein
Datum in ein Register zurückgeschrieben werden soll, dann wird die Adresse
im ESP um vier erhöht und der Stack wird damit vom Dateninhalt her kleiner.
Die Abbildungen unten sollen den Vorgang des Rettens von Registern auf dem Stack
noch ein wenig verdeutlichen. Das jeweils obere „Kästchen“
soll ein Auszug aus dem Stacksegment entsprechen. Mit Hilfe der beiden Pointer
ESP und EBP ist der aktuelle Rahmen, in dem der Stack sich zur Zeit bewegt,
eindeutig festgelegt. Auf der linken Seite sind zwei Standardregister EAX und
EBX sowie ESP und EBP angezeigt. Im rechten Kasten ist andeutungsweise die gerade
auszuführende Operation zu sehen.
Die Register EAX (0x001A) und EBX (0x007F) halten Werte (oberste Abbildung)
und der Stack ist gerade angelegt worden bzw. es ist gerade gesprungen worden,
da der Basepointer vom vorherigen Stackbeginn (0x1210) schon gesichert ist.
In der folgenden Abbildung (mittlere Abbildung) wird hintereinander zwei push-Anweisungen
ausgeführt. Als erstes wird der Wert von EAX, dann der von EBX auf den
Stack geschoben. Es darf nicht vergessen werden, dass die Daten wie auf einem
Stapel, daher ja auch der Name Stack, abgelegt werden. Was zu letzt geschrieben
wurde, wird als erstes wieder gelesen. Man nennt dies auch LIFO, Last In First
Out.
1.

2.
3.

Dass die Register EAX und EBX nun leer (0x0000) sind, ist natürlich nicht
ganz richtig, es soll nur symbolisiert werden, dass sie jetzt für andere
Operationen zur Verfügung stehen. Wie zu erwarten, blieb der Basepointer
(EBP) unverändert, es wurde ja auch nicht neu gesprungen. Dafür hat
sich jetzt der Stackpointer (ESP) gleich um acht Adressen nach unten bewegt.
Es sei hier angenommen, dass es sich wie auf den heutigen Intel-CPU’s
üblich, um 32 Bit (4 Byte) Worte handelt.
Nun sollte das Unterprogramm irgendwelche Operationen durchführen und den
Sprung in die aufrufende Routine wagen. Doch zuerst werden die Register in den
Ursprungszustand zurückversetzt (untere Abbildung). Allerdings wird hier
zum besseren Verständnis nur das Register EBX mit dem Befehl pop vom Stack
geholt. Entsprechend des einen pop’s wird die Adresse im ESP nur um vier
Stellen nach oben gezählt. Nun zeigt der Pointer auf den Wert, der in das
Register EAX gehört. Eine weitere pop-Operation würde auch diesen
Wert in dem im Befehl angegebenen Register ablegen.
Überschreiben der Rücksprungadresse
Bis jetzt ging es in den vorstehenden Kapiteln nur um den prinzipiellen Aufbau
und das Verhalten des Stacks. Deshalb soll nun auf die eigentliche Gefahr, die
von dem Konzept des Stacks ausgeht, eingegangen werden.
Vorab soll beispielhaft auf dem Stack ein lokales Array a mit der Länge
fünf angelegt werden. Der Datentyp entspricht einem DWORD mit 32 Bit Länge.

Wie rechts zu sehen, wird das lokale Array in einem Unterprogramm auf dem Stack
angelegt, da die Rücksprungadresse des aufrufenden Prozesses unter dem
Basepointer (EBP) liegt. Direkt darüber bzw. darunter, von den Adressen
her betrachtet, liegt die Adresse, an der der vorherige Stack beginnt. Schon
als nächstes kommt das Array.
Es ist hier zu beachten, dass trotz der Orientierung des Stacks zu den niedrigen
Adressen hin, das Array in umgekehrter Reihenfolge angelegt und vom Programm
beschrieben wird. Somit liegt das erste Element a[0] auf einer kleineren Adresse
als das letzte a[4].
Dieses widersprüchliche Verhalten lässt sich wohl damit erklären,
das lokale Variablen auf dem Stack genauso wie Daten im Datensegment geschrieben
und gelesen werden. Natürlich von den kleinen zu den großen Adresse.
Zugegriffen wird nicht mit push und pop, was ja auch gar nicht möglich
wäre, da man mit ihnen nur abhängig vom ESP an die Daten gelangen
kann, sondern mit dem Befehl mov. Als der elementarste Datentransferbefehl verbietet
er uns natürlich nicht, auf dem Stack, unabhängig vom Stackpointer
und seiner Position, Daten zu manipulieren.
Ein Array ist im wesentlichen nichts anderes als ein Pointer auf einen Datenbereich,
der mindestens soviel freien Speicher wie im Array deklariert zur Verfügung
stellt. Wird dieses Array nun befüllt, kann der Befehl mov nicht wissen,
wie groß das Array ist, also wo die Grenzen liegen. Es liegt einzig und
allein in der Verantwortung des Programmierers bzw. des Compilers, dass diese
Grenzen eingehalten werden. Unter diesem Aspekt stellt es auch kein großes
Problem dar, die Rücksprungadresse auf dem Stack derartig zu manipulieren,
dass der Rücksprung an eine neue Adresse erfolgt.
Wird nun mit einem strcpy, memcpy oder auch durch explizite Zuweisung das Array
a mit Daten gefüllt, gibt es keine Kontrolle über den tatsächlich
zur Verfügung stehenden Platz im Array. Sollten die zu kopierenden Daten
größer als das Array a sein, wird über das Ende hinaus geschrieben.
In der Regel ist jeder Programmierer versucht, dieses zu vermeiden. Doch Bufferflowangriffe
existieren aufgrund dieser Eigenschaften.
Angenommen, es sei bekannt, an welcher Stelle ein lokales Array auf dem Stack
liegt. Ermittelt man dann die Distanz zu dem Basepointer und damit auch zu der
davor stehenden Rücksprungadresse (RSP), ist es ein Leichtes, die Adresse
mit einer neuen zu überschreiben.
Mit den folgenden Abbildungen sollte dieser Vorgang gut nachvollziehbar sein.
1.

2.

3.
In der ersten Darstellung ist der ursprüngliche Zustand zu sehen. Der
Platz für das Array mit fünf Elementen ist reserviert und noch unbelegt,
darunter befinden sich der alte EBP und die Rücksprungadresse, für
die wir uns interessieren. Angenommen, das Programm macht eine Zuordnung folgender
Art: a[0]=1; a[1]=2; a[2]=3; a[3]=4; a[4]=5;. Dann haben wir den Zustand wie
in der folgenden Abbildung. Nun ist das Array komplett gefüllt und der
Feldindex darf nicht weiter erhöht werden, ansonsten bewegt man sich außerhalb
der Arraygrenzen.
Da dies aber genau das Ziel eines Bufferoverflows ist, werden einfach noch ein
paar weitere Daten geschrieben: a[5]=6; a[6]=0x0012FF08; (Abb. 12). Schade ist
nur, dass hierbei der alte EBP zerstört wird. Viel schlimmer ist allerdings,
dass nun auch die Rücksprungadresse einen völlig anderen Wert darstellt.
Bei näherer Betrachtung zeigt sie sogar auf das erste Element von a[0]
im Stacksegment.
Von dieser bewussten Manipulation hat das Unterprogramm natürlich nichts
mitbekommen und sucht den EBP, um damit auch die darunter liegende Rücksprungadresse
zu ermitteln. Über die Qualität einer solchen Adresse kann der Prozess
keine Aussage treffen und springt wie in diesem Fall auch an eine Stelle, die
auf dem Stack liegt. Es wird dort das Datum gelesen und einfach als Befehl interpretiert
und ausgeführt. Schließlich darf Code im Stacksegment ausgeführt
werden.
In diesem Beispiel machen die Werte 1 bis 5 wohl nicht viel Sinn. Aber stattdessen
kann man das Array ebenso gut mit Befehlen füllen, die dann entsprechend
dem Gedanken des Programmierers solcher Overflows bestimmte Anweisungen ausführen.
Aus dem Beispiel in den Abbildungen wurde angenommen, dass das Array über
eine direkte Zuweisung der einzelnen Felder erfolgt. Dies wird in der Praxis
wohl kaum der Fall sein. Den gleichen Effekt erzielt man bei Einsatz der Stringkopierfunktionen
strcpy, memcpy, ... Man hofft dann darauf, dass der zu übergebende String
so groß ist, dass er einerseits das Zielarray überschreitet und dann
auch noch die Speicherzelle mit der Rücksprungadresse erreicht. Möglichst
auch noch mit der vorher berechneten Adresse.
Der Angreifer sucht sich nun ein Programm, welches einen String empfängt,
z.B. einen Kommandozeilenparameter und formuliert in diesem String den auszuführenden
Code und die neue Rücksprungadresse an den Beginn seiner Befehle.
Download
Die hier aufgeführten Programme dienen lediglich dem Verständnis des Aufbaus des Stacks. Wer sich intensiver mit dieser Problematik beschäftigen möchte, sollte unbedingt einen Debugger einsetzen und die Programme dort mal unter die Lupe nehmen.