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:
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:
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.
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 elropchain
sea un poco más sencillo - En 64 bits no debemos llamar a
int 0x80
sino a la instrucciónsyscall
Este es el exploit:
Y el flag picoCTF{rOp_t0_b1n_sH_w1tH_n3w_g4dg3t5_11cdd436}