1. ROP in einer Format-String Schwachstelle
Planted May 9, 2025
Während einer Lab in HTB bin ich auf einer interessanten Schwachstelle gestoßen. Dabei handelte es sich um eine Format String Schwachstelle mit Besonderheiten.
Anmerkung: Aufgrund der ToS von HTB kann und darf ich nicht sagen, um welche Lab, Challenge oder ProLab es sich handelt. Deshalb sind einige Bilder und Namen sanitarisiert. Ziel dieses Artikels ist es das Konzept und die Vorgehensweise zu vermitteln.
🔍 Die Anwendung und Checksec
Bei der Anwendung handelt es sich um eine Linuxanwendung, die auf der Konsole ausgeführt wird. Das Programm nenne ich App
:
┌──(user㉿Linux)-[~]
└─$ file app
app: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.24, BuildID[sha1]=<SNIP>, not stripped
Es handelt sich um eine ausführbare 64-Bit ELF-Datei für Linux. libc
wurde statisch gelinked, d.h. ist Bestandteil der Anwendung (im Gegensatz zum
dynamischen Linken, bei dem das libc
des jeweiligen Betriebssystems genommen wird).
Es wird nun geprüft, welche Schutzmechanismen bei der Kompilierung aktiviert wurden. Dazu wird das Programm checksec
von pwntools verwendet:
Wichtig sind hier folgende Einstellung:
- NX enabled: Der Stack ist nicht ausführbar.
- Canary found: Am Anfang eines Funktionsaufrufs wird ein zufälliger Wert auf den Stack gelegt, der dann am Ende der Funktion geprüft wird. Hat sich der Wert verändert, ist der Stack komprimitiert und das Programm beendet sich.
- No PIE: ASLR ist nicht aktiviert. Somit bleiben die Adressen nach jedem Neustart gleich. Das erleichtert das Schreiben eines Exploits
- Stripped No: Debug-Informationen wurden nicht entfernt. Dies ist einer Erleichterung beim Reverse-Engineering.
In diesem Szenario spielt der Canary eine eher untergeordnete Rolle. Jedoch sorgt NX dafür, dass der Shellcode, der in der Regel auf dem Stack liegt, nicht ausgeführt werden kann.
Das heißt in Assembler, ein
jmp RSP
wird so erstmal nicht funktionieren und zu einem Absturz des Programmes führen. Darauf komme ich später zurück.
Insgesamt behalten wir die Sicherheitsmerkmale im Hinterkopf, falls wir die Art der Schwachstelle identifiziert haben.
🖥️ Ausführung des Programmes
Nachdem die ersten statischen Analysen durchgeführt wurden, ist es nun an der Zeit, das Programm auszuführen. Ziel soll es sein, ein grundlegendes Verständnis über die Funktionsweise und die angebotenen Features zu bekommen. Vor allem geht es darum zu erfahren, welche Eingaben (z.B. vom Nutzer, vom Filesystem, vom Netzwerk) die Anwendung erwartet oder anbietet.
Die Anwendung begrüßt uns mit einem Banner. Anscheinend ist es eine Anwendung, in der was berechnet wird. Zunächst muss ein Name eingegeben werden.
Anschließend soll ich eingeben, wie viele Stunden ich gearbeitet habe.
Nachdem ich die Stunden eingegeben habe (hier 20
), wird mit der Summe berechnet sowie ein Report ausgegeben. Anschließend wird gefragt,
ob man das Programm beenden möchte. Erwartet wird wohl ein Y
oder ein N
.
Ein schneller Test mit ungültigen Eingaben wie eine lange Zeichenfolge oder mit Formatstring Zeichen führte zu keinem Erfolg. Somit ist es Zeit fürs Reverse Engineering.
🐞 Bug Hunting mit IDA Pro
Für Reverse Engineering wurde IDA Pro eingesetzt (Ghidra wäre auch eine Alternative). Nach dem Laden der Anwendung in IDA ergibt sich folgende grobe Struktur:
Die Anwendung scheint nicht sehr komplex zu sein. Der Block ganz oben ist die Main-Funktion. Erwähnenswert ist,
dass dort eine Variable NOTES
mit dem Wert 0 und der Länge 1000 (Hex: 0x3e8) initialisiert wird:
mov edx, 3E8h
mov esi, 0
mov edi, offset NOTES
call memset
Anschließend erfolgt das Einlesen für Name
und Hours Worked
. Hours Worked
wird dann in ein Integer
umgewandelt:
sub rax, 1
mov [rbp+rax+var_30], 0
lea rax, [rbp+var_30]
mov rdi, rax
call atoi
mov [rbp+var_4C], eax
cmp [rbp+var_4C], 0
jnz short loc_4015CC
Liefert atoi
einen Fehler (eax == 0), erfolgt eine Fehlerausgabe:
<SNIP>
mov esi, offset aInvalidHours
mov edi, offset aInvalidHours
call log_error
mov rax, cs:stdout
mov rdi, rax
call fflush
jmp loc_4014C1
Liefert atoi
den Integerwert, wird die Summe ausgegeben – wie bereits oben gesehen. Ist die Summe 0
oder kleiner,
wird eine Fehlermeldung ausgegeben und in die Datei log.txt
geschrieben:
mov eax, [rbp+var_48]
mov edx, eax
mov eax, [rbp+var_4C]
imul eax, edx
mov [rbp+var_52], ax
...<SNIP>...
cmp [rbp+var_52], 0
jle loc_401762
loc_401762:
mov edi, offset aAnErrorHasOccu
call puts
mov edi, offset aLoggingDetails
call puts
...<SNIP>...
mov edi, offset aInvalidPayResu
call log_error
<SNIP>
Ist die Summe größer als 0
, kommt eine interessante Prüfung:
cmp [rbp+var_4C], 28h
jle short loc_40166A
In var_4c
steht der Wert von Hours Worked
. Ist dieser größer als 0x28
(= 40), so erreicht
man einen Codeblock, den wir oben nicht gesehen haben:
mov eax, [rbp+var_4C]
sub eax, 28h
mov [rbp+var_50], eax
...<SNIP>...
call printf
mov edi, offset aReasonForOvert
mov eax, 0
call printf
mov edx, 3E8h
mov esi, offset NOTES
mov edi, 0
call read
mov eax, [rbp+var_48]
Man schreibt in die Variable NOTES
mit einer Länge von 1000. Es soll ein Grund für die hohe
Stundenzahl angegeben werden. Okay, das merken wir uns.
Anschließend erfolgt die Ausgabe des Reports wie oben. Interessant ist dabei folgendes:
...<SNIP>...
mov edi, offset aHoursWorkedD
mov eax, 0
call printf
cmp [rbp+var_50], 0
jz short loc_4016EF
Die letzten beiden Zeilen prüfen, ob var_50
größer als 0
ist. Das ist genau dann der Fall,
wenn Hours Worked
größer als 40
ist, wie wir oben sahen:
mov eax, [rbp+var_4C]
sub eax, 28h
mov [rbp+var_50], eax
var_4c
hat den Wert von HOURS WORKED
. Dieser wird dann mit 40
subtrahiert und anschließend
in EAX
geschrieben. Danach wird EAX in die lokale Variable var_50
gespeichert.
Ist also HOURS WORKED
größer als 40, erreichen wir einen weiteren, zusätzlichen Codeblock. Interessant ist
dabei folgender Ausschnitt:
...<SNIP>...
mov edi, offset aReason
call puts
mov edi, offset NOTES
mov eax, 0
call printf
...<SNIP>...
In der Zeile drei wird die Adresse von Variable NOTES
in EDI geschrieben. Anschließend wird
EAX
mit 0
initialisiert und printf
aufgerufen. Wie bereits oben gesehen, befindet sich NOTES
unter unserer Kontrolle. Weitere Prüfungen oder Sanitarisierungen erfolgen nicht.
Somit haben wir hier wohl eine klassische Formatstring-Schwachstelle gefunden. Das muss jetzt mit einem (schnellen) Test untermauert werden:
In der Tat, wir haben eine Formatstring Schwachstelle gefunden. Es werden hier Werte (Adressen) ausgegeben, die wohl vom Stack stammen.
🧪 Dynamische Codeanalyse mit GDB
TBD
🧬 Lesen der Adressen vom Stack mit Format Strings
TBD
🛠️ Schreiben in Speicheradressen mit Format Strings
TBD
📦 Exploit entwickeln mit Python und pwntools
TBD
Fazit
TBD