Trusting Trust

Introduzione

Nel 1984, ricevendo il Turing Award, Ken Thompson descrisse nel suo discorso un compilatore che non traduce in modo fedele. Il testo dell'intervento è trascritto in Reflections on Trusting Trust.

Un traduttore

Un compilatore prende un testo e produce le istruzioni che la macchina eseguirà. Da lì in poi conta solo il binario: è quello che viene eseguito, mentre il testo di partenza non viene più riletto.

Finché il compilatore è fedele, le istruzioni corrispondono a ciò che il testo dice. Ma niente lo obbliga a esserlo: può aggiungerne altre, che nel sorgente non compaiono. Il file resta identico a quello che l'autore ha scritto, il binario fa qualcosa di diverso, e chi si limita a leggere il sorgente non se ne accorge.

L'esempio classico è un programma che decide chi può usare una macchina. Un compilatore malevolo riconosce quel programma e, accanto al controllo descritto nel sorgente, ne inserisce un secondo: un nome deciso in anticipo entra comunque. Nel sorgente quel nome non è scritto da nessuna parte. Nelle istruzioni del binario, invece, sì.

Le istruzioni aggiunte

Qui sotto c'è il frammento di main prodotto da un tinyCC modificato, a partire da un sorgente in cui quel nome non compare. La sintassi è Intel e i byte sono quelli reali del binario. Le righe evidenziate sono le istruzioni iniettate: caricano la stringa ken, la confrontano con il nome ricevuto e, se coincidono, stampano una riga che concede i privilegi e ritornano subito con eax a zero, saltando fino a leave; ret.

Le righe non evidenziate sono invece l'inizio del controllo vero e proprio, quello che il sorgente aveva chiesto, ed è esattamente quello che viene scavalcato. Compilando lo stesso file, un compilatore fedele non emette nessuna di queste istruzioni e non inserisce la stringa ken nel binario. Gli indirizzi si riferiscono a questa particolare compilazione.

402666: 48 8d 05 8c eb ff ff    lea    rax, [rip+0xffffffffffffeb8c]  ; "ken"40266d: 48 89 c6                 mov    rsi, rax402670: 48 8d 45 b0              lea    rax, [rbp-0x50]402674: 48 89 c7                 mov    rdi, rax402677: e8 14 fc ff ff           call   strcmp40267c: 83 f8 00                 cmp    eax, 040267f: 0f 85 19 00 00 00        jne    40269e402685: 48 8d 05 71 eb ff ff    lea    rax, [rip+0xffffffffffffeb71]  ; "you have root privileges"40268c: 48 89 c7                 mov    rdi, rax40268f: e8 ac fc ff ff           call   puts402694: b8 00 00 00 00           mov    eax, 0402699: e9 be 00 00 00           jmp    40275c40269e: 48 8b 45 98              mov    rax, [rbp-0x68]4026a2: 48 83 f8 00              cmp    rax, 04026a6: 0f 84 31 00 00 00        je     4026dd

La copia di sé

Scrivere queste istruzioni direttamente nel sorgente del compilatore le renderebbe visibili: basterebbe leggerlo. L'idea di Thompson aggiunge allora una seconda regola. Quando il compilatore compila un altro compilatore, reinserisce la stessa regola nel binario che produce. Il sorgente di partenza, così, può essere del tutto pulito: la backdoor non vive più in un file, ma nel binario che traduce, e da quel binario si tramanda al successivo.

Un compilatore già avvelenato traduce due sorgenti puliti. Dal sorgente del compilatore esce un altro compilatore avvelenato. Dal programma di autenticazione esce un binario che accetta il nome scelto.
Un compilatore già avvelenato traduce due sorgenti puliti. Dal sorgente del compilatore esce un altro compilatore avvelenato. Dal programma di autenticazione esce un binario che accetta il nome scelto.

Questo progetto ricostruisce quel compilatore, partendo da tinyCC e da un piccolo programma di autenticazione.

Struttura del progetto

1 Milestone
~
5 Step