domingo, 11 de septiembre de 2016

Nuestro primer Buffer Overflow.


Siempre tuve ganas de hacer un paper relacionado a buffer overflow.
Creo que es un tema bastante hablado en lo que respecta a la explotación de binarios, pero es un antes y un después en este mundo de búsqueda de vulnerabilidades. O al menos lo fue para mi hace muchos años atrás.

Quiero escribir un paper sencillo, para los que inician y que sea compatible con las ultimas distros de Linux, ojala sirva de partida o impulso a seguir progresando.













# REQUISITOS PREVIOS:

Los únicos requisitos son saber un básico de assembler, gcc y conocimientos de C.



CONTENIDO:
  1. [Segmentacion de la memoria].......................................
  2. [Registros assembler de uso comun]..............................
  3. [Manejo de memoria en una llamada a función]...............
  4. [Ahora si.. ¿Que es un Buffer Overflow?].........................
  5. [Nuestro codigo C vulnerable]........................................
  6. [Desensamblando con GDB]...........................................
  7. [Creando el payload y desbordando el buffer].................



[Segmentacion de la memoria] de un programa en C:

Cuando compilamos un programa con su respectivo código fuente, por ejemplo en C, la memoria se divide en 5 partes fundamentales como muestra la siguiente imagen:






























#Command-Line arguments and environment variables // Argumentos de Linea de Comandos y variables de Entornos:
Acá es donde se almacenan los argumentos que le pasamos al binario al momento de correrlo, y también las variables de entornos. Por ejemplo:


  • $> ./binario [Argumento1] [Argumento2]


#STACK:
La famosa pila, es el lugar donde se usa como memoria temporal para almacenar las variables de funciones locales y contexto durante las llamadas a funciones.
La pila es una estructura de datos abstractos que se usan con frecuencia.
El método en que los datos se ingresan es en modo LIFO del ingles Last Input, First Output. Que quiere decir Ultimo entrado, Primero salido. Imagínenlo como un mazo de cartas. Al poner una carta encima de la otra, para retirar la que esta debajo, primero tendríamos que sacar la que pusimos en ultimo lugar, y por ultimo sacaríamos la que estaba debajo de todo.

#HEAP:
El heap es un segmento de la memoria que puede ser manejado directamente por el programador, quien puede colocar y usar los bloques de memoria del segmento para lo que quiera. Siempre que se use por ejemplo la funcion "malloc" para obtener memoria dinamicamente sera asignado en el HEAP.

#Uninitialized data o No-Inicializadas.. (BSS SEGMENT):
Es el segmento donde se encuentran las variables estáticas y globales no inicializadas.

#Initialized data o Inicializadas.. (DATA SEGMENT):
En su contrapartida a la anterior, es donde se almacenan todas las variables estáticas y globales inicializadas.

#TEXT:
En este lugar los permisos de escritura suelen estar deshabilitados y no se usa para almacenar variables. Solo código. Posee un tamaño fijo, ya que no hay nada que permita cambiarlo. El loader carga instrucciones desde acá y las ejecuta.


[Registros assembler de uso comun]

Explicare ahora algunos de los registros assembler que ya deberían tener en claro, pero por si las dudas:

EAX,EBX,ECX,EDX:
Estos registros son de propósito general.. suelen usarse para varias cosas pero principalmente como variables temporales para la CPU cuando esta ejecutando instrucciones de maquina.


EIP:
Es el registro de puntero de instrucción. Señala la instrucción que el procesador esta leyendo actualmente.. piénsenlo cuando de chicos leíamos un libro y con nuestro dedo seguíamos cada palabra.

ESP:
Es el registro puntero de STACK. Almacena la dirección de lo alto de la pila.
Como dije anteriormente, imaginemos que es un mazo de cartas, este registro almacena la dirección del ultimo elemento que se añadió al mazo.

EBP:
Es el registro puntero que posee la dirección base de la pila, a contrapartida de ESP que mantenía la dirección de la cima de la pila. EBP contiene el puntero al primer bloque de la memoria del rango de bloques mientras que ESP representa el ultimo bloque de la memoria de una función.

[Manejo de memoria en una llamada a funcion]:

Para entender mejor todo esto, veremos un pequeño codigo y explicare paso a paso que va sucediendo:


  1. int test(int a, int b) {
  2.     int x, z;
  3. }
  4. int main() {
  5.     test(10, 12);  
  6. }

Bien, como vemos, tenemos la funcion MAIN(), y la funcion test(). Mediante main llamamos a la función test() pasandole dos enteros y la funcion test() recibe esos dos valores enteros. Ademas se declaran 2 variables enteras llamadas 'x' y 'z' sin ningun valor especifico.

¿Pero como funciona esto a nivel memoria?.. asi:

EIP se encuentra con la llamada a la funcion test(). Automaticamente se ingresan los datos a la pila, el orden es de derecha a izquierda. Por ende primero se hara un PUSH de 12 y luego de 10:


  • 10
  • 12
  • ----


Una vez hecho eso se necesita saber como seguir después de que la función TEST termine, por ende se pone la direccion de la siguiente instrucción en la pila.

Una vez se realiza esto, se pone la dirección de la función TEST() en el EIP ahora apunta a la función TEST() la cual toma control.

Como ahora nos encontramos en la función TEST(), se requiere actualizar el registro EBP. Pero antes guardamos EBP en la STACK, para una vez terminado el trabajo, sepamos regresar a la función MAIN() sin problemas.

Luego, se setea EBP igual a ESP. Ahora EBP apunta al actual puntero de pila o stack pointer, (ESP viene del ingles Extended STACK POINTER).

A continuación se ponen las variables locales que declaramos "x" y "z" en el espacio reservado en la STACK. El valor de ESP a raíz de esto es modificado.

Una vez que la función TEST() finaliza.. se necesita regresar al marco de pila anterior (o stack frame en ingles). Por lo tanto se setea ESP al EBP con el valor anterior. Se retira EBP con el valor anterior de la STACK y se vuelve a almacenar en EBP. Ahora el EBP apunta de regreso a MAIN(). (recuerdan que arriba lo habíamos almacenado para poder acordarnos como regresar?).

Para finalizar, se quita el RET ADDRESS de la pila que habíamos puesto y EIP ahora apunta a la dirección que contenía el RET, justamente una instruccion despues del CALL a la función TEST().

Algo así quedaría la Memoria mientras se ejecuta la funcion TEST():

-------------------------------------------------------
12
-------------------------------------------------------
10
-------------------------------------------------------
<RET ADDRESS>
-------------------------------------------------------
<EBP de MAIN()>                       --------------------> EBP
-------------------------------------------------------
Lugar asignado para variable "x"
-------------------------------------------------------
Lugar asignado para variable "z" -------------------> ESP
-------------------------------------------------------

Después de esto tedioso.. Empecemos con un poco mas de diversion.


[Ahora si.. ¿Que es un Buffer Overflow?]




















En pocas palabras el Buffer Overflow, desde ahora BOF, ocurre cuando en un programa o proceso se intenta almacenar mas datos en un buffer del que este fue previamente programado. Cuando los buffers (espacios de memoria destinados para cierta cantidad de datos), son sobrecargados, como si fuese un vaso de agua que contiene cierta capacidad para contenerla y cuando esta al máximo, la famosa gota que rebalsa el vaso. En esto es lo mismo, los datos extras que rebalsa de un buffer pueden tener distintos impactos desde corromper una aplicación y detenerla, los bytes sobrantes pueden almacenarse en buffers o zonas de memorias adyacentes.. etc.

Un usuario malintencionado podría guiar un BOF para influir en determinado funcionamiento del sistema, como por ejemplo la escalada de privilegios, etc.

[Nuestro código C vulnerable]:

Pueden descargar el codigo de aca: http://pastebin.com/fYZyHxwj






































Bien, este código es muy básico, pero no menos util. Una breve reseña de lo que hace: Ademas de la función main() posee dos funciones VERIFICAR() y FuncionOBJETIVO(). Una vez que se llama a la función Verificar(), se declara un Buffer de 15 caracteres y se solicita una clave; si la clave es diferente a "passw0rd", dará error. Si es correcta, invoca a la FuncionOBJETIVO().

Nuestra idea claramente no es poner el password correcto, es solo un detalle.
Nuestro verdadero objetivo sera en este ejemplo, modificar el RETurn address y lograr ejecutar como RET la address de FuncionOBJETIVO() sin introducir el password correcto, solo desbordando el buffer.

Una vez tienen el código, hay que compilarlo. La siguiente sintaxis compilara mediante GCC el código, atención al flag -fno-stack-protector. Este flag deshabilita la protección de la pila que viene por default. (Mas adelante se tocaran temas de protecciones, etc.). De momento testeamos de esta manera. Si estas usando un sistema operativo de 64bits, hay que agregarle otro flag -m32. Tambien le agrego el flag -g para que luego el gdb me muestre el codigo fuente mientras lo manipulamos.





Una vez compilado, procedemos a ejecutarlo:















Como vemos, ejecutamos el binario ./final, ingresamos un passwd incorrecto. Luego reintentamos con el correcto y nos demuestra que invoca la FuncionOBJETIVO() correctamente, y luego intentamos desbordarlo manualmente con muchos "9".
Nos arroja un Segmentation Fault. Tipico error de crasheo.


[Desensamblando con GDB]:

Es importante aclarar antes que las direcciones de memoria podrían variar levemente en sus maquinas y no ser iguales a las mías.

Vamos a correr nuestro binario en GDB (GNU Debugger) y desensamblamos la función MAIN():

















No hay mucho por ver, ya que solo se limita a llamar a la funcion Verificar().

Ahora desensamblamos la función VERIFICAR() y resalto lo mas importante:







































En la tercer linea vemos como se reserva un espacio de 0x28 (en hexadecimal), que pasado a Decimal es: 40. En este lugar es donde se reservan las variables locales de la función. En la siguiente linea con mas claridad vemos:




Esta linea nos indica donde es que empieza buffer justo 0x1B (1B en hexadecimal; y decimal es: 27) bytes antes a EBP.

Bien, por ultimo nos queda desensamblar la FuncionOBJETIVO():


La FuncionOBJETIVO() empieza en la direccion 080484cb.


[Creando el payload y desbordando el buffer]:

Vamos a reunir lo que comentamos arriba y diseñar el payload, este es el que se va a encargar de desbordar el buffer y hacernos llegar a esa FuncionOBJETIVO(). sin necesidad de poner el password, aprovechándonos de un RET.

Como dije antes, nuestro buffer comienza a 27 bytes antes del EBP. Esto quiere decir que tenemos 27 bytes y luego vienen 4 bytes mas de EBP. El Base Pointer. Luego de eso como habiamos visto al principio, tendríamos que encontrarnos con el RET ADDRESS, que es la direccion de retorno, una vez que la función finalice EIP usara esa direccion para regresar. 

Y ese es el punto en el que vamos a desbordar y abusar del RET para que nos lleve a la FuncionOBJETIVO(). Sabemos que la direccion de nuestra FuncionOBJETIVO(), comienza en "080484cb"..

La cosa estaría mas o menos así:


27 bytes+4bytes = 31bytes tenemos de caracteres random. 
Los 4 bytes restantes serán de la direccion de la FuncionOBJETIVO() en el RET.

Bien haremos lo siguiente, vamos a invocar python para aplicar nuestro payload sobre el binario. Ahora suponiendo que sus maquinas utilicen Little-Endian, cosa que asi es en mi caso (mas info en wikipedia), tenemos que ingresar los bytes del siguiente modo a la inversa para que funcione, si la direccion era "08 04 84 CB" tenemos que aplicarlo al revés... "CB 84 04 08".

Invocamos python del siguiente modo:

Se invoca python, y se envían al binario 31 bytes (letras "A"), y se le suman 4 bytes finales que son la direccion donde empieza la FuncionOBJETIVO(). Mediante el pipe se le pasa al binario y logramos obtener el mensaje de "Buffer Overflow con éxito".

Para finalizar, decir que hay varias funciones vulnerables a BOF: printf(), strcpy()gets()scanf()sprintf(),strcat().. etc

Espero que como introducción haya servido, cualquier duda ya saben, me escriben.

Saludos.



No hay comentarios:

Publicar un comentario

Deja un comentario..