La integridad del flujo de control ( CFI ) es un término general para las técnicas de seguridad informática que impiden que una amplia variedad de ataques de malware redirijan el flujo de ejecución (el flujo de control ) de un programa.
Un programa de computadora cambia comúnmente su flujo de control para tomar decisiones y usar diferentes partes del código. Tales transferencias pueden ser directas , en las que la dirección de destino está escrita en el código mismo, o indirectas , en las 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 la llamó usando la pila: una transferencia indirecta de borde hacia atrás . Cuando se llama a un puntero de función , como desde una tabla virtual , decimos que hay una transferencia indirecta de borde hacia adelante . [1] [2]
Los atacantes intentan inyectar código en un programa para aprovechar sus privilegios o extraer datos de su espacio de memoria. Antes de que el código ejecutable se convirtiera en un código de solo lectura, un atacante podía cambiar arbitrariamente el código mientras se ejecutaba, apuntando a transferencias directas o incluso prescindiendo de cualquier transferencia. Después de que W^X se generalizara, un atacante quiere en cambio redirigir la ejecución a un área separada y desprotegida que contenga el código que se va a ejecutar, haciendo uso de transferencias indirectas: se podría sobrescribir la tabla virtual para un ataque de borde hacia adelante 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 ir a ubicaciones no deseadas. [1]
Las técnicas asociadas incluyen separación de punteros de código (CPS), integridad de punteros de código (CPI), canarios de pila , pilas de sombra y verificación de punteros vtable . [3] [4] [5] Estas protecciones se pueden clasificar en de grano grueso o de grano fino según el número de objetivos restringidos. Una implementación de CFI de borde delantero de grano grueso podría, por ejemplo, restringir el conjunto de objetivos de llamada indirecta a cualquier función que pueda llamarse indirectamente en el programa, mientras que una de grano fino restringiría cada sitio de llamada indirecta a funciones que tengan el mismo tipo que la función que se llamará. De manera similar, para un esquema de borde hacia atrás que proteja los retornos, una implementación de grano grueso solo permitiría que el procedimiento regrese a una función del mismo tipo (de las cuales podría haber muchas, especialmente para prototipos comunes), mientras que una de grano fino impondría una coincidencia de retorno precisa (para que pueda regresar solo a la función que lo llamó).
Hay implementaciones relacionadas 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 ofrece una opción "CFI" que funciona en el borde delantero comprobando errores en tablas virtuales y conversiones de tipos. Depende de la optimización en tiempo de enlace (LTO) para saber qué funciones se supone que deben llamarse en casos normales. [14] Hay un esquema de " pila de llamadas de sombra " independiente que defiende en el borde trasero comprobando modificaciones de la pila de llamadas, disponible solo para aarch64. [15]
Google ha enviado Android con el kernel de Linux compilado por Clang con optimización de tiempo de enlace (LTO) y CFI desde 2018. [16] SCS está disponible para el kernel de Linux como una opción, incluso en Android. [17]
La tecnología de control de flujo de Intel (CET) detecta los riesgos para controlar la integridad del flujo con una pila de sombra (SS) y un seguimiento indirecto de rama (IBT). [18] [19]
El núcleo debe asignar una región de memoria para la pila de sombras que no sea escribible para programas del espacio de usuario excepto mediante instrucciones especiales. La pila de sombras almacena una copia de la dirección de retorno de cada CALL. En un RET, el procesador verifica si la dirección de retorno almacenada en la pila normal y la pila de sombras son iguales. Si las direcciones no son iguales, el procesador genera una INT #21 (Falla de protección de flujo de control).
El seguimiento de bifurcaciones indirectas detecta instrucciones JMP o CALL indirectas dirigidas a destinos no autorizados. Se implementa añadiendo una nueva máquina de estados interna en el procesador. El comportamiento de las instrucciones JMP y CALL indirectas se modifica de modo que cambien la máquina de estados de IDLE a WAIT_FOR_ENDBRANCH. En el estado WAIT_FOR_ENDBRANCH, la siguiente instrucción que se debe ejecutar debe ser la nueva instrucción ENDBRANCH (ENDBR32 en modo de 32 bits o ENDBR64 en modo de 64 bits), que cambia la máquina de estados interna de WAIT_FOR_ENDBRANCH a IDLE. Por lo tanto, cada destino autorizado de una JMP o CALL indirecta debe comenzar con ENDBRANCH. Si el procesador está en un estado WAIT_FOR_ENDBRANCH (es decir, la instrucción anterior era una JMP o CALL indirecta) y la siguiente instrucción no es una instrucción ENDBRANCH, el procesador genera una INT #21 (Fallo de protección de flujo de control). En los procesadores que no admiten el seguimiento de rama indirecta CET, las instrucciones ENDBRANCH se interpretan como NOP y no tienen 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 de enlazador antes de vincular el programa en Visual Studio 2015 o posterior. [20]
A partir de Windows 10 Creators Update (versión 1703 de Windows 10), el kernel de Windows se compila con CFG. [21] El kernel de Windows utiliza Hyper-V para evitar que el código de kernel malicioso sobrescriba el mapa de bits CFG. [22]
CFG funciona 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 de 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 al validar las firmas de llamadas de función para garantizar que las llamadas de función indirectas se realicen únicamente al subconjunto de funciones con la misma firma. La validación de la firma de llamada de 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 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]