# Slot Machine Hallo! Bereit für ein wenig Heap-Exploitation? Heute schauen wir uns **Slot Machine** an, eine Pwn-Challenge, die auf den ersten Blick einfach erscheint, aber eine raffinierte Schwachstelle verbirgt. Beim ersten Ausführen des Programms wirst du vom "SecureSlot Storage Manager" begrüßt. Es ist ein einfaches Tool, mit dem man "Slots" zum Speichern von Daten initialisieren (`init`), lesen (`read`), beschreiben (`write`) und zerstören (`destroy`) kann. Die Herausforderung: Wir haben nur ein **gestripptes Binary**. Kein Quellcode, keine Symbole. Das bedeutet, unser erster Schritt ist der Griff zum treuen Dekompiler – Ghidra –, um die Erkundung zu starten. --- ### Schritt 1: Erkundung (Die "Black Box") Da wir mit einem gestrippten Binary arbeiten, müssen wir die Programmlogik selbst kartografieren. Ein guter erster Schritt bei jeder Pwn-Challenge ist die Suche nach interessanten Zeichenketten (Strings). Suchen wir in Ghidra nach "flag", finden wir sofort einen Treffer: `[*] Congratulations! Here is your flag: %s ` Dieser String wird in einer Funktion an der Adresse `0x109d74` (`FUN_00109d74`) referenziert: ```c void FUN_00109d74(void) { undefined8 uVar1; FUN_00114310("[*] Congratulations! Here is your flag: %s ",&DAT_001f41e0); uVar1 = 0; FUN_001137e0(); FUN_00130be0(uVar1); return; } ``` Dies ist unsere "Win"-Bedingung – wenn wir den Programmfluss auf diese Adresse umleiten können, erhalten wir das Flag! Schauen wir uns nun an, wie der Rest des Programms funktioniert. Wenn wir der Logik vom Einstiegspunkt bis zur `main`-Funktion (bei `0x10a393`) folgen, finden wir das Herzstück des "SecureSlot Manager": eine menügesteuerte Schleife. ```c // Dekompilierte Main-Schleife (FUN_0010a393) undefined8 FUN_0010a393(void) { // ... Setup-Code ... FUN_00123090("--- SecureSlot Storage Manager ---"); do { while( true ) { while( true ) { while( true ) { while( true ) { FUN_00114310(" COMMANDS: init, read, write, destroy, quit > "); FUN_001144a0(&DAT_001c01c7,local_1a); if (local_1a[0] != 'i') break; FUN_00109dc3(); // Dies ist cmd_init } if (local_1a[0] != 'r') break; FUN_00109f95(); // Dies ist cmd_read } if (local_1a[0] != 'w') break; FUN_0010a0d1(); // Dies ist cmd_write } if (local_1a[0] != 'd') break; FUN_0010a238(); // Dies ist cmd_destroy } } while (local_1a[0] != 'q'); // ... Cleanup-Code ... } ``` --- ### Schritt 2: Reverse Engineering des "Slots" Um dies auszunutzen, müssen wir verstehen, wie die "Slots" verwaltet werden. Schauen wir uns die `init`-Funktion an (`FUN_00109dc3`): ```c void FUN_00109dc3(void) { int iVar1; undefined8 uVar2; long in_FS_OFFSET; int local_34; long local_30; long local_28; long local_20; local_20 = *(long *)(in_FS_OFFSET + 0x28); FUN_00114310("Slot Index: "); FUN_001144a0(&DAT_001c00b1,&local_34); FUN_00114310("Element Count: "); FUN_001144a0(&DAT_001c00c4,&local_30); iVar1 = local_34; if ((local_34 < 0) || (0xb < local_34)) { FUN_00123090("Invalid index"); } else if (*(long *)(&DAT_001f4220 + (long)local_34 * 8) == 0) { local_28 = (local_30 + 2) * 8; uVar2 = FUN_001308a0(local_28); *(undefined8 *)(&DAT_001f4220 + (long)iVar1 * 8) = uVar2; thunk_FUN_00133b80(*(undefined8 *)(&DAT_001f4220 + (long)local_34 * 8),0,local_28); if (*(long *)(&DAT_001f4220 + (long)local_34 * 8) == 0) { FUN_00123090("Allocation failed"); } else { **(long **)(&DAT_001f4220 + (long)local_34 * 8) = local_30; *(code **)(*(long *)(&DAT_001f4220 + (long)local_34 * 8) + 8) = FUN_00109da4; FUN_00114310("Allocated slot %d ",local_34); } } else { FUN_00123090("Slot already in use"); } if (local_20 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ FUN_001641a0(); } return; } ``` **Woher kennen wir das Struct-Layout?** Indem wir beobachten, wie das Programm mit dem Speicher interagiert, den `FUN_001308a0` (wir können annehmen, dass dies `malloc` ist) zurückgibt: 1. `**(long **)(&DAT_001f4220 + (long)local_34 * 8) = local_30;` -> Die ersten 8 Bytes des reservierten Puffers speichern `local_30` (unseren "Element Count"). 2. `*(code **)(*(long *)(&DAT_001f4220 + (long)local_34 * 8) + 8) = FUN_00109da4;` -> Die nächsten 8 Bytes (Offset 8) speichern einen Funktionszeiger (`FUN_00109da4`). 3. Wenn man die `read`- und `write`-Funktionen analysiert, sieht man, dass sie auf Daten zugreifen, die bei einem Offset von 16 Bytes beginnen. Dies ermöglicht es uns, die `slot_t`-Struktur zu rekonstruieren: ```c typedef struct { uint64_t count; // Offset 0 (local_30) void (*cleanup)(void*); // Offset 8 (FUN_00109da4) int64_t data[]; // Offset 16 (wo Benutzer lesen/schreiben) } slot_t; ``` Wenn du den "destroy"-Befehl aufrufst, ruft das Programm diesen Funktionszeiger bei Offset 8 auf. Wenn wir diesen überschreiben können, kontrollieren wir die Ausführung! --- ### Schritt 3: Den "Jackpot" finden (Die Schwachstelle) In `FUN_00109dc3` beachte, wie die Allokationsgröße `local_28` berechnet wird: `local_28 = (local_30 + 2) * 8;`. Wenn wir eine massiv große Zahl für `local_30` angeben, kommt es zu einem **Integer Overflow**. Geben wir `2305843009213693952` ein (was $2^{61}$ entspricht), sieht die Rechnung so aus: $(2^{61} + 2)\times8 = 2^{64} + 16$. In einem 64-Bit-System läuft $2^{64}$ auf $0$ zurück. Das Programm ruft also tatsächlich `malloc(16)` auf. Der bei Offset 0 gespeicherte `count` ist jedoch immer noch diese riesige Zahl! Das gibt uns einen "Slot", der glaubt, Milliarden von Einträgen zu haben, aber tatsächlich nur über 16 Bytes physischen Heap-Speicher verfügt. --- ### Schritt 4: Out-of-Bounds & ASLR-Bypass Da `count` riesig ist, werden uns die Befehle `read` und `write` nicht daran hindern, auf Speicherbereiche zuzugreifen, die weit über die allokierten 16 Bytes hinausgehen. Wir haben einen **Out-of-Bounds (OOB) Zugriff**. Aber es gibt einen Haken: **ASLR** ist aktiviert. Wir kennen die absolute Adresse von `print_flag` nicht. Wir benötigen ein Datenleck (Leak). 1. **Initialisiere Slot 0** mit unserer Overflow-Größe (`2305843009213693952`). Er erhält einen winzigen 16-Byte-Block. 2. **Initialisiere Slot 1** mit einer normalen Größe (z. B. `1`). Der Heap-Allokator wird den Block für Slot 1 unmittelbar nach Slot 0 platzieren. Da Slot 0 nur 16 Bytes reserviert bekommen hat, aber glaubt, riesig zu sein, überschneidet sich sein "data"-Bereich (beginnend bei Offset 16) nun mit dem nächsten Block im Speicher (Slot 1). | Index (Slot 0) | Offset | Inhalt | Beschreibung | | :--- | :--- | :--- | :--- | | - | 0x00 | `count` | Slot 0 massive Größe | | - | 0x08 | `cleanup` | Slot 0 Cleanup-Zeiger | | **Eintrag 0** | 0x10 | `prev_size` | Slot 1 Heap-Metadaten | | **Eintrag 1** | 0x18 | `size` | Slot 1 Heap-Metadaten | | **Eintrag 2** | 0x20 | `count` | Slot 1 `count`-Feld | | **Eintrag 3** | 0x28 | `cleanup` | **Slot 1 Cleanup-Zeiger (Ziel!)** | 3. **Die Adresse leaken:** Lies `Slot 0` bei `Eintrag Index 3`. * Wie du aus der Tabelle ersehen kannst, zeigt `Eintrag 3` von Slot 0 genau auf den `cleanup`-Funktionszeiger von Slot 1. * Durch das Lesen dieses Werts erhalten wir die absolute Adresse von `default_destroy` (oder `FUN_00109da4`) und umgehen so ASLR! Durch das Auslesen dieses Werts wissen wir nun, wo das Binary im Speicher geladen wurde. --- ### Schritt 5: Der finale Schlag Da wir nun die geleakte Adresse der Standard-Cleanup-Funktion (`0x109da4`) haben, müssen wir nur noch den Offset zu `print_flag` (`0x109d74`) berechnen. Die Differenz beträgt `0x30`. Wir nehmen also unsere geleakte Adresse, subtrahieren `0x30` und haben die exakte Adresse der Flag-Funktion. 1. **Die Adresse schreiben:** Nutze den OOB-Schreibzugriff auf `Slot 0` (Eintrag 3), um den Cleanup-Zeiger von `Slot 1` mit unserer berechneten `print_flag`-Adresse zu überschreiben. 2. **Den Sieg auslösen:** Rufe den `destroy`-Befehl für **Slot 1** auf. Anstatt den Speicher freizugeben, wird das Programm bereitwillig zu `print_flag` springen und dir den Preis überreichen! --- ### Zusammenfassende Checkliste - [ ] **Initialisiere Slot 0:** Count `2305843009213693952` (setzt Größe auf 16 zurück). - [ ] **Initialisiere Slot 1:** Count `1`. - [ ] **Lies Slot 0, Eintrag 3:** Dies leakt die `default_destroy`-Adresse. - [ ] **Berechne:** `Ziel = geleakte_Adresse - 0x30`. - [ ] **Schreibe Slot 0, Eintrag 3:** Schreibe die `Ziel`-Adresse. - [ ] **Zerstöre Slot 1:** Erfolg! Viel Spaß beim Hacken! Denk daran: Manchmal entstehen die größten Strukturen aus den kleinsten Allokationen.