Articles

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:

checksec von der Datei app

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.

start_app

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.

start app hours

Anschließend soll ich eingeben, wie viele Stunden ich gearbeitet habe.

start app report

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:

Struktur in IDA

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:

Formatstring Schwachstelle

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