Ir al contenido principal

Binary Exploitation (XXII): Buffer overflow (I)

Comienzo con éste una serie de posts en los que compartiré lo que voy aprendiendo sobre ciertas vulnerabilidades software y técnicas para explotarlas.

En esta primera entrada de la miniserie me centraré en la vulnerabilidad de desbordamiento de 'buffer' (en ingles, 'buffer overflow').

En primer lugar, un poco de teoría para recodar los conceptos fundamentales.

¿Qué es la vulnerabilidad 'buffer overflow'?:

La vulnerabilidad 'buffer overflow' se produce cuando el tamaño de los datos que un usuario incluye por medio de un programa excede la cantidad de memoria asignada a la entrada, escribiéndose parte de los datos introducidos en posiciones de memoria adyacentes.

Para evitar esta vulnerabilidad, los programas deben establecer un tamaño máximo para los datos de entrada y garantizar que no se supere éste.

Cierta información de un programa en ejecución se almacena temporalmente de forma contigua en la memoria, en una zona llamada pila. La entrada de los datos que realiza el usuario se incluye en una zona de la pila llamada 'buffer'; si el tamaño de los datos que introduce el usuario supera el asignado al 'buffer' entonces se sobrescribirán datos adyacentes a éste (valores de variables de una función, la dirección de retorno de la misma, etc.) pudiendo un atacante, entre otras cosas, modificar los valores de variables, tomar el control del flujo del programa para que éste ejecute el código que él desee, abrir una 'shell' (la ejecución del intérprete de comandos) en el sistema atacado para permitir que el atacante tome control del mismo, etc.

¿Qué es la pila?:

La pila (en inglés, 'stack') es un área de memoria que se utiliza para almacenar ("apilar") de manera temporal los parámetros de las funciones, sus variables locales y sus direcciones de retorno, y también algunos valores de registros.

Por cada función que se ejecuta en un programa existe en la pila su correspondiente marco o contexto (en inglés, 'stack frame') que contiene de forma diferenciada del resto de funciones todo lo que se almacena en la pila debido a su ejecución.

La pila responde a una estructura de datos LIFO ('last in - first out'. En español, último en entrar - primero en salir) y las instrucciones de ensamblador (en inglés, 'assembler') que operan sobre la misma son push ("apila" elementos en la pila) y pop ("desapila" elementos de la pila).

La pila crece de las posiciones más altas de memoria a las más bajas, mientras que la escritura de datos en la misma se realiza en sentido inverso, es decir, desde las posiciones más bajas de memoria a las más altas.

¿Qué son los registros?:

En la arquitectura de 32 bits el procesador dispone de 8 registros de propósito general de 32 bits (4 bytes) cada uno de ellos: eax, ebx, ecx, edx, esi, edi, ebp y esp, más, entre otros, otro registro también de 32 bits (4 bytes): eipmientras que en la de 64 bits dispone de 16 registros de propósito general de 64 bits (8 bytes) cada uno de ellos: rax, rbx, rcx, rdx, rsi, rdi, rbp, rsp y r8-r15, más, entre otros, otro registro también de 64 bits (8 bytes): rip.

Como se observa los registros indicados para la arquitectura de 32 bits se mantienen en la de 64 bits, cambiando en su denominación la primera letra "e" (del inglés, 'extended') por la "r".

A partir de este momento las explicaciones dadas en los posts de esta miniserie deben entenderse referidas a la arquitectura de 64 bits, haciéndose referencia expresa a la de 32 bits cuando haya diferencias sustanciales respecto a la primera.

Algunos de los registros indicados tienen una función especial:

- Registro rsp ('Stack Pointer' register): registro "Puntero de Pila"; apunta siempre a la cima de la pila (la última dirección de memoria ocupada por ésta), es decir, al último elemento almacenado en ella.

Cuando se "apila" un nuevo dato en la pila con push el dato almacenado en el registro en la instrucción se incluye en la pila y el valor de rsp se actualiza para que apunte a éste en la pila (nueva cima de la pila).

Cuando se "desapila" un elemento con pop el dato apuntado por rsp se almacena en el registro indicado en la instrucción y el valor de rsp se actualiza para que apunte a la nueva cima de la pila.

- Registro rbp ('Base Pointer' register): registro "Puntero Base"; en el ‘stack frame’ de cada una de las funciones, apunta a la dirección de la pila que da una posición relativa con respecto, por una parte, a la dirección de retorno de la función y a los valores de los parámetros que ha recibido ésta y, por otra, a las variables locales de la función. Es decir, apunta a una ubicación fija dentro del marco de una función para que las referencias al contenido del ‘stack frame’ de la misma puedan realizarse siempre mediante un desplazamiento a partir de la dirección en él contenida.

- Registro rip ('Instruction Pointer' register): registro "Puntero instrucción"; apunta a la dirección de la siguiente instrucción a ejecutar.

Ejemplo:

Para poner un ejemplo de todo lo explicado hasta el momento voy a utilizar uno de los retos de un CTF del que he puesto la solución en este blog.

En este reto se proporciona un archivo (chall_00).

0.- Herramientas utilizadas:

- Comando file: identifica o reconoce el tipo de un fichero.

- Comando readelf: visualiza la información correspondiente a ficheros ELF ('Executable and Linkable Format').

- Ghidra:  herramienta de ingeniería inversa desarrollada por la Agencia de Seguridad Nacional ('NSA', por sus siglas en inglés).

- GDB ('GNU Debugger'): desensamblador y depurador (en inglés, 'debugger') por línea de comandos que, tal y como nos cuenta wikipedia, es el depurador estándar para el compilador GNU. Se puede utilizar en varias plataformas Unix y funciona, entre otros, para varios lenguajes de programación: C, C++, etc., y permite ejecutar un programa con “puntos de ruptura” (en inglés, 'breakpoints') para realizar análisis dinámico de un binario (ver los contenidos de la memoria y de los registros del procesador en cualquier momento de la ejecución, seguir y/o modificar el flujo de ejecución, ...).

1.- Identificación del tipo de fichero y recopilación de información sobre el binario:

Utilizo el comando 'file' para reconocer el tipo de fichero y, entre otras características del binario, veo que se trata de un archivo ejecutable en formato ELF de 64 bits:

Con el comando 'readelf' se puede ver información adicional de un archivo 'elf', por ejemplo mediante la opción -h ('--file header') que muestra la información contenida en su cabecera:

Tal y como se puede observar en la imagen anterior, entre otras características del binario, puedo ver  que la dirección de entrada del programa es '0x5c0'.

Ahora, utilizo 'gdb' para ver la información correspondiente a las funciones, y veo que la dirección de entrada del programa se corresponde con la de inicio de la función _start:

Además, también puedo ver que la dirección de inicio de main es '0x6ca'.

La llamada a main almacena en la pila el valor actual del registro rip, que apunta a la siguiente instrucción a ejecutar (la dirección de retorno de main), y después lo modifica para que apunte a la primera instrucción del código de la función main.

Una vez realizada la llamada a main se "apila" el valor actual del registro rbp en la siguiente posición disponible de la pila y se modifica su valor para que apunte a esa posición de la pila.
2.- Código: decompilo el fichero ejecutable utilizando 'Ghidra' y veo que main es donde radica la vulnerabilidad, ya que se utiliza gets, instrucción bien conocida por hacer que un binario sea susceptible de un ataque de desbordamiento de 'buffer', debido a que no controla de ninguna manera el tamaño de la entrada que introduce el usuario respecto al tamaño del 'buffer' donde se va a almacenar:
Veo también que el ‘buffer’ tiene un tamaño de 56 bytes (0x38) y que si el valor de local_c o de local_10, variables locales de main, es igual a ‘0xfacade’ se realiza una llamada a system y se abre una ‘shell’ (esto último no es objeto de este ejemplo, con el que únicamente se persigue explicar los conceptos básicos y poner un ejemplo de los mismos. No obstante, quien quiera ver la solución al reto puede hacerlo en este post).

Además, conforme a la nomenclatura que utiliza 'Ghidra' para nombrar a las variables locales, la dirección de retorno de la función main se encontraría con un desplazamiento en la pila de 0xc bytes (12) con respecto al inicio de la variable local_c:
De 0x10 bytes (16) con respecto al inicio de la variable local_10:
Y de 0x48 bytes (72) con respecto al inicio de la variable local_48 (el 'buffer' donde se incluye la entrada del usuario):

3.- El programa desensamblado:

Desensamblo la función main y veo que, tal y como he comentado antes, la primera instrucción "apila" el valor actual del registro rbp en la siguiente posición disponible de la pila y la segunda modifica su valor para que apunte a esa posición de la pila:

La siguiente instrucción "reserva" 0x40 bytes (64) para la variables locales de main: local_c, local_10 y local_48. Hay que tener en cuenta que para "reservar" bytes en la pila se resta a la dirección contenida en el registro rsp, que apunta a la cima de la pila, ya que pila crece de las posiciones más altas de memoria hacia las más bajas, mientras que para "liberar" espacio en la pila, por el mismo motivo, se suma a la dirección contenida en el registro rsp:
Posteriormente se ve como se incluye en el registro rdi la dirección de inicio de la variable local_48 (el 'buffer' donde el usuario introduce su entrada), rdb - 0x40, se lee la entrada del usuario mediante gets, se compara el contenido de la variable local_c, rdb - 0x4 con '0xfacade' (si es igual de abre una 'shell') y, después, se compara el contenido de la variable local_10, rdb - 0x8 con '0xfacade' (si es igual de abre una 'shell'):
4.- Contenido de la pila:

Pongo un punto de ruptura en la instrucción nop (main+79) y ejecuto el programa. Introduzco como cadena: 56 caracteres "A", 4 caracteres "B", cuatro caracteres "C", ocho caracteres "D" y ocho caracteres "E", y cuando éste se detiene en el punto de ruptura examino el contenido de la pila:
Como se observa, se ha producido el desbordamiento del 'buffer' y se han sobrescrito los datos adyacentes a él en la pila, entre otros, el contenido de las variables locales local_10 y local_c, y el de la dirección de retorno de main.

Por tanto, un atacante podría modificar el contenido de las citadas variables locales con objeto de abrir una 'shell' y tomar el control del sistema atacado, o modificar la dirección de retorno de main para tomar el control del flujo del programa y que éste ejecute la parte de código que desee.

Hasta aquí este primer post de esta miniserie cuyo objetivo ha sido repasar los conceptos fundamentales que nos servirán en las entradas siguientes; lo que he intentado explicar de la forma más comprensible de la que he sido capaz y cometiendo el menor número de errores posible. 

Material consultado:

[1] Guía de auto-estudio para la escritura de exploits.

[2] Hacking Ético (Blog de seguridad de la información). Diciembre de 2015. De camino al Buffer Overflow (I) y De camino al Buffer Overflow (II). Enero de 2016. De camino al Buffer Overflow (III).

Comentarios

Entradas populares de este blog

Criptografía (I): cifrado Vigenère y criptoanálisis Kasiski

Hace unos días mi amigo Iñaki Regidor ( @Inaki_Regidor ), a quien dedico esta entrada :), compartió en las redes sociales un post titulado "Criptografía: el arte de esconder mensajes"  publicado en uno de los blogs de EiTB . En ese post se explican ciertos métodos clásicos para cifrar mensajes , entre ellos el cifrado de Vigenère , y , al final del mismo, se propone un reto consistente en descifrar un mensaje , lo que me ha animado a escribir este post sobre el método Kasiski  para atacar un cifrado polialfabético ( conociendo la clave descifrar el mensaje es muy fácil, pero lo que contaré en este post es la forma de hacerlo sin saberla ). El mensaje a descifrar es el siguiente: LNUDVMUYRMUDVLLPXAFZUEFAIOVWVMUOVMUEVMUEZCUDVSYWCIVCFGUCUNYCGALLGRCYTIJTRNNPJQOPJEMZITYLIAYYKRYEFDUDCAMAVRMZEAMBLEXPJCCQIEHPJTYXVNMLAEZTIMUOFRUFC Como ya he dicho el método de Vigenère es un sistema de sustitución polialfabético , lo que significa que, al contrario que en un sistema de

Criptografía (XXIII): cifrado de Hill (I)

En este post me propongo explicar de forma comprensible lo que he entendido sobre el cifrado de Hill , propuesto por el matemático Lester S. Hill , en 1929, y que se basa en emplear una matriz como clave  para cifrar un texto en claro y su inversa para descifrar el criptograma correspondiente . Hay tres cosas que me gustan de la criptografía clásica, además de que considero que ésta es muy didáctica a la hora de comprender los sistemas criptográficos modernos: la primera de ellas es que me "obliga" a repasar conceptos de matemáticas aprendidos hace mucho tiempo y, desgraciadamente, olvidados también hace demasiado tiempo, y, por consiguiente, que, como dice  Dani , amigo y coautor de este blog, me "obliga" a hacer "gimnasia mental"; la segunda es que, en la mayoría de las ocasiones, pueden cifrarse y descifrase los mensajes, e incluso realizarse el criptoanálisis de los criptogramas, sin más que un simple lápiz y papel, es decir, para mi es como un pasat

¿Qué significa el emblema de la profesión informática? (I)

Todas o muchas profesiones tienen un emblema que las representa simbólicamente y en el caso de la  informática: " es el establecido en la resolución de 11 de noviembre de 1977  para las titulaciones universitarias superiores de informática, y  está constituido por una figura representando en su parte central  un  núcleo toroidal de ferrita , atravesado por  hilos de lectura,  escritura e inhibición . El núcleo está rodeado por  dos ramas : una  de  laurel , como símbolo de recompensa, y la otra, de  olivo , como  símbolo de sabiduría. La  corona  será la  de la casa real  española,  y bajo el escudo se inscribirá el acrónimo de la organización. ". Veamos los diferentes elementos tomando como ejemplo el emblema del COIIE/EIIEO (Colegio Oficial de Ingenieros en Informática del País Vasco/ Euskadiko Informatikako Ingeniarien Elkargo Ofiziala ) . Pero no sólo el COIIE/EIIEO adopta el emblema establecido en dicha resolución, sino que éste se adopta también como im