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): eip, mientras 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'.
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.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:[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
Publicar un comentario