Esistono degli attacchi temporali di confronto delle stringhe proof-of-concept?

4

Ho provato a riprodurre un oracolo di confronto delle stringhe in due lingue (Java e Python), ma non vedo alcuna correlazione nei tempi basata sull'input nel confronto. Ci sono degli esempi là fuori, o vedi un problema con il mio codice? Nota che vorrei qualcosa di semplice, come l'esempio che ho dato. Un attacco a un grande programma "in the wild" non è un buon esempio.

Inoltre, non sono stato in grado di trovare esempi di attacchi temporali semplici e pronti per l'esecuzione. Credo di aver disabilitato l'ottimizzazione di Java con -Djava.compiler = NONE. È necessario molto tempo per eseguire il codice, quindi chiaramente non sta completamente ottimizzando il codice.

Sembra strano che si parli spesso di questo, ma non ci sono esempi reali facilmente reperibili là fuori.

Ecco il mio codice Java così com'è oggi. L'output varia un po 'a caso senza apparentemente correlazione che ho identificato. Ho aggiunto alcune note all'output in modo da poter vedere quali dovrebbero essere i confronti più lenti.

public class TimingAttackJavaTest {
    public static void main(String[] args) {
        TimeCompare("a0", "b0", "a, b      ");
        TimeCompare("aaaaaaaaaa1", "bbbbbbbbbbb1", "a*10, b*10");
        TimeCompare("aaaaaaaaaa10", "bbbbbbbbbbb10", "a*10, b*10");
        TimeCompare("aaaaaaaaaa2", "b2", "a*10, b  ");
        TimeCompare("a3", "bbbbbbbbbbb3", "a, b*10  ");
        TimeCompare("aaaaaaaaaa4", "bbbbbbbbbbb4", "a*10, b*10  ");
        TimeCompare("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1", "a*100, a*100");
        TimeCompare("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1", "a*100, a*100");
        TimeCompare("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "a*100, b*100");
        TimeCompare("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "a*100, b*100");

        TimeCompare("a", "a", "a, a      ");
        TimeCompare("aaaaaaaaaaa", "aaaaaaaaaaa", "a*1, a*10 ");
        TimeCompare("1aaaaaaaaaa10", "2bbbbbbbbbbb10", "a*10, b*10");
    }


    static void TimeCompare(String a, String b, String outputLabel)
    {
        boolean temp = false;
        long startTime;
        long endTime;
        long duration;

        startTime = System.nanoTime();
        for(int i = 0; i < 10000000; i++)
        {
            temp = a.equals(b);
        }
        endTime = System.nanoTime();
        duration = endTime - startTime;
        System.out.println(outputLabel + temp + "\t\t" + duration);

    }
}

Output (nota che il primo confronto è sempre lento, all'avvio del programma):

a, b      false     930418800
a*10, b*10false     513034800
a*10, b*10false     510905300
a*10, b  false      534267200
a, b*10  false      524720700
a*10, b*10  false       509250100
a*100, a*100false       516159000    **This should return slowly**
a*100, a*100false       508714700    **This should return slowly**
a*100, b*100false       511160700    **This should return quickly**
a*100, b*100false       522029800    **This should return quickly**
a, a      true      278492700
a*1, a*10 true      284238900
a*10, b*10false     506245000
    
posta Alex Lauerman 04.02.2014 - 15:39
fonte

1 risposta

1

Il tuo esempio non funziona per diversi motivi, il principale dei quali è il fatto che stai usando i migliori dati possibili sul caso (cioè meno probabile che mostri differenze). Se la differenza nelle stringhe era all'inizio, il confronto anticipato dovrebbe avvisarti. Tuttavia, dal momento che stai utilizzando un linguaggio JIT, ci sono tutti i tipi di strane possibilità che potrebbero ostacolare tale controllo.

Il modo migliore per dimostrare questo tipo di attacco non è con cicli, ma con una singola misurazione in un linguaggio di basso livello come C.

Ecco un rapido esempio che funziona molto bene:

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <time.h>
using namespace std;

// this is just strcmp() wrapped in timer calls
bool CheckEqual(char* a, char* b, bool show)
{
    int i, r, trv;
    long time_pre, time_post;
    struct timespec ts;

    if((trv = clock_gettime(CLOCK_MONOTONIC,&ts)) != 0) {
        printf("Timing error, pre. Code = %d\n", trv);
    }
    time_pre = ts.tv_nsec + (ts.tv_sec * 1000000000L);

    r = strcmp(a, b);

    if((trv = clock_gettime(CLOCK_MONOTONIC,&ts)) != 0) {
        printf("Timing error, post. Code = %d\n", trv);
    }
    time_post = ts.tv_nsec + (ts.tv_sec * 1000000000L);

    if (show)
        printf("Time: %ld\n", time_post - time_pre);
    else
        printf("First compare done.\n");

    return r == 0;
}

// this function helps us avoid the problem of the compiler being too smart.
// if we were to use string literals that were equal, it'd optimise compile-time
// constant strings into a single instance, so strcmp() would just early-out on
// the fact that the two pointers were equal, which is not what we want to
// demonstrate here - in reality we'd be comparing buffers that aren't static.
char* MakeStr(char c, int n)
{
    int i;
    char* s = (char*)calloc(n+1, sizeof(c));
    for(i=0; i<n; i++)
        s[i] = c;
    return s;
}

int main()
{
    // equal apart from last
    char* aa = MakeStr('0', 64);
    char* ab = "0000000000000000000000000000000000000000000000000000000000000001";

    // equal apart from first
    char* ba = MakeStr('0', 64);
    char* bb = "1000000000000000000000000000000000000000000000000000000000000000";

    // inequality in the middle
    char* ca = "0000000000000000000000000000010000000000000000000000000000000000";
    char* cb = "0000000000000000000000000000001000000000000000000000000000000000";

    // equal
    char* da = MakeStr('0', 64);
    char* db = MakeStr('0', 64);

    // first call to strcmp() and time functions will result in cache misses
    // so we call it once and don't display output to account for that.
    CheckEqual(aa, ab, false);

    printf("Equal but last, ");
    CheckEqual(aa, ab, true);
    printf("Equal but first, ");
    CheckEqual(ba, bb, true);
    printf("Equal but mid, ");
    CheckEqual(ca, cb, true);
    printf("Completely equal, ");
    CheckEqual(da, db, true);

    free(aa); free(ba); free(da); free(db);
    return 0;
}

L'output dovrebbe essere simile a questo:

First compare done.
Equal but last, Time: 345
Equal but first, Time: 229
Equal but mid, Time: 234
Completely equal, Time: 270

Hai notato la grande differenza tra i primi due? Ciò è dovuto all'ottimizzazione early-out , per cui il confronto termina non appena trova una differenza. Nel secondo confronto, ci vuole meno tempo perché il primo confronto dei caratteri mostra che non è uguale, quindi può tornare presto. Nel terzo confronto, vediamo che questo va un po 'oltre nella stringa, risultando in un tempo di esecuzione leggermente più lungo rispetto al "uguale ma prima", ma un tempo di esecuzione più breve di "uguale ma ultimo".

Quello completamente uguale è una bestia interessante. Suppongo che, quando l'ultimo carattere non è uguale, ci sia qualche logica aggiuntiva aggiuntiva. In confronto, il tutto uguale richiede meno tempo, presumo perché deve solo restituire 0.

    
risposta data 04.02.2014 - 17:46
fonte

Leggi altre domande sui tag