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.
Questo progetto ricostruisce quel compilatore, partendo da tinyCC e da un piccolo programma di autenticazione.