Files
HIP7CTF_Writeups/de/slot_machine.md
m0rph3us1987 a79656b647 Added writeups
2026-03-08 12:22:39 +01:00

8.7 KiB
Raw Permalink Blame History

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:

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.

// 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):

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:

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!)
  1. 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.