¿Qué es una retpolina y cómo funciona?
Para mitigar la divulgación de memoria del kernel o entre procesos (el ataque Spectre ), el kernel de Linux 1 se compilará con una nueva opción , -mindirect-branch=thunk-extern
introducida para gcc
realizar llamadas indirectas a través de la llamada retpoline .
Este parece ser un término recién inventado, ya que una búsqueda en Google solo muestra un uso muy reciente (generalmente todo en 2018).
¿Qué es una retpoline y cómo previene los recientes ataques de divulgación de información del kernel?
1 Sin embargo, no es específico de Linux: parece que se utiliza una construcción similar o idéntica como parte de las estrategias de mitigación en otros sistemas operativos.
El artículo mencionado por sgbj en los comentarios escritos por Paul Turner de Google explica lo siguiente con mucho más detalle, pero lo intentaré:
Hasta donde puedo reconstruir esto a partir de la información limitada en este momento, un retpoline es un trampolín de retorno que utiliza un bucle infinito que nunca se ejecuta para evitar que la CPU especule sobre el objetivo de un salto indirecto.
El enfoque básico se puede ver en la rama del kernel de Andi Kleen que aborda este problema:
Introduce la nueva __x86.indirect_thunk
llamada que carga el destino de la llamada cuya dirección de memoria (a la que llamaré ADDR
) está almacenada en la parte superior de la pila y ejecuta el salto usando una RET
instrucción. Luego se llama al procesador en sí mediante la macro NOSPEC_JMP/CALL , que se usó para reemplazar muchas (si no todas) llamadas y saltos indirectos. La macro simplemente coloca el destino de la llamada en la pila y establece la dirección de retorno correctamente, si es necesario (tenga en cuenta el flujo de control no lineal):
.macro NOSPEC_CALL target
jmp 1221f /* jumps to the end of the macro */
1222:
push \target /* pushes ADDR to the stack */
jmp __x86.indirect_thunk /* executes the indirect jump */
1221:
call 1222b /* pushes the return address to the stack */
.endm
La colocación de call
in the end es necesaria para que cuando finalice la llamada indirecta, el flujo de control continúe detrás del uso de la NOSPEC_CALL
macro, por lo que se puede utilizar en lugar de una normal.call
El procesador en sí tiene el siguiente aspecto:
call retpoline_call_target
2:
lfence /* stop speculation */
jmp 2b
retpoline_call_target:
lea 8(%rsp), %rsp
ret
El flujo de control puede resultar un poco confuso aquí, así que déjame aclarar:
call
empuja el puntero de instrucción actual (etiqueta 2) a la pila.lea
agrega 8 al puntero de la pila , descartando efectivamente la palabra cuádruple enviada más recientemente, que es la última dirección de retorno (a la etiqueta 2). Después de esto, la parte superior de la pila apunta nuevamente a la dirección de retorno real ADDR.ret
salta*ADDR
y restablece el puntero de la pila al comienzo de la pila de llamadas.
Al final, todo este comportamiento es prácticamente equivalente a saltar directamente a *ADDR
. El único beneficio que obtenemos es que el predictor de rama utilizado para las declaraciones de retorno (Return Stack Buffer, RSB), al ejecutar la call
instrucción, asume que la ret
declaración correspondiente saltará a la etiqueta 2.
La parte después de la etiqueta 2 en realidad nunca se ejecuta, es simplemente un bucle infinito que, en teoría, llenaría el canal de instrucciones con JMP
instrucciones. El uso de LFENCE
, PAUSE
o más generalmente una instrucción que provoca que la canalización de instrucciones se detenga, evita que la CPU desperdicie energía y tiempo en esta ejecución especulativa. Esto se debe a que, en caso de que la llamada a retpoline_call_target regresara normalmente, LFENCE
sería la siguiente instrucción a ejecutar. Esto también es lo que predecirá el predictor de sucursal en función de la dirección del remitente original (la etiqueta 2)
Para citar el manual de arquitectura de Intel:
Las instrucciones que siguen a una LFENCE se pueden recuperar de la memoria antes de la LFENCE, pero no se ejecutarán hasta que se complete la LFENCE.
Sin embargo, tenga en cuenta que la especificación nunca menciona que LFENCE y PAUSE hacen que la tubería se detenga, por lo que estoy leyendo un poco entre líneas aquí.
Ahora volvamos a su pregunta original: la divulgación de información de la memoria del kernel es posible debido a la combinación de dos ideas:
Aunque la ejecución especulativa debería estar libre de efectos secundarios cuando la especulación fue incorrecta, la ejecución especulativa aún afecta la jerarquía de caché . Esto significa que cuando una carga de memoria se ejecuta de forma especulativa, aún puede haber provocado que se desaloje una línea de caché. Este cambio en la jerarquía de la caché se puede identificar midiendo cuidadosamente el tiempo de acceso a la memoria asignada al mismo conjunto de caché.
Incluso puede perder algunos bits de memoria arbitraria cuando la dirección de origen de la memoria leída se leyó a su vez desde la memoria del kernel.El predictor de rama indirecta de las CPU Intel solo utiliza los 12 bits inferiores de la instrucción fuente, por lo que es fácil envenenar los 2^12 historiales de predicción posibles con direcciones de memoria controladas por el usuario. Estos pueden luego, cuando se predice el salto indirecto dentro del kernel, ejecutarse especulativamente con privilegios del kernel. Al utilizar el canal lateral de sincronización de caché, puede perder memoria del kernel arbitraria.
ACTUALIZACIÓN: En la lista de correo del kernel , hay una discusión en curso que me lleva a creer que las retpolines no mitigan completamente los problemas de predicción de ramas, como cuando el Return Stack Buffer (RSB) se ejecuta vacío, las arquitecturas Intel más recientes (Skylake+) retroceden. al vulnerable Branch Target Buffer (BTB):
Retpoline como estrategia de mitigación intercambia ramas indirectas por retornos, para evitar el uso de predicciones que provienen del BTB, ya que pueden ser envenenadas por un atacante. El problema con Skylake+ es que un subdesbordamiento de RSB recurre al uso de una predicción de BTB, lo que permite al atacante tomar el control de la especulación.
Una retpoline está diseñada para proteger contra el exploit de inyección de destino de rama ( CVE-2017-5715 ). Este es un ataque en el que se utiliza una instrucción de rama indirecta en el kernel para forzar la ejecución especulativa de un fragmento arbitrario de código. El código elegido es un "dispositivo" que de alguna manera resulta útil para el atacante. Por ejemplo, se puede elegir un código que filtre datos del kernel a través de cómo afecta el caché. La retpoline evita este exploit simplemente reemplazando todas las instrucciones de rama indirectas con una instrucción de retorno.
Creo que la clave de la retpoline es simplemente la parte "ret", que reemplaza la rama indirecta con una instrucción de retorno para que la CPU use el predictor de pila de retorno en lugar del predictor de rama explotable. Si en su lugar se usara una simple instrucción push y return, entonces el código que se ejecutaría especulativamente sería el código al que la función eventualmente regresará de todos modos, no algún dispositivo útil para el atacante. El principal beneficio de la parte del trampolín parece ser mantener la pila de retorno, de modo que cuando la función realmente regresa a su llamador, esto se predice correctamente.
La idea básica detrás de la inyección de destino de rama es simple. Aprovecha el hecho de que la CPU no registra la dirección completa del origen y destino de las ramas en sus buffers de destino de ramas. Por lo tanto, el atacante puede llenar el búfer mediante saltos en su propio espacio de direcciones que darán como resultado aciertos de predicción cuando se ejecute un salto indirecto particular en el espacio de direcciones del kernel.
Tenga en cuenta que retpoline no evita la divulgación directa de información del kernel, solo evita que se utilicen instrucciones de rama indirectas para ejecutar especulativamente un dispositivo que revelaría información. Si el atacante puede encontrar algún otro medio para ejecutar especulativamente el dispositivo, entonces la retpoline no previene el ataque.
El artículo Spectre Attacks: Exploiting Speculative Execution de Paul Kocher, Daniel Genkin, Daniel Gruss, Werner Haas, Mike Hamburg, Moritz Lipp, Stefan Mangard, Thomas Prescher, Michael Schwarz y Yuval Yarom ofrece la siguiente descripción general de cómo se pueden explotar las ramas indirectas. :
Explotación de sucursales indirectas. A partir de la programación orientada al retorno (ROP), en este método el atacante elige un dispositivo del espacio de direcciones de la víctima e influye en la víctima para que ejecute el dispositivo de forma especulativa. A diferencia de ROP, el atacante no depende de una vulnerabilidad en el código de la víctima. En cambio, el atacante entrena el Branch Target Buffer (BTB) para predecir erróneamente una bifurcación desde una instrucción de bifurcación indirecta a la dirección del dispositivo, lo que resulta en una ejecución especulativa del dispositivo. Si bien se abandonan las instrucciones ejecutadas especulativamente, sus efectos en la memoria caché no se revierten. El dispositivo puede utilizar estos efectos para filtrar información confidencial. Mostramos cómo, con una cuidadosa selección de un dispositivo, este método se puede utilizar para leer la memoria arbitraria de la víctima.
Para desviar el BTB, el atacante encuentra la dirección virtual del dispositivo en el espacio de direcciones de la víctima y luego realiza ramificaciones indirectas a esta dirección. Este entrenamiento se realiza desde el espacio de direcciones del atacante y no importa lo que resida en la dirección del gadget en el espacio de direcciones del atacante; todo lo que se requiere es que la sucursal utilizada para las sucursales de capacitación use la misma dirección virtual de destino. (De hecho, siempre que el atacante maneje las excepciones, el ataque puede funcionar incluso si no hay ningún código asignado en la dirección virtual del dispositivo en el espacio de direcciones del atacante). Tampoco es necesaria una coincidencia completa de la dirección de origen. de la sucursal utilizada para la capacitación y la dirección de la sucursal objetivo. Por tanto, el atacante tiene una flexibilidad significativa a la hora de preparar el entrenamiento.
Una entrada de blog titulada Lectura de memoria privilegiada con un canal lateral del equipo de Project Zero de Google proporciona otro ejemplo de cómo se puede utilizar la inyección de destino en rama para crear un exploit que funcione.
Esta pregunta se hizo hace un tiempo y merece una respuesta más reciente.
Resumen ejecutivo :
Las secuencias "Retpoline" son una construcción de software que permite aislar las ramas indirectas de la ejecución especulativa. Esto se puede aplicar para proteger archivos binarios confidenciales (como sistemas operativos o implementaciones de hipervisor) de ataques de inyección de destino de sucursales contra sus sucursales indirectas.
La palabra " ret poline " es un acrónimo de las palabras "retorno" y "trampolín", al igual que la mejora " rel poline " se acuñó a partir de "llamada relativa" y "trampolín". Es una construcción de trampolín construida mediante operaciones de retorno que, en sentido figurado, también garantiza que cualquier ejecución especulativa asociada “rebotará” sin cesar.
Para mitigar la divulgación de memoria del kernel o de procesos cruzados (el ataque Spectre), el kernel de Linux [1] se compilará con una nueva opción,
-mindirect-branch=thunk-extern
introducida en gcc para realizar llamadas indirectas a través de la llamada retpoline.[1] Sin embargo, no es específico de Linux: parece que se utiliza una construcción similar o idéntica como parte de las estrategias de mitigación en otros sistemas operativos.
El uso de esta opción del compilador solo protege contra Spectre V2 en los procesadores afectados que tienen la actualización de microcódigo requerida para CVE-2017-5715. Funcionará con cualquier código (no sólo con un núcleo), pero sólo vale la pena atacar el código que contenga "secretos" .
Este parece ser un término recién inventado, ya que una búsqueda en Google solo muestra un uso muy reciente (generalmente todo en 2018).
El compilador LLVM ha tenido un -mretpoline
cambio desde antes del 4 de enero de 2018 . Esa fecha es cuando se informó públicamente por primera vez sobre la vulnerabilidad . GCC puso a disposición sus parches el 7 de enero de 2018.
La fecha CVE sugiere que la vulnerabilidad fue " descubierta " en 2017, pero afecta a algunos de los procesadores fabricados en las últimas dos décadas (por lo que probablemente se descubrió hace mucho tiempo).
¿Qué es una retpoline y cómo previene los recientes ataques de divulgación de información del kernel?
Primero, algunas definiciones:
Trampolín : a veces denominados trampolines de vectores de salto indirecto, son ubicaciones de memoria que contienen direcciones que apuntan a rutinas de servicio de interrupción, rutinas de E/S, etc. La ejecución salta al trampolín y luego inmediatamente salta o rebota, de ahí el término trampolín. GCC tradicionalmente ha admitido funciones anidadas mediante la creación de un trampolín ejecutable en tiempo de ejecución cuando se toma la dirección de una función anidada. Este es un pequeño fragmento de código que normalmente reside en la pila, en el marco de la pila de la función contenedora. El trampolín carga el registro de cadena estática y luego salta a la dirección real de la función anidada.
Thunk : un procesador es una subrutina que se utiliza para inyectar un cálculo adicional en otra subrutina. Los procesadores se utilizan principalmente para retrasar un cálculo hasta que se necesite su resultado, o para insertar operaciones al principio o al final de otra subrutina.
Memoización : una función memorizada "recuerda" los resultados correspondientes a algún conjunto de entradas específicas. Las llamadas posteriores con entradas recordadas devuelven el resultado recordado en lugar de recalcularlo, eliminando así el costo principal de una llamada con parámetros dados de todas las llamadas realizadas a la función con esos parámetros, excepto la primera.
En términos muy generales, un retpoline es un trampolín con un retorno como un golpe seco , para " estropear " la memorización en el predictor de rama indirecta.
Fuente : La retpoline incluye una instrucción PAUSE para Intel, pero una instrucción LFENCE es necesaria para AMD ya que en ese procesador la instrucción PAUSE no es una instrucción de serialización, por lo que el bucle pausa/jmp utilizará el exceso de energía, ya que se especula sobre la espera del retorno. predecir mal al objetivo correcto.
Arstechnica tiene una explicación simple del problema:
"Cada procesador tiene un comportamiento arquitectónico (el comportamiento documentado que describe cómo funcionan las instrucciones y del que dependen los programadores para escribir sus programas) y un comportamiento microarquitectónico (la forma en que se comporta una implementación real de la arquitectura). Estos pueden divergir de manera sutil. Por ejemplo, desde el punto de vista arquitectónico, un programa que carga un valor desde una dirección particular en la memoria esperará hasta que se conozca la dirección antes de intentar realizar la carga. Sin embargo, desde el punto de vista microarquitectónico, el procesador podría intentar adivinar especulativamente la dirección para poder comenzar. cargar el valor de la memoria (que es lento) incluso antes de estar absolutamente seguro de qué dirección debe usar.
Si el procesador adivina mal, ignorará el valor adivinado y realizará la carga nuevamente, esta vez con la dirección correcta. De este modo se conserva el comportamiento definido arquitectónicamente. Pero esa suposición errónea perturbará otras partes del procesador, en particular el contenido de la caché. Estas alteraciones de la microarquitectura pueden detectarse y medirse cronometrando el tiempo que lleva acceder a los datos que deberían (o no) estar en la memoria caché, lo que permite a un programa malicioso hacer inferencias sobre los valores almacenados en la memoria".
Del artículo de Intel: " Retpoline: una mitigación de inyección de objetivos de sucursales " ( .PDF ):
"Una secuencia de retpoline evita que la ejecución especulativa del procesador utilice el "predictor de rama indirecta" (una forma de predecir el flujo del programa) para especular en una dirección controlada por un exploit (que satisface el elemento 4 de los cinco elementos de la inyección de destino de rama (variante Spectre 2). ) explotar la composición enumerada anteriormente).".
Tenga en cuenta que el elemento 4 es: "El exploit debe influir con éxito en esta rama indirecta para predecir erróneamente y ejecutar de forma especulativa un dispositivo. Este dispositivo, elegido por el exploit, filtra los datos secretos a través de un canal lateral, generalmente mediante sincronización de caché".