Added writeups
This commit is contained in:
202
de/slot_machine.md
Normal file
202
de/slot_machine.md
Normal file
@@ -0,0 +1,202 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user