Mat
Aktives Mitglied
So, wie in diesem Thread angekündigt, poste ich hier eine CTF-Übungsaufgabe, mit der ich nicht weiter komme.
Es geht um die Übung Guessing Game 1 auf picoCTF, Kategorie "Binary Exploitation". Es gibt eine kurze Anweisung, 2 Hinweise und 3 Dateianhänge sowie eine per TCP ansprechbare Serveradresse, auf der eine Instanz des anzugreifenden Programms läuft. Auf dem Server müsste sich im Programmverzeichnis eine Textdatei mit der Flagge befinden. Wenn man diese findet, kann man sie auf der Seite einlösen und die Aufgabe gilt als gelöst. Ich habe die Dateien in den Anhang gepackt, falls ihr selbst gucken wollt, ohne euch bei picoCTF anmelden oder das Programm bauen zu müssen.
Hier mein aktueller Stand, inklusive bisheriger Lösungsversuche und Abläufe.
------------------------------------------------------------------------
------------------------------------------------------------------------
Sagt mir nicht viel, ich schaue mir das nur genauer an, wenn es der Lösung dient. Bis dahin nehme ich einfach Sachen an:
Um den Server nicht mit automatisierten Anfragen zu belasten, greifen wir lieber das lokale Programm auf dem eigenen PC an. Dafür könnten wir eine Binärdatei direkt in das Programm pipen:
Verschiedene Versionen von input (1 Version pro Zeile) :
Ich raffe C nicht. Warum verhält es sich so, als würde es immer wieder Input aus der Pipe erhalten? Die Datei ist doch längst ausgelesen. Und die Versuche mit Signalterminierung ignoriert er auch. Übergebe ich das aus Versehen als Text statt binär?
Ergebnis:
Schauen wir mal, ob rand hier auch wirklich wie vermutet funktioniert:
Ich teste die Eingaben lokal und auf dem Server (sie sollten alle stimmen).
Läuft! Für die manuellen Tests kann man sich die ersten 4 Zahlen der Sequenz merken: \( 84, 87, 78, 16 \)
Auswertung:
Der Stringdatentyp für die winner-Variable scheint für 120 Zeichen reserviert zu sein (15x8). Alles darüber hinaus wird direkt als Anweisung ausgeführt? Das gibt uns ca 240 Zeichen an Spielraum. Danach wird printf ausgeführt.
Idee: Um zu testen, ob das Programm überhaupt Anweisungen so ausführt, wie ich mir das vorstelle, möchte ich die Ausgabe des Namens überspringen oder den Namen nochmals abfragen lassen. Das sollte mit einem Sprung mit bestimmtem relativen Offset möglich sein, oder vielleicht ein Sprung direkt zu einer anderen Funktion.
Die Methode mit dem Pipen von Binärdateien funktioniert jetzt ohne Endlosschleife (wohl wegen der fehlerbedingten Programmterminierung):
Inhalt:
Es gilt allerdings noch herauszufinden, wie breit die Anweisungen sind und wo genau er mit dem Lesen beginnt. In gdb springt er bei den Funktionsaufrufen mal 4 mal 9 Adressnummern weiter, also haben wir keine feste Breite?
Wie ich mir die weiteren Schritte vorstelle:
Für einige von euch ist das wahrscheinlich Kinderkram, aber für mich ist das ziemlich schwer. Ich glaub, ich muss mir Assembler mal wieder anschauen. Hab diese Woche leider keine Zeit mehr dafür, aber die Aufgabe wird schon noch geknackt.
Es geht um die Übung Guessing Game 1 auf picoCTF, Kategorie "Binary Exploitation". Es gibt eine kurze Anweisung, 2 Hinweise und 3 Dateianhänge sowie eine per TCP ansprechbare Serveradresse, auf der eine Instanz des anzugreifenden Programms läuft. Auf dem Server müsste sich im Programmverzeichnis eine Textdatei mit der Flagge befinden. Wenn man diese findet, kann man sie auf der Seite einlösen und die Aufgabe gilt als gelöst. Ich habe die Dateien in den Anhang gepackt, falls ihr selbst gucken wollt, ohne euch bei picoCTF anmelden oder das Programm bauen zu müssen.
Hier mein aktueller Stand, inklusive bisheriger Lösungsversuche und Abläufe.
------------------------------------------------------------------------
Anweisung
⚠ Ich weiß nicht, ob man die Serveradressen einfach so teilen darf, sonst könnt ihr euch da einfach registrieren, um sie anzuzeigen oder mich anschreiben. ⚠Ich habe ein einfaches Spiel geschrieben, um mit meinen Programmierkenntnissen anzugeben. Versuche, es zu gewinnen!
vuln vuln.c Makefile "nc xxx.picoctf.org XXXX"
Hinweise
Nicht übersetzt, weil manchmal der Hinweis in der Wortwahl steckt:Hinweis1: Tools can be helpful, but you may need to look around for yourself.
Hinweis2: Remember, in CTF problems, if something seems weird it probably means something...
------------------------------------------------------------------------
Dateien und Analyse
Rumprobieren
Um nicht vom Quellcode beeinflusst zu werden, verbinden wir uns zunächst direkt mit dem Server und probieren Eingaben aus:- Akzeptierte Eingaben:
- ganzen Zahlen
- Kommazahlen
- Zahlen in der wissenschaftlichen Schreibweise
- Abgelehnte Eingaben:
- 0 und andere Darstellungsweisen von 0
- Texte
- Besonderheiten:
- führende Leerzeichen werden bereinigt:
5
ist eine gültige Zahl - es reicht, dass am Anfang eine gültige Sequenz steht, der Rest kann ungültig sein:
5klopapierrollen
- 0 ist keine gültige Zahl, wie gemein!
- sehr lange Zahlen werden scheinbar aufgeteilt, weil wir mehrfach "Nope" erhalte, so als hätte man mehrfach hintereinander geraten
- spricht dafür, dass es pufferweise verarbeitet wird
- Puffer umfasst wohl ca100 Zeichen (98 Zeichen + LF + 00?)
- bei 100 1en kommt 2 mal "Nope"
- bei 99 1en heißt es "Not a valid number"
- bei 98 1en ist es OK
- führende Leerzeichen werden bereinigt:
Quellcode und Makefile
Meine Kommentare sind auf Deutsch:
Quellcode:
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h> // warum ist das hier?
// wird für Puffergröße und Zufallszahlenbegrenzung verwendet
#define BUFSIZE 100
long increment(long in) { return in + 1; }
// Mooment.. 0-99 mit immer gleicher Zahlensequenz, da kein variabler Seed?
long get_random() { return rand() % BUFSIZE; }
// fragt zu ratende Zahl ab, Rückgabe wird als boolean betrachtet
int do_stuff() {
long ans = get_random();
ans = increment(ans); // verschiebt Skala auf 1-100
int res = 0;
printf("What number would you like to guess?\n");
char guess[BUFSIZE]; // Zahlen mit bis zu 99 Stellen akzeptiert?, naja, OK
fgets(guess, BUFSIZE, stdin);
long g = atol(guess); // 0, wenn ungültig oder 0, so weit OK
if (!g) {
// bei Eingabe der Zahl 0 kommt man hier auch rein, aber OK
printf("That's not a valid number!\n");
} else {
if (g == ans) {
printf("Congrats! You win! Your prize is this print statement!\n\n");
res = 1;
} else {
printf("Nope!\n\n");
}
}
return res;
}
void win() {
char winner[BUFSIZE];
printf("New winner!\nName? ");
fgets(winner, 360, stdin); // Ups, da haben wir doch was, BUFSIZE ist nur 100 aber wir lesen 360
printf("Congrats %s\n\n", winner);
}
int main(int argc, char **argv) {
setvbuf(stdout, NULL, _IONBF, 0); // ist das wichtig? ist ja nur die Ausgabe
// Set the gid to the effective gid
// this prevents /bin/sh from dropping the privileges
gid_t gid = getegid();
// gibt dem Prozess die vollen Rechte des ausführenden Users?
setresgid(gid, gid, gid);
int res;
printf("Welcome to my guessing game!\n\n");
while (1) {
res = do_stuff();
if (res) {
win();
}
}
return 0;
}
Makefile:
all:
gcc -m64 -fno-stack-protector -O0 -no-pie -static -o vuln vuln.c
clean:
rm vuln
Sagt mir nicht viel, ich schaue mir das nur genauer an, wenn es der Lösung dient. Bis dahin nehme ich einfach Sachen an:
m64
: 64bit?fno-stack-protector
: verhindert, dass beim Linken Maßnahmen für Overflowschutz verwendet werden?static
: man kann mit festen Speicheradressen verlinkter Libraries rechnen?O0
: keine Optimierung, damit der Code besser dekompiliert werden kann?no-pie
: irgendwas mit Kuchen?
Angriff!
Es sieht aus, als wäre der Eintrag des Namens ein guter Angriffsvektor, weil da so ca 260 Zeichen mehr eingelesen werden können, als der Puffer hergibt. Dafür muss aber erstmal der Gewinner-Dialog erreicht werden.Schritt 1: Zufallszahl
Versuch: Bruteforce
Idee: Es sind nur die Zahlen zwischen 1-100 möglich und die Sequenz sollte immer die gleiche sein. Das heißt, wenn wir eine Zahl testen, dann das Programm neu starten und die nächste Zahl testen, startet auch die Sequenz wieder von vorne. So kann man relativ schnell die erste Zahl der Zufallssequenz ermitteln.Um den Server nicht mit automatisierten Anfragen zu belasten, greifen wir lieber das lokale Programm auf dem eigenen PC an. Dafür könnten wir eine Binärdatei direkt in das Programm pipen:
./vuln < input
Verschiedene Versionen von input (1 Version pro Zeile) :
Versuche:
// Alles endete in Endlosschleifen
00000000: 31
00000000: 310a
00000000: 310a 00
00000000: 310a 0d
00000000: 310a 0d00
00000000: 310a 0003 04
Ich raffe C nicht. Warum verhält es sich so, als würde es immer wieder Input aus der Pipe erhalten? Die Datei ist doch längst ausgelesen. Und die Versuche mit Signalterminierung ignoriert er auch. Übergebe ich das aus Versehen als Text statt binär?
Versuch: Halt den rand()
In den Decompiler wollte ich an der Stelle noch nicht reinschauen, wir können hier auch das nichtzufällige rand ausnutzen. Dafür kopieren wir Quellcode und Makefile und passen den Code entsprechend an, um die ersten 10 Zahlen der Sequenz ausgeben zu lassen:
test.c:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv) {
int i = 0;
while (i <= 10) {
i++;
printf("%d\n", ((rand() % 100) + 1));
}
return 0;
}
Ergebnis:
Code:
./testprog/test
84
87
78
16
94
36
87
93
50
22
63
Schauen wir mal, ob rand hier auch wirklich wie vermutet funktioniert:
Ich teste die Eingaben lokal und auf dem Server (sie sollten alle stimmen).
Code:
Welcome to my guessing game!
What number would you like to guess? > 84
Congrats! You win! Your prize is this print statement!
New winner! Name? > a
Congrats a
What number would you like to guess? > 87
Congrats! You win! Your prize is this print statement!
New winner! Name? > a
Congrats a
usw...
Läuft! Für die manuellen Tests kann man sich die ersten 4 Zahlen der Sequenz merken: \( 84, 87, 78, 16 \)
Namenseingabe
Weil wir jetzt die Zufallszahlen haben, können wir die Namenseingabe angreifen.Schritt 1: Grenzen testen
Überlegungen:- er liest bis zu 360 Zeichen in einen CharArray der Größe 100 und gibt dann diesen Array mit printf aus
- was passiert bei 98-101 Zeichen?
- was passiert bei 358-361 Zeichen?
- passiert dazwischen irgendetwas auffälliges?
Code:
// 98-101: Alles OK, wird auch vollständig ausgegeben
Name? > xxxxxxxxx1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx7xxxxxxxxx8xxxxxxxxx9xxxxxxxxx0x
Congrats xxxxxxxxx1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx7xxxxxxxxx8xxxxxxxxx9xxxxxxxxx0x
// 358+359: Lokal sagt "Segmentation fault", Server sagt "coredump", aber gibt die Eingabe vollständig aus:
Name? > xxxxxxxxx1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx7xxxxxxxxx8xxxxxxxxx9xxxxxxxxx0xxxxxxxxx1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx7xxxxxxxxx8xxxxxxxxx9xxxxxxxxx0xxxxxxxxx1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx7xxxxxxxxx8xxxxxxxxx9xxxxxxxxx0xxxxxxxxx1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxxx6
Congrats xxxxxxxxx1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx7xxxxxxxxx8xxxxxxxxx9xxxxxxxxx0xxxxxxxxx1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx7xxxxxxxxx8xxxxxxxxx9xxxxxxxxx0xxxxxxxxx1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx7xxxxxxxxx8xxxxxxxxx9xxxxxxxxx0xxxxxxxxx1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx
Segmentation fault
// 360: Fehler wie bei 358 und 359, aber gibt das letzte Zeichen nicht aus
Name? > xxxxxxxxx1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx7xxxxxxxxx8xxxxxxxxx9xxxxxxxxx0xxxxxxxxx1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx7xxxxxxxxx8xxxxxxxxx9xxxxxxxxx0xxxxxxxxx1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx7xxxxxxxxx8xxxxxxxxx9xxxxxxxxx0xxxxxxxxx1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6
Congrats xxxxxxxxx1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx7xxxxxxxxx8xxxxxxxxx9xxxxxxxxx0xxxxxxxxx1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx7xxxxxxxxx8xxxxxxxxx9xxxxxxxxx0xxxxxxxxx1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx7xxxxxxxxx8xxxxxxxxx9xxxxxxxxx0xxxxxxxxx1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx
Segmentation fault
// Schauen wir mal, wann er damit anfängt: ab 119 kommt ein anderer Fehler
Name? xxxxxxxxx1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx7xxxxxxxxx8xxxxxxxxx9xxxxxxxxx0xxxxxxxxx1xxxxxxxx?
Congrats xxxxxxxxx1xxxxxxxxx2xxxxxxxxx3xxxxxxxxx4xxxxxxxxx5xxxxxxxxx6xxxxxxxxx7xxxxxxxxx8xxxxxxxxx9xxxxxxxxx0xxxxxxxxx1xxxxxxxx?
Illegal instruction
Auswertung:
Der Stringdatentyp für die winner-Variable scheint für 120 Zeichen reserviert zu sein (15x8). Alles darüber hinaus wird direkt als Anweisung ausgeführt? Das gibt uns ca 240 Zeichen an Spielraum. Danach wird printf ausgeführt.
Schritt 2: Methodenaufruf einfügen
Idee: Um zu testen, ob das Programm überhaupt Anweisungen so ausführt, wie ich mir das vorstelle, möchte ich die Ausgabe des Namens überspringen oder den Namen nochmals abfragen lassen. Das sollte mit einem Sprung mit bestimmtem relativen Offset möglich sein, oder vielleicht ein Sprung direkt zu einer anderen Funktion.
Die Methode mit dem Pipen von Binärdateien funktioniert jetzt ohne Endlosschleife (wohl wegen der fehlerbedingten Programmterminierung):
Code:
./vuln < cmd
Welcome to my guessing game!
What number would you like to guess?
Congrats! You win! Your prize is this print statement!
New winner!
Name? Congrats _______________________________________________________________________________________________________________________
Illegal instruction
Inhalt:
cmd:
00000000: 3834 0a5f 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f 84._____________
00000010: 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f ________________
00000020: 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f ________________
00000030: 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f ________________
00000040: 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f ________________
00000050: 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f ________________
00000060: 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f ________________
00000070: 5f5f 5f5f 5f5f 5f5f 5f5f 00 __________.
Es gilt allerdings noch herauszufinden, wie breit die Anweisungen sind und wo genau er mit dem Lesen beginnt. In gdb springt er bei den Funktionsaufrufen mal 4 mal 9 Adressnummern weiter, also haben wir keine feste Breite?
Stand
Weiter bin ich noch nicht gekommen. Wollte wissen, ob ich auf dem richtigen Weg bin oder ob ihr andere Ideen habt. Ihr könnt es gerne auch selbst probieren.Wie ich mir die weiteren Schritte vorstelle:
- über das Pipen einer Binärdatei ins Programm einen einfachen Sprung oder Anweisung hinbekommen
- lokal getestete Binärdatei per ncat an den Server pipen
- wenn es online ebenso funktioniert, dann kommt das mit der Adressierung hin
- wenn nicht, wieder zu 1 + Recherche
- ab hier wieder lokal weiter testen
- in einem Debugger Adressen von Systemaufrufen herausfinden, die einen bashbefehl ausführen können (system, exec oder sowas)
- testen, ob
ls
ausgeführt werden kann und Text zurückgibt (evtl mit Weiterleitung an puts) - wenn das zu viel Arbeit ist, online einen Binären Payload suchen, der zur Systemarchitektur passt
- falls Flag im Ordner vorhanden, direkt mit
cat
zurückgeben - falls nicht, muss ein find + cat + grep - Befehl her
- sollten die Systemaufrufe zu umständlich sein, bleibt noch die Möglichkeit, Dateinamen zu raten und mit read() auszulesen
Für einige von euch ist das wahrscheinlich Kinderkram, aber für mich ist das ziemlich schwer. Ich glaub, ich muss mir Assembler mal wieder anschauen. Hab diese Woche leider keine Zeit mehr dafür, aber die Aufgabe wird schon noch geknackt.
Anhänge
Zuletzt bearbeitet: