Articoli approfonditi sulla tecnologia che plasma il futuro.

Jailbreak Switch 2: catena di exploit contro il Secure Boot

Il jailbreak dello Switch 2 ha concatenato overflow in BASIC, bug nel path di fs e race nel loader. Cosa insegna su exploit chain e secure boot.

Una console portatile collegata a hardware di controllo remoto, con una leva robotica che preme il tasto di accensione su un banco da lavoro.
Gli attuali rig per l'hacking di console abbinano KVM-over-IP a una leva Bluetooth che preme fisicamente il tasto di accensione dopo ogni crash.

Una console appena uscita viene spedita con un microkernel irrobustito, pointer authentication su ogni indirizzo di ritorno, memoria execute-only, control-flow integrity e una catena di boot firmata. Due settimane dopo, un hobbyista esegue codice nativo arbitrario come servizio del filesystem, il processo più fidato dell'intera macchina. È la storia del primo jailbreak pubblico dello Switch 2, e dovrebbe mettere un po' a disagio chiunque costruisca sistemi sicuri. Non perché le mitigazioni abbiano fallito, perché la maggior parte ha funzionato esattamente come progettata, ma perché l'exploit non ha mai dovuto affrontarle di petto. Le ha aggirate, un bug di logica alla volta.

Da anni mi occupo di sicurezza infrastrutturale dal lato difensivo, e lo schema che vedo qui è uno che ho incontrato nelle revisioni post-incidente in produzione più volte di quanto vorrei: una catena di bug singolarmente poco impressionanti, ognuno dei quali garantisce il privilegio sufficiente per raggiungere il successivo. Il primo appoggio non era nemmeno nello Switch 2. Era un bug di corruzione della memoria in un gioco dello Switch 1, l'interprete SmileBASIC, in esecuzione in modalità di retrocompatibilità. Da lì la catena è salita attraverso il servizio del filesystem, ha abusato di una race nel loader dei programmi e si è conclusa con la capacità di eseguire codice nativo con l'identità di qualsiasi sysmodule. Ogni anello merita attenzione, perché ciascuno corrisponde a un errore che ho visto fare ai team anche nei sistemi server-side.

Perché la porta del browser era chiusa, e quella dei giochi no

WebKit è il bersaglio classico per i jailbreak delle console, e il ricercatore ha effettivamente ottenuto un appoggio lì tramite un captive portal e un n-day. Ma si è fermato presto. Lo stack browser dello Switch 2 è protetto da tre mitigazioni che si combinano in modo davvero efficace: la memoria execute-only (XOM) fa sì che, anche con lettura e scrittura arbitrarie, non si riescano a localizzare gadget di codice utili; la pointer authentication (PAC) impedisce di falsificare un puntatore di codice senza un oracolo di firma; e la CFI fa sì che anche un puntatore firmato correttamente non superi i controlli di tipo nella maggior parte dei call site. Aggirarne una è un progetto di ricerca. Aggirarle tutte e tre, concatenate, partendo da una sandbox JavaScript, è una carriera.

Così l'attacco si è spostato sul tenant più debole della macchina: i giochi di terze parti dello Switch 1. Il ragionamento era freddo e corretto. I binari dei giochi sono prodotti dagli studi, non dal team di sicurezza di Nintendo, quindi è molto meno probabile che siano compilati con l'intero stack di mitigazioni; e lo Switch 1 non ha mai avuto la PAC, quindi tutto ciò che gira nel suo ambiente di compatibilità eredita quella debolezza. Ancora meglio: il root of trust dello Switch 1 è stato compromesso anni fa, il che significa che il codice dei giochi può essere decifrato e analizzato offline. SmileBASIC, un enorme interprete BASIC in C++ già sfruttato sul 3DS, era il bersaglio morbido ovvio.

C'è una lezione qui che va oltre le console: la postura di sicurezza del tuo sistema è quella del codice meno irrobustito che accetti di eseguire. La retrocompatibilità è una superficie d'attacco che erediti, non una che scegli. Il modello di sicurezza dell'Xbox l'ha imparato a proprie spese negli anni 2000: gli exploit dei salvataggi nei titoli non irrobustiti sono stati il cuneo che ha scardinato un kernel molto più stretto, una storia che abbiamo raccontato in cosa insegna l'hack della Xbox originale sui modelli di sicurezza hardware. Vent'anni dopo, la stessa mossa funziona ancora.

L'integer overflow che ha aperto la prima porta

Il bug di SmileBASIC è un integer overflow da manuale nel bounds checking, il tipo di cosa che ogni checklist di revisione del codice sicuro dice di catturare e che continua comunque a finire in produzione. Il comando di rimozione dagli array dell'interprete validava gli argomenti in questo modo:

// intent: ensure the removed range is inside the array
if (position + count <= array.length) {
memmove(backing + position * 8,          // start of the gap
backing + (position + count) * 8, // elements after the gap
(length - position) * 8);          // bytes to copy
}

Chiamando REMOVE R, &H60000000, &H3FFFFFFF su un array di quattro elementi, position + count va in overflow e diventa 0x9FFFFFFF, che come valore con segno a 32 bit è profondamente negativo, comodamente «minore» della lunghezza. Anche le moltiplicazioni per 8 vanno in overflow, e finisci per eseguire memmove(backing, backing - 8, 32). Il comando stampa poi un errore di memoria esaurita, ma la copia è già avvenuta. E qui sta la parte deliziosa: gli 8 byte immediatamente prima di qualsiasi array di SmileBASIC contengono un puntatore all'array stesso. Dopo la rimozione, R[0] viene interpretato come una stringa il cui campo lunghezza è in realtà quel puntatore, cioè una stringa astronomicamente lunga su cui puoi leggere e scrivere. Lettura e scrittura arbitrarie, confezionate in regalo dal layout di memoria dell'interprete stesso.

Se scrivi C o C++ per mestiere, fissa per un minuto quel controllo. position + count <= length è l'idioma di bounds-check più comune nel codice di sistema, ed è sbagliato ogni volta che la somma può andare in overflow. La forma corretta è position <= length && count <= length - position, oppure l'aritmetica con controllo. Ho trovato varianti di questo bug in un parser di pacchetti in un vecchio lavoro e in un storage engine open source. Sopravvive alla code review perché sembra ovviamente corretto. È il divario specifica-implementazione in miniatura: la specifica dice «valida il range», l'implementazione valida qualcosa di adiacente al range, e la differenza è sfruttabile. Abbiamo scritto di come il divario tra specifica e implementazione è dove vivono i bug; gli interpreti delle console sono solo il posto dove vengono trovati da ragazzi motivati invece che dai fuzzer.

Salire fino al servizio del filesystem con un overflow nella gestione dei path

La lettura e scrittura in userspace all'interno di un processo di gioco da sola non ti porta a nulla: il design microkernel dello Switch fa sì che ogni servizio sia un processo separato con la propria superficie IPC. Il bersaglio prezioso era fs, il sysmodule del filesystem integrato, per una ragione deliziosamente ricorsiva: fs è il componente che serve il codice decifrato al loader. Se possiedi fs, puoi estrarre il codice di ogni altro sysmodule per l'analisi offline. È il quartiermastro della fortezza che ti consegna le planimetrie.

Il bug era un heap overflow nella normalizzazione dei path. Il comando IPC GetEntryType normalizza il suo argomento path, e i path che contengono un componente di directory padre con una barra rovesciata, come /..\, finiscono in un helper che alloca un buffer fisso da 0x301 byte e poi copia il path al suo interno senza un controllo di lunghezza adeguato. Un path abbastanza lungo fa traboccare il buffer negli oggetti heap adiacenti. Il commento del ricercatore alla scoperta è stato una sola parola: «lol».

Quel «lol» dovrebbe bruciare, perché la normalizzazione dei path è esattamente il tipo di codice di plumbing poco appariscente che non riceve mai l'attenzione di sicurezza che riceve la crittografia. L'exploit ha trasformato l'overflow in lettura e scrittura arbitrarie corrompendo un oggetto di cache vicino il cui dirty bit reindirizza l'I/O dei file tramite un puntatore controllato dall'attaccante, un primitivo di heap grooming pulito. Poi, con la scansione della memoria, l'exploit ha trovato l'oggetto di connessione del proprio processo nelle tabelle di fs e ha riscritto l'ID di processo memorizzato con 1, concedendo al processo di gioco il «FullPermission» sul filesystem. Nota cosa è appena successo: nessun controllo crittografico è stato infranto. Il modello dei permessi era imposto da dati in memoria, e l'attaccante poteva scrivere la memoria.

Sezione isometrica di una fortezza su più piani, con una piccola figura che aggira cancelli chiusi attraverso uno stretto corridoio di servizio.
Ogni fase della catena ha aggirato porte irrobustite passando per un corridoio che nessuno aveva pensato di proteggere.

Superare i controlli di firma scambiando il filesystem durante l'esecuzione

L'anello più intelligente della catena è quello che aggira la firma del codice senza toccare una singola chiave. Il loader dei programmi (ldr) verifica due firme prima di eseguire qualsiasi cosa: quella di Nintendo sui metadati dei permessi del processo e della chiave di sviluppo, e quella dello sviluppatore sull'hash dell'header dell'archivio dei contenuti. In teoria, quell'hash fissa transitivamente ogni byte dell'eseguibile. In pratica, c'è una falla: spetta a fs assicurarsi che il filesystem che serve corrisponda davvero all'hash che ha già riportato, e l'attaccante aveva lettura e scrittura arbitrarie dentro fs.

Quindi l'exploit aspetta che il loader abbia richiesto il filesystem dell'eseguibile e che fs abbia già consegnato l'hash valido e la firma, poi sostituisce l'oggetto filesystem dietro l'handle IPC del loader con uno costruito ad hoc, che serve il file di metadati firmato genuino ma un eseguibile sostitutivo arbitrario. I controlli di firma passano perché erano controlli su byte che l'attaccante non ha mai dovuto modificare. La tempistica è imposta riscrivendo l'handle su cui fs attende la connessione del loader, in modo che punti a un evento controllato dall'attaccante dal processo di gioco, trasformando una race condition in un'interposizione deterministica.

La catena crittografica era solida dall'hash dell'header fino al contenuto dell'eseguibile, sulla carta. L'unico anello che esisteva solo come assunzione a runtime, «il processo che serve i file è onesto», è stato quello che si è rotto.

È un bug di time-of-check/time-of-use travestito, ed è la lezione più trasferibile dell'intero articolo. Ogni volta che il componente A valida un dato e il componente B agisce su di esso più tardi tramite un riferimento mutabile, hai una finestra di swap. Una volta ho revisionato un sistema di deployment interno che validava il digest di un'immagine container, poi faceva il pull per tag dal registry al momento del rollout: stesso bug, altro cappello. La soluzione in entrambi i casi è la stessa: validare nel punto di utilizzo, oppure consegnare al consumatore un oggetto immutabile e verificato invece di un handle a qualcosa di cui deve fidarsi che un terzo mantenga onesto. Un design difensivo che presume che ogni intermediario sia compromesso non è paranoia; su una macchina in cui l'attaccante può già scrivere in parte, è semplicemente un modello accurato. È l'argomento centrale di l'ingegneria difensiva per sistemi che resistono alle catastrofi, e questo exploit ne è un caso di studio pulito.

Prendere in prestito un handle del kernel e patchare fs dall'interno

Il trucco finale è piccolo e bellissimo. Per patchare il codice del processo fs, l'exploit aveva bisogno di un handle del kernel verso di esso. Lo ha ottenuto sovrascrivendo in memoria un campo handle di 4 byte con il valore magico 0xffff8001, che il kernel interpreta come «l'handle del processo corrente». Il processo di gioco ha chiamato un'API innocua, ha ricevuto indietro il proprio handle di processo, lo ha passato a un processo canaglia con i permessi del sysmodule di caricamento del codice, e quel processo ha usato chiamate legittime al kernel per mappare e patchare il codice di fs. Ogni singola operazione era autorizzata. È la composizione a costituire il compromesso completo del processo più fidato del dispositivo.

Se suona familiare, è perché i problemi di confused deputy sono ovunque, appena inizi a cercarli: regole sudo che permettono un binario innocuo con una configurazione scrivibile, ruoli IAM cloud che possono passare se stessi a un servizio più potente, sistemi di build che firmano qualsiasi cosa il job CI gli passi. Abbiamo trattato il lato dell'escalation dei privilegi di questo schema in la storia dell'escalation dei privilegi in Unix: i meccanismi cambiano, la forma del bug no. Handle, capability e token sono forti solo quanto l'integrità della memoria che li conserva.

Dove continuano a sbagliare i progettisti del Secure Boot

Se fai un passo indietro, lo schema lungo tutta la catena è coerente, ed è lo stesso dietro la maggior parte delle violazioni enterprise serie che ho revisionato: il nucleo verificato ha retto, e tutto ciò che stava intorno no. Il boot ROM di Nintendo, lo schema di firma e le mitigazioni del kernel erano a posto. A cedere sono stati un bounds check dell'interprete, un buffer di normalizzazione dei path, un campo dei permessi memorizzato in memoria scrivibile, una finestra TOCTOU tra due servizi fidati e una tabella di handle che accettava puntatori del kernel da strutture scrivibili dall'userspace. Nessuno di questi è esotico. Sono tutti il genere di cosa che sopravvive alla revisione perché ognuno sembra ragionevole se preso localmente.

  • Controlla il codice noioso. Normalizzatori di path, controlli di lunghezza e tabelle di handle sono dove le catene di exploit iniziano davvero, non nella crittografia. Dedica tempo di revisione proporzionale alla raggiungibilità da parte dell'attaccante, non alla sofisticazione algoritmica.
  • Tratta ogni lacuna nelle mitigazioni come una roadmap. Se un processo del sistema non ha PAC/CFI, come un layer di compatibilità, un binario di terze parti o un interprete legacy, presumi che sia il punto d'ingresso e modella ciò che può raggiungere.
  • Elimina la fiducia basata sulla posizione. Un componente che valida i dati e passa a valle un handle mutabile ha creato una finestra di swap. Passa oggetti immutabili e auto-verificanti, o riverifica al momento dell'uso.
  • Presumi che la memoria scrivibile ovunque sia scrivibile ovunque. Flag dei permessi, campi PID e valori degli handle memorizzati in memoria raggiungibile dall'attaccante fanno parte della tua superficie d'attacco, non del tuo meccanismo di applicazione.

L'altra conclusione scomoda riguarda gli strumenti. L'exploit è stato sviluppato con un agente AI che aveva accesso fisico all'hardware tramite dispositivi KVM-over-IP: video in ingresso, input del controller via USB e una leva Bluetooth per premere il tasto di accensione dopo ogni crash. Più istanze dell'agente hanno lavorato in parallelo su diverse superfici d'attacco, ventiquattro ore su ventiquattro, per due settimane. Qualunque sia l'opinione sulla ricerca di sicurezza assistita dall'AI, l'economia della scoperta di vulnerabilità su hardware chiuso è cambiata. Bug che richiedevano un esperto paziente ora richiedono un esperto paziente capace di supervisionare tre assistenti instancabili. Se il tuo modello di minaccia presumeva che «nessuno si prenderà la briga di fare reverse engineering di questo sysmodule», quell'ipotesi è scaduta questo mese.

La catena è il prodotto

Viene la tentazione di leggere un write-up su un jailbreak come un elenco degli errori di Nintendo. Leggilo invece come uno specchio. Ogni sistema che ho aiutato a difendere aveva un equivalente di SmileBASIC (il componente legacy che nessuno ha irrobustito), un equivalente del buffer del path (il codice di plumbing cresciuto oltre le proprie assunzioni) e un equivalente della race del loader (la validazione che si fidava di un intermediario mutabile). Il vantaggio degli attaccanti non sta nella genialità di un singolo passo: sta nel fatto che pensano in catene, mentre i difensori pensano in componenti.

Quindi l'esercizio per il tuo team questa settimana non è «qualcuno potrebbe falsificare le nostre firme?». È questo: scegli il servizio interno di cui ti fidi di più, presumi che l'attaccante abbia già una scrittura arbitraria nel processo meno irrobustito che gli parla, e percorri la catena in avanti. Dove atterra la prima escalation? Cosa permette di raggiungere quell'identità? Troverai presto il tuo sysmodule fs. Meglio tu di qualcuno con un KVM, una leva Bluetooth e due settimane di pazienza.