Control Flow Guard para Clang / LLVM y Rust

Como parte de nuestros esfuerzos continuos hacia una programación de sistemas más segura, nos complace anunciar que Home windows Handle de flujo de guardia (CFG) El soporte ahora está disponible en el compilador Clang C / C ++ y Oxido.

¿Qué es Handle Move Guard?

CFG es una tecnología de seguridad de plataforma diseñada para reforzar la integridad del flujo de regulate. Ha estado disponible desde Windows 8.1 y ahora se usa ampliamente en Home windows 10. Por ejemplo, dada una vulnerabilidad de seguridad de la memoria inicial, un atacante podría intentar lanzar un ataque de reutilización de código. Esto casi siempre requiere que el atacante cambie el flujo de handle del programa, en otras palabras, violar la integridad del flujo de control. Por ejemplo, el atacante podría intentar corromper el puntero de una función para continuar con la ejecución desde una ubicación arbitraria en el código del programa.

CFG tiene como objetivo mitigar este tipo de explotación mediante la aplicación de la integridad del flujo de regulate de avanzada de grano grueso. Específicamente, utiliza verificaciones en tiempo de ejecución para validar la dirección de destino de cada instrucción de rama indirecta (llamada, salto, etc.) antes de permitir que la rama se full. Para lograr esto, CFG requiere que el compilador haga dos cosas: agregar verificaciones de tiempo de ejecución en los lugares apropiados y proporcionar una lista de objetivos de rama indirecta válidos. Durante la compilación, el compilador identifica todas las ramas indirectas y agrega una verificación CFG en cada una de esas ramas. También emite metadatos que contienen las direcciones relativas de todas las funciones tomadas por direcciones. En tiempo de ejecución, si el binario se ejecuta en un sistema operativo appropriate con CFG, el cargador utiliza estos metadatos de CFG para generar un mapa de bits del espacio de direcciones y marca qué direcciones contienen destinos de rama válidos. En cada rama indirecta, el código insertado verifica que la dirección de destino esté marcada en este mapa de bits, o si no, termina el proceso.

CFG es complementario a otras mitigaciones de exploits, como Tackle Place Structure Randomization (ASLR) y Info Execution Prevention (DEP). Anteriormente, CFG solo estaba disponible para código C / C ++ compilado con Microsoft Visible C ++.

Regulate Movement Guard en LLVM y Clang

El Proyecto LLVM es una colección de tecnologías de cadena de herramientas y compiladores modulares y reutilizables. En specific, las bibliotecas LLVM Core forman una foundation independiente del lenguaje para varios frontends de compiladores diferentes, incluido el compilador Clang C / C ++ y rustc, el compilador de Rust. Las bibliotecas centrales incluyen un conjunto común de optimizaciones y proporcionan generación de código de máquina para múltiples arquitecturas de CPU.

LLVM 10. ahora es suitable con CFG. Nuestra implementación de CFG está completamente contenida dentro de las bibliotecas centrales, lo que la hace reutilizable por cualquier compilador construido en LLVM el compilador frontend simplemente necesita establecer los indicadores correctos. Nuestra implementación admite todas las arquitecturas de destino para las que CFG está disponible actualmente, a saber, x86 (32 bits y 64 bits), ARM y Aarch64. Agregamos soporte CFG a Clang 10. para proyectos C / C ++. Para habilitar CFG en sus proyectos, simplemente use el -cfguard opción cc1 (p. ej. -Xclang -cfguard), o si está utilizando el controlador de compatibilidad clang-cl, utilice el mismo /guard:cf banderas como en MSVC. Clang también es compatible con __declspec(guard(nocf)) modificador para eludir las comprobaciones de CFG en la función especificada, pero esto solo debe usarse si es absolutamente necesario, ya que puede permitir exploits.

Dado que el código foundation de Chromium se compila con Clang, el equipo de Chromium está trabajando para habilitar CFG en compilaciones de Home windows como primer paso hacia su adopción en Google Chrome y Microsoft Edge.

Regulate Circulation Guard en óxido

Como se explicó en publicaciones anteriores, Microsoft está explorando el uso de Rust como un lenguaje de programación de sistemas seguros. Anteriormente, Rust no era appropriate con CFG, lo que podría haber sido un obstáculo para su uso dentro de nuestro application. Uno de los principales puntos de venta de Rust es que su modelo de propiedad ofrece sólidas garantías de seguridad de la memoria, lo que debería evitar las vulnerabilidades utilizadas como punto de partida para un exploit. Entonces, ¿por qué un lenguaje así necesitaría mitigaciones de explotación como CFG? Hay dos casos principales en los que le gustaría utilizar CFG en Rust:

  • Bases de código donde Rust coexiste con C / C ++,
  • Bases de código Pure Rust que incluyen cualquier archivo.

1. Rust vinculado con C / C ++

El primer caso para habilitar CFG es siempre que Rust interopere con código C / C ++, ya sea como un programa de Rust que llame a una biblioteca C / C ++ o viceversa. El ejemplo simple de Rust a continuación demuestra dos formas de llamar al incorporate_a single() función: llamándolo directamente por su nombre, o llamándolo indirectamente a través de un puntero de función pasado desde main(). Hay un init() función proporcionada por una biblioteca C externa. La biblioteca C se compila con CFG habilitado. Como era de esperar, la llamada a la función C está en un unsafe bloquear. Sin embargo, el puntero de función (fptr) nunca es manejado por este código inseguro, por lo que no esperamos que sea modificado.

#(link(name = "init"))
extern "C"  fn init(y: u32) 
 
// A simple function to increment an integer
fn add_a single(x: &mut i32) 
    *x += 1


fn do_math(fptr: fn(&mut i32)) 
    unsafeinit(incorporate_one as u32)
 
    let mut x = 1
    add_one(&mut x)
    println!("Calling function by name: 1 + 1 = ", x)
 
    let mut x = 1
    fptr(&mut x)
    println!("Calling via function pointer: 1 + 1 = ", x)

 
fn main() 
    do_math(increase_just one)

Pero cuando ejecutamos este sencillo programa, los resultados son sorprendentes:

Calling function by identify: 1 + 1 = 2
Calling through functionality pointer: 1 + 1 = 1

En este ejemplo, el init() La función llega más allá de su marco de pila y corrompe la pila para hacer fptr apuntar a algún lugar en el medio del incorporate_one() función (si reproduce este ejemplo, es posible que deba cambiar la 0x2D offset, dependiendo de su plataforma y la configuración del compilador).

#include 
void init(uint32_t y) 
    for (uint32_t n = 0x01 n < 0x20 n++) 
        if (*(&y + n) == y)  
            *(&y + n) += 0x2D
        
    

Si bien este es un ejemplo muy elaborado (y con suerte ningún código base real contiene este código), ilustra la posibilidad de que un atacante encuentre una vulnerabilidad de corrupción de memoria en el código C / C ++ vinculado y lo use para violar la integridad del flujo de control en el (seguro) Código de óxido. Aunque el código C / C ++ está compilado con CFG habilitado, también necesitamos habilitar CFG para el código Rust para mitigar esta vulnerabilidad.

Además, incluso si no está vinculando explícitamente con C / C ++, es muy probable que cualquier programa de Rust en el espacio de usuario use la funcionalidad C / C ++ proporcionada por el sistema operativo bajo el capó (por ejemplo, para imprimir la salida en el terminal, como se indicó anteriormente), por lo que siempre es una buena idea habilitar CFG.

2. Bases de código Pure Rust que utilizan

El segundo caso para habilitar CFG es en bases de código Rust puro que utilizan cualquier código inseguro. De manera similar al ejemplo anterior, el init() La función podría haber sido una función Rust pura, que contenía código inseguro que modificó el puntero de la función en la pila, como se muestra a continuación.

fn init(y: u32) 
    for n in 0x20..0x50 
        unsafe  if *(&y as *const u32).offset(n) == y  
            *((&y as *const u32).offset(n) as *mut _) = y + 0x2D  
    

Nuevamente, este es un ejemplo artificial, pero ilustra la posibilidad de que las vulnerabilidades en el código inseguro de Rust afecten a otras partes del programa (seguro) de Rust. Como en el ejemplo anterior, esto también se mitiga habilitando CFG para Rust.

Además de las bases de código que usan inseguras, CFG también puede ayudar a mitigar las vulnerabilidades que surgen de errores en el lenguaje principal o la biblioteca estándar de Rust.

¿Cómo habilito CFG para Rust?

CFG está disponible en Rust 1.47 (actualmente el nocturno versión). Para habilitar CFG, simplemente agregue el -C control-flow-guard bandera. Si está construyendo con carga, puede habilitar CFG usando el comando rustc cargo rustc -- -C control-flow-guard. Es importante destacar que, para obtener una protección completa, debe utilizar una versión de la biblioteca estándar de Rust que también tenga CFG habilitado. En la actualidad, CFG aún no está habilitado en las versiones predefinidas de la biblioteca estándar, pero puede habilitarlo en sus propias compilaciones agregando control-flow-guard = true en su archivo config.toml.

Sobrecarga del protector de flujo de control

Habilitar este tipo de aplicación de la integridad del flujo de control suele generar algunos gastos generales en términos de tamaño binario y rendimiento en tiempo de ejecución. CFG está altamente optimizado para minimizar ambos aspectos. Las implementaciones de MSVC y LLVM generan una sobrecarga muy similar ya que ambas utilizan la misma lógica de verificación proporcionada por el sistema operativo. La magnitud de cualquier sobrecarga depende del número y la frecuencia de las llamadas indirectas en el programa que se está compilando. Por ejemplo, habilitar CFG para la biblioteca estándar de Rust aumenta el tamaño binario en aproximadamente un 0,14%. Habilitar CFG en el conjunto de pruebas comparativas SPEC CPU 2017 Integer Speed ​​compilado con Clang / LLVM incurre en gastos generales de tiempo de ejecución aproximados de hasta un 8%, con una media geométrica de 2.9%, como se muestra en la siguiente tabla:

Punto de referencia Sin CFG (segundos) Con CFG (segundos) Gastos generales
600.perlbench_s 314 322 2,5%
602.gcc_s 538 546 1,5%
605.mcf_s 723 767 6,1%
620.omnetpp_s 486 521 7,2%
623.xalancbmk_s 225 243 8,0%
625.x264_s 186 193 3,8%
631.deepsjeng_s 326 323 -0,9%
641.leela_s 435 428 -1,6%
657.xz_s 487 488 0,2%
Significado geometrico 381,6 392,7 2,9%

Estos puntos de referencia se ejecutaron en una CPU Intel Xeon W-2155 a 3,30 GHz utilizando clang-cl con los indicadores de CPU SPEC predeterminados para Windows / MSVC. Los tiempos citados son la mediana de tres carreras. El banco de pruebas 648.exchange2_s requiere Fortran, por lo que no se incluyó. Rendimiento en los puntos de referencia 631.deepsjeng_s y 641.leela_s en realidad mejorado al habilitar CFG, probablemente debido a una mejor alineación de la caché.

Mirando hacia el futuro

La integridad del flujo de control es un tema importante y se han propuesto muchas soluciones tanto en la literatura académica como en los sistemas del mundo real. Algunos enfoques ofrecen una aplicación más detallada para reducir aún más el conjunto de objetivos de rama válidos. Por ejemplo, Microsoft ha anunciado recientemente XFG, como sucesor de CFG. Otras soluciones hacen uso de nuevo hardware de CPU, como el recientemente anunciado por Intel CET, que podría mejorar aún más el rendimiento una vez que se haya implementado ampliamente. CFG es solo un punto en este espacio de diseño, y aunque hay otras soluciones en el horizonte, CFG aún puede ayudar a defenderse de exploits y está disponible hoy en todos los dispositivos con Windows 10.

Agradecimientos

Trabajar con las comunidades de código abierto LLVM y Rust ha sido una experiencia muy positiva. Agradecemos particularmente a los miembros de las comunidades que contribuyeron a este trabajo mediante sugerencias de diseño, revisiones de códigos y otros consejos.

Andrew Paverd, investigador principal, MSRC y Microsoft Research




Fuente del articulo