Des articles approfondis sur les technologies qui façonnent l'avenir.

Jailbreak Switch 2 : la chaîne qui a battu le Secure Boot

Le jailbreak de la Switch 2 a enchaîné un dépassement dans l'interpréteur BASIC, un bug de chemin du fs et une course dans le loader. Leçons sur les chaînes d'exploits.

Une console portable branchée à du matériel de contrôle à distance, avec un levier robotique appuyant sur son bouton d'alimentation sur un établi.
Les rigs modernes de piratage de consoles associent un KVM sur IP à un levier Bluetooth qui appuie physiquement sur le bouton d'alimentation après chaque plantage.

Une console toute neuve arrive avec un micro-noyau durci, une authentification de pointeurs sur chaque adresse de retour, une mémoire en exécution seule, un contrôle d'intégrité de flux (CFI) et une chaîne de démarrage signée. Deux semaines plus tard, un bidouilleur exécute du code natif arbitraire sous l'identité du service de fichiers, le processus le plus fiable de la machine. C'est l'histoire du premier jailbreak public de la Switch 2, et elle devrait mettre mal à l'aise tout ingénieur qui conçoit des systèmes sécurisés. Non pas parce que les protections ont échoué, la plupart ont fonctionné exactement comme prévu, mais parce que l'exploit n'a jamais eu besoin de les affronter de front. Il les a contournées, un bug de logique à la fois.

Je travaille depuis des années du côté défensif de la sécurité des infrastructures, et le schéma ici, je l'ai vu plus souvent que je ne voudrais dans des revues d'incidents en production : une chaîne de bugs individuellement peu impressionnants, chacun offrant juste assez de privilèges pour atteindre le suivant. Le point d'entrée ne se trouvait même pas dans la Switch 2 elle-même. C'était un bug de corruption mémoire dans un jeu de la Switch 1, l'interpréteur SmileBASIC, exécuté en mode de compatibilité ascendante. De là, la chaîne a remonté jusqu'au service de fichiers, a exploité une course dans le chargeur de programmes, et s'est terminée par la capacité d'exécuter du code natif sous l'identité de n'importe quel sysmodule. Chaque maillon mérite qu'on s'y arrête, car chacun correspond à une erreur que j'ai vu des équipes commettre dans leurs systèmes serveur.

Pourquoi la porte du navigateur était fermée, et celle du jeu ne l'était pas

WebKit est la cible classique pour les jailbreaks de consoles, et le chercheur a bien obtenu un point d'appui de ce côté via un portail captif et un n-day. Mais l'approche s'est vite heurtée à un mur. La pile navigateur de la Switch 2 est enveloppée de trois protections qui se combinent redoutablement bien : la mémoire en exécution seule (XOM) empêche de localiser des gadgets de code utiles, même avec une lecture et une écriture arbitraires ; l'authentification de pointeurs (PAC) empêche de forger un pointeur de code sans oracle de signature ; et le CFI fait que même un pointeur valablement signé ne passera pas les vérifications de type sur la plupart des sites d'appel. Contourner une seule de ces protections est un projet de recherche. Les contourner toutes les trois, en chaîne, depuis un bac à sable JavaScript, c'est une carrière.

L'attaque s'est donc tournée vers le locataire le plus faible de la machine : les jeux tiers de la Switch 1. Le raisonnement était froid et juste. Les binaires de jeux sont compilés par des studios, pas par l'équipe sécurité de Nintendo, donc ils ont beaucoup moins de chances d'être compilés avec toute la pile de protections. Et la Switch 1 n'a jamais eu de PAC, donc tout ce qui tourne dans son environnement de compatibilité hérite de cette faiblesse. Mieux encore, la racine de confiance de la Switch 1 a été compromise il y a des années, ce qui permet de déchiffrer et d'auditer le code des jeux hors ligne. SmileBASIC, un énorme interpréteur BASIC en C++ déjà exploité sur la 3DS, était la cible molle évidente.

Il y a là une leçon qui dépasse largement les consoles : la posture de sécurité de votre système est celle du code le moins durci que vous acceptez d'exécuter. La compatibilité ascendante est une surface d'attaque que vous héritez, et non une que vous choisissez. Le modèle de sécurité de la Xbox l'a appris à ses dépens dans les années 2000 : les exploits de sauvegardes dans des titres non durcis ont servi de coin pour fissurer un noyau bien plus strict, une histoire que nous avons racontée dans ce que le piratage de la Xbox originale enseigne sur les modèles de sécurité matériels. Vingt ans plus tard, la même manœuvre fonctionne toujours.

Le dépassement d'entier qui a ouvert la première porte

Le bug de SmileBASIC est un dépassement d'entier classique dans une vérification de bornes, le genre de chose que toute checklist de revue de code sécurisé demande de repérer, et qui continue pourtant d'être livré. La commande de suppression d'élément de l'interpréteur validait ses arguments ainsi :

// 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
}

Appeler REMOVE R, &H60000000, &H3FFFFFFF sur un tableau de quatre éléments fait boucler position + count vers 0x9FFFFFFF, qui, en entier signé sur 32 bits, est fortement négatif, donc bien « inférieur » à la longueur. Les multiplications par 8 débordent aussi, et on finit par exécuter memmove(backing, backing - 8, 32). La commande affiche ensuite une erreur de mémoire insuffisante, mais la copie a déjà eu lieu. Et voici la partie savoureuse : les 8 octets qui précèdent immédiatement n'importe quel tableau SmileBASIC contiennent un pointeur vers le tableau lui-même. Après la suppression, R[0] est interprété comme une chaîne dont le champ de longueur est en fait ce pointeur, une chaîne astronomiquement longue, en lecture et en écriture. Une lecture et une écriture arbitraires, offertes sur un plateau par la disposition mémoire de l'interpréteur lui-même.

Si vous écrivez du C ou du C++ pour gagner votre vie, regardez cette vérification une minute. position + count <= length est l'idiome de vérification de bornes le plus courant dans le code système, et il est faux dès que la somme peut déborder. La forme correcte est position <= length && count <= length - position, ou une arithmétique vérifiée. J'ai trouvé des variantes de ce bug dans un analyseur de paquets lors d'un ancien poste et dans un moteur de stockage open source. Il survit à la revue de code parce qu'il semble évidemment juste. C'est l'écart entre spécification et implémentation en miniature : la spec dit « valider la plage », l'implémentation valide quelque chose de voisin, et la différence est exploitable. Nous avons écrit sur pourquoi les bugs vivent dans l'écart entre spécification et implémentation ; les interpréteurs de consoles sont simplement l'endroit où ils sont trouvés par des adolescents motivés plutôt que par des fuzzers.

Escalader vers le service de fichiers grâce à un dépassement lié à la gestion des chemins

La lecture et l'écriture en espace utilisateur dans un processus de jeu ne rapportent rien en soi, car la conception en micro-noyau de la Switch fait que chaque service est un processus séparé avec sa propre surface IPC. La cible précieuse était fs, le sysmodule de système de fichiers intégré, pour une raison délicieusement récursive : fs est le composant qui sert le code déchiffré au chargeur. En contrôlant fs, on peut extraire le code de tous les autres sysmodules pour l'analyser hors ligne. L'intendant de la forteresse vous remet lui-même les plans.

Le bug était un dépassement de tas dans la normalisation des chemins. La commande IPC GetEntryType normalise son argument de chemin, et les chemins contenant un composant de répertoire parent avec une barre oblique inverse, /..\, passent dans un utilitaire qui alloue un tampon fixe de 0x301 octets, puis y copie le chemin sans vérification de longueur adéquate. Un chemin suffisamment long déborde sur les objets de tas voisins. Le commentaire du chercheur à la découverte tenait en un mot : « lol ».

Ce « lol » devrait piquer, car la normalisation des chemins est exactement le genre de plomberie peu glamour qui ne reçoit jamais l'examen de sécurité réservé à la cryptographie. L'exploit a transformé le dépassement en lecture et écriture arbitraires complètes en corrompant un objet de cache voisin dont le bit « sale » redirige les E/S de fichiers vers un pointeur contrôlé par l'attaquant, une primitive propre de manipulation du tas. Puis, en scannant la mémoire, l'exploit a trouvé l'objet de connexion de son propre processus dans les tables de fs et a réécrit l'identifiant de processus stocké à 1, accordant au processus de jeu la permission « FullPermission » sur le système de fichiers. Remarquez ce qui vient de se passer : aucun contrôle cryptographique n'a été cassé. Le modèle de permissions était appliqué par des données en mémoire, et l'attaquant pouvait écrire en mémoire.

Coupe isométrique d'une forteresse à plusieurs étages, avec une petite silhouette contournant des portes verrouillées par un étroit couloir de service.
Chaque étape de la chaîne a contourné des portes renforcées en empruntant un couloir que personne n'avait pensé à garder.

Contourner les vérifications de signature en échangeant le système de fichiers en cours de route

Le maillon le plus malin de la chaîne est celui qui contourne la signature de code sans toucher à une seule clé. Le chargeur de programmes (ldr) vérifie deux signatures avant d'exécuter quoi que ce soit : celle de Nintendo sur les métadonnées de permissions du processus et la clé développeur, et celle du développeur sur le hachage d'en-tête de l'archive de contenu. En théorie, ce hachage fixe transitivement chaque octet de l'exécutable. En pratique, il y a une faille : c'est au travail de fs de s'assurer que le système de fichiers qu'il sert correspond bien au hachage qu'il a déjà annoncé, et l'attaquant disposait d'une lecture et d'une écriture arbitraires dans fs.

L'exploit attend donc que le chargeur ait demandé le système de fichiers de l'exécutable et que fs ait déjà transmis le hachage et la signature valides. Il remplace alors l'objet de fichiers derrière le handle IPC du chargeur par un objet forgé qui sert les vraies métadonnées signées, mais un exécutable de remplacement arbitraire. Les vérifications de signature passent, car elles portaient sur des octets que l'attaquant n'a jamais eu besoin de modifier. Le timing est imposé en réécrivant le handle sur lequel fs attend la connexion du chargeur, pour le transformer en événement contrôlé par l'attaquant depuis le processus de jeu : une condition de course devenue une interposition déterministe.

La chaîne cryptographique était saine, du hachage d'en-tête au contenu de l'exécutable, sur le papier. Le seul maillon qui n'existait que comme hypothèse d'exécution, « le processus qui sert les fichiers est honnête », est précisément celui qui a cédé.

C'est un bug de type time-of-check/time-of-use déguisé, et c'est la leçon la plus transposable de tout ce compte rendu. Dès que le composant A valide des données et que le composant B agit ensuite dessus via une référence modifiable, il existe une fenêtre d'échange. J'ai un jour examiné un système de déploiement interne qui validait le condensat d'une image de conteneur, puis tirait l'image par tag depuis le registre au moment du déploiement : même bug, autre chapeau. La correction est la même dans les deux cas : valider au moment de l'usage, ou remettre au consommateur un objet immuable et vérifié plutôt qu'un handle vers quelque chose qu'il doit faire confiance à un tiers pour garder honnête. Une conception défensive qui suppose que chaque intermédiaire est compromis n'est pas de la paranoïa ; sur une machine où l'attaquant peut déjà écrire en partie, c'est simplement une modélisation juste. C'est l'argument central de l'ingénierie défensive pour des systèmes qui résistent aux catastrophes, et cet exploit en est une étude de cas limpide.

Emprunter un handle noyau et patcher fs de l'intérieur

La dernière astuce est petite et magnifique. Pour patcher le code du processus fs, l'exploit avait besoin d'un handle noyau vers lui. Il l'a obtenu en écrasant en mémoire un champ de handle de 4 octets avec la valeur magique 0xffff8001, que le noyau interprète comme « le handle du processus courant ». Le processus de jeu a appelé une API anodine, a reçu en retour son propre handle de processus, l'a transmis à un processus voyou disposant des permissions du sysmodule de chargement de code, et ce processus a utilisé des appels noyau parfaitement légitimes pour mapper et patcher le code de fs. Chaque opération individuelle était autorisée. C'est la composition qui a donné une compromission totale du processus le plus fiable de l'appareil.

Si cela vous semble familier, c'est que les problèmes de « député confus » (confused deputy) sont partout dès qu'on commence à regarder : des règles sudo qui autorisent un binaire anodin avec une configuration inscriptible, des rôles IAM cloud qui peuvent se transmettre à un service plus puissant, des systèmes de build qui signent tout ce que le job CI leur remet. Nous avons traité le versant escalade de privilèges de ce schéma dans l'histoire de l'escalade de privilèges sous Unix : les mécanismes changent, la forme du bug, non. Les handles, capacités et jetons ne valent que l'intégrité de la mémoire qui les stocke.

Ce que les concepteurs de Secure Boot continuent de mal faire

Prenez du recul et le schéma de toute la chaîne est cohérent, et c'est le même que derrière la plupart des brèches graves en entreprise que j'ai examinées : le noyau vérifié a tenu, tout ce qui l'entourait non. La ROM de démarrage de Nintendo, son schéma de signature et les protections du noyau étaient corrects. Ce qui a cédé, c'est une vérification de bornes d'interpréteur, un tampon de normalisation de chemins, un champ de permissions stocké en mémoire inscriptible, une fenêtre TOCTOU entre deux services de confiance, et une table de handles qui acceptait des pointeurs noyau depuis des structures modifiables depuis l'espace utilisateur. Rien de tout cela n'est exotique. Tout cela relève du genre de chose qui survit à la revue parce que chaque élément paraît raisonnable pris isolément.

  • Auditez le code ennuyeux. Les normaliseurs de chemins, les vérifications de longueur et les tables de handles sont là où les chaînes d'exploits commencent réellement, pas dans la cryptographie. Consacrez du temps de revue proportionnel à l'accessibilité pour un attaquant, pas à la sophistication algorithmique.
  • Traitez chaque lacune de protection comme une feuille de route. Si un processus du système n'a pas de PAC ni de CFI (une couche de compatibilité, un binaire tiers, un interpréteur hérité), supposez que c'est le point d'entrée et modélisez ce qu'il peut atteindre.
  • Éliminez la confiance fondée sur la position. Un composant qui valide des données puis transmet en aval un handle modifiable a créé une fenêtre d'échange. Passez des objets immuables et auto-vérifiables, ou revérifiez au moment de l'usage.
  • Supposez que toute mémoire inscriptible est inscriptible partout. Les drapeaux de permissions, les champs PID et les valeurs de handles stockés dans une mémoire accessible à l'attaquant font partie de votre surface d'attaque, pas de votre mécanisme d'application.

L'autre constat dérangeant concerne l'outillage. L'exploit a été développé avec un agent IA disposant d'un accès physique au matériel via des dispositifs KVM sur IP : vidéo en entrée, commandes de manette via USB, et un levier Bluetooth pour appuyer sur le bouton d'alimentation après chaque plantage. Plusieurs instances de l'agent ont travaillé en parallèle sur différentes surfaces d'attaque, jour et nuit, pendant deux semaines. Quoi que l'on pense de la recherche en sécurité assistée par IA, l'économie de la découverte de vulnérabilités sur du matériel fermé vient de changer. Des bugs qui demandaient un expert patient demandent maintenant un expert patient capable de superviser trois assistants infatigables. Si votre modèle de menace supposait que « personne ne prendra la peine de faire du reverse engineering sur ce sysmodule », cette hypothèse a expiré ce mois-ci.

La chaîne, c'est le produit

Il est tentant de lire un compte rendu de jailbreak comme une liste d'erreurs de Nintendo. Lisez-le plutôt comme un miroir. Chaque système que j'ai aidé à défendre avait son équivalent de SmileBASIC (le composant hérité que personne n'avait durci), son équivalent du tampon de chemin (le code de plomberie qui a dépassé ses hypothèses) et son équivalent de la course du chargeur (la validation qui faisait confiance à un intermédiaire modifiable). L'avantage des attaquants n'est pas une brillance à une étape donnée : ils raisonnent en chaînes, tandis que les défenseurs raisonnent en composants.

L'exercice pour votre équipe cette semaine n'est donc pas « quelqu'un pourrait-il forger nos signatures ? ». C'est : choisissez votre service interne le plus fiable, supposez que l'attaquant dispose déjà d'une écriture arbitraire dans le processus le moins durci qui lui parle, et parcourez la chaîne en avant. Où atterrit la première escalade ? Qu'est-ce que cette identité lui permet d'atteindre ? Vous trouverez bien assez tôt votre propre sysmodule fs. Mieux vaut que ce soit vous qui le trouviez plutôt que quelqu'un qui dispose d'un KVM, d'un levier Bluetooth et de deux semaines de patience.