2016-04-21 5 views
9

Declinazione di responsabilità: Sono consapevole che l'implementazione della propria crittografia è una pessima idea. Questo fa parte di una tesi di master, il codice non verrà utilizzato nella pratica.Implementazione di un ordinamento, tipo di attacco resistente alla cache in C

Come parte di un algoritmo crittografico più grande, ho bisogno di ordinare una matrice di lunghezza costante (piccola, 24 per essere precisi), senza perdite di informazioni sul contenuto di questo array. Per quanto ne so (si prega di correggermi se questi non sono sufficienti a prevenire temporizzazione e di cache attacchi), questo significa:

  1. L'ordinamento deve essere eseguito nella stessa quantità di cicli in termini di lunghezza della matrice, indipendentemente dai valori particolari della matrice
  2. l'ordinamento non dovrebbe diramare o memoria ad accesso a seconda delle particolari valori dell'array

esistono tali implementazioni? In caso contrario, ci sono buone risorse su questo tipo di programmazione?

Per essere onesti, sto persino lottando con il sottoprogetto più semplice, vale a dire trovare il valore più piccolo di un array.

double arr[24]; // some input 
double min = DBL_MAX; 

int i; 
for (i = 0; i < 24; ++i) { 
    if (arr[i] < min) { 
     min = arr[i]; 
    } 
} 

Sarebbe l'aggiunta di un else con un incarico fittizio essere sufficiente per rendere i tempi di sicurezza? In tal caso, come faccio a garantire che il compilatore (GCC nel mio caso) non annulli il mio duro lavoro? Sarebbe suscettibile agli attacchi della cache?

+2

Una rete di ordinamento più un confronto e scambio a tempo costante è ciò che si desidera. Vedi [qui] (http://hoytech.github.io/sorting-networks/) (diapositiva 32) per l'idea. Poiché la dimensione del tuo problema è piccola, il costo della rete probabilmente non sarà proibitivo. –

+0

Se stai davvero andando * così * in profondità, devi pensare a cosa il compilatore * effettivamente * fa * dal tuo codice - ad esempio "pensa in assemblatore". Lavorare a livello "C" non sarà sufficiente, specialmente con i compilatori moderni e molto aggressivi. 'min = arr [i]' molto probabilmente funzionerà con un singolo assegnamento di registro (controllare questo;)), senza un reale impatto temporale misurabile esternamente. Qualsiasi 'else'clause non sarebbe necessaria. Nel caso in cui le cose in un 'se'clause si stanno rivelando più complesse, il tuo approccio è quello giusto. – tofro

+1

La mia intuizione mi dice che non puoi cavartela con l'ordinamento di alcuni dati e non perdere informazioni sulle sue proprietà attraverso la temporizzazione della filiale e il comportamento della cache. Perché non iniettare rumore nel sistema per nascondere invece le cose? Mescola il tuo array con fisher-yates alimentati da un CSPRNG, quindi ordinalo normalmente con quicksort. Intuitivamente questo sembra sufficiente poiché tutti gli attacchi temporali che ho visto fino ad ora dipendono da ripetute operazioni di crittografia che ogni volta perdono un po 'di informazioni. Quindi, a meno che un attacco di temporizzazione non possa estrarre l'intera chiave usata per lo shuffle, dovrebbe andare bene. – Art

risposta

3

Utilizzare una rete di ordinamento, una serie di confronti e scambi.

La chiamata di scambio non deve dipendere dal confronto. Deve essere implementato in modo da eseguire la stessa quantità di istruzioni, indipendentemente dal risultato del confronto.

Ti piace questa:

void swap(int* a , int* b , bool c) 
{ 
    const int min = c ? b : a; 
    const int max = c ? a : b; 
    *a = min; 
    *b = max; 
} 

swap(&array[0] , &array[1] , array[0] > array[1]); 

poi trovare il rete di ordinamento e utilizzare swap. Qui è un generatore che fa per voi: http://pages.ripco.net/~jgamble/nw.html

Esempio 4 elementi, i numeri sono indici di array, generati dal link:

SWAP(0, 1); 
SWAP(2, 3); 
SWAP(0, 2); 
SWAP(1, 3); 
SWAP(1, 2); 
+0

Questo avrà un comportamento di ramo diverso a seconda dei dati ordinati, quindi sono abbastanza sicuro che perderà informazioni. Gli attacchi temporali sono meno relativi alle istruzioni eseguite e maggiori informazioni sui rami mancanti e le linee della cache toccate. Tocchi sempre le stesse linee di cache, quindi va bene, ma non riesci ancora ad allontanarti dai rami. – Art

+0

@Art: Potresti spiegare come la ramificazione nello swap di 2501 potrebbe perdere informazioni? Per quanto posso dire, sarebbero state eseguite esattamente le stesse istruzioni, tranne in un ordine diverso. 1 ramo sarà perso per qualsiasi input, giusto? – bkjvbx

+0

@Art Le filiali possono essere rimosse tramite trucchi: https://stackoverflow.com/questions/227383/how-do-i-programmatically-return-the-max-of-two-integers-without-using-any-compa ma in un modo meno portabile. – 2501

0

Un molto banale, costante di tempo (ma anche altamente in-efficiente) genere ad

  • avere uno src e arrivo matrice
  • per ciascun elemento della filtrate) matrice di destinazione (, scorrere la matrice sorgente completo per trovare l'elemento che appartiene exa solo in questa posizione.

Nessuna interruzione anticipata, (quasi) tempismo costante, non dipendente dalla ordinamento parziale della sorgente.

2

Questo è un ordinamento di bolle molto stupido che in realtà funziona e non si dirama o modifica il comportamento di accesso alla memoria a seconda dei dati di input. Non sono sicuro che questo possa essere inserito in un altro algoritmo di ordinamento, hanno bisogno dei loro confronti separati dagli swap, ma forse è possibile, lavorarci adesso.

#include <stdint.h> 

static void 
cmp_and_swap(uint32_t *ap, uint32_t *bp) 
{ 
     uint32_t a = *ap; 
     uint32_t b = *bp; 
     int64_t c = (int64_t)a - (int64_t)b; 
     uint32_t sign = ((uint64_t)c >> 63); 
     uint32_t min = a * sign + b * (sign^1); 
     uint32_t max = b * sign + a * (sign^1); 
     *ap = min; 
     *bp = max; 
} 

void 
timing_sort(uint32_t *arr, int n) 
{ 
     int i, j; 
     for (i = n - 1; i >= 0; i--) { 
       for (j = 0; j < i; j++) { 
         cmp_and_swap(&arr[j], &arr[j + 1]); 
       } 
     } 
} 

La funzione cmp_and_swap compila a (versione di Apple LLVM 7.3.0 (clang-703.0.29), compilato con -O3):

_cmp_and_swap: 
00000001000009e0  pushq %rbp 
00000001000009e1  movq %rsp, %rbp 
00000001000009e4  movl (%rdi), %r8d 
00000001000009e7  movl (%rsi), %r9d 
00000001000009ea  movq %r8, %rdx 
00000001000009ed  subq %r9, %rdx 
00000001000009f0  shrq $0x3f, %rdx 
00000001000009f4  movl %edx, %r10d 
00000001000009f7  negl %r10d 
00000001000009fa  orl  $-0x2, %edx 
00000001000009fd  incl %edx 
00000001000009ff  movl %r9d, %ecx 
0000000100000a02  andl %edx, %ecx 
0000000100000a04  andl %r8d, %edx 
0000000100000a07  movl %r8d, %eax 
0000000100000a0a  andl %r10d, %eax 
0000000100000a0d  addl %eax, %ecx 
0000000100000a0f  andl %r9d, %r10d 
0000000100000a12  addl %r10d, %edx 
0000000100000a15  movl %ecx, (%rdi) 
0000000100000a17  movl %edx, (%rsi) 
0000000100000a19  popq %rbp 
0000000100000a1a  retq 
0000000100000a1b  nopl (%rax,%rax) 

Solo accessi alla memoria sono la lettura e la scrittura della matrice , niente rami. Il compilatore ha capito cosa effettivamente fa la moltiplicazione, in realtà abbastanza intelligente, ma non ha usato i rami per quello.

I cast per int64_t sono necessari per evitare overflow. Sono abbastanza sicuro che possa essere scritto più pulito.

Come richiesto, ecco una funzione di confronto per il doppio:

void 
cmp_and_swap(double *ap, double *bp) 
{ 
     double a = *ap; 
     double b = *bp; 
     int sign = signbit(a - b); 
     double min = a * sign + b * (sign^1); 
     double max = b * sign + a * (sign^1); 
     *ap = min; 
     *bp = max; 
} 

codice compilato è senza rami e non cambia modello di accesso alla memoria a seconda dei dati di input.

+0

Se non sbaglio, questo è molto simile alla risposta di 2501, con i "trucchi" che ha collegato nel suo commento per evitare implementazioni ramificate, corretto?Dal momento che bubblesort è essenzialmente un tipo specifico di rete di smistamento. Conoscete un paragone simile senza diramazioni per galleggianti e/o doppi? – bkjvbx

+1

sì, è la stessa idea. Volevo semplicemente scaricarlo in forma di codice. Per i float e i doppi dovrebbe essere più semplice da quando l'estrazione del bit del segno è più semplice e non dobbiamo preoccuparci dell'overflow. Aggiornerò in un secondo – Art

+1

Risposta grande e molto portatile. +1 (Si spera che il segnale non introduca nessun tipo di parentesi.) :-) – 2501

Problemi correlati