Erste Schritte mit pwn: Detaillierte Erläuterung allgemeiner Befehle für GDB-Debugging-Programme

Inhaltsverzeichnis

schreibe am Anfang

1. Bereitstellung der PWN-Fragenumgebung

2. Problemlösungsideen (nicht im Fokus) 

3. Der Debugging-Prozess von gdb (wichtige Punkte) 

Vollständiger laufender Prozess (Run)  

Debugging-Programme (wichtige Punkte)

Zum Anfang des Programms laufen

Haltepunkt setzen

Speicher ansehen 

Ändern Sie den Wert der Adresse

Einzelschritt-/Ansichtsregister

4. Python-Skripte schreiben

Zusammenfassung und Überlegungen

schreibe am Anfang

   Vor kurzem habe ich wieder angefangen, pwn zu lernen. Ich habe mir das Video des staatlichen Sozialtierchefs von Station B angesehen. Ich habe eine Zusammenfassung basierend auf der Erklärung des Chefs erstellt und einige meiner Gedanken hinzugefügt. Das Erlernen von pwn beinhaltet hauptsächlich das Verständnis des Speicherlayouts und des Programmausführungsprozesses. Ich habe das Gefühl, dass der Lernpfad tatsächlich steil ist. Ich werde diese Serie in Zukunft von Zeit zu Zeit aktualisieren.

      Dieser Artikel nimmt hauptsächlich den Debugging-Prozess eines Programms als Beispiel, stellt die allgemeinen Befehle des GDB-Debuggers vor und zeigt intuitiv den Effekt der sogenannten „Überlauf“-Abdeckung und das Problem von Big und Small Endianness . Zu den beteiligten Wissenspunkten gehören: Die Bereitstellung der PWN-Fragenumgebung, allgemeine Befehle für das GDB-Debugging, die Auswirkungen von Big und Small Endianness und das Schreiben einfacher Skripte zur Lösung von PWN-Fragen . Aufgrund des begrenzten Platzes werden in diesem Artikel insbesondere nicht zu viele Assembleranweisungen und der Änderungsprozess des Funktionsaufrufstapels bei laufendem Programm vorgestellt. Diese Inhalte können in nachfolgenden Blogs vorgestellt werden. Der Schwerpunkt dieses Artikels liegt auf der Verwendung von GDB zum Debuggen des Programms. Zu den für diesen Artikel erforderlichen Tools gehören hauptsächlich gdb und pwntools. Wenn Leser mitmachen und reproduzieren möchten, benötigen sie lediglich gdb und pwntools. Einzelheiten zum Installationsprozess finden Sie unter:

Erste Schritte mit pwn (1): Kali-Konfigurationsumgebung (pwntools+gdb+peda)_gdb-Plug-in peda-CSDN-Blog

  Hinweis: Am Ende des Artikels habe ich allgemeine GDB-Anweisungen zusammengefasst. Bedürftige Leser können direkt bis zum Ende des Artikels lesen.​ 

1. Bereitstellung der PWN-Fragenumgebung

   Dieser Teil stellt die Voraussetzung für den Debugging-Prozess dar. Wenn Sie eine PWN-Umgebung lokal bereitstellen möchten, verwenden Sie socat, um einen lokalen Port zu öffnen, um die PWN-Frage auszuführen (weitere Details unten). Wenn Leser nur GDB kennen, müssen sie natürlich nicht so viel nachdenken und verwenden einfach p = Process(./file). Es wird empfohlen, eine Systembereitstellungsumgebung und einen Debugger wie Ubuntu oder Kali zu verwenden.

   Zuerst müssen wir eine Binärdatei mit einer „Überlauf“-Schwachstelle erstellen. Hier können wir die vom staatlichen Sozial- und Tierhaltungschef bereitgestellte Datei „question.c“ verwenden, deren Inhalt wie folgt lautet:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
char sh[]="/bin/sh";

int init_func(){
    setvbuf(stdin,0,2,0);
    setvbuf(stdout,0,2,0);
    setvbuf(stderr,0,2,0);
    return 0;
}

int func(char *cmd){
        system(sh);
        return 0;
}

int main(){
    init_func();
    volatile int (*fp)();
    fp=0;
    int a;
    puts("input:");
    gets(&a);  //gets没有对输入字符的长度做限制,存在溢出
    if(fp){
        fp();
    }
    return 0;
}

   Verwenden Sie dann gcc, um diese Datei zu kompilieren und eine Binärdatei mit einer Schwachstelle zu generieren. Beachten Sie, dass der Kuchenschutz während des Kompilierungsprozesses deaktiviert sein muss. Der Befehl lautet wie folgt:

gcc question.c -no-pie -o question1

Hier finden Sie eine kurze Erläuterung des PIE-Schutzes (nicht Gegenstand dieses Artikels).

1. Der PIE-Schutz ist während der Kompilierung standardmäßig aktiviert. Wenn Sie den Pie-Schutz deaktivieren möchten, müssen Sie den Parameter -no-pie manuell hinzufügen

2.PIE (positionsunabhängige ausführbare Datei)  ist eine Technologie, die adressunabhängige ausführbare Programme generiert. Es handelt sich um einen Schutzmechanismus. Wenn das Programm den PIE-Schutz aktiviert, wird die geladene Basisadresse jedes Mal geändert, wenn das Programm geladen wird. Natürlich ist dieser Mechanismus für unser lokales Debugging-Programm nicht förderlich und wird daher hier deaktiviert.

   Als nächstes können wir zur Verbesserung des Zeremoniells ein neues Datei-Flag im aktuellen Verzeichnis erstellen. Das Erhalten dieses Flags entspricht einer erfolgreichen Lösung des Problems.

 Verwenden Sie dann socat, um Port 8888 zu öffnen und diese Frage Frage1 bereitzustellen. Natürlich können Sie auch jeden anderen nicht belegten Port verwenden:

socat tcp-l:8888,fork exec:./question1,reuseaddr

  Öffnen Sie dann ein neues Terminal (Strg+Umschalt+T) und versuchen Sie herauszufinden, ob Sie mit nc eine Verbindung zu diesem Thema herstellen können. Die Verbindung ist erfolgreich, wie unten gezeigt:

 Geben Sie einfach eine Zeichenfolge ein und sehen Sie sich den Effekt an. Es scheint in Ordnung zu sein:

2. Problemlösungsideen (nicht im Fokus) 

   Wenn wir aus der Perspektive der Problemlösung denken, können wir nur eine Binärdatei erhalten und den Quellcode nicht sehen. Natürlich können Sie es zur Analyse in ida einfügen oder ein Demontagetool verwenden, um den Pseudoquellcode anzuzeigen und ihn dann langsam zu analysieren. Da der Schwerpunkt dieses Artikels jedoch auf der Verwendung von gdb zum Debuggen von Programmen liegt, finden Sie hier eine kurze Analyse der Stelle, an der die Sicherheitsanfälligkeit direkt aus dem obigen Quellcode ausgelöst wird:

1. Überlaufpunkt: Der Code enthält eine gefährliche Funktion gets(). Die Funktion „gets“ wird zum Abrufen von Benutzereingaben verwendet und begrenzt die Anzahl der eingegebenen Zeichen nicht, sodass sie überlaufen und anderen Speicher belegen kann.

2. Backdoor-Funktion: Es gibt eine Backdoor-Funktion func. Diese Funktion kann die Shell erhalten. Wir müssen einen Weg finden, diese Funktion auszuführen.

3. Funktionszeiger fp. Die ursprüngliche Logik besteht darin, einen Funktionszeiger fp zu definieren und dann den fp-Zeiger auf 0 zu setzen. Die if-Anweisung ist immer falsch. Wenn wir den Wert des fp-Zeigers so ändern können, dass fp auf die Adresse zeigt der Backdoor-Funktion func, dann Sie können getshell, und der Wert des fp-Zeigers kann durch die Eingabe der get-Funktion überschrieben werden, sodass er von uns gesteuert werden kann.

3. Der Debugging-Prozess von gdb (wichtige Punkte) 

Vollständiger laufender Prozess (Run)  

Kopieren Sie zunächst diese Binärdatei in einen anderen Ordner (Sie können sie jetzt auch direkt im Verzeichnis bearbeiten). Ich möchte nur ein Gefühl für das Ritual haben und nicht, dass das Verzeichnis für die Problemlösung und das Debuggen mit der Bereitstellung identisch ist Verzeichnis für das Problem). Der Bin im Bild unten ist der vorherige. Für Frage 1 habe ich den Namen geändert:

Beginnen Sie dann mit dem Debuggen mit gdb und führen Sie den Befehl direkt aus:

gdb ./bin

 Sie können diese Datei direkt ausführen, um den Effekt zu sehen. Geben Sie zuerst run und dann eine sehr kurze Zeichenfolge ein, z. B. abc, wie unten gezeigt:

  Es kann festgestellt werden, dass der Befehl „run“ das aktuelle Programm von Anfang bis Ende ausführt (zu diesem Zeitpunkt gibt es keinen Überlauf). Was passiert also, wenn ein Überlauf auftritt? Lassen Sie es uns noch einmal ausführen und geben dieses Mal eine sehr lange Zeichenfolge ein, um einen Überlauf sicherzustellen:

   Wie in der Abbildung oben gezeigt, ist im aktuellen Programm ein Segmentierungsfehler aufgetreten. Dann möchten wir wissen, wo der aktuelle Befehl ausgeführt wird, und können ihn über das Rip-Register finden. In der x64-Architektur zeigt das Rip-Register auf die Adresse des aktuell ausgeführten Befehls. Mit dem folgenden Befehl können wir sehen, wo wir gerade laufen:

x/20i $rip

 x wird verwendet, um den Inhalt des Speichers anzuzeigen, /20i bedeutet, 20 Anweisungen anzuzeigen, die in Assemblerform (i) angezeigt werden. Das Rip-Register speichert die Adresse des aktuell ausgeführten Befehls, sodass x/20i $rip sehen kann, welchen Befehl das aktuelle Programm ausgeführt hat.

 Es kann festgestellt werden, dass beim Ausführen des Aufruf-RDX-Befehls ein Problem auftritt. Es sollte ein Problem mit der Adresse vorliegen, auf die das RDX-Register zeigt, und es tritt ein Segmentierungsfehler auf. Dies liegt genau daran, dass die Zeichen, die wir gerade über die Get-Funktion eingegeben haben, zu lang sind. Nach einer Reihe von Operationen verursachte der Wert des RDX-Registers schließlich ein Problem und zeigte auf eine bedeutungslose Adresse. Gehen Sie zurück zum Programm und debuggen Sie es erneut.

Debugging-Programme (wichtige Punkte)

Zum Anfang des Programms laufen

 Geben Sie q ein, um den Vorgang zu beenden. Debuggen Sie das Programm erneut, verwenden Sie diesmal start, um die Ausführung des Programms zu starten:

  Der Befehl „run“ führt das Programm vollständig aus, während der Befehl „start“ zunächst zum Einstiegspunkt des Programms führt, der normalerweise der Anfang der Hauptfunktion ist. An dieser Position verwenden wir rip, um die Zeigeposition der aktuellen Anweisung zu ermitteln:

 Natürlich können wir den Quellcode der Hauptassembly auch direkt anzeigen, was den gleichen Effekt hat:

disassemble main

 Kurz gesagt, es wird zum Anfang der Hauptfunktion des Programms ausgeführt. Denken Sie daran, dass das Programm, wenn run zuvor einen Überlauf verursacht hat, bei der Anweisung „call rdx“ angehalten hat. Wer hat ihm also den Wert des rdx-Befehls zugewiesen? Wir können zwei Zeilen nachschlagen und feststellen, dass es einen Befehl mov rdx, QWORD, gibt PTR[rbp-0x10] weist rdx den Wert in der rbp-0x10-Adresse zu:

Haltepunkt setzen

Sie können sehen, dass die Adresse der Anweisung, die rdx einen Wert zuweist, 0x0000000000401293 ist. Dann können wir einen Haltepunkt an der Adresse dieser Anweisung setzen.

b *0x0000000000401293

 Sie können den folgenden Befehl verwenden, um Haltepunkte anzuzeigen:

i b

Dieser Haltepunkt kann mit dem Befehl d 2 gelöscht werden, wobei 2 die Haltepunktnummer (num) ist:

 Setzen Sie diesen Haltepunkt zurück. Normalerweise ist es nicht erforderlich, den Haltepunkt beim Debuggen des Programms zu löschen. Schließen Sie einfach den Haltepunkt. Der Befehl zum Schließen und Aktivieren des Haltepunkts lautet:

disable b 2
enable b 2

 Kurz gesagt, wir setzen einen Haltepunkt an der Anweisung mov rdx,QWORD PTR[rbp-0x10]. Als nächstes verwenden wir die Anweisung c oder continue, um die Ausführung des Programms fortzusetzen. Das Programm läuft bis zur Haltepunktposition und stoppt:

c

  Bevor wir zum Haltepunkt laufen, werden wir aufgefordert, eine Zeichenfolge einzugeben. Wir geben eine relativ lange Zeichenfolge abcdefghijk ein, um einen Überlauf zu erstellen (zu diesem Zeitpunkt wissen wir nicht, ob diese Zeichenfolge überläuft, was weiter unten ausführlich erläutert wird) und dann Drücken Sie Enter. Verwenden Sie erneut die Rip-Adressierung, und tatsächlich wird die Haltepunktposition erreicht. Die nächste Anweisung sollte darin bestehen, die 8 Bytes beginnend mit der rbp-0x10-Adresse (QWORD ist 8 Bytes) rdx zuzuweisen. Aber bevor wir das noch einmal tun, sollten wir einen Blick auf die Erinnerung werfen:

Speicher ansehen 

  Zuvor haben wir versucht, den Speicher über x/20i $rip anzuzeigen. Worauf wir uns zu diesem Zeitpunkt konzentrieren möchten, ist rbp-0x10, das mit dem folgenden Befehl angezeigt werden kann:
 

x/20g $rbp-0x10

Der Zweck dieses Befehls besteht darin, 20 lange Gleitkommazahlen mit doppelter Genauigkeit anzuzeigen, beginnend mit der Adresse, auf die das $rbp-Register zeigt, minus 16 Bytes (0x10). Das Ergebnis ist wie folgt:

 Wenn Sie empfindlich auf ASCII-Codes reagieren, können Sie feststellen, dass 65, 66, 67, 68 im Hexadezimalformat ... Diese Zahlen (d. h. 101, 102, ... im Dezimalformat) sollten das efgh sein, das wir gerade eingegeben haben ..., von Dieses Bild kann auch die Little-Endian-Anzeige widerspiegeln, dh die Adresse von \x65 ist 0x7fffffffdf20. Es ist möglicherweise nicht so schnell offensichtlich. Schauen wir uns einfach die Position von rbp-0x20 an (das heißt, wir schauen 16 Bytes weiter):

x/20g $rbp-0x20

   Wie im Bild oben gezeigt, sollte diese Antwort klar erkennen können, wo sich das von uns eingegebene Zeichen a (d. h. 0x61) im Speicher befindet, sowie den Little-Endian-Anordnungsprozess der gesamten Zeichenfolge. (a entspricht 0x61 usw.) Wenn wir möchten, dass der Wert von [rbp-0x10] die Adresse der Funktion func ist, müssen wir einen Überlauf erstellen, damit der Wert der Adresse 0x7fffffffdf20 (d. h. die Position) ist beginnend mit \x65 in der obigen Abbildung) ist die Adresse der Funktion func.

Ändern Sie den Wert der Adresse

   Während des lokalen Debugging-Prozesses können wir den Set-Befehl verwenden, um den Wert der Adresse manuell zu ändern. Hier können wir beispielsweise den Wert der Adresse 0x7fffffffdf20 auf die Adresse der Funktion func setzen. Verwenden Sie zunächst die p-Anweisung, um die Adresse der Funktion func zu ermitteln:

p &func

Es kann festgestellt werden, dass die Adresse der Funkfunktion 0x40121f lautet. Anschließend verwenden wir den Set-Befehl, um den Wert von 0x7fffffffdf20 in 0x40121f zu ändern

set *0x7fffffffdf20=0x40121f

  Überprüfen Sie den Speicher erneut und stellen Sie fest, dass der Wert der Adresse tatsächlich geändert wurde, der Inhalt ab 0x7fffffffdf24 jedoch immer noch der ursprüngliche \x69\x6a\x6b ist und nicht überschrieben wurde. Dies liegt hauptsächlich an der Größe des Datentyps , was dazu führt, dass nur die unteren 4. Bytes abgedeckt werden. Hier setzen wir einfach die 4 Bytes beginnend bei 0x7fffffffdf24 auf Null.

set *0x7fffffffdf24=0

   In diesem Fall werden die 8 Bytes ab der Adresse rbp-0x10 auf die Adresse der Funktion func gesetzt und wir kehren zur laufenden Position des Programms zurück:

  Wenn Sie als Nächstes den Befehl mov rdx,QWORD PTR [rbp-0x10] ausführen, wird der Wert von rdx der Adresse von func zugewiesen.

Einzelschritt-/Ansichtsregister

  Als nächstes debuggen wir, indem wir das Programm Schritt für Schritt ausführen. Beim Einzelschrittbetrieb des Programms überprüfen wir zunächst den Registerstatus:

i r

  Wie in der Abbildung oben gezeigt, ist rdx zu diesem Zeitpunkt immer noch 0 und rip zeigt auf die Adresse des nächsten Befehls, bei der es sich um die Adresse des Befehls mov rdx, QWORD PTR [rbp-0x10] handelt. Wir führen das Programm Schritt für Schritt durch die ni-Anweisung und führen diese Anweisung aus:

ni

  Wie im Bild oben gezeigt, wurde mov rdx,QWORD PTR [rbp-0x10] ausgeführt. Überprüfen Sie den Registerwert erneut:

   Tatsächlich wurde RDX zu diesem Zeitpunkt geändert. Schauen wir uns den Ausführungsprozess der Hauptfunktion noch einmal an:

disassemble main

  Zu diesem Zeitpunkt wird der Aufruf von rdx ausgeführt, solange Sie zwei weitere Befehlszeilen ausführen. Dies entspricht dem Aufruf der Backdoor-Funktion func, und Sie sollten in der Lage sein, die Shell abzurufen. Wenn wir ni zweimal ausführen, sollten wir in der Lage sein, die Shell zu erhalten.​ 

   Schön! Tatsächlich habe ich die Shell erhalten. Hier ist übrigens der Unterschied zwischen den GDB-Anweisungen ni und si. ni bedeutet die nächste Anweisung und si bedeutet Einzelschrittoperation. Mit anderen Worten, wenn ni auf einen Funktionsaufruf stößt , führt es die Funktion direkt aus, während si Geben Sie die Funktion ein und führen Sie sie Schritt für Schritt aus. Zu diesem Zeitpunkt hoffen wir, func aufzurufen, um es direkt abzuschließen, damit die Verwendung von ni-Anweisungen schneller ist. Natürlich können Sie auch si verwenden, um die Funktion einzugeben und sie dann auszuführen.

4. Python-Skripte schreiben

  Aus Sicht der Problemlösung wird, solange wir einen angemessenen Überlauf konstruieren, dieser ab dem 5. Byte der Eingabe zur Adresse von rbp-0x10 überlaufen, sodass unsere Nutzlast „a“*4 + func sein kann Funktion. Leider haben die \x40\x12lx1f-Bytes, die der Adresse 0x40121f der Funktion func entsprechen, keine sichtbare Zeichenkorrespondenz (dh 0x61 entspricht dem Zeichen a und 0x65 entspricht dem Zeichen e), sodass die Nutzlast nur erstellt werden kann Vervollständigen Sie die Abdeckung der spezifischen Adresse mithilfe der Python-Skriptmethode. Das geschriebene Python3-Skript exp-pwn.py lautet wie folgt:

from pwn import *
p = remote("127.0.0.1", 8888) #连接题目部署的环境,相当于nc 127.0.0.1 8888
p.recv() #接受程序输出的"input"字符串
func_addr = 0x40121f  #func函数的地址
#payload = b"a"*4 + p64(addr)   #可以这么写,用pwntools中的p64函数会自动设置成小端序,不过为了理解更深刻,还是用下面这行 
payload = b'a' * 4 + b"\x1f\x12\x40\x00\x00\x00\x00\x00"  #小端序构造溢出
p.send(payload) #将payload发送给程序
p.interactive()

   Achten Sie besonders auf das Verständnis der Little-Endian-Reihenfolge. Sie sollten in der Lage sein, die Shell zu erhalten, indem Sie dieses Programm ausführen:

 Beachten Sie, dass der aktuelle Speicherort dieser Shell der Speicherort der Fragenbereitstellungsumgebung ist. Dies ist gleichbedeutend damit, dass wir die Shell des Zielcomputers über das anfällige Programm a.out erhalten. Natürlich muss das eigentliche CTF auch die Flagge lesen!​ 

Zusammenfassung und Überlegungen

  In diesem Artikel wird als Beispiel eine PWN-Frage verwendet, um den Prozess des Debuggens eines Programms mit GDB im Detail zu erläutern. Die Frage selbst ist nicht schwierig, der Schwerpunkt liegt auf dem GDBD-Debugging-Prozess. Als sehr leistungsfähiger Debugger ist GDB sehr hilfreich bei der Lösung von PWN-Problemen. Wir müssen verstehen, wie das Programm debuggt wird (Einzelschrittausführung, Haltepunkte setzen, Speicher anzeigen, Register anzeigen, Adresswerte ändern usw.). Ich habe es hier zusammengefasst Eine Tabelle allgemeiner Befehle für gdb:

verwenden Anweisung veranschaulichen
Programm ausführen laufen Führen Sie das Programm komplett neu aus
Zum Anfang des Programms laufen Start Führen Sie den Eintrag zum Einstiegspunkt aus, normalerweise zum Hauptfunktionseintrag
Einzelschrittanleitung Und Führen Sie das Programm im Einzelschritt aus und geben Sie die Funktion ein, wenn ein Funktionsaufruf auftritt.
nächste Anweisung In Führen Sie die nächste Anweisung aus. Wenn eine Funktion gefunden wird, wird sie direkt ausgeführt.
Haltepunkt setzen b*Adresse Einen Haltepunkt an einer Adresse festlegen (standardmäßig aktiviert)
Haltepunkte anzeigen ich b Alle aktuellen Haltepunkte auflisten (aktiviert/nicht aktiviert)
Haltepunkt löschen d b Haltepunktnummer Löschen Sie einen Haltepunkt
Haltepunkte deaktivieren b-Haltepunktnummer deaktivieren Einen Haltepunkt ausschalten (nicht aktiviert)
Haltepunkte aktivieren Aktivieren Sie die b-Haltepunktnummer Setzen Sie einen Haltepunkt auf „Aktiviert“.
Zusammenstellung Zerlegen Sie die Hauptleitung Bauen Sie die gesamte Hauptleitung zusammen und beachten Sie die aktuelle Betriebsanweisung
Speicher ändern setze *adresse=wert Den an einer Adresse gespeicherten Wert zwangsweise ändern
Speicher ansehen x/20i $rip Sehen Sie sich die 20 Assembleranweisungen im aktuellen Programm an, beginnend mit der Adresse, auf die das $rip-Register zeigt
x/20g-Adresse Sehen Sie sich den Inhalt der Adresse an, es werden 20 Zeilen angezeigt, jede Zeile ist 8 Bytes groß
x/20b-Adresse Zeigen Sie den Inhalt der Adresse an, zeigen Sie 20 Zeilen an, jede Zeile ist 1 Byte
p$register Sehen Sie sich den im Register gespeicherten Wert an
p*Adresse Sehen Sie sich den an einer Adresse gespeicherten Wert an
p &func Zeigen Sie die Adresse der Funkfunktion an

  Ich hatte in letzter Zeit große Probleme. Aus beruflichen Gründen muss ich pwn schnell machen. Möglicherweise werde ich mein Wissen über pwn in Zukunft aktualisieren. Ich hoffe, dass jeder mich mögen, mir folgen und mich unterstützen kann. Wenn Sie Fragen haben, wenden Sie sich bitte an mich. Sie können Kommentare abgeben und darauf hinweisen. Ich werde Ihnen auf jeden Fall alles erzählen, was ich weiß.

Supongo que te gusta

Origin blog.csdn.net/Bossfrank/article/details/134204664
Recomendado
Clasificación