La integridad del flujo de control ( CFI ) es un término general para las técnicas de seguridad informática que evitan que una amplia variedad de ataques de malware redirija el flujo de ejecución (el flujo de control ) de un programa.
Un programa de computadora comúnmente cambia su flujo de control para tomar decisiones y utilizar diferentes partes del código. Dichas transferencias pueden ser directas , en el sentido de que la dirección de destino está escrita en el código mismo, o indirectas , en el sentido de que la dirección de destino en sí es una variable en la memoria o un registro de la CPU. En una llamada de función típica, el programa realiza una llamada directa, pero regresa a la función que llama usando la pila, una transferencia indirecta hacia atrás . Cuando se llama a un puntero de función , como desde una tabla virtual , decimos que hay una transferencia indirecta de avance . [1] [2]
Los atacantes buscan inyectar código en un programa para hacer uso de sus privilegios o extraer datos de su espacio de memoria. Antes de que el código ejecutable fuera comúnmente de solo lectura, un atacante podía cambiar arbitrariamente el código a medida que se ejecuta, apuntando a transferencias directas o incluso prescindir de ninguna transferencia. Después de que W^X se generalizó, un atacante quiere redirigir la ejecución a un área separada y desprotegida que contiene el código que se va a ejecutar, haciendo uso de transferencias indirectas: se podría sobrescribir la tabla virtual para un ataque de vanguardia o cambiar la pila de llamadas. para un ataque de borde hacia atrás ( programación orientada al retorno ). CFI está diseñado para proteger las transferencias indirectas de destinos no deseados. [1]
Las técnicas asociadas incluyen separación de puntero de código (CPS), integridad de puntero de código (CPI), valores controlados de pila , pilas de sombra y verificación de puntero de vtable . [3] [4] [5]
Las implementaciones relacionadas están disponibles en Clang (LLVM en general), [6] Control Flow Guard de Microsoft [7] [8] [9] y Return Flow Guard, [10] Indirect Function-Call Checks de Google [11] y Reuse Attack Protector ( RAP). [12] [13]
LLVM/Clang proporciona una opción "CFI" que funciona en el borde delantero al verificar errores en tablas virtuales y conversiones de tipos. Depende de la optimización del tiempo de enlace (LTO) saber qué funciones se supone que deben llamarse en casos normales. [14] Existe un esquema de " pila de llamadas en la sombra " independiente que defiende en el borde posterior comprobando las modificaciones de la pila de llamadas, disponible sólo para aarch64. [15]
Google ha enviado Android con el kernel de Linux compilado por Clang con optimización del tiempo de enlace (LTO) y CFI desde 2018. [16] SCS está disponible para el kernel de Linux como opción, incluso en Android. [17]
La tecnología Intel Control-flow Enforcement Technology (CET) detecta compromisos para controlar la integridad del flujo con una pila de sombra (SS) y un seguimiento de rama indirecta (IBT). [18] [19]
La pila de sombra almacena una copia de la dirección de retorno de cada LLAMADA en una pila de sombra especialmente protegida. En un RET, el procesador verifica si la dirección de retorno almacenada en la pila normal y en la pila oculta son iguales. Si las direcciones no son iguales, el procesador genera un INT #21 (Fallo de protección de flujo de control).
El seguimiento de sucursales indirectas detecta instrucciones JMP o CALL indirectas con objetivos no autorizados. Se implementa agregando una nueva máquina de estados interna en el procesador. El comportamiento de las instrucciones JMP y CALL indirectas se cambia para que cambien la máquina de estado de IDLE a WAIT_FOR_ENDBRANCH. En el estado WAIT_FOR_ENDBRANCH, se requiere que la siguiente instrucción a ejecutar sea la nueva instrucción ENDBRANCH (ENDBR32 en modo de 32 bits o ENDBR64 en modo de 64 bits), que cambia la máquina de estado interna de WAIT_FOR_ENDBRANCH nuevamente a IDLE. Por lo tanto, cada objetivo autorizado de un JMP o CALL indirecto debe comenzar con ENDBRANCH. Si el procesador está en un estado WAIT_FOR_ENDBRANCH (es decir, la instrucción anterior fue un JMP o CALL indirecto) y la siguiente instrucción no es una instrucción ENDBRANCH, el procesador genera un INT #21 (Fallo de protección de flujo de control). En los procesadores que no admiten el seguimiento indirecto de sucursales de CET, las instrucciones ENDBRANCH se interpretan como NOP y no tienen ningún efecto.
Control Flow Guard (CFG) se lanzó por primera vez para Windows 8.1 Update 3 (KB3000850) en noviembre de 2014. Los desarrolladores pueden agregar CFG a sus programas agregando el /guard:cf
indicador del vinculador antes de vincular el programa en Visual Studio 2015 o posterior. [20]
A partir de Windows 10 Creators Update (Windows 10 versión 1703), el kernel de Windows está compilado con CFG. [21] El kernel de Windows utiliza Hyper-V para evitar que el código malicioso del kernel sobrescriba el mapa de bits CFG. [22]
CFG opera creando un mapa de bits por proceso, donde un bit establecido indica que la dirección es un destino válido. Antes de realizar cada llamada de función indirecta, la aplicación verifica si la dirección de destino está en el mapa de bits. Si la dirección de destino no está en el mapa de bits, el programa finaliza. [20] Esto hace que sea más difícil para un atacante explotar un uso después de la liberación reemplazando el contenido de un objeto y luego usando una llamada de función indirecta para ejecutar una carga útil. [23]
Para todas las llamadas a funciones indirectas protegidas, _guard_check_icall
se llama a la función, que realiza los siguientes pasos: [24]
Existen varias técnicas genéricas para evitar CFG:
eXtended Flow Guard (XFG) aún no se ha lanzado oficialmente, pero está disponible en la vista previa de Windows Insider y se presentó públicamente en Bluehat Shanghai en 2019. [29]
XFG extiende CFG validando las firmas de llamadas a funciones para garantizar que las llamadas a funciones indirectas sean solo para el subconjunto de funciones con la misma firma. La validación de la firma de la llamada a la función se implementa agregando instrucciones para almacenar el hash de la función de destino en el registro r10 inmediatamente antes de la llamada indirecta y almacenando el hash de la función calculado en la memoria inmediatamente antes del código de la dirección de destino. Cuando se realiza la llamada indirecta, la función de validación XFG compara el valor en r10 con el hash almacenado de la función de destino. [30] [31]