La programación orientada al retorno ( ROP ) es una técnica de explotación de seguridad informática que permite a un atacante ejecutar código en presencia de defensas de seguridad [1] [2], como la protección del espacio ejecutable y la firma de código . [3]
En esta técnica, un atacante obtiene el control de la pila de llamadas para secuestrar el flujo de control del programa y luego ejecuta secuencias de instrucciones de máquina cuidadosamente seleccionadas que ya están presentes en la memoria de la máquina, llamadas "gadgets". [4] [nb 1] Cada dispositivo generalmente termina en una instrucción de devolución y está ubicado en una subrutina dentro del programa existente y/o código de biblioteca compartida. [nb 1] Encadenados entre sí, estos dispositivos permiten a un atacante realizar operaciones arbitrarias en una máquina empleando defensas que frustran ataques más simples.
DrawLine
ha sido llamada por DrawSquare
. Tenga en cuenta que la pila crece hacia arriba en este diagrama.La programación orientada al retorno es una versión avanzada de un ataque de destrucción de pilas . Generalmente, este tipo de ataques surgen cuando un adversario manipula la pila de llamadas aprovechando un error en el programa, a menudo una saturación del búfer . En una saturación del búfer, una función que no realiza una verificación de límites adecuada antes de almacenar los datos proporcionados por el usuario en la memoria aceptará más datos de entrada de los que puede almacenar correctamente. Si los datos se escriben en la pila, el exceso de datos puede desbordar el espacio asignado a las variables de la función (por ejemplo, "locales" en el diagrama de pila a la derecha) y sobrescribir la dirección de retorno. Esta dirección será utilizada posteriormente por la función para redirigir el flujo de control nuevamente al autor de la llamada . Si se ha sobrescrito, el flujo de control se desviará a la ubicación especificada por la nueva dirección del remitente.
En un ataque de desbordamiento de búfer estándar, el atacante simplemente escribiría el código de ataque (la "carga útil") en la pila y luego sobrescribiría la dirección del remitente con la ubicación de estas instrucciones recién escritas. Hasta finales de los años 1990, los principales sistemas operativos no ofrecían ninguna protección contra estos ataques; Microsoft Windows no proporcionó ninguna protección contra el desbordamiento del búfer hasta 2004. [5] Con el tiempo, los sistemas operativos comenzaron a combatir la explotación de errores de desbordamiento del búfer marcando la memoria donde se escriben los datos como no ejecutables, una técnica conocida como protección del espacio ejecutable . Con esto habilitado, la máquina se negaría a ejecutar cualquier código ubicado en áreas de memoria en las que el usuario pueda escribir, evitando que el atacante coloque la carga útil en la pila y salte a ella mediante una sobrescritura de la dirección de retorno. Posteriormente estuvo disponible soporte de hardware para fortalecer esta protección.
Con la prevención de ejecución de datos, un adversario no puede ejecutar directamente instrucciones escritas en un búfer porque la sección de memoria del búfer está marcada como no ejecutable. Para vencer esta protección, un ataque de programación orientada al retorno no inyecta instrucciones maliciosas, sino que utiliza secuencias de instrucciones ya presentes en la memoria ejecutable, llamadas "gadgets", mediante la manipulación de direcciones de retorno. Una implementación típica de prevención de ejecución de datos no puede defenderse contra este ataque porque el adversario no ejecutó directamente el código malicioso, sino que combinó secuencias de instrucciones "buenas" cambiando las direcciones de retorno almacenadas; por lo tanto, el código utilizado se marcaría como ejecutable.
La implementación generalizada de la prevención de ejecución de datos hizo que las vulnerabilidades tradicionales de desbordamiento del búfer fueran difíciles o imposibles de explotar de la manera descrita anteriormente. En cambio, un atacante estaba restringido al código que ya estaba en la memoria marcado como ejecutable, como el código del programa en sí y cualquier biblioteca compartida vinculada . Dado que las bibliotecas compartidas, como libc , a menudo contienen subrutinas para realizar llamadas al sistema y otras funciones potencialmente útiles para un atacante, son las candidatas más probables para encontrar código para montar un ataque.
En un ataque de regreso a la biblioteca, un atacante secuestra el flujo de control del programa explotando una vulnerabilidad de desbordamiento del búfer, exactamente como se analizó anteriormente. En lugar de intentar escribir una carga útil de ataque en la pila, el atacante elige una función de biblioteca disponible y sobrescribe la dirección del remitente con su ubicación de entrada. Luego se sobrescriben otras ubicaciones de la pila, obedeciendo las convenciones de llamada aplicables , para pasar cuidadosamente los parámetros adecuados a la función para que realice una funcionalidad útil para el atacante. Esta técnica fue presentada por primera vez por Solar Designer en 1997, [6] y luego se amplió al encadenamiento ilimitado de llamadas a funciones. [7]
El auge de los procesadores x86 de 64 bits trajo consigo un cambio en la convención de llamada de subrutinas que requería que los primeros argumentos de una función se pasaran en registros en lugar de en la pila. Esto significaba que un atacante ya no podía configurar una llamada a una función de biblioteca con los argumentos deseados simplemente manipulando la pila de llamadas mediante un exploit de desbordamiento del búfer. Los desarrolladores de bibliotecas compartidas también comenzaron a eliminar o restringir funciones de biblioteca que realizaban acciones particularmente útiles para un atacante, como envoltorios de llamadas al sistema . Como resultado, los ataques de regreso a la biblioteca se volvieron mucho más difíciles de realizar con éxito.
La siguiente evolución se produjo en forma de un ataque que utilizaba fragmentos de funciones de la biblioteca, en lugar de funciones enteras en sí, para explotar las vulnerabilidades de desbordamiento del búfer en máquinas con defensas contra ataques más simples. [8] Esta técnica busca funciones que contengan secuencias de instrucciones que extraigan valores de la pila en registros. La selección cuidadosa de estas secuencias de código permite a un atacante colocar valores adecuados en los registros adecuados para realizar una llamada a una función según la nueva convención de llamada. El resto del ataque se desarrolla como un ataque de regreso a la biblioteca.
La programación orientada al retorno se basa en el enfoque de fragmentos de código prestado y lo extiende para proporcionar al atacante una funcionalidad completa de Turing , incluidos bucles y ramas condicionales . [9] [10] Dicho de otra manera, la programación orientada al retorno proporciona un "lenguaje" completamente funcional que un atacante puede utilizar para hacer que una máquina comprometida realice cualquier operación deseada. Hovav Shacham publicó la técnica en 2007 [11] y demostró cómo todas las construcciones de programación importantes se pueden simular utilizando programación orientada al retorno contra una aplicación de destino vinculada con la biblioteca estándar C y que contiene una vulnerabilidad de desbordamiento de búfer explotable.
Un ataque de programación orientado al retorno es superior a los otros tipos de ataque discutidos, tanto en poder expresivo como en resistencia a las medidas defensivas. Ninguna de las técnicas de contraexplotación mencionadas anteriormente, incluida la eliminación total de funciones potencialmente peligrosas de bibliotecas compartidas, es eficaz contra un ataque de programación orientado al retorno.
Aunque los ataques de programación orientados al retorno se pueden realizar en una variedad de arquitecturas, [11] el artículo de Shacham y la mayoría del trabajo de seguimiento se centran en la arquitectura Intel x86 . La arquitectura x86 es un conjunto de instrucciones CISC de longitud variable . La programación orientada a retornos en x86 aprovecha el hecho de que el conjunto de instrucciones es muy "denso", es decir, es probable que cualquier secuencia aleatoria de bytes sea interpretable como algún conjunto válido de instrucciones x86.
Por lo tanto, es posible buscar un código de operación que altere el flujo de control, en particular la instrucción de retorno (0xC3) y luego buscar hacia atrás en el binario los bytes anteriores que formen instrucciones posiblemente útiles. Estos conjuntos de "dispositivos" de instrucciones se pueden encadenar sobrescribiendo la dirección de retorno, mediante un exploit de desbordamiento del búfer, con la dirección de la primera instrucción del primer dispositivo. La primera dirección de los siguientes dispositivos se escribe sucesivamente en la pila. Al finalizar el primer gadget, se ejecutará una instrucción de retorno, que extraerá la dirección del siguiente gadget de la pila y saltará a él. Al finalizar ese gadget, la cadena continúa con el tercero, y así sucesivamente. Al encadenar pequeñas secuencias de instrucciones, un atacante puede producir un comportamiento de programa arbitrario a partir de código de biblioteca preexistente. Shacham afirma que dada una cantidad suficientemente grande de código (incluida, entre otras, la biblioteca estándar C), existirán suficientes dispositivos para una funcionalidad completa de Turing. [11]
Se ha desarrollado una herramienta automatizada para ayudar a automatizar el proceso de localización de dispositivos y construcción de un ataque contra un binario. [12] Esta herramienta, conocida como ROPgadget, busca a través de un binario dispositivos potencialmente útiles e intenta ensamblarlos en una carga útil de ataque que genera un shell para aceptar comandos arbitrarios del atacante.
La aleatorización del diseño del espacio de direcciones también tiene vulnerabilidades. Según el artículo de Shacham et al., [13] el ASLR en arquitecturas de 32 bits está limitado por el número de bits disponibles para la aleatorización de direcciones. Sólo 16 de los 32 bits de dirección están disponibles para la aleatorización, y 16 bits de aleatorización de direcciones pueden ser derrotados mediante un ataque de fuerza bruta en minutos. Las arquitecturas de 64 bits son más robustas, con 40 de los 64 bits disponibles para aleatorización. Es posible un ataque de fuerza bruta para la aleatorización de 40 bits, pero es poco probable que pase desapercibido. [ cita necesaria ] Además de los ataques de fuerza bruta, existen técnicas para eliminar la aleatorización .
Incluso con una aleatorización perfecta, si hay alguna fuga de información sobre el contenido de la memoria, sería útil calcular la dirección base de, por ejemplo, una biblioteca compartida en tiempo de ejecución. [14]
Según el artículo de Checkoway et al., [15] es posible realizar programación orientada al retorno en arquitecturas x86 y ARM sin utilizar una instrucción de retorno (0xC3 en x86). En su lugar, utilizaron secuencias de instrucciones cuidadosamente elaboradas que ya existen en la memoria de la máquina para comportarse como una instrucción de retorno. Una instrucción de retorno tiene dos efectos: en primer lugar, lee el valor de cuatro bytes en la parte superior de la pila y establece el puntero de instrucción en ese valor y, en segundo lugar, aumenta el valor del puntero de la pila en cuatro (equivalente a una operación pop). . En la arquitectura x86, las secuencias de instrucciones jmp y pop pueden actuar como instrucción de retorno. En ARM, las secuencias de instrucciones de carga y bifurcación pueden actuar como instrucciones de retorno.
Dado que este nuevo enfoque no utiliza una instrucción de devolución, tiene implicaciones negativas para la defensa. Cuando un programa de defensa comprueba no sólo varios retornos sino también varias instrucciones de salto, se puede detectar este ataque.
La técnica G-Free fue desarrollada por Kaan Onarlioglu, Leyla Bilge, Andrea Lanzi, Davide Balzarotti y Engin Kirda. Es una solución práctica contra cualquier forma posible de programación orientada al retorno. La solución elimina todas las instrucciones de rama libre no alineadas (instrucciones como RET o CALL que los atacantes pueden usar para cambiar el flujo de control) dentro de un ejecutable binario y protege las instrucciones de rama libre para que no sean utilizadas por un atacante. La forma en que G-Free protege la dirección del remitente es similar al canario XOR implementado por StackGuard. Además, verifica la autenticidad de las llamadas a funciones agregando un bloque de validación. Si no se encuentra el resultado esperado, G-Free provoca que la aplicación falle. [dieciséis]
Se han propuesto varias técnicas para subvertir ataques basados en programación orientada al retorno. [17] La mayoría se basa en la aleatorización de la ubicación del código del programa y de la biblioteca, de modo que un atacante no pueda predecir con precisión la ubicación de las instrucciones que podrían ser útiles en los dispositivos y, por lo tanto, no pueda montar una cadena exitosa de ataque de programación orientada al retorno. Una implementación bastante común de esta técnica, la aleatorización del diseño del espacio de direcciones (ASLR), carga bibliotecas compartidas en una ubicación de memoria diferente en cada carga del programa. Aunque ampliamente implementado por los sistemas operativos modernos, ASLR es vulnerable a ataques de fuga de información y otros enfoques para determinar la dirección de cualquier función de biblioteca conocida en la memoria. Si un atacante puede determinar con éxito la ubicación de una instrucción conocida, se puede inferir la posición de todas las demás y se puede construir un ataque de programación orientado al retorno.
Este enfoque de aleatorización se puede llevar más allá reubicando todas las instrucciones y/u otros estados del programa (registros y objetos de pila) por separado, en lugar de solo las ubicaciones de la biblioteca. [18] [19] [20] Esto requiere un amplio soporte en tiempo de ejecución, como un traductor dinámico de software, para reconstruir las instrucciones aleatorias en tiempo de ejecución. Esta técnica logra hacer que los dispositivos sean difíciles de encontrar y utilizar, pero conlleva importantes gastos generales.
Otro enfoque, adoptado por kBouncer, modifica el sistema operativo para verificar que las instrucciones de devolución realmente desvíen el flujo de control a una ubicación inmediatamente después de una instrucción de llamada. Esto evita el encadenamiento de dispositivos, pero conlleva una gran penalización de rendimiento [ se necesita aclaración ] y no es efectivo contra ataques de programación orientados a saltos que alteran saltos y otras instrucciones que modifican el flujo de control en lugar de retornos. [21]
Algunos sistemas modernos, como Cloud Lambda (FaaS) y las actualizaciones remotas de IoT, utilizan la infraestructura de la nube para realizar una compilación sobre la marcha antes de la implementación del software. Una técnica que introduce variaciones en cada instancia de un software en ejecución puede aumentar drásticamente la inmunidad del software a los ataques ROP. La fuerza bruta de Cloud Lambda puede dar lugar a un ataque a varias instancias del software aleatorio, lo que reduce la eficacia del ataque. Asaf Shelly publicó la técnica en 2017 [22] y demostró el uso de aleatorización binaria en un sistema de actualización de software. Para cada dispositivo actualizado, el servicio basado en la nube introdujo variaciones en el código, realiza una compilación en línea y envía el binario. Esta técnica es muy eficaz porque los ataques ROP se basan en el conocimiento de la estructura interna del software. El inconveniente de esta técnica es que el software nunca se prueba completamente antes de implementarlo porque no es factible probar todas las variaciones del software aleatorio. Esto significa que muchas técnicas de aleatorización binaria son aplicables para interfaces de red y programación de sistemas y son menos recomendadas para algoritmos complejos.
La protección contra sobrescritura del controlador de excepciones estructurado es una característica de Windows que protege contra los ataques de desbordamiento de pila más comunes, especialmente contra ataques a un controlador de excepciones estructurado.
A medida que proliferan los pequeños sistemas integrados debido a la expansión del Internet de las cosas , también aumenta la necesidad de protección de dichos sistemas integrados. Utilizando el control de acceso a memoria basado en instrucciones (IB-MAC) implementado en hardware, es posible proteger sistemas integrados de bajo costo contra ataques maliciosos de flujo de control y desbordamiento de pila. La protección se puede proporcionar separando la pila de datos y la pila de retorno. Sin embargo, debido a la falta de una unidad de gestión de memoria en algunos sistemas integrados, la solución de hardware no se puede aplicar a todos los sistemas integrados. [23]
En 2010, Jinku Li et al. propuso [24] que un compilador adecuadamente modificado podría eliminar completamente los "dispositivos" orientados al retorno reemplazando cada uno con la secuencia de instrucciones y cada uno con la secuencia de instrucciones , donde representa una tabulación inmutable de todas las direcciones de retorno "legítimas" en el programa y representa una índice específico en esa tabla. [24] : 5–6 Esto evita la creación de un gadget orientado al retorno que regresa directamente desde el final de una función a una dirección arbitraria en medio de otra función; en cambio, los gadgets sólo pueden regresar a direcciones de devolución "legítimas", lo que aumenta drásticamente la dificultad de crear gadgets útiles. Li y col. afirmó que "nuestra técnica de indirección de retorno esencialmente desgeneraliza la programación orientada a retorno al antiguo estilo de retorno a libc". [24] Su compilador de prueba de concepto incluyó una fase de optimización de mirilla para tratar con "ciertas instrucciones de máquina que contienen el código de operación de retorno en sus códigos de operación u operandos inmediatos", [24] como .call f
pushl $index
; jmp f
ret
popl %reg
; jmp table(%reg)
table
index
movl $0xC3, %eax
La arquitectura ARMv8.3-A introduce una nueva característica a nivel de hardware que aprovecha los bits no utilizados en el espacio de direcciones del puntero para firmar criptográficamente direcciones de puntero utilizando un cifrado de bloque modificable especialmente diseñado [25] [26] que firma el valor deseado. (normalmente, una dirección de retorno) combinado con un valor de "contexto local" (por ejemplo, el puntero de la pila).
Antes de realizar una operación sensible (es decir, regresar al puntero guardado), se puede verificar la firma para detectar manipulación o uso en el contexto incorrecto (por ejemplo, aprovechar una dirección de retorno guardada de un contexto de trampolín de explotación).
En particular, los chips Apple A12 utilizados en los iPhone se han actualizado a ARMv8.3 y utilizan PAC. Linux obtuvo soporte para la autenticación de puntero dentro del kernel en la versión 5.7 lanzada en 2020; En 2018 se agregó soporte para aplicaciones de espacio de usuario. [27]
En 2022, investigadores del MIT publicaron un ataque de canal lateral contra PAC denominado PACMAN. [28]
La arquitectura ARMv8.5-A introduce otra característica nueva a nivel de hardware que identifica explícitamente objetivos válidos de instrucciones de bifurcación. El compilador inserta una instrucción especial, código de operación denominado "BTI", en cada punto de aterrizaje esperado de instrucciones de rama indirecta. Estos destinos de sucursal identificados generalmente incluyen puntos de entrada de funciones y bloques de códigos de interruptor/caja.
Las instrucciones BTI se utilizan en páginas de memoria de código que están marcadas como "protegidas" por el compilador y el vinculador. Cualquier instrucción de derivación indirecta que aterrice, en una página protegida, en cualquier instrucción que no sea una BTI genera una falla.
Los destinos identificados donde se inserta una instrucción BTI representan aproximadamente el 1% de todas las instrucciones en el código de aplicación promedio. Por lo tanto, el uso de BTI aumenta el tamaño del código en la misma cantidad. [29]
Los dispositivos que se utilizan en un ataque ROP se encuentran en cualquier parte del código de la aplicación. Por lo tanto, en promedio, el 99% de los dispositivos comienzan con una instrucción que no es una BTI. Por lo tanto, la bifurcación a estos dispositivos produce una falla. Teniendo en cuenta que un ataque ROP está formado por una cadena de múltiples dispositivos, la probabilidad de que todos los dispositivos de una cadena formen parte del 1% que comienza con un BTI es muy baja.
PAC y BTI son mecanismos complementarios para prevenir inyecciones de código no autorizado mediante ataques de programación orientados al retorno y al salto. Mientras que PAC se centra en el origen de la operación de una sucursal (un puntero firmado), BTI se centra en el destino de la sucursal. [30]
Por lo tanto, diseñamos QARMA, una nueva familia de cifrados de bloques ligeros y modificables.