Ir al contenido principal

Binary Exploitation (XXIII): Buffer overflow (II) - Shellcode injection

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

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