Le 25 décembre dernier, j'ai eu la bonne surprise de me faire offrir une Wii (la version « anniversaire des 25 ans de Mario ») par quelqu'un de ma famille. Très vite, étant développeur et curieux, je me suis intéressé à comment fonctionne cette console et comment, potentiellement, exécuter du code dessus. Cela a notamment mené à ma série d'article sur le format des DVD de jeux de Wii, mais également à une tonne de recherche et de reverse engineering de mon côté pour comprendre le fonctionnement du jeu en question.
Cependant, même si j'en ai appris beaucoup, cela ne me permettait pas d'exécuter mon propre code sur la console. C'est plutôt triste, étant donné que j'aimerais bien faire plus que lire des descriptions du hardware : manipuler, c'est priceless. Pour exécuter des binaires non officiels sur une plateforme fermée, il faut passer par une méthode que l'on appelle le jailbreak. Cela passe en général par l'exploitation d'une faille dans un logiciel sur la plateforme (dans le cas d'une console, un jeu par exemple) qui permet d'exécuter du code, puis de rooter la plateforme en question. Sur l'iPhone, il y a par exemple eu la faille du lecteur PDF qui permettait de jailbreaker le téléphone directement en accédant à une page web. Sur la Wii, l'exploit Bannerbomb permettait l'an dernier de jailbreaker une Wii en lui faisant lire une image malformée. Bref, les exemples ne manquent pas.
La Wii fait tourner un firmware, composé d'un OS et d'une interface graphique permettant de lancer un jeu, de changer les paramètres de la console ou de copier les sauvegardes sur une carte SD. Ce firmware est disponible dans de nombreuses versions et est régulièrement updaté. La dernière version, fournie avec ma Wii, est la version 4.3. Les seules méthodes de jailbreak fonctionnant sur cette version de la console passent par des failles dans des jeux, comme par exemple Lego Indiana Jones ou Yu-Gi-Oh Wheelie Breaker. Le problème est que ces jeux, une fois prouvés vulnérables, ne sont plus vendus dans le commerce et se revendent à prix très élevé sur internet (étant donné que les gens les achètent uniquement pour jailbreaker et le revendent juste après). Je n'ai aucun de ces jeux, et après les avoir cherché pendant quelques temps dans les magasins près de chez moi, je me suis rendu compte qu'il n'y avait surement aucune chance que je les trouve à un prix peu élevé.
Je n'ai en effet que 4 jeux de Wii chez moi : Madworld, New Super Mario Bros Wii, Final Fantasy Crystal Chronicles: Echoes of Time et Tales of Symphonia: Dawn of the New World. C'est ce dernier jeu que j'ai en grande partie exploré lors de mes tentatives de mieux comprendre la console et son fonctionnement. Parmi ces tentatives, j'ai passé un certain temps à essayer de comprendre le format des sauvegardes du jeu. À cette époque, je n'avais aucune envie de jailbreaker ma Wii (pas besoin pour le moment, les jeux étaient chers, etc.) et je faisais uniquement ça dans le but de documenter le format, afin de pourquoi pas modifier les sauvegardes, ce qui me permettrait de tester plein de choses le moment venu.
Les sauvegardes du jeu sont en gros composées de deux parties : une header d'une taille très réduite, puis le corps de la sauvegarde, pesant environ 200K. Le header contient uniquement les données à afficher dans la liste des sauvegardes (temps de jeu, personnages dans l'équipe, endroit dans le jeu où la sauvegarde a été faite, etc.). Le corps de la sauvegarde, lui, contient tout : il duplique les données du header, mais contient également des informations plus détaillées, notamment sur les personnages (pas besoin d'avoir leurs statistiques complètes si c'est juste pour la liste des sauvegardes, par exemple). Je me suis donc lancé dans des modifications, pour voir l'effet que cela a sur le jeu. J'ai changé le nom d'un personnage dans le header, ce qui m'a donné ceci :
Échec n°1 (je vais les compter, just for fun). J'ai donc cherché ce qui permettant de détecter la corruption des données, et après analyse de différentes sauvegardes correctes que j'ai fait avec le jeu, j'en ai déduit que les 8 premiers octets du header servaient de checksum. Cependant, il me restait à trouver la méthode par laquelle était faite le checksum. C'est beaucoup plus difficile, étant donné qu'il y a plus de 20 mégaoctets de code compilé dans le jeu. J'ai donc modifié les sources de l'interpréteur PowerPC de l'émulateur Dolphin afin de m'indiquer lorsqu'une lecture était faite dans ces 8 octets de checksum, pour savoir quand ils étaient effectivement vérifiés. Grâce à cela, j'ai pu arriver sur le code de la fonction qui vérifie ce checksum, et en extraire l'algorithme. Beaucoup plus simple que je le pensais : les 4 premiers octets de checksum servent à vérifier le header, et sont le résultat de la somme des 162 entiers de 32 bits suivant le checksum. Les 4 derniers octets de checksum servent à vérifier l'ensemble de la sauvegarde, et sont eux le résultat de la somme des 51908 entiers de 32 bits suivant le checksum. J'ai codé un petit outil minimaliste calculant ce checksum, et j'ai ainsi pu charger des sauvegardes modifiées. Cela m'a pris 2 jours.
Ensuite, j'ai essayé de comprendre l'intérêt des différentes valeurs contenues dans le fichier binaire représentant une sauvegarde. Certains étaient évidents : tout ce qui est affiché à l'écran, par exemple, est simple à interpréter. D'autres me sont encore inconnus. Ce n'est cependant pas vraiment l'objectif de l'article de parler de cela. Quelque chose a attiré mon attention pendant cette exploration du fichier : les noms des personnages semblent avoir une taille fixe réservée, mais ils sont en fait copiés en mémoire jusqu'au premier caractère nul. Ce n'est à mon avis pas une erreur idiote dans le code de chargement (un strcpy sans vérifier la taille, par exemple) mais plutôt le résultat d'un chargement écrit comme ceci :
struct personnage
{
char name[32];
int level;
int hp;
int mp;
// ...
};
// Code de chargement
fread(&perso, 1, sizeof (struct personnage), fp);
Du coup, lors de la manipulation du nom du personnage, on peut potentiellement utiliser une chaîne qui fait plus de 32 caractères alors que le code semble indiquer que sa taille maximale est de 32. Mon esprit de hacker s'allumant, j'ai donc tenté d'y mettre une chaîne contenant un très grand nombre de A (environ 1000). J'avais donc un personnage dont le nom était très grand. Lorsque j'ai essayé d’accéder à l'écran de statut du personnage j'ai eu le droit à ceci venant de mon émulateur :
Cela pouvait vouloir dire 2 choses :
- L'émulateur a essayé de lire ou écrire à cette adresse et a échoué. Cela pourrait arriver « normalement » si des pointeurs étaient sauvegardés en dur dans la sauvegarde (vu qu'on est dans un environnement contrôlé, sans ASLR et avec du code toujours chargé à la même adresse, pourquoi pas !). Cependant, après inspection d'une sauvegarde non altérée, rien qui ne ressemble à un pointeur valide est stocké dans la zone contenant uniquement des A. La seule solution pour qu'une lecture ou écriture ait été faite à cette adresse serait que le nom à rallonge ait écrasé un pointeur, ce qui nous permet de faire des trucs drôles.
- L'émulateur a tenté de jumper à cette adresse et a échoué. C'est encore pire ! Cela veut dire que j'aurais écrasé avec mes A un pointeur sur fonction ou une adresse de retour stockée dans la pile. L'exploitation serait même plus simple que dans le premier cas.
Après modification de l'émulateur pour clarifier le message, il se trouve que l'on est dans le deuxième cas : j'ai écrasé l'adresse de retour de la fonction dans la pile avec mes A. À coup de modifications successives de la sauvegarde et de tests, j'ai pu trouver exactement l'endroit dans la chaîne qui écrasait l'adresse de retour : il s'agit de l'offset 0x144. En mettant à cet offset une adresse valide, l'émulateur y saute et exécute le code.
Il m'a donc fallu du code à exécuter. J'ai tout d'abord fait très simple et assemblé deux instructions qui allaient lire à une adresse connue (0x13371337) un entier en mémoire, ce qui faisait évidemment une erreur de lecture (qui me serait reportée par l'émulateur). J'ai ensuite placé cela à un endroit dans la sauvegarde qui semblait ne contenir que des 0. J'ai réalisé un dump de la mémoire de la console après le chargement de la sauvegarde pour trouver exactement l'adresse à laquelle jumper, et je l'ai mise à l'offset 0x144 du nom. Success !
Je pouvais donc à ce point exécuter du code sur la console. Sauf que placer du code directement dans la sauvegarde, en plus d'être archaïque, ne permet pas de mettre beaucoup de code (40-50K au grand maximum), et est tout sauf pratique. J'ai donc largement profité du fait que les gens avant moi ont déjà développé des payloads permettant de faire cela. Le dépot savezelda de Segher contient notamment un dossier loader avec du code dédié à charger un binaire ELF depuis la carte SD. J'ai donc recompilé ce code en faisant en sorte qu'il soit bien linké pour être lancé à l'adresse où il serait en mémoire. Un bug inexplicable de GCC fait que le code ne linke pas en -Os, ce qui m'a fait perdre une bonne douzaine d'heures (qui se douterait que l'utilisation de -Os causerait des problèmes au link liés à l'EABI ?). Après un peu de boulot, j'ai donc réussi à exécuter un homebrew basique sur Dolphin, l'émulateur Wii :
Vient ensuite l'étape fatidique : l'exécution sur du vrai hardware. Échec n°2 : le jeu freeze au lieu de charger le payload. J'ai changé le code du loader par une simple instruction allumant la lumière du lecteur CD de la Wii, rien ne fonctionne. Le 20 janvier, j'arrête de travailler sur cet exploit, en me disant qu'il venait très certainement d'un bug dans l'émulateur. Je me suis concentré sur le reverse engineering de l'interpréteur de script du jeu.
Cependant, il y a une semaine, je me suis souvenu de ce début de faille, et j'ai décidé d'investiguer le problème, ma motivation étant revenue. Je me suis rendu compte que le stack overflow me permettant d'écraser l'adresse de retour dans la stack est fait au tout début d'une grosse fonction, et le jump vers mon code est fait à la fin de cette grosse fonction. J'ai ainsi émis l'hypothèse que j'écrasais également des variables locales, faisant planter le jeu.
J'ai ainsi changé la valeur avec laquelle j'écrasais la stack, en passant de A (0x61) à simplement 0x01. Et là, magie ! Mon code PowerPC disait « Que la lumière soit », et la lumière fut :
Joie. J'ai ensuite repris le loader afin de charger un homebrew (un simple pong en mode texte) depuis ma carte SD. J'ai fait une petite vidéo de l'exploit (mauvaise qualité, iPhone, toussa). Cependant, les 3/4 des programmes que je tentais de lancer plantaient aléatoirement, notamment parmi eux le Hackmii Installer, merveilleux outil permettant d'installer le Homebrew Channel qui permet définitivement de lancer des homebrews sans avoir besoin d'exploit.
Hier, je me suis enfin rendu compte du problème : j'exécutais le code sans avoir cleanup le contexte vidéo avant cela. M'inspirant des autres exploits de jeux, j'ai donc cherché dans le code du jeu l'offset de la fonction video_stop qui fait partie du SDK de Nintendo. Avec ça, j'ai pu lancer le Hackmii Installer et définitivement jailbreaker ma Wii.
Voilà ce que j'appelle jailbreaker une console comme un homme :-)
Pour la suite des événements, selon comment mes partiels se dérouleront la semaine prochaine, j'essaierai de faire une release le plus rapidement possible d'un truc propre et qui marche sur toutes les Wii PAL (je n'ai rien pour tester sur une Wii NTSC mais ça devrait être la même chose à quelques offsets près). Je tiens à remercier tous les gens de #wiidev @ efnet qui m'ont bien aidé à debugger quelques conneries, malgré ma grande newbiness, mais également tous les gens qui ont release des outils libres m'ayant permis de gagner des semaines (notamment segher et la Team Twiizers). Enfin, merci aux gens du LSE (Laboratoire Système de l'EPITA) qui ont supporté mes monologues, mes « YES! » de joie, et qui m'ont apporté une expertise de l'architecture PowerPC que je n'avais pas (ainsi que beaucoup de coca).