En esta segunda entrada de la miniserie en la que comparto lo que voy aprendiendo sobre ciertas vulnerabilidades software y técnicas para explotarlas me centraré, con relación a la vulnerabilidad de desbordamiento de 'buffer' (en ingles, 'buffer overflow'), en la técnica de inyección de 'shellcode' (en inglés, 'shellcode injection').
Los conceptos básicos sobre: la vulnerabilidad 'buffer overflow', la pila (en inglés, 'stack') y los registros los traté en el post anterior de esta miniserie.
¿En qué consiste la técnica 'shellcode injection'?:
Con relación a la vulnerabilidad 'buffer overflow', la técnica de 'shellcode injection' consiste en provocar el desbordamiento del 'buffer' para sobrescribir la dirección de retorno de una función con la dirección de inicio de un código malicioso ('shellcode') que el atacante inyecta en memoria con objeto de que el programa ejecute lo que éste desee, habitualmente abrir una 'shell' (la ejecución del intérprete de comandos) en el sistema atacado para permitir que el atacante tome control del mismo.
Para evitar esta vulnerabilidad, el bit de no ejecución, o bit NX, debe estar habilitado para el código ejecutable del programa. Esta medida de seguridad garantiza que ciertas áreas de memoria, como la pila, no sean ejecutables, y otras, como la sección del código, no puedan ser escritas.
Ejemplo:
Para poner un ejemplo 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_03).
0.- Herramientas utilizadas:
- Comando file: identifica o reconoce el tipo de un fichero.
- checksec: utilidad para conocer los mecanismos de seguridad de los que dispone un programa ejecutable, entre otros, si el bit de no ejecución, o bit NX, está habilitado o deshabilitado.
- 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 'checksec' verifico las medidas de seguridad del binario. El bit NX está deshabilitado, por lo que la pila es ejecutable:
2.- Código: decompilo el fichero ejecutable utilizando 'Ghidra' y veo que en main se utiliza fgets en lugar de gets lo que, en principio, evitaría un desbordamiento de ‘buffer’:
Sin embargo, también veo que
main llama a
la función vuln, que
es donde radica la vulnerabilidad, ya que se utiliza gets; como dije en el
post anterior, 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 112 bytes (0x70) y que, justo antes de la instrucción
gets, cuando se ejecuta el binario, se muestra (
printf) la dirección de inicio del
‘buffer’ (esto último es importante para resolver el reto que sirve de base para poner este ejemplo, ya que la medida de seguridad PIE está habilitada, protección de la que se hablará en posts posteriores de esta miniserie, y, por tanto, la dirección de inicio del
'buffer' es diferente en cada ejecución del programa, pero esto no es objeto de este ejemplo, con el que únicamente se persigue tanto profundizar algo más en los conceptos básicos como explicar brevemente la vulnerabilidad
'shellcode injection'. No obstante, quien quiera ver la solución al reto puede hacerlo en
este post).
La llamada a vuln 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 vuln), y después lo modifica para que apunte a la primera instrucción del código de la función vuln.
Una vez realizada la llamada a vuln 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.
Además, conforme a la nomenclatura que utiliza 'Ghidra' para nombrar a las variables locales, la dirección de retorno de la función vuln se encontraría con un desplazamiento en la pila de 0x78 bytes (120) con respecto al inicio de la variable local_78 (el 'buffer' donde se incluye la entrada del usuario):
3.- El programa desensamblado:
Desensamblo la función vuln 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" 0x70 bytes (112) para la variable local
local_78 (el
'buffer' donde se incluye la entrada del usuario). 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 llama tanto a
printf, para mostrar la dirección de inicio del
'buffer', y a
gets para introducir en él la entrada del usuario:
4.- Contenido de la pila:
Antes de incluir en el 'buffer' el 'shellcode' para obtener la 'shell' y de modificar la dirección de retorno de vuln para que se bifurque a éste y, por tanto, se ejecute, comentar que en el ejemplo se filtra en tiempo de ejecución la dirección de inicio del 'buffer' en la que hay que ubicar el 'shellcode', pero en el caso de que no se sepa exactamente la dirección donde está el código a ejecutar, se suele introducir antes o después del mismo lo que se conoce como tobogán de NOP's (en inglés, 'NOP sled'), que no es más que una sucesión de instrucciones NOP ('No Operation') que no hacen nada, cada una de ellas - 0x90 en 'assembler' - ocupa un byte y consume un ciclo máquina -, y después simplemente se ejecuta la siguiente instrucción situada de forma consecutiva a la misma. Su misión consiste en "deslizar" el flujo de ejecución hasta que el procesador encuentre el código de 'shell' , caso de que se introduzca antes, o hasta la dirección de retorno modificada, caso de que se incluya después.
Dicho lo cual, pongo un punto de ruptura en la instrucción nop (vuln+49) y ejecuto el programa. En primer lugar introduzco un carácter "A", y cuando el binario se detiene en el punto de ruptura incluyo: 48 caracteres "B" (supongamos que esta cadena es el 'shellcode' para obtener la 'shell'), 72 caracteres "C" (Supongamos que son los caracteres correspondientes al tobogán de NOP's. 120 bytes de desplazamiento desde el inicio del 'buffer' hasta la dirección de retorno de la función vuln - 48 bytes de tamaño del 'shellcode' a incluir = 72), y ocho caracteres "D" (supongamos que es la dirección de inicio del 'buffer' que se filtra por el binario al ejecutarse), y después examino el contenido del 'stack frame' de vuln:
Como se observa en la imagen anterior, en el principio del
'buffer' se ha incluido lo que sería el
'shellcode' que abriría la
'shell' al ejecutarse, inmediatamente después, en lo que queda de
'buffer' más en el contenido almacenado del registro
rbp, lo que sería el
'NOP sled', y, finalmente, se ha sobrescrito la dirección de retorno de la función
vuln con la de inicio del
'buffer', con lo que el flujo del programa bifurcaría al
'shellcode', se ejecutaría éste, se abriría la
'shell' y el atacante tomaría el control del sistema.
Hasta aquí este segundo post de esta miniserie cuyos objetivos han sido profundizar un poco más en los conceptos básicos y explicar brevemente la técnica 'shellcode injection'; lo que, como siempre, he intentado hacer de la forma más comprensible de la que he sido capaz y cometiendo el menor número de errores posible.
Comentarios
Publicar un comentario