203 lines
8.7 KiB
Markdown
203 lines
8.7 KiB
Markdown
# 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.
|