2011
01.02

Comme promis, cette deuxième partie va parler du système de fichiers utilisé sur les DVD de jeux de Wii, et plus précisément sur leur partition de données. La partie 1 de cette série de 3 articles présentait comment accèder à cette partition de données à partir d'un DVD et comment la déchiffrer. Pour cet article nous allons supposer que la partition de données a été déchiffrée et dumpée afin de se simplifier la tâche, mais ça n'est pas nécessaire (on peut déchiffer à la volée). Commençons par de la théorie 🙂

Lorsque la Wii charge un jeu sur DVD, elle commence par charger un bootloader très léger qui se trouve à l'adresse 0x2440. Ce bootloader va ensuite charger un fichier DOL, en gros l'OS tournant derrière le jeu, qui va lui même charger l'exécutable du jeu qui se trouve sur le système de fichiers du DVD (dont le nom dépend de l'ID du jeu). Le bootloader et l'OS ne sont pas sur le système de fichiers et ne nous intéressent pas tant que ça : pour les jeux commerciaux, ce sont toujours les mêmes (ils font partie du SDK de Nintendo). À côté de ces deux composants il y a ce que l'on va tenter d'ouvrir : le système de fichiers.

Sa structure est très simple, et c'est plutôt normal pour un système de fichiers minimaliste en lecture seule (pas de fragmentation à gérer vu qu'on peut l'éviter lorsqu'on build le FS). Déjà, toutes les données sont rassemblées à un endroit et toutes les métadonnées sont rassemblées à un autre endroit. Les métadonnées sont contenues dans ce qu'on appelle la FST pour FileSystem Table et contient l'ensemble des informations sur les fichiers. En pratique, ces informations sont juste le nom du fichier et l'adresse et la taille des données dans le cas d'un fichier classique, ou le nom et la liste des fils pour un dossier. L'emplacement de cette FST n'est pas fixe : il est indiqué à l'offset 0x424. Comme la Wii compte en blocs de 32 bits et que l'on veut une adresse en octets, on multiplie par 4 l'entier trouvé à l'adresse 0x424.

La FST est construite avec une brique de base : le descripteur de fichier. Un descripteur pèse 12 octets et représente les métadonnées d'un fichier ou d'un dossier. Sur ces 12 octets, les 4 premiers sont l'offset vers le nom du fichier, les 4 suivants sont l'offset vers les données (toujours en bloc de 32 bits, donc à multiplier par 4 pour avoir en octets) et les 4 derniers sont la taille des données (en octets cette fois, la cohérence est d'ordre). Pour savoir si un descripteur représente un fichier ou un dossier, on regarde les 8 bits de poids fort de l'offset vers le nom du fichier : s'ils sont tous à 0, c'est un fichier, sinon c'est un dossier.

Qui dit système de fichier dit hiérarchie. Sauf que la FST est une structure linéaire : tous les descripteurs sont les uns à la suite des autres. Pour représenter la hiérarchie, les descripteurs de dossiers vont se servir de leur champ « taille » pour stocker le premier descripteur dont ils ne sont pas père. Par exemple, si la racine contient 3 fichiers, son champ « taille » vaudra 4. Un petit exemple en ASCII art :

+---------------------+
| Dossier 1           |
|   size = 7          |
+---------------------+
    +---------------------+
    | Fichier 1           |
    |   size = 42         |
    +---------------------+
    +---------------------+
    | Dossier 2           |
    |   size = 6          |
    +---------------------+
        +---------------------+
        | Fichier 2           |
        |   size = 1337       |
        +---------------------+
        +---------------------+
        | Fichier 3           |
        |   size = 1234       |
        +---------------------+
        +---------------------+
        | Fichier 4           |
        |   size = 4321       |
        +---------------------+
    +---------------------+
    | Fichier 5           |
    |   size = 101010     |
    +---------------------+

Enfin, le premier descripteur représente la racine, donc sa « taille » est le nombre total de descripteurs. Il n'a pas de nom. Après tous les descripteurs vient la zone où sont stockés les noms de fichiers. Tous les offsets de noms sont relatifs à cet emplacement. Et c'est tout ! Plutôt simple quand on compare à ce qui se fait ailleurs, et efficace pour le besoin d'un jeu de Wii. Passons donc à l'implémentation. Comme précédemment, je ne code rien de « propre » dans cet article : c'est uniquement un transcript d'une session interactive Python. Le beau code viendra dans la dernière partie. On va commencer par importer les modules et ouvrir l'image dumpée de la partition :

Python 2.7.1 (r271:86832, Dec 20 2010, 11:54:29) 
[GCC 4.5.1 20101125 (prerelease)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from collections import namedtuple
>>> from struct import unpack as up
>>> fp = open('tos-2-dumped.img', 'rb')

Ensuite, quelque chose que j'aurais déjà dû faire dans l'article précédent : une tout petite fonction bien utile qui seek puis qui lit. C'est très pratique et ça évite d'oublier de seek après un read :

>>> def read_at(off, len):
...     fp.seek(off)
...     return fp.read(len)

La première étape est de récupérer l'adresse de la FST. On n'oublie pas de la multiplier par 4 :

>>> fst_off = up('>L', read_at(0x424, 4))[0] * 4

On récupère ensuite l'adresse de la fin de la FST, donc du début de la zone qui contient les noms de fichiers. Pour cela, on récupère la taille du premier descripteur (le dossier racine), on la multiplie par 12 pour avoir la taille en octets de la FST et on l'ajoute à l'offset de la FST :

>>> str_off = fst_off + up('>8xL', read_at(fst_off, 12))[0] * 12

La zone contenant les noms de fichiers est une suite de chaines de longueur max 256 séparées par des \0. Python n'a rien de standard pour lire ça (malheureusement !), on va donc faire une petite fonction qui lit un nom de fichier à partir de son offset :

>>> def read_filename(off):
...     s = read_at(str_off + off, 256)
...     return s[:s.index('\0')]
... 
>>> read_filename(0)
'BATTLE'
>>> read_filename(7)
'BG'
>>> read_filename(10)
'btl000.brres'

On va ensuite faire une fonction qui lit un descripteur de fichier et tous ses fils récursivement pour construire l'arborescence. C'est pas forcément simple à comprendre à la première lecture, je commente tout ça après :

>>> def read_descr(idx):
...     descr = read_at(fst_off + 12 * idx, 12)
...     (name_off, data_off, size) = up('>LLL', descr)
...     data_off *= 4
...     
...     is_dir = bool(name_off & 0xFF000000)
...     name_off &= ~0xFF000000
...     name = read_filename(name_off) if idx else ''
...     
...     if not is_dir:
...         return idx + 1, name, (data_off, size)
...     else:
...         children = {}
...         idx += 1
...         while idx < size:
...             idx, child_name, child = read_descr(idx)
...             children[child_name] = child
...         return idx, name, children

Déjà, identifions le retour de la fonction : un triplet qui contient l'index du premier descripteur qui n'a pas été traité (ça nous permet de faciliter la récursion), le nom du fichier ou du dossier représenté par le descripteur, et enfin selon qu'il s'agisse d'un dossier ou d'un fichier, les fils ou l'emplacement des données. Ensuite, au niveau du code, on commence à lire les 12 octets du descripteur. On regarde si c'est un dossier ou nom et on récupére le vrai offset du nom avec un petit masque binaire, en n'oubliant pas que le nom ne veut rien dire pour la racine (idx == 0). Ensuite, si le descripteur est un fichier, on retourne directement son couple (data, size), sinon on va itérer sur tous les indexes entre le premier fils et le dernier fils et on va ajouter tous les descripteurs dans l'ensemble des fils. Et c'est fini !

Pour conclure cet article, on va dumper tous les fichiers et dossiers dans un répertoire sur notre PC. Il suffit de parcourir l'arbre que read_descr, appliqué à la racine, nous donne :

>>> from os.path import exists, join
>>> from os import mkdir
>>> def dump(name, data, where):
...     print 'dumping', name, 'to', where
...     if isinstance(data, dict): # directory
...         path = join(where, name)
...         if not exists(path):
...             mkdir(path)
...         for name, data in data.iteritems():
...             dump(name, data, path)
...     else:
...         print data[0], data[1]
...         data = read_at(data[0], data[1])
...         open(join(where, name), 'wb').write(data)

C'est gagné © ! Pour la prochaine et dernière partie nous verrons comment implémenter un filesystem sous Linux pour pouvoir faire simplement un mount sur notre image de DVD de Wii et y accèder comme on accèderait à une clé USB, sans avoir besoin de tout dumper.

4 comments so far

Add Your Comment
  1. C’est vraiment intéressant.
    Je pensais que c’était plus compliqué que ça leur système de fichiers.

    Merci à toi pour ce rythme de publication soutenu. 😉

    • Au niveau du rythme, la partie 3 attendra un peu car elle ne m’intéresse pas autant que les 2 autres (maintenant que j’ai dumpé tous les fichiers…) et surtout car j’ai des projets pour l’école / des partiels / des cours qui reprennent bientôt.

      Par contre je vais surement faire quelques articles sur les différents formats de fichiers que j’ai trouvé sur le FS : tout ce qui est textures, sons, musiques, vidéos, etc. C’est très peu documenté, à tout casser on trouve 2 ou 3 bouts de code qui savent lire ça sur le net, donc je vais essayer de rassembler ce que je trouve et ce que j’en déduit pour en faire des articles aussi construits que ces deux derniers.

  2. Bien sympa. L’idée finale de pouvoir faire un mount sur cette image à la fin l’est encore plus. :p

    Bonne continuation pour la 3eme partie !

  3. […] of the three part article I wrote in January called "Lire des disques de Wii en Python" (part 1 / part 2 / part 3). Thanks a lot to Kalenz for helping me translate […]

*