¿Qué es un retpoline y cómo funciona?

244

Con el fin de mitigar la divulgación de memoria de kernel o de procesos cruzados (el ataque Spectre ), el kernel 1 de Linux se compilará con una nueva opción , -mindirect-branch=thunk-externintroducida gccpara realizar llamadas indirectas a través de una 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 evita los recientes ataques de divulgación de información del núcleo?


1 Sin embargo, no es específico de Linux: parece que se usa una construcción similar o idéntica como parte de las estrategias de mitigación en otros sistemas operativos.

BeeOnRope
fuente
66
Un interesante artículo de soporte de Google.
sgbj
2
oh, entonces se pronuncia / ˌtræmpəˈlin / (American) o / ˈtræmpəˌliːn / (British)
Walter Tross
2
Puede mencionar que este es el kernel de Linux , ¡aunque gccapunta de esa manera! No reconocí lkml.org/lkml/2018/1/3/780 como en el sitio de la lista de correo del kernel de Linux, ni siquiera una vez que miré allí (y recibí una instantánea ya que estaba fuera de línea).
PJTraill
@PJTraill - agregó una etiqueta de kernel de Linux
RichVel
@PJTraill: buen punto, actualicé el texto de la pregunta. Tenga en cuenta que lo vi por primera vez en el kernel de Linux debido a su proceso de desarrollo relativamente abierto, pero sin duda se están utilizando las mismas técnicas o similares como mitigaciones en todo el espectro de sistemas operativos de código abierto y cerrado. Así que no veo esto como específico de Linux, pero el enlace ciertamente lo es.
BeeOnRope

Respuestas:

158

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, una 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 núcleo de Andi Kleen que aborda este problema:

Introduce la nueva __x86.indirect_thunkllamada que carga el destino de la llamada cuya dirección de memoria (que llamaré ADDR) se almacena en la parte superior de la pila y ejecuta el salto usando la RETinstrucción. El thunk en sí mismo se llama utilizando la macro NOSPEC_JMP / CALL , que se utilizó para reemplazar muchas (si no todas) las llamadas indirectas y saltos. La macro simplemente coloca el destino de la llamada en la pila y establece la dirección de retorno correctamente, si es necesario (observe 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 callal final es necesaria para que cuando finalice la llamada indirecta, el flujo de control continúe detrás del uso de la NOSPEC_CALLmacro, por lo que se puede usar en lugar de un comando regular.call

El thunk en sí se ve de la siguiente manera:

    call retpoline_call_target
2:
    lfence /* stop speculation */
    jmp 2b
retpoline_call_target:
    lea 8(%rsp), %rsp 
    ret

El flujo de control puede ser un poco confuso aquí, así que déjenme aclarar:

  • call empuja el puntero de instrucción actual (etiqueta 2) a la pila.
  • leaagrega 8 al puntero de la pila , descartando efectivamente la última palabra cuádruple, que es la última dirección de retorno (a la etiqueta 2). Después de esto, la parte superior de la pila apunta a la dirección de retorno real ADDR nuevamente.
  • retsalta *ADDRy 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 callinstrucción, supone que la retdeclaración correspondiente saltará a la etiqueta 2.

La parte posterior a la etiqueta 2 en realidad nunca se ejecuta, es simplemente un bucle infinito que en teoría llenaría la tubería de JMPinstrucciones con instrucciones. Al usar LFENCE, PAUSEo más generalmente, una instrucción que hace que el canal de instrucciones se detenga, la CPU no desperdicia energía y tiempo en esta ejecución especulativa. Esto se debe a que en caso de que la llamada a retpoline_call_target regrese normalmente, LFENCEsería la siguiente instrucción que se ejecutará. Esto también es lo que predecirá el predictor de sucursal en función de la dirección de retorno original (la etiqueta 2)

Para citar del manual de arquitectura de Intel:

Las instrucciones que siguen a un LFENCE pueden recuperarse de la memoria antes del LFENCE, pero no se ejecutarán hasta que el LFENCE se complete.

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 núcleo 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 era incorrecta, la ejecución especulativa todavía afecta la jerarquía de caché . Esto significa que cuando una carga de memoria se ejecuta especulativamente, aún puede haber provocado el desalojo de una línea de caché. Este cambio en la jerarquía de caché se puede identificar midiendo cuidadosamente el tiempo de acceso a la memoria que se asigna al mismo conjunto de caché.
    Incluso puede perder algunos bits de memoria arbitraria cuando la dirección de origen de la lectura de la memoria fue leída de la memoria del núcleo.

  • El predictor indirecto de ramificación de las CPU Intel solo usa los 12 bits más bajos de la instrucción fuente, por lo que es fácil envenenar todos los 2 ^ 12 posibles historiales de predicción con direcciones de memoria controladas por el usuario. Estos pueden, entonces, cuando se predice el salto indirecto dentro del núcleo, ejecutarse especulativamente con privilegios de núcleo. Usando el canal lateral de sincronización de caché, puede perder memoria arbitraria del núcleo.

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 por completo los problemas de predicción de la rama, como cuando el Buffer de pila de retorno (RSB) se ejecuta vacío, las arquitecturas Intel más recientes (Skylake +) retroceden al búfer de destino de rama vulnerable (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 desbordamiento de RSB recurre al uso de una predicción BTB, que permite al atacante tomar el control de la especulación.

Tobias Ribizel
fuente
No creo que la instrucción LFENCE sea importante, la implementación de Google utiliza una instrucción PAUSE en su lugar. support.google.com/faqs/answer/7625886 Tenga en cuenta que la documentación que cita dice "no se ejecutará" no se "no se ejecutará especulativamente".
Ross Ridge
1
De esa página de preguntas frecuentes de Google: "Las instrucciones de pausa en nuestros bucles especulativos anteriores no son necesarias para la corrección. Pero sí significa que la ejecución especulativa no productiva ocupa menos unidades funcionales en el procesador". Por lo tanto, no respalda su conclusión de que LFENCE es la clave aquí.
Ross Ridge
@RossRidge Estoy parcialmente de acuerdo, para mí esto parece dos implementaciones posibles de un bucle infinito que insinúan a la CPU que no ejecute especulativamente el código después de PAUSE / LFENCE. Sin embargo, si LFENCE se ejecutó especulativamente y no se revertió porque la especulación era correcta, esto contradiría la afirmación de que solo se ejecutará una vez que las cargas de memoria hayan finalizado. (De lo contrario, todo el conjunto de instrucciones que se han ejecutado especulativamente tendrían que revertirse y ejecutarse nuevamente para cumplir con las especificaciones)
Tobias Ribizel
1
Esto tiene la ventaja de push/ retque no desequilibra la pila de predictores de dirección de retorno. Hay un error de predicción (ir lfenceantes de que se use la dirección de retorno real), pero usar una callmodificación + lo rspequilibró ret.
Peter Cordes
1
Uy, ventaja sobre push / ret(en mi último comentario). re: su edición: el desbordamiento de RSB debería ser imposible porque la retpoline incluye a call. Si la prevención del núcleo hizo un cambio de contexto allí, reanudaríamos la ejecución con el RSB preparado desde callel programador. Pero tal vez un controlador de interrupciones podría terminar con suficientes rets para vaciar el RSB.
Peter Cordes
46

Un retpoline está diseñado para proteger contra la explotación de la inyección de objetivo de ramificación ( CVE-2017-5715 ). Este es un ataque donde se usa una instrucción indirecta de ramificación en el núcleo para forzar la ejecución especulativa de un fragmento arbitrario de código. El código elegido es un "gadget" que de alguna manera es útil para el atacante. Por ejemplo, se puede elegir el código para que filtre los datos del núcleo a través de cómo afecta a la memoria caché. La retpoline evita esta vulnerabilidad simplemente reemplazando todas las instrucciones indirectas de ramificación con una instrucción de retorno.

Creo que la clave de la retpoline es solo 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 usó 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 finalmente 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, por lo 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 objetivo de rama es simple. Aprovecha el hecho de que la CPU no registra la dirección completa del origen y el destino de las sucursales en sus búferes de destino de sucursal. Por lo tanto, el atacante puede llenar el búfer utilizando saltos en su propio espacio de direcciones que resultarán en aciertos de predicción cuando se ejecute un salto indirecto particular en el espacio de direcciones del núcleo.

Tenga en cuenta que retpoline no impide la divulgación de información del núcleo directamente, solo evita que se usen instrucciones indirectas de ramificación para ejecutar especulativamente un gadget que divulgaría información. Si el atacante puede encontrar algún otro medio para ejecutar especulativamente el dispositivo, entonces la retpoline no evita el ataque.

El documento Specter Attacks: Explotando la ejecución especulativa por Paul Kocher, Daniel Genkin, Daniel Gruss, Werner Haas, Mike Hamburg, Moritz Lipp, Stefan Mangard, Thomas Prescher, Michael Schwarz y Yuval Yarom ofrecen la siguiente descripción general de cómo se pueden explotar las ramas indirectas :

Explotación de ramas indirectas. A partir de la programación orientada al retorno (ROP), en este método el atacante elige un gadgetdesde el espacio de direcciones de la víctima e influye en la víctima para ejecutar el gadget 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 rama desde una instrucción de rama indirecta a la dirección del dispositivo, lo que resulta en una ejecución especulativa del dispositivo. Si bien las instrucciones ejecutadas especulativamente se abandonan, sus efectos en el caché no se revierten. El gadget puede utilizar estos efectos para filtrar información confidencial. Mostramos cómo, con una cuidadosa selección de un dispositivo, este método puede usarse para leer la memoria arbitraria de la víctima.

Para entrenar mal el BTB, el atacante encuentra la dirección virtual del dispositivo en el espacio de direcciones de la víctima, luego realiza ramificaciones indirectas a esta dirección. Este entrenamiento se realiza desde el espacio de direcciones del atacante, y no importa qué reside en la dirección del dispositivo en el espacio de direcciones del atacante; todo lo que se requiere es que la sucursal utilizada para entrenar sucursales 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 un código asignado en la dirección virtual del gadget en el espacio de direcciones del atacante). Tampoco hay necesidad de 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 lo tanto, el atacante tiene una flexibilidad significativa en la configuración del entrenamiento.

Una entrada de blog titulada Lectura de memoria privilegiada con un canal lateral por el equipo del Proyecto Cero en Google proporciona otro ejemplo de cómo se puede utilizar la inyección de objetivo de rama para crear un exploit funcional.

Ross Ridge
fuente
9

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 ramas indirectas de la ejecución especulativa. Esto se puede aplicar para proteger los binarios confidenciales (como el sistema operativo o las implementaciones de hipervisor) de los ataques de inyección de destino de rama contra sus ramas indirectas.

La palabra " ret poline " es un acrónimo de las palabras "de retorno" y "trampolín", al igual que la mejora " rel poline " fue acuñado de "llamada relativa" y "cama elástica". Es una construcción de trampolín construida usando operaciones de retorno que también asegura figurativamente que cualquier ejecución especulativa asociada "rebotará" sin cesar.

Con el fin de mitigar la divulgación de memoria de kernel o de procesos cruzados (el ataque Spectre), el kernel de Linux [1] se compilará con una nueva opción, -mindirect-branch=thunk-externintroducida en gcc para realizar llamadas indirectas a través de una 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. Lo hará ' trabajo ' en cualquier código (no sólo un núcleo), pero sólo el código que contiene "secretos" vale atacante.

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 -mretpolinecambio desde antes del 4 de enero de 2018 . Esa fecha es cuando la vulnerabilidad se informó públicamente por primera vez . GCC puso a disposición sus parches el 7 de enero de 2018.

La fecha CVE sugiere que la vulnerabilidad se ' descubrió ' en 2017, pero afecta a algunos de los procesadores fabricados en las últimas dos décadas (por lo tanto, probablemente se descubrió hace mucho tiempo).

¿Qué es una retpoline y cómo evita los recientes ataques de divulgación de información del núcleo?

Primero, algunas definiciones:

  • Trampolín : en ocasiones, los trampolines de vectores de salto indirectos son ubicaciones de memoria que contienen direcciones que apuntan a interrumpir las rutinas de servicio, las rutinas de E / S, etc. GCC ha apoyado tradicionalmente las funciones anidadas al crear 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 que lo contiene. El trampolín carga el registro de la cadena estática y luego salta a la dirección real de la función anidada.

  • Thunk : un thunk es una subrutina utilizada para inyectar un cálculo adicional en otra subrutina. Los thunks se usan principalmente para retrasar un cálculo hasta que se necesite su resultado, o para insertar operaciones al principio o al final de la otra subrutina

  • Memoization : 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 volver a calcularlo, eliminando así el costo primario de una llamada con parámetros dados de todos menos la primera llamada realizada a la función con esos parámetros.

En términos generales, un retpoline es un trampolín con un retorno como un golpe , para " estropear " la memorización en el predictor indirecto de ramas.

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 de pausa / jmp usará un exceso de energía ya que se especula sobre la espera de retorno predecir erróneamente el 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 de los 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 maneras sutiles". Por ejemplo, arquitectónicamente, un programa que carga un valor de una dirección particular en la memoria esperará hasta que se conozca la dirección antes de intentar realizar la carga. Sin embargo, microarquitecturalmente, el procesador podría intentar adivinar especulativamente la dirección para que pueda comenzar cargar el valor de la memoria (que es lento) incluso antes de que sea 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. El comportamiento arquitectónicamente definido se conserva así. Pero esa suposición errónea perturbará otras partes del procesador, en particular el contenido de la memoria caché. Estas perturbaciones microarquitectónicas se pueden detectar y medir midiendo el tiempo que se tarda en acceder a los datos que deberían (o no deberían) estar en la memoria caché, lo que permite que un programa malicioso haga inferencias sobre los valores almacenados en la memoria ".

Del documento de Intel: " Retpoline: una rama de mitigación de inyección objetivo " ( .PDF ):

"Una secuencia retpoline evita que la ejecución especulativa del procesador use el" predictor indirecto de ramificación "(una forma de predecir el flujo del programa) para especular a una dirección controlada por un exploit (satisfaciendo el elemento 4 de los cinco elementos de inyección de ramificación objetivo (variante 2 del espectro) ) explotar la composición mencionada anteriormente) ".

Tenga en cuenta que el elemento 4 es: "El exploit debe influir con éxito en esta rama indirecta para predecir especulativamente y ejecutar un gadget. Este gadget, elegido por el exploit, filtra los datos secretos a través de un canal lateral, generalmente mediante temporización de caché".

Robar
fuente