domingo, 22 de noviembre de 2009

Exploting (II): Stack Overflow Simple (I)

La semana pasada pusimos un ejemplo de como podemos aprovechar una vulnerabilidad de Buffer Overflow para saltarnos un proceso de autenticación sin tener que acudir a sobreescribir la dirección de retorno en la pila. El de la semana pasada es probablemente uno de los casos más sencillos de Buffer Overflow, ya que es bastante inmediato ver que si podemos desbordar el buffer podemos tener control de todas las variables que se hayan declarado en las siguientes posiciones de memoria.

Esta semana vamos a dar un paso más allá y vamos a irnos al caso del Stack Overflow típico, es decir, hemos hecho un pequeño programa de ejemplo que contiene una vulnerabilidad de Buffer Overflow que puede ser aprovechada para ejecutar código en la máquina, y esto es lo que vamos a hacer.

El código de ejemplo, aunque se llama desde dentro de un código más amplio para favorecer un tamaño de pila adecuado para no complicar más las cosas, es el siguiente:



Como podemos ver, la variable buffer tiene una longitud de 10 caracteres (osea, 10 bytes). Sin embargo, se está obteniendo por teclado una cadena que está siendo guardada en esta variable sin ningún tipo de control de la cantidad de caracteres que se van a escribir. Por lo tanto, aquí tenemos una vulnerabilidad de Buffer Overflow de libro.

En este caso tenemos el código y podemos ver que la vulnerabilidad existe con total seguridad, pero en otras ocasiones no tendremos tanta suerte. En cualquier caso, vamos a olvidarnos que disponemos del código y vamos a realizar una prueba. Para ello llamamos al binario y cuando nos solicite que escribamos vamos a intruducir una cadena muy muy larga de A's, para comprobar si podemos realizar un Buffer Overflow que sobreescriba la dirección de retorno almacenada en la pila y provocar un casque (es lo que ocurre si hacemos saltar la ejecución del código a una dirección inválida).

Primero que nada, lanzamos el binario a través de OllyDbg y lo ejecutamos sin poner ningún tipo de breakpoint ni nada similar. Si sobreescribimos correctamente la dirección de retorno que está almacenada en la pila, cuando la función termine, la dirección de retorno va a ser "basura", por lo que la ejecución del software va a saltar a una dirección inválida. En OllyDbg, veremos algo así:



Como podemos ver, la ventana en la que veriamos las intrucciones de código aparece en blanco, y la ejecución del proceso se ha detenido con un EIP (puntero de instrucción, que apunta a la instrucción que se está ejecutando) claramente no válido "AAAA" (0x41 = "A").

Ahora que ya sabemos como reconocer cuando se ha producido un Buffer Overflow, vamos a lanzar la aplicación vulnerable a través de OllyDbg y vamos a dejar que se ejecute sin ninguna restricción. Cuando nos ofrezca la posibilidad de escribir, vamos a escribir una cantidad muy grande de A's, obteniendo el siguiente resultado:



Si nos fijamos en la información resaltada en la imagen, el EIP ahora es 0x41414141, es decir, la dirección de retorno de la función vulnerable que estaba almacenada en la pila ha sido sobreescrita con A's. Genial! Eso quiere decir que podemos empezar a jugar...

Vale, ya sabemos que podemos sobreescribir la dirección de retorno, pero nos interesa saber exactamente que A's están sobreescribiendo esa dirección de retorno, para así poder hacer saltar la ejecución a donde nosotros queramos. ¿Cómo hacemos eso? Pues está claro que poniendo todos los caracteres iguales seguro que no.

Para hacerlo, lo mejor es tener primero una aproximación de con cuantos caracteres sobreescribimos la dirección de retorno. Si hemos probado con 10000 A's, podemos probar ahora con 5000, si sigue funcionando entonces con 2500, luego con 1250, hasta que lleguemos a un punto suficientemente acotado (un rango de 2000 o 3000 es suficiente) como para poder utilizar la siguiente técnica que vamos a ver.

En este caso, la cantidad de A's es entre 50 y 100, así que lo que vamos a hacer es escribir 50 A's y a continuación 50 caracteres diferentes, que nos permitan reconocer al ver 4 de ellos (estarán en el EIP) en que posición están exactamente. Para generar estos caracteres utilizaremos una de las herramientas de la tremenda y fantastica Metasploit (miooo, mi tesoooroooo): pattern_create.rb

# cd /opt/msf3
# cd tools
# ./pattern_create.rb 50
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab


Con esto, ya solo tenemos que concatenar 50 A's con esta secuencia y repetir el proceso. Como supongo que no quereis tener que escribir cada vez a mano 50 A's, lo mejor es hacerse un pequeño script. Yo por ejemplo me lo he hecho en perl:

#!/usr/bin/perl
# EXPLOIT DE PRUEBAS - jselvi

# PARTES DEL EXPLOIT
my $relleno = "A" x 50;
my $msfpatron = "
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab";

# CONSTRUIMOS EXPLOIT
my $exploit = $relleno.$msfpatron;

# LANZAMOS EXPLOIT
my $file = "exploit.txt";
open( $FILE, ">$file" );
print $FILE $exploit;
close( $FILE );

print "Fichero exploit.txt creado\n";


Con este script podemos crear un fichero de texto que contenga el exploit y utilizarlo (de momento, como no hay caracteres "raros", podemos simplemente copiar y pegar). Si lanzamos de nuevo con OllyDbg igual que lo hicimos antes e introducimos ahora esta cadena, el resultado será el siguiente:



Como podemos ver, ahora el EIP ha sido sobreescrito por 0x61413361, que por ser el procesador un Intel (Little Endian), quiere decir que hemos sobreescrito el EIP con {0x61, 0x33, 0x41, 0x61}, es decir, con el patron "a3Aa".

Ahora que ya tenemos el patrón, sólo nos tenemos que ir de nuevo a Metasploit y utilizar la herramienta complementaria a la de antes: pattern_offset.rb

# ./pattern_offset.rb a3Aa 50
10


Así que tenemos 50 A's que hemos introducido a mano nosotros más 10 A's más que podemos seguir metiendo hasta alcanzar la dirección de retorno en memoria, y después de eso ya podemos introducir lo que queramos que sobreescriba la dirección de retorno. Hagamos una última prueba, vamos a escribir 60 A's seguidas de "BBBB" seguido de un patron "CCC1CCC2CCC3CCC4". Si todo va bien, el EIP acabará siendo BBBB y podremos ver en la memoria la cadena "CCC1CCC2...". Podemos generar todo el exploit modificando un poco el código perl de antes:

# PARTES DEL EXPLOIT
my $relleno = "A" x 60;
my $eip = "BBBB";
my $espacio = "CCC1CCC2CCC3CCC4";

# CONSTRUIMOS EXPLOIT
my $exploit = $relleno.$eip.$espacio;


Vamos a ver lo que pasa:



Parece que la cosa va MUY bien, ya sabemos cuantos bytes tenemos que escribir para sobreescribir la dirección de retorno, con lo que tenemos el control del EIP, y además podemos observar que la cadena "CCC1CCC2..." se encuentra apuntada por el registro ESP (puntero a la cima de la pila) después de salir de la función que hemos explicado. Lógico, pero interesante para nuestros propósitos, verdad?.

Lo tenemos chupado entonces, ¿no? Sólo tenemos que poner nuestro shellcode donde hemos puesto "CCC1CCC2..." y en el EIP de retorno almacenado en la pila sobreescribirlo con la dirección 0x0022C0D0, así cuando se salga de la función se recuperará este como EIP y saltará a ejecutar el shellcode que le pongamos.

En teoría, esto podría ser así, pero... no va a funcionar.
¿A alguien se le ocurre por qué?

19 comentarios :

Thor dijo...

Cuidado lo has declarado como
int buffer[10]; (40 bytes)
supongo que querrías poner
char buffer[10];

No se me ocurre porque puede fallar, todo parece que va viento en popa.

Esperare impaciente la siguiente entrega :)

byteinsanity dijo...

No funcionara ya que no nos dejara ejecutar código en el espacio de la pila.

Jose Selvi dijo...

Ooops! sí, vaya metida de pata al hacer el código de ejemplo :P

Si, efectivamente, debería ser un:
char buffer[10];

De todas maneras, para el caso de un poco igual, es como si fuera un:
char buffer[40];

En cuanto lo que dice @byteinsanity ,supongo que te refieres al DEP (Data Execution Prevention), la protección de los sistemas Windows. DEP viene por defecto con la opción "OptIn", por razones de compatibilidad, que solo protege a los binarios del sistema operativo, es decir, cualquier servicio externo a este (como es este programita de ejemplo) no está protegido por DEP.

Otros servicios como el SMB sí que estaría protegido.

Xavier Sala dijo...

Aunque te estés centrando en Windows es fácil probar tus ejemplos en Linux. (igual me meto donde no me llaman, pero quizás puede interesar a alguien..)

Sólo hay que:

1) Se desactiva la protección ASLR que viene por defecto en los núcleos 2.6


# sysctl -w kernel.randomize_va_space=0


2) Se compila el programa sin la protección de desbordamiento de pila. En la mayoría de Linux actuales gcc está configurado por defecto para impedir la sobre escritura:


$ gcc -o bofex1 bofex1.c
$ ruby -e 'print "A"*50' | ./bofex1
*** stack smashing detected ***: bofex1 terminated


Pero no lo hace si lo desactivamos:


$ gcc -o bofex1 bofex1.c -fno-stack-protector
$ ruby -e 'print "A"*50' | ./bofex1
Segmentation fault (Core dumped)


Podemos ver que se ha sobrescrito la dirección de retorno inspeccionando el core


$ gdb ./bofex1 core
...
(gdb) info register eip
eip 0x41414141 0x41414141


La putada de Linux para estas cosas es la inexistencia de Olly ...

Jose Selvi dijo...

Muy buen apunte @Xavier!

El código del ejemplo es C estandar, así que en principio debería compilar en cualquier parte, incluido en un Linux. Y la vulnerabilidad sigue existiendo, por supuesto.

La única diferencia, como tú muestras, son las protecciones que ofrece cada sistema operativo, que son completamente diferentes.

El caso de Windows yo creo que es el más sencillo, ya que un binario que no pertenece al sistema operativo... tiene más bien pocas protecciones a priori. Entre eso y que muchos administradores se olvidan del software no-Microsoft y no lo actualizan...

En Linux las protecciones son otras (ASLR/Canaries), como tú comentas.

Seguramente más adelante hagamos algo parecido a lo que estamos haciendo con exploiting para Windows pero para Linux.

Gracias por el comentario!

Adrián dijo...

No sé si los tiros van por aquí, porque he de reconocer que estoy muy lejos de manejarme bien en el mundo windows (yo vengo de los UNIX y de Linux en particular). Dicho esto, ¿no existía una protección en Windows que comprobaba si la dirección estaba comprendida dentro del espacio de una lista de módulos cargados? Si no caía dentro se ejecutaba. Si caía dentro se comprobaba contra una lista de handlers.
El truco aquí era encontrar una instrucción de call/jmp que nos permitiera retornar a nuestro código, pero no recuerdo qué formato tiene que tener ni por qué.

Igual me he subido a la parra mucho :P

Maria dijo...

Genial la explicación, pero no te confíes, seguro que cuando la examine con detalle me salen mil dudas.

Jose Selvi dijo...

Yo también vengo más del mundo Unix/Linux, pero en un momento dado decidí que tenía que ponerme al día con Windows, queramos o no está muy muy extendido.

@Adrián, no sé muy bien a que protección te refieres, o no te he entendido bien o sencillamente la desconozco. Te acercas mucho a la solución buena, pero el motivo es otro.

@María, gracias por los alagos :) Si te surgen dudas cuando lo mires en profundidad no hay problema, aquí estamos para responderlas.

Estoy pensando que lo que quizá ha faltado es algún post previo de como funciona la pila y las llamadas a funciones. Si alguien lo hecha de menos que lo diga y haremos una pequeña "marcha a atrás" (niños, no hagais esto en casa) y lo explicaremos un poco.

Gracias por los comentarios.
Saludos!

Adrián dijo...

Venga, que lo intento otra vez. Asumiendo que windows tenga una guarda en la pila justo antes de la dirección de retorno, dicha guarda sería sobreescrita antes de poder machacar el ebp(retorno) de la función.
En este caso, antes de retornar, el mecanismo compararía el valor de la guarda almacenado en la pila con el que se encuentre en .data. Si son diferentes, se llamará a algún handler que hará las operaciones oportunas antes de cerrar el proceso y chafarnos el juego.
La solución (aunque sigo sin saberlo de forma exacta): localizar en dichas rutinas de excepción instrucciones de jmp/call que puedan sernos útiles.

¿Vuelvo a errar?

Thor dijo...

Te diría que es por el 00 de la dirección de retorno que queremos escribir:
0x0022C0D0

Ya que al leer una cadena se va a a leer hasta el primero 00 y no podemos escribir más después de encontrar dicho carácter en la entrada. Pero coincide con que ese 00 es lo último que queremos escribir en la shellcode. No será eso...
Alguna vez he visto que se busca una instrucción RET o JMP ESP en kernel32.dll para sobrescribir el RET con ese valor, pero no me acuerdo y ni se me ocurre cual puede ser la razón.

Estamos impacientes, se nota, ¿no? :D

Jose Selvi dijo...

@Thor, BINGO! Eso es exactamente lo que va a pasar, al escribir 0x00 (lo mismo que \0, es decir, el caracter final de cadena), la función scanf parara de copiar, lo que aunque se escriba correctamente la dirección de retorno, a continuación no se va a escribir el "CCC1CCC2..." (lo que en el siguiente paso sería nuestro shellcode) porque la copia finalizará en ese punto.

Que putada! ¿Cómo lo hacemos entonces? ¿Ideas?

Las ideas que llevais son buenas, por ahí anda la cosa ;)

Thor dijo...

Aaamigo, había olvidado que solo había 50 bytes para escribir la shellcode jeje, si hubieran sido 500 bytes directamente se podría haber escrito la shellcode primero y después sobrescribir el RET. Pero puesto que solo hay 50 bytes (46 en realidad) para escribir la shellcode y no sabemos hacer shellcodes tan pequeñas tenemos que sobreescribir el RET con una dirección que no contenga un 00, y para eso buscamos en kernel32.dll direcciones sin 00 donde haya una instrucción JMP ESP o RET. Claro que habrá que tener mucho cuidado de que en el tope de la pila tengamos la dirección de la shellcode. Menudo jaleo.

Espero la siguiente entrega para ver como se resuelven estos problemas jeje.
Un saludo!

Anónimo dijo...

Buscamos "jmp esp" en una libreria .dll con una direccion que no contenga bytes nulos.

Keyser Soze

Jose Selvi dijo...

Exacto! Tendremos que encontrar en alguna dirección que no contenga caracteres "malos" como el \0 en la que tengamos código que nos produzca el salto a nuestro shellcode.

"JMP ESP" ó "PUSH ESP" seguido de "RET" podrían ser buenas opciones.

Lo veremos en el post de la semana que viene :)

Gracias a todos por vuestros comentarios! Así da gusto! ;)

Anónimo dijo...

A+

Jose Selvi dijo...

A+? es una nota? :)

Anónimo dijo...

Hola, tengo una duda, si quiero introducir una direccion a la cual quiero que salte, no puedo porque hay valores ascii que no se pueden representar, mi pregunta es ¿Como puedo inyectar una shellcode en ese programa?, he visto otro tipos de ejemplos en los cuales usan int main (int argc, char **argv) y asi puedo desde otro programa pasar el shellcode por parametro, pero en este programa como aria yo?

Jose Selvi dijo...

Hola @Anónimo, escribes en un post de hace bastante bastante tiempo xD

Depende del servicio que estés explicando, puede que el que los caracteres sean representables o no no sea un problema, siempre y cuando no tengas que utilizar ningún badchar (que también puede ser diferente depende de la vulnerabilidad).

¿Estás seguro que ese es el problema? Una cosa es que tú no los puedas ver en la salida (no son representables) y otra cosa es que el programa no los esté leyendo.

Puedes encontrar mi correo en el blog, escríbeme y lo vemos.

Anónimo dijo...

Si Xd mucho tiempo... jaja. no se donde ver tu correo :/ .
lo que quiero saber es como hacer un programa que pueda inyectar la shellcode en este programa vulnerable.