venerdì 9 gennaio 2015

Su srand() e OpenBSD

Salve a tutti i miei quattro lettori. In questo che è il mio primo articolo del 2015 tratterò di una questione apparentemente marginale ma che invece ha numerosi risvolti pratici.

Abbiamo visto in uno degli articoli precedenti ("Sui vizi e le virtù di /dev/random") che un buon generatore di numeri pseudocasuali (PRNG: Pseudo Random Number Generator) sia un componente essenziale per la sicurezza di un sistema informatico (leggetevi l'articolo per maggiori informazioni... Vi prego!) e che /dev/random in generale non è la soluzione ottimale. Ma allora cosa dovremmo usare?

L'esempio classico che viene dato nei libri sul linguaggio C e nei tutorial su internet in merito a generazione di numeri pseudocasuali è il seguente:

#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    /*Seed the RNG*/
    srand(time(NULL));

    puts("'s' followed by 'Enter' to start the RNG. Ctrl+C to exit\n");

    while(fgetc(stdin) != 's')
    {
        usleep(100000); /*Sleep for 0.1 seconds*/
    }

    while(1)
    {
        int r = rand(); /*Get random number*/
        printf("%d\n", r);
        usleep(100000);
    }

    return 0;

}

Questo esempio canonico inizializza il generatore pseudocausale con lo UNIX EPOCH corrente e poi stampa numeri tra 0 e RAND_MAX. Come da manuale insomma.

Dove sta il problema con questo codice?

Se andiamo a leggere il manuale di srand scopriamo che:

The srand() function sets its argument as the seed for a  new
sequence of  pseudo-random  integers  to be returned by rand().
These sequences are repeatable by calling srand() with the same seed
value.

In Italiano per non addetti ai lavori: se io passo a srand lo stesso numero due volte di seguito otterrò la stessa sequenza pseudocasuale di numeri. In altre parole la coppia srand/rand è DETERMINISTICA e gli standard C89 e POSIX impongono che sia così.

Si, Virginia, tutto ciò è Male e i due lettori che hanno letto l'articolo su /dev/random sanno anche perché (vi ho già pregato di leggere quell'articolo, vero?). Per coloro i quali fossero stati disattenti la risposta è insita nelle caratteristiche richieste ad un buon generatore di numeri pseudocasuali:

1. Che non ripeta la sequenza generata troppo presto (idealmente pescando centomila numeri interi a 64 bit al secondo il tempo in cui la sequenza comici a ripetersi dovrebbe eccedere i 5 milioni di anni).

2. Che renda difficile predire il numero successivo basandosi sui numeri già usciti (dove per "difficile" intendiamo: "data una sequenza lunga N indovinare il numero alla posizione N+1 deve richiedere più di 2M - N operazioni dove M è il numero di bit dei numeri interi generati". Idealmente la lunghezza N dovrebbe essere ininfluente e quindi ogni volta occorrerebbe effettuare 2M operazioni).

Ma tutto questo cosa c'entra con srand e rand ? C'entra nel momento in cui qualcuno si mette ad usare codice simile a quello dell'esempio riportato innanzi per compiti che richiederebbero un vero e proprio PRNG. Come è scritto nel manuale dare lo stesso seme a srand produce la medesima sequenza, quindi è facile intuire che, con un po' di prove ed errori è possibile stimare l'ora in cui un servizio è stato avviato e quandi il momento in cui è stata fatta la chiamata a srand (che non è thread-safe e quindi va fatta una volta sola oppure va racchiusa tra due global-lock). A quel punto ricavare la sequenza generata, per quanto difficile, non è impossibile e da lì il passo per fare danni è breve. Se aggiungiamo poi che in certi casi esiste la possibilità di far crshare un servizio e obbligarlo a riavviarsi si intuisce subito che un attaccante può ridurre al minimo l'incertezza sul momento del seed e quindi massimizzare le possibilità di riuscita dell'attacco.

Ok, è una cosa da paranoici, ma è possibile e se è possibile è probabile che qualcuno stia provando a sfruttare questa feature per i suoi loschi fini. In fin dei conti nessuno pensava che OpenSSL potesse divulgare informazioni in giro prima di Heartbleed.

E questo ci porta a vedere cosa hanno deciso di fare quei paranoici di OpenBSD; hanno rotto la compatibilità con POSIX e sostituito le chiamate a rand con chiamate al loro PRNG (una trattazione esaustiva di detto generatore esula dagli scopi di questo articolo, ma invito i più curiosi ad informarsi dando un'occhiata alla presentazione che Theo de Raadt ha presentato allo EuroBSD Con del 2014 ). Per mantenere il vecchio comportamento occorre chiamare esplicitamente la nuova funzione srand_deterministic invece di srand.

L'idea di spezzare la compatibilità è derivata da un'analisi del codice dei programmi di terze parti inclusi nei ports: la maggiorparte di essi usa rand come una genuina sorgente di numeri pseudocasuali (o crede di farlo) e solo pochissimi software assumono il comportamento deterministico di srand e molti solo a scopo di test.

Quindi come misura ulteriore di sicurezza hanno deciso di rompere la compatibilità e di dare un migliore PRNG a quanti utilizzano srand su OpenBSD.

Se volete saperne di più:

E con questo è tutto! Aloha!