2011
03.25

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 :

Sauvegarde corrompue

É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.

Première modification de sauvegarde Deuxième modification de sauvegarde

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 :

Unknown pointer 0x41414140

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 :

Ensemble de Mandelbrot affiché par une 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 :

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).

20 comments so far

Add Your Comment
  1. Félicitations. 😉 C’est très intéressant tout ça et pas mal enrichissant. Bref, heureux que tu maintiennes ton blog et que tu le mettes à jour assez souvent pour nous faire partager tes aventures.

    Encore bravo pour ta patience et ton ingéniosité.

  2. Haha, j’avais pas vu l’heure à laquelle l’article a été posté. 😀 Un vrai hackeur des ténèbres. \o/

  3. Bon travail !
    Petite question : est-ce que ton jailbreak fonctionne quelque soit le mode d’affichage de la Wii : 16/9, 4/3, 50Hz, 60HZ, … car avec Yu Gi Ho 5’D les sauvegardes sont différentes ?
    Encore bravo !

    • A priori (je n’ai pas tout testé) ça devrait marcher. Si ça n’est pas le cas, patch welcomed 🙂

  4. Yop, tout d’abord félicitation pour cet exploit et cet article 😀
    J’aurais cependant quelques petites questions: j’essaye de reproduire ton épopée sur Mario Kart Wii (26.5M de ventes). Les variables doivent-elles être forcément des strings? (Je pense à des “replays” ou fantômes enregistrés dans la sauvegarde).
    Est-ce possible d’avoir ton adresse MSN? (C’est fucking la classe d’avoir un hacker sur MSN quand même ^^)
    Bye, et encore bravo pour cet article très explicite.

    • Ça dépend du bug exploité. Dans mon cas c’était un buffer overflow sur une copie de chaine, mais ça pourrait très bien être autre chose.

      Par ailleurs, note que si tu poses ce genre de questions, c’est qu’il te manque définitivement des bases de sécurité informatique qui sont plus que nécessaires.

      Et non. je n’ai pas d’adresse MSN. J’ai une adresse mail par contre, delroth@gmail.com.

  5. Salut, c’est vrai que c’est un article très intéressant qui nous permet un peu de conaître le backstage d’un tel exploit. Félicitation pour tout ça !

    • je m’associe à RiderFx3 pour te féliciter !

      • La même, bravo !

        • +1,
          Sérieux, félicitations! Tu pourrais sans doute même tirer partie de tes recherches dans le cadre de ton mémoire de fin d’études. Dans la catégorie “sécurité informatique”!
          🙂
          Bien joué !!!!
          Et merci !!!

  6. Et bien, j’ai bien aimé te lire. C’était très intéressant!

    C’est moi ou les développeurs sur wii protège mal leurs jeux des overflows?

    Sinon, j’aimerais bien tenter l’expérience un jour. j’ai déjà essayer sur psp, mais je crois que la plupart des jeux sont bien construits…. Ah aussi, je remarque que ça semble plus facile sur la wii, tu n’as pas eu de problème au niveau des buffers? Car sur la psp, il faut que ce soit un tel buffer touché pour que ça soit possible… Enfin bon…

    En tout cas, merci pour tout ça, c’était très enrichissant!

  7. chapeau !

  8. mille bravo mais j aimerais savoir comment as tu su qu il y avait une faille dans ce jeu et quel langage doit on maitriser ?
    encore bravo

  9. tu as mon plus grand respect

    tes un vrais géni Mr.HP (Hackeur Pro) 🙂

  10. Félicitation delroth pour ton exploit.
    @cam : je pense qu’il faut bien connaitre le C
    Et savoir compter hexadecimal, savoir se démerder avec de l’hexadécimal.

  11. Hello again,
    au fait, tu dis avoir résolu le soucis du freeze sur le jeu lors de l’appel du payload (heu… intégré à ton exploit, ou appelé et donc à placer quelque part sur la SD ?…)… hors, je vis cette même frustrante expérience.
    Je n’ai peut-être pas la dernière version de ton code, j’ai pourtant pris celle indiquée en lien…
    un conseil?
    Merci par avance

  12. Oops… je parle d’un lien qui fait référence à la “dernière” version de ton code, pas de ton source que j’aurai compilé (chui pas équipé pour !! 🙂 )
    A++

    • C’était simplement un problème de données corrompues sur la carte SD que j’utilisais. Refait la copie et espère que ça marche mieux 🙂

      Ça freeze quand exactement ? Est-ce que la lumière du lecteur CD clignote ?

  13. Salut delroth,

    merci pour cet article très intéressant !
    J’ai une petite question de curiosité, pas vraiment au niveau du hack en lui même mais plus de la prog’ Wii.
    Tu as dit avoir fait un programme pour allumer la LED de la Wii.
    Pour cela, tu as fait du C/C++ “natif”, ou bien utilisé une lib comme LibOGC (de DevKitProPPC) ?

    Car pour tout dire, je suis pas (encore) trop à l’aise avec les notions électronique, enfin je ne sais pas vraiment où trouver les docs pour avoir l’adresse mémoire pour contrôler la LED par exemple..

    Donc si tu as fait du “natif”, ça m’intéresserait grandement si tu pouvais m’expliquer ^^

    En tout cas, merci encore pour l’article 🙂

    • Bon laisses tomber j’ai trouvé quelque chose sur WiiBrew.
      Tu as dû faire un copier coller du code de là bas.

*