← Torna al progetto

Trusting Trust

Trusting Trust C Step 1 di 5

Login program

L'obiettivo di una qualsiasi backdoor è garantire un accesso privilegiato confidenziale ad un sistema. L'accesso è confidenziale perché non tutti conoscono dell'esistenza della backdoor. Solo chi l'ha inserita la può utilizzare. In quale programma ha senso inserire una backdoor? In tutti quei programmi che hanno un risvolto interessante per la sicurezza di un sistema.

Consideriamo ad esempio il programma login, che nei sistemi basati su UNIX gestisce la logica di autenticazione iniziale. Il programma legge dall'input il nome utente, legge la password, controlla se la password inserita dall'utente equivale a quella salvata in un proprio database, applicando le opportune funzioni di trasformazione, e se c'è un match l'utente viene autenticato, altrimenti viene stampato un messaggio di errore.

Il login legge il nome utente dall'input, poi la password. Chiama crypt(password, salt) e confronta il risultato con il valore salvato. Se i due coincidono, l'utente è autenticato. Nell'altro caso il programma stampa un messaggio di errore.
Il login legge il nome utente dall'input, poi la password. Chiama crypt(password, salt) e confronta il risultato con il valore salvato. Se i due coincidono, l'utente è autenticato. Nell'altro caso il programma stampa un messaggio di errore.

Iniziamo dunque scrivendo un semplice programma di autenticazione. Il nostro programma sarà ispirato da quello storico presente in Unix, il cui sorgente è disponibile qui: login.c.

Password handling

Il nostro programma utilizzerà la funzione crypt della libreria crypt.h per andare a proteggere le password come mostra il seguente codice esempio.

#include <stdio.h>
#include <crypt.h>

int main(void) {
    /* i primi due caratteri sono il salt DES */
    printf("demo   %s\n", crypt("demo", "aa"));
    printf("secret %s\n", crypt("secret", "bb"));
    return 0;
}

Dato che utilizziamo crypt.h, per compilare il programma dobbiamo linkare con -lcrypt:

$ gcc -o hash hash.c -lcrypt
$ ./hash
demo   aacoGZy0DVVSw
secret bbXi4Hst6ytRc

High level logic

Il flusso da implementare in C è il seguente:

  1. Se c'è un argomento dalla linea di comandi (argv), bisogna leggerlo come nome, altrimenti il programma deve richiedere il nome dallo stdin con il seguente prompt: login:.

  2. Dopo aver letto l'utente si legge la password. Se stdin è un terminale si utilizza getpass("Password: "). Altrimenti si stampa Password: (con lo spazio finale, senza andare a capo) e si legge la password dallo stdin. Negli esempi qui sotto il prompt e la riga successiva stanno attaccati proprio perché manca il newline.

  3. Si calcola il crypt della password. Se l'utente non esiste, si utilizza come salt il valore xx. Il nome letto sta in name. Il puntatore restituito da crypt sta in namep. L'hash salvato è il campo u->hash.

  4. Il confronto è la riga riportata subito dopo questo elenco. Va ricopiata spazi compresi.

  5. Se il confronto fallisce, stampa Login incorrect e il programma esce con return 1.

  6. Altrimenti stampa Welcome <nome>. Se il nome è root, stampa anche you have root privileges. Il programma esce con 0.

Il nome letto sta in name, il puntatore restituito da crypt sta in namep, l'hash salvato è u->hash. La riga è questa:

if (!u || !namep || strcmp(namep, u->hash) != 0)

Gli step 3 e 5 assumono che login.c sia stato scritto così. Cercano quella stringa e inseriscono la backdoor subito prima, lasciando la riga al suo posto. Un altro nome di variabile, o uno spazio diverso, e la backdoor non trova il punto in cui inserirsi.

Users to support

Il programma di login deve supportare i seguenti utenti con le seguenti password.

nome password crypt
alice demo aacoGZy0DVVSw
bob secret bbXi4Hst6ytRc
root demo aacoGZy0DVVSw

Da notare che nel codice non andiamo ad inserire la password in chiaro ma il risultato del calcolo di crypt. Il tipo e la tabella sono questi. Il campo usato da quella riga è hash.

struct user {
  const char *name;
  const char *hash;
};

/*
 * hard-coded DES. first two bytes are for salt.
 */
static const struct user USERS[] = {
  { "alice", "aacoGZy0DVVSw" }, /* demo */
  { "bob",   "bbXi4Hst6ytRc" }, /* secret */
  { "root",  "aacoGZy0DVVSw" }, /* demo */
  { NULL, NULL }
};

Osserviamo che ken non è un utente del sistema.

Makefile

Andiamo infine a creare un makefile per semplificare la compilazione del programma login. Nello specifico andiamo a supportare i seguenti target:

  • all, che dipende da login

  • login, che compila il programma login.c con le flag di compilazione -Wall -Wextra -O2 e le flag di linking -lcrypt per produrre il programma login

  • clean, che rimuove il binario login dal file system

Il binario login non si committa. .gitignore contiene una riga, esattamente login.

Login in action!

Dopo aver compilato il programma dobbiamo ottenere i seguenti risultati.

$ make
gcc -Wall -Wextra -O2 -o login login.c -lcrypt

Un utente conosciuto dal sistema, con la password giusta, entra nel sistema correttamente. Il valore di ritorno qui deve essere 0.

$ printf 'demo\n' | ./login alice
Password: Welcome alice
$ printf 'secret\n' | ./login bob
Password: Welcome bob

L'utente root riceve un messaggio aggiuntivo e ritorna sempre 0.

$ printf 'demo\n' | ./login root
Password: Welcome root
you have root privileges

Se invece abbiamo un utente valido con una password sbagliata, oppure un utente non valido come ken, allora la risposta deve essere l'errore e il valore di ritorno deve essere 1.

$ printf 'nope\n' | ./login alice
Password: Login incorrect
$ printf 'demo\n' | ./login ken
Password: Login incorrect
$ printf 'demo\n' | ./login nessuno
Password: Login incorrect

Senza argomenti nome e password sono letti dal terminale.

$ printf 'alice\ndemo\n' | ./login
login: Password: Welcome alice

Inizia il progetto per avere un repository e poter consegnare gli step.

Congratulazioni, hai completato il progetto Trusting Trust

Tutti i 5 step sono superati.