Impossibile spiegare i dati dal tentativo di attacco del canale laterale

8

Ho trovato la funzione di confronto di seguito (leggermente modificata) da una libreria di crittografia che stavo usando. Ero curioso della potenziale vulnerabilità agli attacchi ai canali laterali. Nello specifico, il confronto dei caratteri viene eseguito solo se il carattere che si sta confrontando rientra nei limiti delle due stringhe. Ho il sospetto che questo potrebbe consentire a un utente malintenzionato di determinare la lunghezza della stringa.

Forse questa differenza è semplicemente troppo piccola per essere soggetta ad un attacco di temporizzazione, ma ho giocato con un tentativo di seguito. Fondamentalmente creo stringhe di lunghezze crescenti e mi confronto a una data stringa iniziale. Mi aspettavo forse di vedere una crescita lineare nel tempo di confronto sia prima che dopo il punto in cui la seconda stringa diventa più lunga, ma forse con una pendenza diversa poiché le operazioni eseguite sono diverse.

Invece, vedo i dati sottostanti (nota che la stringa che viene confrontata è lunga 27 caratteri). Qualsiasi spiegazione sul motivo per cui non ho idea di cosa sto parlando sarebbe molto apprezzato:)

Una piccola nota, ho provato con -O0 nel caso in cui qualche strana ottimizzazione fosse sbagliata. L'unica cosa che posso pensare di fare da qui è iniziare a scavare nell'assemblaggio generato.

#include<string.h>#include<sys/time.h>#include<stdio.h>intCompareStrings(constchar*s1,constchar*s2){inteq=1;ints1_len=strlen(s1);ints2_len=strlen(s2);if(s1_len!=s2_len){eq=0;}constintmax_len=(s2_len<s1_len)?s1_len:s2_len;//topreventtimingattacks,shouldcheckentirestring//don'texitafterfoundtobefalseinti;for(i=0;i<max_len;++i){if(s1_len>=i&&s2_len>=i&&s1[i]!=s2[i]){eq=1;}}returneq;}doubletime_diff(structtimevalx,structtimevaly){doublex_ms,y_ms,diff;x_ms=(double)x.tv_sec*1000000+(double)x.tv_usec;y_ms=(double)y.tv_sec*1000000+(double)y.tv_usec;diff=(double)y_ms-(double)x_ms;returndiff;}voidtest_with_length(char*str1,intn){charstr2[n+1];structtimevaltp1;structtimevaltp2;inti;for(i=0;i<n;i++){str2[i]='a';}str2[n]='
#include<string.h>#include<sys/time.h>#include<stdio.h>intCompareStrings(constchar*s1,constchar*s2){inteq=1;ints1_len=strlen(s1);ints2_len=strlen(s2);if(s1_len!=s2_len){eq=0;}constintmax_len=(s2_len<s1_len)?s1_len:s2_len;//topreventtimingattacks,shouldcheckentirestring//don'texitafterfoundtobefalseinti;for(i=0;i<max_len;++i){if(s1_len>=i&&s2_len>=i&&s1[i]!=s2[i]){eq=1;}}returneq;}doubletime_diff(structtimevalx,structtimevaly){doublex_ms,y_ms,diff;x_ms=(double)x.tv_sec*1000000+(double)x.tv_usec;y_ms=(double)y.tv_sec*1000000+(double)y.tv_usec;diff=(double)y_ms-(double)x_ms;returndiff;}voidtest_with_length(char*str1,intn){charstr2[n+1];structtimevaltp1;structtimevaltp2;inti;for(i=0;i<n;i++){str2[i]='a';}str2[n]='%pre%';gettimeofday(&tp1,NULL);for(i=0;i<20000000;i++){CompareStrings(str1,str2);}gettimeofday(&tp2,NULL);printf("%d %.01f\n", n, time_diff(tp1, tp2));
}

int main() {
    char *str1 = "XXXXXXXXXXXXXXXXXXXXXXXXXXX";

    int i = 0;
    for (i = 1; i <= 100; i++) {
        test_with_length(str1, i);
    }
}
'; gettimeofday(&tp1, NULL); for (i = 0; i < 20000000; i++) { CompareStrings(str1, str2); } gettimeofday(&tp2, NULL); printf("%d %.01f\n", n, time_diff(tp1, tp2)); } int main() { char *str1 = "XXXXXXXXXXXXXXXXXXXXXXXXXXX"; int i = 0; for (i = 1; i <= 100; i++) { test_with_length(str1, i); } }
    
posta Michael Mior 13.09.2012 - 01:18
fonte

2 risposte

6

Innanzitutto, alcuni commenti sul codice:

  • Le tue chiamate a strlen sono una scatola nera - non puoi sapere come reagiscono agli attacchi temporali.
  • Il tuo ciclo di comparazione si chiude dopo aver esaurito la corda più corta, perdendo la lunghezza più corta.
  • Non hai rimosso il bias generato dal ciclo in test_with_length .
  • Gli accessi alla matrice sono non O (1) complessità temporale, soprattutto se ogni elemento è più piccolo della larghezza della parola della CPU.

Un modo migliore per compensare tali attacchi:

bool CompareStr(char* strA, char* strB)
{
    bool result = true;
    int lenA, lenB = 0;

    while(strA[n] != 0)
        lenA++;
    while(strB[n] != 0)
        lenB++;

    int maxLen = (lenA > lenB) ? lenA : lenB;

    // compensate for array access to some extent
    int diff = (lenA > lenB) ? lenA - lenB : lenB - lenA;
    char dummy = '
 xor ecx, ecx                         ; clear counter to 0
 mov dword ptr ds:[0040AAAA], 1       ; set result to true
loopStart:
 mov ah, byte ptr ds:[ecx+00401234]   ; read char from string A
 mov bh, byte ptr ds:[ecx+00402468]   ; read char from string B
 cmp ah, bh                           ; compare the chars
 jz match                             ; are they equal?
 mov dword ptr ds:[0040AAAA], 0       ; strings don't match! set result to true
match:
 cmp ah, 0                            ; are we at the end of string A?
 jz done
 cmp bh, 0                            ; are we at the end of string B?
 jz done
 inc ecx                              ; increment counter and continue loop
 jmp loopStart
done:
 ret
'; for(int i = 0; i < diff; i++) { dummy = strA[0]; } // better way to avoid timing attacks for(int i = 0; i < maxLen; i++) { if ( ((i >= lenA) ? strA[0] : strA[i]) != ((i >= lenB) ? strB[0] : strB[i]) ) result = false; } return false; } // compute loop overhead time_pre_loop = getTime(); for(int i = 0; i < LOOP_COUNT; i++) { } time_post_loop = getTime(); double diff = time_diff(time_post_loop, time_pre_loop); time_pre_test = getTime(); for(int i = 0; i < LOOP_COUNT; i++) CompareStr(strA, strB); time_post_test = getTime(); double result = time_diff(time_post_test, time_pre_test) - diff;

In ogni caso, ecco dove inizia la parte divertente. Considera il seguente codice assembly che può essere utilizzato per calcolare se due stringhe sono uguali, con l'ottimizzazione early-out rimossa come difesa di base del timing attack:

int r, d = 0;
int* bufferA = (int*)stringA;
int* bufferB = (int*)stringB;
for(int i = 0; i < BUFFER_SIZE; i += 4)
{
    d = bufferA[i] ^ bufferB[i]; // if equal, d = 0
    r |= d; // if all cases are 0, r will be 0
}
return r; // result is 0 if equal, non-zero if not equal

In realtà ci sono alcuni attacchi temporali qui. Le prime due istruzioni sono O (1) . Tuttavia, le due letture a singolo byte che seguono non sono.

Mentre le letture della memoria sono solitamente O (1) , in pratica non lo saranno. In realtà, non si adattano affatto al modello big-O. Su un sistema a 32 bit, ogni 4 byte letto sarà più veloce degli altri. Su un sistema a 64 bit, ogni 4 byte sarà più veloce e ogni 8 byte sarà ancora più veloce. Ciò è dovuto a letture di memoria non allineate. La CPU è ottima per il recupero di blocchi di dati allineati alla sua dimensione di bus, ma non così grandi per il recupero di singoli byte casuali. La CPU tenterà di ottimizzare questo pipelining istruzioni e il caricamento del blocco nella sua cache L2, che rende questo problema più sottile, ma è ancora lì.

Il trucco successivo è per le stringhe che sono più grandi della cache della CPU. Se si dispone di una stringa che supera questa dimensione (in genere 128 KB o superiore per la cache L2), si verificheranno lievi ritardi nei recuperi di memoria ogni volta che una lettura supera questo limite. Questo di solito è solo un piccolo ritardo, poiché la cache L3 verrà utilizzata per memorizzare il blocco successivo, ma possiamo vedere una differenza ancora maggiore per le stringhe più grandi (8 MB +) dal momento che i recuperi di memoria devono essere eseguiti dalla memoria di sistema.

Se consideriamo una copia di memoria a blocchi come O (n), dove n è la lunghezza della memoria, questo rende alcuni senso, ma non descrive correttamente le piccole variazioni nei tempi causate dalle idiosincrasie di implementazione.

Infine, possiamo vedere che l'istruzione mov dword ptr ds:[0040AAAA], 0 viene eseguita ogni volta che una stringa non corrisponde ad un'altra. Questa informazione perde in due modi:

  • Consente di stimare la lunghezza relativa della stringa più breve identificando il tempo extra utilizzato quando si imposta ripetutamente il risultato.
  • Se siamo in grado di misurare con precisione, ci consente di identificare quali caratteri sono diversi.

Come possiamo vedere, è piuttosto difficile prevenire questi problemi. Ecco il mio tentativo di un sistema migliore, supponendo che le stringhe a lunghezza fissa siano state riempite per allineare i byte null:

bool CompareStr(char* strA, char* strB)
{
    bool result = true;
    int lenA, lenB = 0;

    while(strA[n] != 0)
        lenA++;
    while(strB[n] != 0)
        lenB++;

    int maxLen = (lenA > lenB) ? lenA : lenB;

    // compensate for array access to some extent
    int diff = (lenA > lenB) ? lenA - lenB : lenB - lenA;
    char dummy = '
 xor ecx, ecx                         ; clear counter to 0
 mov dword ptr ds:[0040AAAA], 1       ; set result to true
loopStart:
 mov ah, byte ptr ds:[ecx+00401234]   ; read char from string A
 mov bh, byte ptr ds:[ecx+00402468]   ; read char from string B
 cmp ah, bh                           ; compare the chars
 jz match                             ; are they equal?
 mov dword ptr ds:[0040AAAA], 0       ; strings don't match! set result to true
match:
 cmp ah, 0                            ; are we at the end of string A?
 jz done
 cmp bh, 0                            ; are we at the end of string B?
 jz done
 inc ecx                              ; increment counter and continue loop
 jmp loopStart
done:
 ret
'; for(int i = 0; i < diff; i++) { dummy = strA[0]; } // better way to avoid timing attacks for(int i = 0; i < maxLen; i++) { if ( ((i >= lenA) ? strA[0] : strA[i]) != ((i >= lenB) ? strB[0] : strB[i]) ) result = false; } return false; } // compute loop overhead time_pre_loop = getTime(); for(int i = 0; i < LOOP_COUNT; i++) { } time_post_loop = getTime(); double diff = time_diff(time_post_loop, time_pre_loop); time_pre_test = getTime(); for(int i = 0; i < LOOP_COUNT; i++) CompareStr(strA, strB); time_post_test = getTime(); double result = time_diff(time_post_test, time_pre_test) - diff;

Questo è quasi perfettamente O (n) per i seguenti motivi:

  • xor di bit di allineati è O (1)
  • Bitwise o di allineati è O (1)
  • Le letture della memoria sono allineate.
  • Nessuna lunghezza è trapelata, a causa di BUFFER_LENGTH padding.
  • Nessuna filiale, escluso il ciclo.
risposta data 13.09.2012 - 08:42
fonte
2

Innanzitutto, c'è un bug nella funzione di confronto; " eq = 1; " nel ciclo dovrebbe leggere " eq = 0; ". Non dovrebbe cambiare la tempistica, tuttavia, assumendo che il compilatore non giochi con l'ottimizzatore. Per rendere il tuo codice più resistente ai giochi di ottimizzazione, devi inserire la funzione benchmark (il tuo CompareStrings() ) e il codice benchmark ( test_with_length() ) in moduli distinti (file sorgente C compilati separatamente e pregare affinché il compilatore non esegua inter -module ottimizzazioni - gcc dovrebbe essere sicuro per questo).

Quello che vedi sembra corretto. Le due chiamate strlen() dovrebbero avere un costo che aumenta approssimativamente linearmente con la somma delle lunghezze delle due stringhe; ma strlen() riproduce alcuni giochi eleganti per essere più veloce. In particolare, strlen() proverà a leggere i byte dei dati per interi blocchi allineati (forse 4, 8, 16 byte), e questo funzionerà perché il compilatore sa che il controllo dell'accesso alla memoria da parte di MMU funziona sulla granularità di una pagina, quindi in molti casi è possibile rendere accessi" sicuri "fuori dai limiti. Le stringhe più piccole avranno versioni specializzate e le lunghezze qui (al massimo 100 byte) sono troppo piccole per mostrare comportamenti asintotici.

È probabile che il costo del due strlen() sia piccolo rispetto al ciclo for . introdurrà un po 'di "rumore" nel grafico, però, quindi non dovresti guardare troppo da vicino i punti.

Il ciclo for verrà eseguito per% stepmax_len, che è la lunghezza del più lungo delle due stringhe. Pertanto, come prima approssimazione, ci aspettiamo che il costo di quel loop sia lineare nella lunghezza della più grande delle due stringhe, quindi dovresti vedere un tempo approssimativamente costante quando la lunghezza del secondo operando cresce da 1 a 27, quindi un aumento - che è ciò che vedi nel grafico.

    
risposta data 16.09.2012 - 16:46
fonte

Leggi altre domande sui tag