MIT-Kurs "Computer Systems Security". Vorlesung 3: Pufferüberläufe: Exploits und Schutz, Teil 1

Massachusetts Institute of Technology. Vorlesung # 6.858. "Sicherheit von Computersystemen." Nikolai Zeldovich, James Mickens. 2014 Jahr


Computer Systems Security ist ein Kurs zur Entwicklung und Implementierung sicherer Computersysteme. Die Vorträge behandeln Bedrohungsmodelle, Angriffe, die die Sicherheit gefährden, und Sicherheitstechniken, die auf jüngsten wissenschaftlichen Arbeiten basieren. Zu den Themen gehören Betriebssystemsicherheit, Funktionen, Informationsflussmanagement, Sprachsicherheit, Netzwerkprotokolle, Hardwaresicherheit und Sicherheit von Webanwendungen.

Vorlesung 1: „Einführung: Bedrohungsmodelle“ Teil 1 / Teil 2 / Teil 3
Vorlesung 2: „Kontrolle von Hackerangriffen“ Teil 1 / Teil 2 / Teil 3
Vorlesung 3: „Pufferüberläufe: Exploits und Schutz“ Teil 1 / Teil 2 / Teil 3

Willkommen zur Vorlesung über Exploits für Pufferüberläufe. Heute werden wir die Diskussion über Baggy-Grenzen beenden und dann zu anderen Methoden zum Schutz vor Pufferüberlauf übergehen.



Als nächstes werden wir über die gedruckten Materialien der heutigen Vorlesung sprechen, die der Blind Return Oriented Programming (BROP) gewidmet sind - Blind Reverse Oriented Programming. Dies ist eine Exploit-Technik, die auch dann ausgeführt werden kann, wenn der Angreifer nicht über die Zielbinärdatei verfügt. Diese Exploits zielen darauf ab, "Kanarienvögel" in den Stapeln von 64-Bit-Systemen zu zerstören. Wenn Sie also wie ich waren, als ich diese Materialien zum ersten Mal las, hätten Sie Lust haben, Christopher Nolans Film zu sehen. Es war nur eine Gehirnexplosion!

Wir werden überlegen, wie diese Geräte richtig funktionieren. Daher hoffe ich, dass Sie am Ende der Vorlesung alle diese in den Vorlesungsmaterialien beschriebenen Hochtechnologien verstehen können. Aber zuerst werden wir, wie gesagt, die Diskussion über Baggy-Grenzen beenden. Betrachten Sie ein sehr einfaches Beispiel.

Angenommen, wir werden einen Zeiger p zuweisen und ihm eine Größe von 44 Bytes zuweisen. Angenommen, die Steckplatzgröße beträgt 16 Byte.

Was passiert, wenn wir die Malloc- Funktion zuweisen? Sie wissen bereits, dass in diesem Fall das Baggy-Bounds- System versucht, diese Verteilung durch einen 2n- Logarithmus zu ergänzen. Für unseren Zeiger von 44 Bytes werden also 64 Bytes Speicher zugewiesen. Die Steckplatzgröße beträgt jedoch 16 Byte, sodass 64/16 = 4 Grenztabellen mit jeweils 16 Byte erstellt werden. Jeder dieser Einträge wird in das Größenverteilungsprotokoll eingefügt.

Weisen Sie als nächstes einen anderen Zeiger char * q = p + 60 zu . Wir sehen, dass dieser Wert außerhalb der Grenzen liegt, da die Größe von p 44 Bytes beträgt und hier 60 Bytes. Aber Baggy Bounds funktionieren so, dass in diesem Fall nichts Schlimmes passiert, obwohl der Programmierer dies nicht hätte tun sollen.

Nehmen wir nun an, dass wir als nächstes einen anderen Zeiger zuweisen, der gleich char * r = q + 16 ist . Dies führt nun tatsächlich zu einem Fehler, da die Versatzgröße 60 + 16 = 76 beträgt, was 12 Byte größer ist als die 4 Slots (4x16 = 64 Byte), die das Baggy-Bounds- System zugewiesen hat . Und dieser Überschuss ist wirklich mehr als die Hälfte des Slots.



Wenn Sie sich erinnern, reagiert das Baggy-Bounds- System in diesem Fall sofort auf einen kritischen Synchronisationsfehler, der zum Absturz des Programms führt und es tatsächlich stoppt.

Stellen wir uns also vor, wir haben nur zwei Zeilen:

char * p = malloc (44)
char * q = p + 60

Und es gibt keine dritte Zeile mit dem Code. Stattdessen werden wir dies tun:

char * s = q + 8

In diesem Fall hat der Zeiger einen Wert von 60 + 8 = 68 Bit, was 4 Byte mehr ist als die durch Baggy- Grenzen zugewiesenen Grenzen . Tatsächlich verursacht dies keinen kritischen Fehler, obwohl der Wert die Grenzen überschreitet. Was wir hier gemacht haben, ist ein höherwertiges Bit für den Zeiger zu setzen. Wenn also jemand später versucht, ihn zu dereferenzieren, führt dies an dieser Stelle zu einem kritischen Fehler.



Und das Letzte, was wir tun werden, ist einen weiteren Zeiger zuzuweisen:

char * t = s - 32

Tatsächlich haben wir dies getan - wir haben den Zeiger auf die Grenze zurückgegeben. Wenn also anfangs s darüber hinausging, haben wir es jetzt auf das ursprünglich zugewiesene Volume zurückgesetzt, das wir für den Zeiger erstellt haben. Daher hat t jetzt kein Bit höherer Ordnung in seiner Zusammensetzung und kann leicht dereferenziert werden.



Teilnehmerin: Woher weiß das Programm, dass r einen Überschuss von mehr als der Hälfte des Stapels hat?

Professor Mickens: Beachten Sie, dass wir beim Erstellen von r einen Werkzeugcode erhalten haben, der in all diesen Operationen mit Zeigern funktioniert. So können wir sagen, wo sich q befinden wird, und wir wissen, dass es innerhalb der Baggy-Grenzen liegt . Wenn wir diese Operation q + 16 ausführen, wissen die Baggy-Bounds- Tools daher, woher dieser Anfangswert stammt. Wenn dann ein Versatz dieser ursprünglichen Größe auftritt , können Baggy-Grenzen leicht feststellen, dass der Versatz größer als die Hälfte der Schlitzgröße ist.

Wenn Sie Operationen mit Zeigern ausführen, sollten Sie grundsätzlich prüfen, ob diese die zugewiesene Größe überschritten haben oder nicht. Irgendwann befindet sich ein Zeiger innerhalb der Grenzen der Baggy-Grenzen , und dann passiert etwas, das ihn über die Grenzen hinausgehen lässt. In diesem Fall werden wir feststellen, dass eine Art Häkeln aus unserem Code „herausgekommen“ ist.

Hoffe das ist verständlich. Es war ein sehr kurzer Überblick über die Hausaufgaben, aber ich hoffe, Sie können es leicht verstehen.

Wir haben also einen Zeiger, der so aussieht:

char * p = malloc (256) , dann fügen wir den Zeiger char * q = p + 256 hinzu , wonach wir versuchen, diesen Zeiger zu dereferenzieren.

Was wird also passieren? Beachten Sie, dass 256 eine 2n- Sequenz ist, also innerhalb der Baggy-Grenzen liegt . Wenn wir also weitere 256 Bits hinzufügen, bedeutet dies, dass wir einen weiteren Durchgang bis zum Ende der Baggy- Grenzen machen. Wie im vorherigen Beispiel ist diese Zeile gut genug, führt jedoch dazu, dass für q ein Bit höherer Ordnung gesetzt wird. Wenn wir versuchen, es zu dereferenzieren, explodiert daher alles und wir müssen unseren Versicherungsagenten anrufen. Das ist klar?



Anhand dieser beiden Beispiele können Sie verstehen, wie das Baggy-Bounds- System funktioniert . Wie ich in der letzten Vorlesung erwähnt habe, müssen Sie nicht wirklich jede Zeigeroperation instrumentieren, wenn Sie mithilfe der statischen Code-Analyse herausfinden können, dass ein bestimmter Satz von Zeigeroperationen sicher ist. Ich werde die weitere Erörterung einiger statischer Analysen verschieben, aber es genügt zu sagen, dass Sie diese mathematischen Aktionen nicht immer ausführen müssen. Wir haben dies bereits zuvor überprüft.

Eine weitere Frage, die auf der Piazza erwähnt wird: Wie kann die Kompatibilität von Baggy-Grenzen mit früheren Nicht-Tool-Bibliotheken sichergestellt werden ? Die Idee ist, dass Baggy-Grenzen beim Initialisieren von Randtabellen festlegen , dass alle Datensätze innerhalb von 31 Bit liegen müssen. Wenn wir also die Begrenzungstabelle lesen, repräsentiert jeder Datensatz darin einen Wert der Form 2n + 31 . Wenn wir also die Anfangsgrenzen der Größe 31 Bit initialisieren, nehmen wir an, dass jeder Zeiger die maximal mögliche Größe von 2n + 31 hat . Lassen Sie mich Ihnen ein sehr einfaches Beispiel geben, das dies deutlich macht.

Angenommen, wir haben einen Speicherplatz, den wir für einen Heap verwenden. Dieser Speicherplatz besteht aus zwei Komponenten. Oben haben wir einen Heap, der mit Nicht-Tool-Code zugewiesen wurde, und unten ist ein Heap, der mit Tool-Code zugewiesen wurde. Was werden Baggy Bounds tun? Wie Sie sich erinnern, hat dieses System das Konzept eines Steckplatzes, dessen Größe 16 Bit beträgt. Daher besteht die Begrenzungstabelle aus 2 Abschnitten, die aus 31 Bits initiiert werden.

Beim Ausführen des Toolcodes wird jedoch tatsächlich der Baggy-Bounds- Algorithmus verwendet, um die entsprechenden Werte für diese Tabellenzeile festzulegen.



Wenn ein Zeiger vom oberen Rand des Speicherplatzes kommt, wird er immer auf die maximal möglichen 2n + 31 Grenzen gesetzt. Dies bedeutet, dass Baggy-Grenzen niemals berücksichtigen werden, dass eine Zeigeroperation, die aus einer Nicht-Tool-Bibliothek stammt, über die Grenzen hinausgehen kann.

Die Idee ist, dass wir im Werkzeugcode diese Vergleiche immer für Zeiger durchführen. Wenn wir jedoch die Zeigerschreibgrenzen für einen Nicht-Werkzeugcode der Form 2n + 31 festlegen, wird niemals ein Dereferenzierungsfehler auftreten. Das heißt, wir haben eine gute Interaktion zwischen Baggy-Bound- Code- Einträgen und nicht instrumentellen Datensätzen früherer Bibliotheken.

Dies bedeutet, dass wir dieses System haben, was gut ist, da es das Programm bei Verwendung von Nicht-Tool-Bibliotheken nicht zum Absturz bringt, aber ein Problem hat. Das Problem ist, dass wir niemals die Grenzen von Zeigern bestimmen können, die durch Nicht-Tool-Code generiert werden. Weil wir niemals ein Bit höherer Ordnung setzen werden, wenn dieser Zeiger beispielsweise zu viel oder zu wenig Platz erhält. Daher können wir tatsächlich keine Speichersicherheit für Vorgänge gewährleisten, die bei Verwendung von nicht instrumentellem Code auftreten. Sie können auch nicht bestimmen, wann wir einen Zeiger übergeben, der die Größengrenzen von Instrumentalcode zu Nicht-Instrumentalcode überschritten hat. In diesem Fall kann etwas Unvorstellbares passieren. Wenn Sie einen solchen Zeiger aus dem Werkzeugcode gezogen haben, ist ein höherwertiges Bit auf 1 gesetzt. Es scheint also gigantische Dimensionen zu haben.

Wir wissen, dass wir dieses Flag an einigen Stellen löschen können, wenn wir diesen Code nur in den Werkzeugcode einfügen, wenn er an die Ränder zurückkehrt. Aber wenn wir diese riesige Adresse nur an den nicht instrumentellen Code übergeben, kann sie etwas Unvorstellbares bewirken. Es kann sogar sein, dass dieser Zeiger wieder an die Grenzen zurückkehrt, aber wir werden nie die Gelegenheit haben, dieses Bit höherer Ordnung zu löschen. So können wir auch bei Verwendung der hier gezeigten Schaltung immer noch Probleme haben.

Zielgruppe: Wenn wir Toolcode zum Zuweisen von Speicher haben, verwendet er dieselbe Malloc- Funktion wie der Attributcode?

Professor: Dies ist eine heikle Frage. Wenn wir den Fall hier betrachten, wird dies strikt eingehalten, da wir zwei Speicherbereiche haben, von denen jeder den dafür festgelegten Regeln folgt. Im Prinzip hängt dies jedoch vom Code ab, der die ausgewählte Programmiersprache verwendet. Stellen Sie sich vor, Sie können beispielsweise in C ++ Ihr eigenes Qualifikationsmerkmal zuweisen. Es kommt also auf bestimmte Details des Codes an.

Zielgruppe: Wie kann der Qualifizierer prüfen, ob das Limit auf 31 Bit festgelegt ist oder nicht?

Professor: Auf den unteren Ebenen funktionieren Verteilungsalgorithmen so, dass beim Aufrufen eines unbekannten Systems der Zeiger nach oben bewegt wird. Wenn Sie also mehrere Zuweiser haben, versuchen alle, Speicher zuzuweisen. Jeder von ihnen hat seinen eigenen Speicher, den sie im Grunde genommen korrekt für sich reservieren. Im wirklichen Leben kann es also stärker fragmentiert sein als auf hohem Niveau.

Alles, was wir oben untersucht haben, bezog sich also auf den Betrieb von Baggy-Grenzen in 32-Bit-Systemen. Überlegen Sie, was bei Verwendung von 64-Bit-Systemen passiert. In solchen Systemen können Sie die Begrenzungstabelle tatsächlich entfernen, da wir einige Informationen über die Begrenzungen im Zeiger selbst speichern können.

Überlegen Sie, wie ein normaler Zeiger in Baggy-Grenzen aussieht. Es besteht aus 3 Teilen. 21 Bits werden für den ersten Nullteil zugewiesen, weitere 5 Bits werden für die Größe zugewiesen, dies ist die Hauptgröße des Protokolls und weitere 38 sind die Bits der üblichen Adresse.



Der Grund, warum dies die Größe der Adresse des von Ihnen verwendeten Programms nicht massiv einschränkt, besteht darin, dass die meisten höherwertigen Bits des Betriebssystems und / oder der Ausrüstung in den ersten beiden Teilen des Zeigers die Verwendung der Anwendung aus verschiedenen Gründen nicht zulassen. Wie sich herausstellte, reduzieren wir die Anzahl der im System verwendeten Anwendungen nicht wesentlich. So sieht ein normaler Zeiger aus.

Was passiert, wenn wir nur einen dieser Zeiger haben? Nun, auf einem 32-Bit-System können wir nur ein Bit höherer Ordnung setzen und hoffen, dass dieses Ding nie mehr als die Hälfte der Größe des Steckplatzes erreicht. Aber jetzt, da wir all diesen zusätzlichen Adressraum haben, können Sie den Offset außerhalb der OOB-Grenzen (außerhalb der Grenzen) direkt in diesen Zeiger setzen. Wir können also so etwas wie das in der Abbildung gezeigte tun, indem wir den Zeiger in 4 Teile teilen und seine Größe neu verteilen.

Somit können wir 13 Bits für die Versatzgrenzen erhalten, dh aufschreiben, wie weit dieser OOB-Zeiger von der Stelle entfernt sein wird, an der er sein sollte. Andererseits können Sie die tatsächliche Größe des hier angegebenen Objekts auf 5 und den Rest des Nullteils auf 21-13 = 8 Bit einstellen. Und dann folgt unser Adressenteil von 38 Bits. In diesem Beispiel sehen Sie die Vorteile der Verwendung von 64-Bit-Systemen.



Beachten Sie, dass wir hier die übliche Größe für einen regulären Zeiger haben. In beiden Fällen beträgt diese Größe 64 Bit und die Beschreibung ist elementar. Und das ist gut so, denn wenn wir "dicke" Zeiger verwenden, brauchen wir viele Wörter, um sie zu beschreiben.
Ich stelle auch fest, dass Nicht-Tool-Code hier leicht angewendet werden kann, da er funktioniert und dieselbe Größe wie normale Zeiger verwendet. Wir können diese Dinge zum Beispiel in eine Struktur einfügen , und die Größe dieser Struktur bleibt unverändert. Das ist also sehr gut, wenn wir die Möglichkeit haben, in einer 64-Bit-Welt zu arbeiten.

Zielgruppe: Warum befindet sich der Versatz im zweiten Fall vor der Größe und nicht wie im vorherigen Fall, und was passiert, wenn der Versatz groß ist?

Professor: Ich denke, dass wir in einigen Fällen bestimmte begrenzende Probleme haben, an denen wir arbeiten müssen. Beispielsweise tritt ein Problem auf, wenn mehr Bits vorhanden sind. Aber im Grunde glaube ich nicht, dass es einen Grund gibt, warum Sie einige dieser Dinge nicht lesen konnten. Es sei denn, bestimmte strenge Bedingungen, über die ich jetzt nicht nachdenke, hätten die Größe des Nullteils festlegen sollen, da sonst Probleme mit der Hardware auftreten könnten.

Sie können also immer noch einen Pufferüberlauf im Baggy-Bounds- System initiieren, da die Anwendung der oben genannten Ansätze nicht alle Probleme löst, oder? Ein weiteres Problem, auf das Sie möglicherweise stoßen, wenn Sie einen nicht instrumentellen Code haben, da wir keine Probleme im nicht instrumentellen Code erkennen können. Möglicherweise treten auch Speicherschwachstellen auf, die sich aus einem dynamischen Speicherzuweisungssystem ergeben. Wenn Sie sich erinnern, haben wir uns in einem früheren Vortrag diesen seltsamen Hinweis auf freie Malloc angesehen , und Baggy-Grenzen konnten das Auftreten solcher Dinge nicht verhindern.

In der letzten Vorlesung haben wir auch die Tatsache diskutiert, dass Codezeiger keine Grenzen haben, die mit ihnen verbunden wären. Angenommen, wir haben eine Struktur, in der sich der Speicherpuffer unten und der Zeiger oben befindet und der Puffer überläuft. Wir gehen davon aus, dass der Pufferüberlauf immer noch innerhalb der Baggy-Grenzen liegt . Dann müssen Sie diesen Funktionszeiger neu definieren. Andernfalls kann es, wenn wir versuchen, es zu verwenden, an bösartigen Code gesendet werden, um einen kontrollierten Teil des Speichers anzugreifen. In diesem Fall helfen uns die Grenzen nicht weiter, da wir keine mit diesen Funktionszeigern verknüpften Grenzen haben.

Was kostet die Verwendung von Baggy Bounds ? Tatsächlich haben wir nur 4 Komponenten dieses Preises.



Der erste ist Raum. Denn wenn Sie einen „dicken“ Zeiger verwenden, ist es offensichtlich, dass Sie die Zeiger größer machen müssen. Wenn Sie das Baggy-Bounds- System verwenden, über das wir gerade gesprochen haben, sollten Sie eine Grenztabelle speichern. Und diese Tabelle hat eine solche Steckplatzgröße, dass Sie steuern können, wie groß diese Tabelle sein wird, bis Sie die Möglichkeiten des dafür zugewiesenen Speichers ausgeschöpft haben.

Darüber hinaus haben Sie auch die CPU belastet, die gezwungen ist, alle diese instrumentellen Operationen mit Zeigern auszuführen. Da für jeden oder fast jeden Zeiger die Grenzen mit denselben Betriebsarten überprüft werden müssen, wird die Ausführung Ihres Programms verlangsamt.

Es gibt auch ein Problem mit Fehlalarmen. Wir haben bereits besprochen, was passieren kann, wenn ein Programm Zeiger generiert, die über die Grenzen hinausgehen, aber niemals versucht, sie zu dereferenzieren. Genau genommen ist dies kein Problem. Baggy-Grenzen kennzeichnen diese "außerhalb der Grenzen " -Flaggen, wenn sie zumindest in der 32-Bit-Lösung die Hälfte der Steckplatzgröße überschreiten.

Was Sie in den meisten Sicherheitstools sehen werden, ist, dass Fehlalarme die Wahrscheinlichkeit verringern, dass Benutzer diese Tools verwenden. In der Praxis hoffen wir alle, dass uns die Sicherheit am Herzen liegt, aber was reizt die Menschen wirklich? Sie möchten ihre dummen Fotos auf Facebook hochladen können, sie möchten den Upload-Prozess beschleunigen und so weiter. Wenn Sie also wirklich möchten, dass Ihre Sicherheitstools gefragt sind, sollten sie keine Fehlalarme enthalten. Der Versuch, alle Schwachstellen zu erkennen, führt normalerweise zu Fehlalarmen, die entweder Entwickler oder Benutzer stören.

Dies führt auch zu unproduktiven Kosten, für die Sie Compiler-Unterstützung benötigen. Weil Sie alle Tools zum System hinzufügen müssen, die Zeigerprüfung umgehen und so weiter und so fort.

Für die Verwendung des Baggy-Bounds- Systems müssen wir also einen Preis zahlen, der aus einer übermäßigen Speicherplatznutzung, einer erhöhten CPU-Auslastung, Fehlalarmen und der Verwendung eines Compilers besteht.

Damit ist die Diskussion der Baggy-Grenzen abgeschlossen .

Jetzt können wir uns zwei andere Strategien zur Reduzierung des Pufferüberlaufs vorstellen. Tatsächlich sind sie viel einfacher zu erklären und zu verstehen.

Einer dieser Ansätze wird als nicht ausführbarer Speicher bezeichnet . Seine Hauptidee ist, dass die Swap-Hardware 3 Bits von R , W und X anzeigt - Lesen, Schreiben und Ausführen - für jede Seite, die Sie im Speicher haben. , , . 2 , , , , .

, . , , , , - . , . « W X » , , , , , . , , . . , , . ?

- , . , . , , .

, , , , . , , . .

, . – just-in-time , .

-, JavaScript . JavaScript, , - - «» , - «» , x86 . , .
. , , just-in-time W , X . , , .

— . , , , .



, , . GDB, , . , . , . .

, . , , , , , .

: , , — . , , , , , , , - . , , .

, , , GDB , , , , . .
, , , , . - , - , . , . , , .

, ? , . , , . , , , , - , .

27:10

:

MIT-Kurs "Computer Systems Security". 3: « : », 2


.

Vielen Dank für Ihren Aufenthalt bei uns. Gefällt dir unser Artikel? Möchten Sie weitere interessante Materialien sehen? Unterstützen Sie uns, indem Sie eine Bestellung aufgeben oder sie Ihren Freunden empfehlen. Habr-Benutzer erhalten 30% Rabatt auf ein einzigartiges Analogon von Einstiegsservern, das wir für Sie erfunden haben: Die ganze Wahrheit über VPS (KVM) E5-2650 v4 (6 Kerne) 10 GB DDR4 240 GB SSD 1 Gbit / s $ 20 oder wie teilt man den Server? (Optionen sind mit RAID1 und RAID10, bis zu 24 Kernen und bis zu 40 GB DDR4 verfügbar).

Dell R730xd 2 mal günstiger? Nur wir haben 2 x Intel Dodeca-Core Xeon E5-2650v4 128 GB DDR4 6 x 480 GB SSD 1 Gbit / s 100 TV von 249 US-Dollar in den Niederlanden und den USA! Lesen Sie mehr über den Aufbau eines Infrastrukturgebäudes. Klasse mit Dell R730xd E5-2650 v4 Servern für 9.000 Euro für einen Cent?

Source: https://habr.com/ru/post/de416839/


All Articles