2011
06.10

Reading Wii discs with Python

This big article is a translation 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 it!

What I mean by reading a Wii disc is simple: from a Wii DVD image, being able to get metadata about the game, like its name or its unique ID, but also being able to read the filesystem on the disc to access the game executable and data. We'll do this in three parts: first, we'll decrypt the disc clusters to be able to access the raw partition data, then we'll parse the filesystem to access files and directories, and we'll end this by a presentation of wiiodfs, the software I created to mount Wii discs on Linux using FUSE.

I currently only have one game disc image on my computer: the one from Tales of Symphonia: Dawn of the New World, PAL version, whose ID is RT4PAF (sha1sum: b2fb05a7fdf172ea61b5d1872e6b121140c95822). I'm going to work on this disc image for my tests, and if needed fix things when I'll have to open another game DVD image which doesn't work. To write this article, I'm using documentation from WiiBrew, a wiki about Wii homebrew with a lot of technical informations, and the source code of Dolphin, the Wii emulator (mostly in the Source/Core/DiscIO directory). Thanks a lot to all of the contributors to these projects.

All of the examples from this article are given in the form of Python shell transcripts. If you only want a working version of the software to use at home, look at the third part of this article, where I'm talking about wiiodfs.

Reading raw data from a partition

Some facts about Wii discs: they start with a simple header containing metadata about the game (name, ID, disc number, disc type, etc.), placed at offset 0. At offset 0x40000 is the volume group table, which contains all the partitions on the disc. Wii discs generally contain several partitions: at least one for the game and one containing the system upgrades. Each of these partitions have a type: 0 is a data partition, 1 is a system upgrade partition. We'll only look at the data partition, I'm not interested in the system upgrade files. The data partition starts with another header containing informations needed to decrypt the partition data. All of the data on a Wii disc are encrypted using the AES algorithm, with a 128 bytes key.

Let's start by importing useful modules and opening our disc image:

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
>>> from Crypto.Cipher import AES
>>> fp = open('tos-2.img', 'rb')

We'll read the disc header at offset 0x0 using struct.unpack. It's really easy: the header has a fixed size and fixed size fields.

>>> DiscHeader = namedtuple('DiscHeader',
...     'disc_id game_code region_code maker_code disc_number disc_version '
...     'audio_streaming stream_bufsize wii_magic gc_magic title'
... )
>>> disc_hdr = DiscHeader(*up('>c2sc2sBBBB14xLL64s', fp.read(96)))
>>> disc_hdr
DiscHeader(disc_id='R', game_code='T4', region_code='P', maker_code='AF',
           disc_number=0, disc_version=0, audio_streaming=0,
           stream_bufsize=0, wii_magic=1562156707, gc_magic=0,
           title='Tales of Symphonia: Dawn of the New World' + n*'\x00')

The game metadata are all contained in this header: game ID (RT4PAF, separated into disc_id, game_code, region_code and maker_code), game title (zero terminated), and a magic number, 0x5d1c9ea3, which confirms that this is actually a Wii disc. Next step is the volume group table. Let's see what it looks like using xxd:

$ xxd -s 0x40000 -l 48 tos-2.img
0040000: 0000 0002 0001 0008 0000 0000 0000 0000  ................
0040010: 0000 0000 0000 0000 0000 0000 0000 0000  ................

There are 4 volume groups containing multiple partitions. That means a partition is identified by its VG id and its partition ID. The VG table we just dumped contains for each VG two 32 bytes words: the first one is the number of partitions in that VG, and the second one is the offset to the partition table (the offset is in block of 4 bytes, so we must multiply it by 4 to get an offset in bytes). Here, we have the first VG containing 2 partitions, and its partition table is at offset 0x10008 * 4, which is 0x40020. Let's use xxd again:

$ xxd -s 0x40020 -l 16 tos-2.img
0040020: 0001 4000 0000 0001 03e0 0000 0000 0000  ..@.............

Each entry in this table contains first the offset to the partition (again, in block of 4 bytes), but also the type of the partition. As said above, type 1 = system upgrade partition and type 0 = game data. Here, we have an update partition at offset 0x14000 * 4 and a game data partition at offset 0x3e00000 * 4. Using Python, we can dump all of the VG entries and all of the partitions in an easy to read format:

>>> PartEntry = namedtuple('PartEntry', 'offset type')
>>> def read_part_entry(offset):
...     fp.seek(offset)
...     (data_offset, type) = up('>LL', fp.read(8))
...     data_offset *= 4
...     return PartEntry(data_offset, type)
>>> 
>>> read_part_entry(0x40020)
PartEntry(offset=327680, type=1)
>>> read_part_entry(0x40028)
PartEntry(offset=260046848, type=0)
>>> 
>>> VGEntry = namedtuple('VGEntry', 'part_count table_offset')
>>> def read_vg_entry(offset):
...     fp.seek(offset)
...     (part_count, table_offset) = up('>LL', fp.read(8))
...     table_offset *= 4
...     return VGEntry(part_count, table_offset)
... 
>>> read_vg_entry(0x40000)
VGEntry(part_count=2, table_offset=262176)
>>> read_vg_entry(0x40008)
VGEntry(part_count=0, table_offset=0)
>>> 
>>> def read_part_table():
...     base_off = 0x40000
...     vgs = {}
...     for vg_num in xrange(4):
...         vg_ent = read_vg_entry(base_off + 8 * vg_num)
...         if vg_ent.part_count == 0:
...             continue
...         vgs[vg_num] = {}
...         for part_num in xrange(vg_ent.part_count):
...             off = vg_ent.table_offset + 8 * part_num
...             part = read_part_entry(off)
...             vgs[vg_num][part_num] = part
...     return vgs
... 
>>> read_part_table()
{0: {0: PartEntry(offset=327680, type=1),
     1: PartEntry(offset=260046848, type=0)}}

The game data partition is encrypted using AES in CBC mode with a key stored in the partition header. This key is itself encrypted using a master key, which is the same on every Wii console, and is stored in One Time Programmable memory inside the Wii CPU at manufacturing time. This key has been known for a long time and can be found for example in the Dolphin code. AES CBC needs a key and an IV (Initial Vector) to proceed. The IV is also stored in the partition header: it is part of the title ID which uniquely identifies a game and is for example used to know where a game stores its data on the console NAND. We have all the informations we need to recover the game specific key (aka. title key), let's start by parsing the ticket, which is the data structure containing all the keys and infos we need about a title:

>>> Ticket = namedtuple('Ticket',
...     'enc_tit_key tit_id data_off data_len'
... )
>>> part = read_part_table()[0][1]
>>> fp.seek(part.offset)
>>> ticket = Ticket(*up('>447x16s13x16s204xLL', fp.read(704)))

For a reason I don't know, Nintendo did not use the whole title ID as the IV but only the first 8 bytes. The 8 other bytes are filled with 0. Let's decrypt the title key!

>>> master_key = '\xeb\xe4\x2a\x22\x5e\x85\x93\xe4'
>>> master_key += '\x48\xd9\xc5\x45\x73\x81\xaa\xf7'
>>> 
>>> iv = ticket.tit_id[:8] + '\x00' * 8
>>> 
>>> aes = AES.new(master_key, AES.MODE_CBC, iv)
>>> key = aes.decrypt(ticket.enc_tit_key)
>>> key
'U\x84\xfb\x8b\x10\xdfu=B;\xdcyF\xd4G\x9d'

With that, we are now able to decrypt the partition contents. Data are stored at an offset found in the ticket (data_off in the ticket parsing code above). From that offset, data is organized in clusters of 0x8000 bytes. These clusters contain each 0x400 bytes of hashing informations to validate data integrity, and 0x7C00 bytes of encrypted data. To decrypt the data, we use the title key, and the IV is stored in the first 0x400 bytes of the cluster, at offset 0x3D0:

>>> def read_cluster(idx):
...     data_offset = part.offset + ticket.data_off * 4
...     cluster_offset = data_offset + idx * 0x8000
...     fp.seek(cluster_offset)
...     data_enc = fp.read(0x8000)
...     iv = data_enc[0x3D0:0x3E0]
...     aes = AES.new(key, AES.MODE_CBC, iv)
...     return aes.decrypt(data_enc[0x400:])

Let's test that by decoding the first 20 clusters and looking at their content:

>>> for i in xrange(20):
...     open('/tmp/cluster%d' % i, 'wb').write(read_cluster(i))

$ strings /tmp/cluster* | less
[...]
This Apploader built %s %s for RVL
APPLOADER WARNING >>> Older version of DEVKIT BOOT PROGRAM.
APPLOADER WARNING >>> Use v1.07 or later.
APPLOADER ERROR >>> FSTLength(%d) in BB2 is greater than FSTMaxLength(%d)
APPLOADER ERROR >>> Debug monitor size (%d) should be a multiple of 32
APPLOADER ERROR >>> Simulated memory size (%d) should be a multiple of 32
[...]

Success! Data seems to have been read and decrypted correctly from the partition (if it's working for the first 20 clusters, we can certainly assume that it will work for the rest of the partition). Let's end this part by dumping the entire partition to be able to analyze the filesystem easily later. The partition size is stored in the ticket, let's go!

>>> nclusters = ticket.data_len * 4 / 0x8000
>>> out_fp = open('/path/to/tos-2-dumped.img', 'wb')
>>> for i in xrange(nclusters):
...     print '%f%%' % (i * 100.0 / nclusters)
...     out_fp.write(read_cluster(i))

We now have a tos-2-dumped.img file containing the raw partition data, with the filesystem and the game data on it.

Parsing the filesystem

On the partition we just dumped, there are three main parts to analyze:

  • The apploader, which is a small stub of code identical on each game and distributed in the Nintendo Wii SDK, whose role is to load the game executable in memory
  • The game executable, whose sections are not stored linearly. I won't spend a lot of time on how it is stored, but if I remember correctly each section contains the offset to the next section, and you can easily recreate a DOL executable (the format of most Wii/GC executables) which can be converted to an ELF
  • The filesystem, where all of the images, textures, sounds, musics, 3D models, animation data, scripts, etc. are stored for a game. Nintendo provides an API to access the filesystem so everyone uses the same format. That's what we are going to talk about here

The structure of a Wii filesystem is really simple: it is a fully read only filesystem optimized to avoid seeking (slow on an optical disc) so there are no things to handle like data fragmentation. First of all, all of the metadata are stored in the same place, called the FST (FileSystem Table). These metadata are actually only the name of the file, its size and where it is stored on the disc. We can find the FST by looking at offset 0x424 on the partition, which contains the 4 bytes offset to the FST (which means we need to multiply it by 4 to get an offset in bytes).

A filesystem is hierarchical: it is a tree of directories containing leaves which are the regular files. However, the FST is a linear structure (a table). To represent the hierarchy, a directory descriptor uses its size field to store the index of the first descriptor which is not their child. For example, if the root directory contains only 3 files, its size field will contain the value 4 (descriptor 0 is the root directory, 1/2/3 are the files, so 4 is the first which is not a child of the root directory). A little example in ASCII art:

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

After the last descriptor comes the string table containing all the file names. In a file descriptor, the file name is stored as an offset (in bytes this time... consistency!) from the beginning of this string table. To find the offset to this string table, we can use the fact that the first descriptor size is also the total number of descriptors in the FST. And that's all there is to know about a Wii disc filesystem! Quite simple, isn't it?

Let's start writing a bit of code. First, let's import the modules we need and open the image we dumped at the end of the first part of this article:

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

Then, let's define a useful function which reads data at a specific offset in the file (basically, seek then read):

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

We're all set! First step, let's get the FST offset and multiply it by 4 to get it in bytes:

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

Next, the string table offset. We take the FST offset, reads the size of the first descriptor, multiply it by 0xC which is the size of a descriptor, and adds those two to get the offset we need:

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

Filenames are stored as zero terminated strings of at most 256 characters. Python has nothing standard to read that in the struct module, we will need a small function which reads 256 bytes and cut at the first \0 encountered:

>>> 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'

Now comes the interesting code: the function which reads a file descriptor and all its children recursively to build the filesystem tree. This is not easy to understand with a single reading of the function, I'll explain it just after:

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

First, let's identify what this function is returning: three values, the first one if the index of the first descriptor which has not yet been handled (makes the recursion easier), the name of the file or directory described by the descriptor we just read, and finally, depending on the file type (regular or directory), the children or the data offset. How this function does that is actually not that difficult: first, we read the 12 bytes of the descriptor from its index. Then, to check if it is a regular file or a directory, we check if one of the top 8 bits of the name is set: if there is at least one, this is a directory. We then read the name. If it is a regular file, we just end there saying that the first non handled descriptor is the one just after us. If it is a directory, we loop while the non handled offset is smaller than our last child and we insert the parsed descriptor into our children. That's all!

To conclude this part, let's dump all of the files and directories in a local directory on our PC. We just have to walk the tree returned by read_descr(0):

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

Here we go! We just extracted all the files and directories from a Wii disc onto our computer with just a Python shell. The last step is to make something usable from all of these informations.

Creating a FUSE filesystem for Wii discs

This last part of the article will talk about how to make a usable filesystem from all of that. Indeed, writing code snippets in a Python shell is far from clean, even though it is useful for hacking around.

The result is named wiiodfs, which stands for Wii Optical Disc FileSystem. It is an application able to mount a Wii disc image on Linux, but also a library that people who need to access Wii discs from their software can use. There is still a bit of work to do on it but 99% of the code is done and working.

The first difference of importance when comparing to the two previous parts is that we can not afford to dump the whole parition in a temporary file or dump all the files in a directory. It's ugly, slow and of little interest. The whole decyphering and data access in wiiodfs is done on the fly, when needed. The library is designed with that need in mind.

wiiodfs has a 4-layer design. The n-th layer can access every lower layers. Here is a detailed explanation of the different layers:

  • Raw access to the disc image, metadata, and to the volume groups table. Quite straightforward: no decyphering, no filesystem support, only what is necessary to read the image from a location to another and to recover informations on the disc partitions.
  • Raw access to decyphered partition data. It is almost certainly the most interesting part therefore I will describe it further later, but to sum up, it reads raw data off the partition, decyphers it and returns it to the user.
  • Access to the partition files with an easy-to-use API: Filesystem.open returns an object behaving like a Python file, on which we can call methods like read or seek. There are also methods to list files in a directory, checking if a file exists, etc.
  • Finally, an interface to use our filesystem with Pyfilesystem, a library which aims to define a common interface for all file systems in order to use them in a uniform way.

As I said before, I think the most interesting part in all of this is the second layer, handling the partition decyphering. Indeed, decrypting blocs of 0x8000 bytes is quite slow and some kind of caching is needed to avoid decrypting the same cluster 20 times in a row. I implemented a simple LRU cache to solve this problem. LRU caches are the most simple caches you can think of: they keep a certain number of values sorted by their last use time. That way, the most recently used values will be kept in the cache, and the least recently used values will slowly be replaced by other values which are more used. There are probably a lot of way to cache clusters more efficiently but this was not the goal of this project, and this LRU cache is enough to get a good throughput.

wiiodfs is distributed with a script named wiiodmount which mounts a Wii disc image on a local filesystem folder. To do this, we simply use a feature of Pyfilesystem, called expose.fuse. It's magic and there is almost nothing to do on my side to handle that.

Wrapping up

I knew almost nothing about the Wii when I started writing this article at the beginning of January 2011. This was a really interesting adventure, and I know now in detail how Nintendo decided to store data on their optical discs (at least on the logical layer, I don't know anything about how Wii discs are physically different from classic DVDs). wiiodfs is almost certainly incomplete and buggy, but it is a really simple implementation of the Wii Filesystem and it is as far as I know the only one able to use FUSE to mount the disc on a local folder.

I hope you liked this article :) Let's end that with links and thanks to the projects and people who helped me:

  • Wiiodfs, Mercurial repository of the project
  • Wiibrew, a great Wiki about Wii homebrew, with a lot of technical informations
  • Dolphin, the best Wii/GC emulator, which is licensed under the GPL license
  • Thanks to kalenz who helped me translate this long article to English
2011
06.09

Most games nowadays avoid hardcoding behavior in the main program code. It makes the development process a lot easier by allowing people with less programming experience than the core engine developers to contribute by writing scripts which defines how conversations happen in the game, how menus work, how cinematic scenes go, etc. Scripts are usually written in a higher level language than the game engine, as they require less performance and must be portable when the game needs to run on different platforms. Easy, common script languages like Lua or Python are often used (for example, CCP uses Python to describe the EVE Online client GUI, and Microsoft uses Lua in Freelancer to describe cinematics), but some companies like to create their own language and their own interpreter to do this job.

I'm a fan of Namco's "Tales of" RPG games. I was given a Wii last december as a gift, and bought "Tales of Symphonia: Dawn of the New World". As a true hacker interested in game development, after finishing the game, I started investigating how this game actually works behind the scenes. After writing software to read the encrypted Wii disc of the game, I analyzed the data contained on the DVD, trying to find what was each file and how the game engine made all of this into a game.

For those who don't know, the Tales RPG often have a system to display optional conversation between the game protagonists, which are not directly tied to the game scenario but sometimes give more precision on characters or on some story points from before the game. Some examples on Youtube. These are known as "skits" and are displayed as the characters 2D portraits talking and being animated on the screen. I thought this was a good starting point to try to understand how the game works, and thus tried to find the files related to these skits on the disc. What I'm exposing in this article is work that took me about 3 to 4 months and ended at the start of April.

First, using Dolphin, the best (and probably the only) Wii emulator, I logged all the disc files being opened using the "file monitor" feature of this emulator. When opening the first skit in the game, the emulator output was:

W[FileMon]:      31 kB Chat/FC01_001.bin
W[FileMon]:       4 kB Chat/e/FC01_001.bin
I[FileMon]:     248 kB Sound/stream/FC01_001.brstm

It was easy enough to understand that Chat/ probably contained graphics elements as well as a description of the character picture animation, and Sound/ contained the voices of the characters. Let's not talk about the sound data (maybe in a next article, but they are basically 4-bit ADPCM encoded sound files) and concentrate ourselves on the Chat/ files.

Both of the .bin files are actually Microsoft Cabinet archives (.cab) which can easily be extracted with cabextract or any program using libmspack. Note that one is at the root of the Chat/ directory, while one is in the Chat/e/ directory. Actually, here are all the FC01_001.bin files:

./e/FC01_001.bin
./g/FC01_001.bin
./f/FC01_001.bin
./s/FC01_001.bin
./i/FC01_001.bin
./FC01_001.bin

If you've played the game, it's easy enough to understand what are these directories. The european version of the game I'm working on have been released in five languages: english, german, french, spanish and italian. Looking at the size of the files (31K for Chat/FC01_001.bin and 4.1K for the language specific ones), we can assume that the non language specific one contains only images while the others contains the subtitles for example. Let's extract these .cab files!

In the non language specific one:

-rw-r--r-- 1 119K Aug 28  2008 ar.dat

In the english one:

-rw-r--r-- 1 33K Aug 15  2009 FC01_001.so

Both of these files seem to be in an unknown format. ar.dat does not even have a magic number in its header, but FC01_001.so starts with "TSS\0" which nobody seems to have heard of on the internet. There are a few strings in the .so file: subtitles, as expected, but also things like restTime (%f) or TCP balloon(%d). Not looking too good so far! That's when static analysis of the files start to show its limits, and it's time to run the Dolphin debugger to find what is actually accessing the .so file when it is loaded in memory. First, I paused the code execution while it was displaying a skit and dumped the contents of the RAM. By locating the "TSS\0" string, I found out that the .so file was loaded at offset 0x816D8394 when executing the skit. I proceeded to modify the PowerPC interpreter in the Dolphin emulator to dump the CPU state in JSON at each memory read or write in the zone containing the .so file, and ran the skit once more to get a full dump of what code is actually accessing our file.

First, there seems to be some copying going on : the same instruction is writing data in the zone from top to bottom, starting at 0x816E0393 down to 0x816D8394. Classic memcpy, nothing to see here. After that, the four first bytes are read by an instruction at 0x80071E0C. Let's fire IDA to see what this is about:

.text2:80071E0C                 lwz     %r3, 0(%r21)
.text2:80071E10                 addis   %r0, %r3, -0x5453
.text2:80071E14                 cmplwi  %r0, 0x5300
.text2:80071E18                 beq     loc_80071E40

If you are not familiar with PowerPC assembly, what this does line by line is loading a word (32 bytes) at the address contained in r21, then add -0x54530000 to it, and compare it to 0x5300. In other words, it compares the first four bytes of the file to 0x54535300, which is "TSS\0", the four characters code which describe the file type. The rest of the code in this function is very large, let's not waste time and go to the next memory access to find an interesting one.

Some fields in the header seems to be read : offsets 0x4, 0xC, 0x14, 0x18, 0x1C, 0x8, 0x10, and then a lot of repeated accesses from code at address 0x80091DBC on always different offsets (0x7D40, 0x7D48, then 0x742C, etc.). Let's look at the code control flow there:

An experienced eye may directly recognize a switch statement here: loads of code without a visible predecessor, all leading to the same exit. I'd even go further and assume that is the main loop of a script interpreter. That would make of the .so file a Script Object file, containing compiled bytecode. Sounds consistent. If we look more precisely at the code 0x80091DBC which is reading the bytecode in memory, we can see that it is used to choose where to jump in the switch. In other words, it is the dispatch code of the bytecode interpretation loop, reading an opcode and dispatching it to handling code. From there, we can get nice informations: if it loads the opcode from memory then we can find where is the "current instruction" pointer, aka. PC (program counter). Let's look for that! This is the interesting part of the code:

lwz     %r0, 0xC(%r15)
lwz     %r4, 4(%r15)
mulli   %r0, %r0, 0x1420
add     %r5, %r15, %r0
lwz     %r3, 0x142C(%r5)
lwzx    %r17, %r4, %r3  # Read opcode

The last instruction loads a word indexed from memory. This means it loads the word at address r4 + r3. Looking at the memory dump we did with Dolphin, we can see the value of those two registers: 'r3':'00007D40', 'r4':'816D83B4'. So r4 is the address of the bytecode file (0x816D8394) + 0x20, which we can assume to be the address of the first opcode in the file, after the bytecode header. r3 is the PC, which varies while we are executing the code. They are both stored in some sort of interpreter state, whose address is stored in r15. In this state is the code base address, at offset 0x4 (second instruction of the listing), and some sort of table index at offset 0xC (loaded in the first instruction of the listing, then multiplied by a constant, and used as an index). This index is used to find the current PC. From that, we can assume that there can be multiple PCs stored at the same time, depending on the value of r15[0xC]. Hm...

Let's increase the usefulness of our memory dump by also dumping memory accesses to the whole interpreter state. After dumping all of that, we have log files containing 188MB of JSON with the CPU state at each access. In the next part of this article, I'll explain how to make this dump useful and readable by filtering it a little bit, and we'll start analyzing the 20 instructions of this bytecode :)

For impatient people, the final result of this reverse engineering work is a completely rewritten version of the bytecode interpreter, which can be found on my Bitbucket : cscript-interpreter. This is able to execute skit scripts quite well, even if there is still a bit of reverse engineering to do on the syscalls part.

2011
06.07

As in the other binary l33tness problems, only a single file was provided in the problem description:

b300_b258110ad2d6100c4b8: gzip compressed data

Decompressing this gives us a tar archive containing these files:

./0/
./0/heap-dump-tm1306902723-pid12959.hprof
./0/classes.dex
./1/
./1/1306902613084.jpgs
./1/1306903692478.jpgs
./2/
./2/1306902613084.jpgs
./2/1306903692478.jpgs

The binary is classes.dex, which is bytecode for the Dalvik virtual machine found on Android devices. The hprof file is a heap profiler output file which contains the state of the program heap at some point during the execution. The .jpgs files seems to contain random data at first, which leaded us to think it was encrypted data we needed to decrypt.

But first, let's decompile the bytecode. We first used dex2jar to get a classes.jar file, which we unzipped to get .class Java bytecode files. Then we used JAD to decompile the Java bytecode. It's not a perfect tool (a lot of things are not handled, for example exception handling is kind of crappy in the decompiled output) but it did the job quite well. The code was obfuscated: all classes, method and fields names were transformed to strings like "a", "b", "ab", "g", ...

We first tried to find where the .jpgs files were created in the code. Grepping the code gives us three matching classes: as.class, a Runnable instance which is probably the main thread code, i.class, which seems to take pictures from the Android device camera and saving them as JPEG, and r.class which is a FilenameFilter instance filtering files ending with .jpgs. But reading all the code to find where the encryption is done was a bit too tedious, so we decided to grep for another thing: Java standard encryption library. Grepping "java.security" returns us a single class: g.class, which seems to contain methods to generate random bytes, to compute the SHA1 hash of a string or to XOR encrypt/decrypt a string. Interesting.

Reading the code in this file gives us a lot of informations about the encryption method. This is simply a XOR cipher using the first 8 bytes of SHA1(password) as the key. Now we just need to find the key in the heap profiler dump we got in the TAR archive. The .hprof file first needs to be converted to the standard (non Android) hprof format using the hprof-conf tool found in the Android SDK. Then, using the Eclipse Memory Analyzer we need to find the "g" class instance. Using OQL, a SQL like used to query memory dumps, this does what we need:

select * from com.closecrowd.lokpixlite.g
Using Eclipse MAT to find the b300 key

Using Eclipse MAT to find the b300 key

Now we just need a simple tool to decrypt the images. Behold!

#! /usr/bin/env python

import sys

KEY = (44, -47, -51, -106, 72, -106, 61, 104)

def decrypt(infile, outfile):
    intext = infile.read()
    outbytes = []
    key_index = 0
    for byte in intext:
        key_byte = KEY[key_index] % 256 # wrap to 0-255
        outbytes.append(byte ^ key_byte)
        key_index = (key_index + 1) % len(KEY)
    outfile.write(bytes(outbytes))

if __name__ == '__main__':
    for filename in sys.argv[1:]:
        infile = open(filename, 'rb')
        outfile = open(filename + '.jpg', 'wb')
        decrypt(infile, outfile)

Let's launch this on the "1" directory images:

$ ./decrypt.py *.jpgs
$ file *.jpg
1306902613084.jpgs.jpg: data
1306903692478.jpgs.jpg: data
$ xxd -l 64 1306902613084.jpgs.jpg 
0000000: 1dd1 c716 4f11 9c20 fdc8 0f64 619c d87d  ....O.. ...da..}
0000010: ffd8 ffe0 0010 4a46 4946 0001 0100 0001  ......JFIF......
0000020: 0001 0000 ffdb 0043 0010 0b0c 0e0c 0a10  .......C........
0000030: 0e0d 0e12 1110 1318 281a 1816 1618 3123  ........(.....1#

Woops. It looks like the JFIF marker is not at the correct offset. Looking at a JPEG picture, we see that it should be at 0x6, but it is instead at 0x16. Let's see what happens if we assume that the first 0x10 bytes are junk:

$ diff -u decrypt.py.old decrypt.py
--- decrypt.py.old      2011-06-07 07:26:45.603332762 +0200
+++ decrypt.py  2011-06-07 07:26:10.253333216 +0200
@@ -5,6 +5,7 @@
 KEY = (44, -47, -51, -106, 72, -106, 61, 104)
 
 def decrypt(infile, outfile):
+    infile.read(0x10)
     intext = infile.read()
     outbytes = []
     key_index = 0
$ ./decrypt.py *.jpgs
$ file *.jpg
1306902613084.jpgs.jpg: JPEG image data, JFIF standard 1.01
1306903692478.jpgs.jpg: JPEG image data, JFIF standard 1.01
$ md5sum *.jpg
0527c046512de51504790d03f00bda1c  1306902613084.jpgs.jpg
0527c046512de51504790d03f00bda1c  1306903692478.jpgs.jpg

Success! Here is the image duplicated in the "1" directory:

Thumbnail in b300

Looks like a thumbnail... well, let's look into the "2" directory. Again, two images with the same md5sum. Let's open one:

Image found in b300

And this is the key for b300: "ANDROIDSEKURITYisOXYMORON". IMHO this was a really easy problem for 300 points, as long as you know Java and its ecosystem a bit (and I think even without knowing anything about DEX files, JAD, MAT, etc. this could easily be done in an hour or two with enough Googling...).

EDIT: @Kachakil "write-up" -- cheater :P

2011
06.06

gb100 took a lot of time to pwn for us as we ran out of ideas really fast and it was mostly guessing. Anyway, this is a small writeup about this really simple problem from the DEFCON 19 CTF.

The description of this problem contained only a host:port which we had to connect to. For the first 4 to 6 hours of the contest the server simply closed any incoming connection on the specified port, which caused us to try a lot of strange protocols, only to find out 4 hours later that the problem was fixed and was simply an HTTP server.

On every request, the server replied a “HTTP/1.1 408 Too Slow” error code, followed by some fixed Date and Last-Modified headers which turned out to be useless. The solution to that problem was to connect to the server using the SPDY protocol from Google, which is implemented in Google Chrome. You can force Chrome to use SPDY to connect to a website by launching it with the “–use-spdy=no-ssl” flag on the command line. After this is done, the server simply returns a text/plain content with “you are speedy enough, but not good enough”.

After a lot of time spent trying to fuzz SPDY headers and racing to be the first to connect to the server after each of its apparently scheduled downtime, we discovered that requesting /cgi-bin/ did not display the string but an HTTP 404 error, which was really odd. We then tried to guess what could be in this /cgi-bin/ directory: printenv, phpmyadmin, etc. and found a /cgi-bin/phf binary, which is mostly known to be the most vulnerable CGI script in the universe. We were able to launch commands on the web server, and a “ls” showed that a “key” file was present in $PWD. The query string was filtered and denied the command launch if the string “key” was found in the command line, but doing “cat *” was fine and gave us the key for this problem.

Stay tuned for a b300 writeup soon (today or tomorrow depending on my schedule) :-)

2011
04.25

Comme vous avez peut-être pu le remarquer, je n’ai pas du tout posté d’articles ici pendant le mois dernier. Raison simple : j’étais principalement overbooké par l’organisation de la finale de Prologin, qui s’est déroulée du vendredi 22 au lundi 25 avril. Pour ceux qui ne le savent pas, Prologin est le concours national d’informatique pour les moins de 20 ans, et je suis secrétaire et administrateur système principal de l’association qui organise ce concours.

Pendant la finale du concours, 100 candidats doivent programmer une intelligence artificielle pour un jeu dont les règles changent tous les ans et sont rédigés par une petite équipe de gens motivés de l’association. Ils ont pour cela 36h, qu’ils passent dans les locaux de l’EPITA au Kremlin-Bicêtre à programmer, mais également à s’amuser un peu grâce aux différentes animations proposées par les organisateurs du concours (tournois de jeux, bataille d’eau, château gonflable, piscine à mousse, etc.). Côté administration système, il y a donc le problème de fournir à 100 candidats et (au moins) 10 organisateurs un système fonctionnant correctement et pouvant être déployé rapidement. L’autre problématique est de fournir un système permettant aux candidats de lancer des matches entre les intelligences artificielles qu’ils ont programmé, et permettant aux organisateurs de lancer des tournois contenant des milliers de matches de manière rapide.

Système utilisé par les candidats

Afin de répondre à nos contraintes de déploiement rapide, nous avons utilisé une solution à base de boot en PXE + disque système monté en NFS3 sur un serveur central dupliqué (nfsroot0 et nfsroot1). Ces deux serveurs utilisent DRBD en mode actif-actif afin de répliquer en temps réel les données modifiées d’un côté sur l’autre serveur. Pour profiter de cette fonctionnalité, il a également fallu utiliser un système de fichiers gérant l’écriture simultanée depuis plusieurs machines. Les principaux compétiteurs de ce côté là étaient OCFS2 et GFS, nous avons choisi le premier car il semblait plus simple à configurer et à administrer pour des néophytes de ce genre de technologies. La moitié des machines des candidats utilise nfsroot0 et l’autre moitié utilise nfsroot1 afin de distribuer la charge assez intense que supportent ces deux serveurs. Cette distribution est faite à base de round-robin DNS très classique.

La même chose a été mise en place pour /home, contenant les dossiers personnels de tous les candidats, et /srv/stechec, qui est utilisé pour stocker les intelligences artificielles et logs de matches qui servent au concours. Ces deux points de montage NFS sont tous deux sur nfshome0 et nfshome1, dupliqués de la même manière que pour / (DRBD + OCFS2).

Pour gérer tout ce qui est PXE, DHCP et DNS, nous avons utilisé dnsmasq, solution tout en un qui fait tout ce qu’il faut à partir de /etc/ethers et /etc/hosts. Même s’il a crashé plusieurs fois lors du reboot de 120 machines simultanément, nous n’avons globalement pas à nous plaindre de cette brique qui a fait tout ce qu’il fallait.

Enfin, tout tourne sur du Archlinux. Choix totalement subjectif et surement assez mauvais sur le long terme (c’est loin d’être un système stable comme Debian), mais on migrera ce qu’il faut quand ça sera nécessaire.

Distribution du lancement de matches

Lorsque les candidats programment leur intelligence artificielle, ils ont à leur disposition un intranet (programmé avec Django) leur permettant d’uploader leur code afin de le faire concourir. Lorsque le code est uploadé, il est automatiquement compilé, puis peut être utilisé pour lancer des matches. Lorsqu’un match est lancé via l’intranet, les logs des IA ainsi qu’un replay du match peuvent être téléchargés dès sa complétion.

Afin de distribuer les compilations des IA et le lancement des matches, j’ai programmé durant les semaines dernières deux programmes : un worker qui est lancé sur toutes les machines des candidats, et un master qui tourne sur nfsroot0 et qui sert à dispatcher les matches sur les différents workers. Ces deux programmes sont codés en Python avec Gevent (bibliothèque de programmation asynchrone), et communiquent via du XML-RPC dans les deux sens afin de s’échanger des messages. Du failover automatique est mis en place pour gérer le cas de la perte d’une machine, et le dispatching se fait de manière à ne pas surcharger une seule machine : les tâches sont distribuées équitablement sur les 120 machines.

Lors du lancement d’un match, l’intranet stocke donc dans une base de données PostgreSQL toutes les informations nécessaires, puis le master (qui poll la base de données toutes les 2 secondes) va redispatcher vers un worker, qui va s’occuper du lancement du match et renvoyer les résultats au master, qui va les inscrire dans la base de données.

Le lancement d’un match côté worker

Pour lancer des matches entre intelligences artificielles, Prologin utilise un programme codé vers 2005 par des membres de l’association, nommé Stechec. Il s’agit d’un serveur permettant de charger dynamiquement une bibliothèque dynamique contenant les règles d’un jeu, et une bibliothèque dynamique représentant l’intelligence artificielle jouant à ce jeu.

Stechec est codé en C++ et est totalement libre (sous licence GPLv2). Les sources de la version utilisée pour la finale de Prologin 2011 seront bientôt mises sur Github, mais la version précédente peut être trouvée sur Bitbucket.

Conclusion

Au final, tout a bien marché. Quelques pannes ont évidemment eu lieu (souvent à cause de problèmes de configuration réseau), mais globalement ce fut une bonne finale du point de vue système, surtout en prenant en compte que tout a été refait cette année. Ce fut par contre très crevant (une seule nuit de vendredi matin à lundi matin…) et je vais prendre un peu de temps à me remettre en forme… oh, wait, j’ai une soutenance demain :’( .

2011
03.27

Eri HaKawai is a new exploit for PAL Wiis, which work for all currently released System Menu versions (<= 4.3). It works by using a bug in the savegame loading code of Tales of Symphonia: Dawn of the New World, the sequel to the Gamecube game Tales of Symphonia.

I'm releasing it in a source format (no binary data.bin) under the GPLv2. You'll need a Broadway cross-compilation toolchain, as well as a checkout of Segher's Wii Git repository. Do whatever you want with it (as long as it is allowed by the license, of course!), I'm just too lazy to distribute binaries.

Download Eri HaKawai v0.1

How to use (directly copied from the README file in the tarball):

Usage:
 - Compile it with ./make.sh
 - Copy the "private" folder to the root of your SD card.
 - Put the homebrew you want to load on the root of your SD card, named
   "boot.elf"
 - Start the game and load the first save
 - Press the PLUS button to enter the game menu
 - Scroll to the STATUS button and press A
 - Scroll to the monster named "Eri HaKawai" and press A
 - The boot.elf file should be loaded from your SD card.

That's it! Have fun with this new exploit, and don't forget to play the great game that Tales of Symphonia: Dawn of the New World is :-) .

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

2011
01.04

Voici donc la troisième et dernière partie de cette série d'articles sur la lecture de disques Wii en Python. Maintenant que tout le travail de documentation des différentes structures et des différents formats dont on a besoin a été fait, la dernière étape est de faire quelque chose de propre et d'utilisable à partir de tout ça. En effet, des bouts de code à balancer dans un shell Python c'est loin d'être propre, même si c'est bien pour expérimenter rapidement.

Le résultat de tout ça s'appelle wiiodfs, pour Wii Optical Disc FileSystem. Il s'agit d'une application capable de monter une image de disque Wii sous Linux, mais également d'une bibliothèque pouvant être utilisée dans d'autres programmes qui auraient besoin d'accèder à un disque de Wii. Il reste encore un peu de travail à faire au niveau du packaging mais 99% du code utile est là, utilisable et fonctionnel.

La première grande différence avec ce qui a été fait lors des deux articles précédents est qu'on ne peut pas se permettre de dumper toute la partition dans un fichier temporaire, ou dumper tous les fichiers dans un dossier. C'est sale, lent et ça a peu d'intérêt. Dans wiiodfs, toute la partie déchiffrement et accès aux fichiers est faite à la volée lorsqu'on en a besoin. Toute l'architecture de la bibliothèque est donc basée sur ce besoin.

wiiodfs est séparé en 4 couches de différents niveaux. La couche de niveau n peut utiliser toutes les couches de niveau inférieur. Voici ces différentes couches expliquées en détail :

  • L'accès brut à l'image disque, aux métadonnées et à la table des partitions/volume groups. Très simple, pas de déchiffrement, pas de gestion du filesystem, uniquement de quoi lire l'image d'un point A à un point B et de quoi récupérer les informations sur les partitions du disque.
  • L'accès brut aux données d'une partition déchiffrée. C'est surement la partie la plus intéressante donc je reviendrai dessus plus tard, mais elle va en gros lire les données brutes sur la partition, les déchiffrer et retourner ces données à l'utilisateur.
  • L'accès aux fichiers sur la partition avec une API facile à utiliser : on utilise Filesystem.open qui nous renvoie un objet qui se comporte comme un fichier Python, sur lequel on peut appeler les méthodes read ou seek par exemple. On peut également lister les dossiers, récupérer la taille d'un fichier, vérifier si un fichier existe, etc.
  • Enfin, une interface permettant d'utiliser le filesystem via PyFS, une bibliothèque dont le but est de définir une interface commune pour différentes classes de gestion des systèmes de fichier afin de pouvoir les utiliser uniformément.

Comme dit juste au dessus, je pense que la partie sur laquelle il faut le plus s'attarder est la deuxième couche, celle qui gère le déchiffrement des partitions. En effet, déchiffrer des blocs de 0x8000 octets n'est pas spécialement rapide et il faut donc un moyen de garder en cache les clusters déchiffrés pour ne pas déchiffrer 20x le même cluster à la suite. Pour cela, j'ai réimplémenté un cache de type LRU. Les caches LRU sont les caches les plus simples, qui gardent un certain nombre de valeurs dans leur ordre de dernière utilisation. Ainsi, toutes les valeurs les plus récemment utilisées sont dans le cache, et les autres sont éliminées du cache au fur et à mesure qu'elles ne sont plus utilisées. On pourrait surement faire beaucoup plus intelligent mais ça n'était pas mon but ici (et de toute façon c'est déjà bien rapide).

wiiodfs fournit également un script nommé wiiodmount qui permet donc de monter une image disque de Wii sur un dossier. Pour cela, le script utilise la 4ème couche ainsi qu'une fonctionnalité de PyFS qui permet d'exporter un filesystem conforme à l'API de PyFS via FUSE. C'est magique et ça marche plutôt très bien lors de mes tests.

Pour conclure cette série d'articles, je vais simplement dire que c'était une aventure très intéressante pour moi : je connaissais très peu la Wii il y a une semaine et l'implémentation de wiiodfs m'a permi de comprendre un certain nombre de notions utilisées par la console de Nintendo, par exemple tout ce qui concerne le chiffrement des données. wiiodfs est surement loin d'être fini ou même utilisable pour tout le monde (il faudrait que je prenne le temps de faire le packaging par exemple) mais c'est une implémentation simple et efficace d'un filesystem assez peu documenté, dont j'espère qu'elle sera utile à des gens :)

Finissons par des liens et des remerciements :

  • Wiiodfs, dépot Mercurial du projet.
  • Wiibrew, une de mes principales sources de documentation.
  • Dolphin, un émulateur GC/Wii assez génial et libre.

Pendant les prochains jours j'essaierai de prendre le temps d'écrire de nouveaux articles sur les formats de fichier que j'ai trouvé dans le FS de Tales of Symphonia : Dawn of the New World. Des trucs comme les textures, les modèles, les sons, les musiques, etc. Stay tuned!

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.

2011
01.01

En cette nuit qui marque le passage vers l'année 2011, je me lance dans l'écriture d'une petite série d'articles sur la lecture d'un disque de Wii en Python. Lire un disque de Wii, pour moi, c'est être capable de lire tout d'abord le nom et l'ID du jeu, mais aussi être capable de lire le système de fichiers du disque pour accèder à l'exécutable ou aux données « brutes » du jeu. Je prévois pour cela 3 articles : le premier, celui ci, ira jusqu'à la lecture des secteurs décryptés de la partition de jeu du disque. Le deuxième parlera de la lecture du système de fichiers de la console : répertoires, fichiers, etc. Enfin, le troisième implémentera ce système de fichier via fuse-python ou PyFS selon mes envies pour pouvoir simplement le monter sur un système Linux. C'est parti !

Je n'ai pour le moment qu'une seule image sous la main : celle de Tales of Symphonia : Dawn of the new World (dont je ferai surement une review ici dans une semaine, par ailleurs), version PAL, dont l'ID est RT4PAF (sha1sum: b2fb05a7fdf172ea61b5d1872e6b121140c95822). Je vais donc me baser là dessus pour faire mes tests, puis si nécessaire fixer des choses quand je trouverai une autre image qui ne fonctionne pas. Au niveau de la documentation sur laquelle je me suis basé, il y a tout d'abord le code source de l'émulateur Dolphin (dans le dossier Source/Core/DiscIO) et le site WiiBrew, un wiki qui contient beaucoup d'informations techniques sur la Wii. Merci beaucoup à eux \o/

Quelques facts sur les disques de Wii : ils commencent par un header qui contient des metadata sur le jeu (son nom, son ID, le numéro du CD, le type de disque, etc.) à l'adresse 0. À l'adresse 0x40000 se trouvent 32 octets qui représentent la table des partitions du disque. En effet, un disque de Wii contient en général plusieurs partitions : une pour le jeu et d'autres qui contiennent par exemple les upgrades. Chacune de ces partitions a un type; celui qui nous intéressera est le type 0, partition de données. Enfin, cette partition commence elle même par un header qui contient notamment des informations nécessaires au déchiffrement des données du disque. En effet, toutes les données contenues dans une partition de Wii sont chiffrées en utilisant le bien connu algorithme AES avec une clé de 128 bits.

Tous les exemples que je donne ici seront uniquement des transcripts d'un shell Python. Je publierai un module qui marchera bien quand j'aurai tout fini et testé, donc surement en même temps que la partie 3 de cette série. Commençons par importer des modules utiles et ouvrons notre fichier :

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
>>> from Crypto.Cipher import AES
>>> fp = open('tos-2.img', 'rb')

Ensuite on va lire le header. C'est plutôt simple car il a un format fixe et des champs de longueur non variables :

>>> DiscHeader = namedtuple('DiscHeader',
...     'disc_id game_code region_code maker_code disc_number disc_version '
...     'audio_streaming stream_bufsize wii_magic gc_magic title'
... )
>>> disc_hdr = DiscHeader(*up('>c2sc2sBBBB14xLL64s', fp.read(96)))
>>> disc_hdr
DiscHeader(disc_id='R', game_code='T4', region_code='P', maker_code='AF',
           disc_number=0, disc_version=0, audio_streaming=0,
           stream_bufsize=0, wii_magic=1562156707, gc_magic=0,
           title='Tales of Symphonia: Dawn of the New World' + n*'\x00')

Étape suivante, la table des partitions. On va déjà regarder à quoi s'attendre en utilisant xxd :

$ xxd -s 0x40000 -l 48 tos-2.img
0040000: 0000 0002 0001 0008 0000 0000 0000 0000  ................
0040010: 0000 0000 0000 0000 0000 0000 0000 0000  ................

Elle est organisée en 4 volume group qui contiennent plusieurs partitions. Une partition est donc indexée par son numéro de VG et son index dans le VG. La table que l'on vient de dumper contient pour chaque VG deux entiers de 32 bits : le premier est le nombre de partitions dans le VG, le deuxième est l'offset en blocs de 32 bits (et pas en octets, il faut donc multiplier par 4) de la table des partitions du VG. Ici, on a donc le premier VG qui contient 2 partitions, et la table qui décrit ces partitions se trouve à l'adresse 0x10008 * 4 = 0x40020 :

$ xxd -s 0x40020 -l 16 tos-2.img
0040020: 0001 4000 0000 0001 03e0 0000 0000 0000  ..@.............

Chaque entrée de cette table contient tout d'abord l'offset vers la partition (en blocs de 32 bits comme au dessus) mais également le type de partition. En gros, il faut retenir que type 1 = update firmware et type 0 = données du jeu. On a donc une partition d'update et une partition de données ici. On va coder un petit truc pour dumper toutes les entrées des VG avec Python histoire de pouvoir visualiser ça un peu mieux :

>>> PartEntry = namedtuple('PartEntry', 'offset type')
>>> def read_part_entry(offset):
...     fp.seek(offset)
...     (data_offset, type) = up('>LL', fp.read(8))
...     data_offset *= 4
...     return PartEntry(data_offset, type)
>>> 
>>> read_part_entry(0x40020)
PartEntry(offset=327680, type=1)
>>> read_part_entry(0x40028)
PartEntry(offset=260046848, type=0)
>>> 
>>> VGEntry = namedtuple('VGEntry', 'part_count table_offset')
>>> def read_vg_entry(offset):
...     fp.seek(offset)
...     (part_count, table_offset) = up('>LL', fp.read(8))
...     table_offset *= 4
...     return VGEntry(part_count, table_offset)
... 
>>> read_vg_entry(0x40000)
VGEntry(part_count=2, table_offset=262176)
>>> read_vg_entry(0x40008)
VGEntry(part_count=0, table_offset=0)
>>> 
>>> def read_part_table():
...     base_off = 0x40000
...     vgs = {}
...     for vg_num in xrange(4):
...         vg_ent = read_vg_entry(base_off + 8 * vg_num)
...         if vg_ent.part_count == 0:
...             continue
...         vgs[vg_num] = {}
...         for part_num in xrange(vg_ent.part_count):
...             off = vg_ent.table_offset + 8 * part_num
...             part = read_part_entry(off)
...             vgs[vg_num][part_num] = part
...     return vgs
... 
>>> read_part_table()
{0: {0: PartEntry(offset=327680, type=1),
     1: PartEntry(offset=260046848, type=0)}}

Concrétement, la partition d'upgrade firmware ne m'intéresse vraiment pas. Je vais me concentrer sur la partition qui contient les données du jeu, donc la deuxième partition du VG 0 (qui est de type 0, donc données de jeu). Comme à peu près tout sur la Wii, c'est chiffré en AES avec une clé qui est sur le DVD. Cette clé est elle même chiffrée avec une master key qui est dans le firmware de la console. Le tout est signé, mais comme on ne veut rien modifier on ignore totalement la signature : elle ne nous intéresse pas.

AES est un algorithme de chiffrement symétrique qui procède par blocs de 16 octets. Il peut être utilisé dans différents modes : ECB, CBC ou CFB. CBC, le mode que l'on va utiliser, prend en entrée la valeur précédente (ou, dans le cas du premier bloc, l'initial value aka. IV), le bloc courant et la clé. À partir de cela il chiffre ou déchiffre le bloc, qui devient la valeur précédente pour le bloc suivant, et ainsi de suite. Notre premier objectif va être de récupérer ce que l'on appelle la title key, qui est la clé de chiffrement de la partition de données. Elle est chiffrée avec un IV fourni sur le DVD (nommé title id) et une clé qui se trouve dans le firmware et qui est maintenant publique. Toutes les infos qu'ils nous faut sont donc à notre disposition, soit dans le header de la partition (appelé ticket) soit en dur. On va donc commencer par parser le header de la partition :

>>> Ticket = namedtuple('Ticket',
...     'enc_tit_key tit_id data_off data_len'
... )
>>> part = read_part_table()[0][1]
>>> fp.seek(part.offset)
>>> ticket = Ticket(*up('>447x16s13x16s204xLL', fp.read(704)))

Pour une raison louche que je ne connais pas (ping Nintendo ? :) ), seuls les 8 premiers octets de l'IV sont utilisées, les 8 autres sont mis à 0. Ça ne change pas grand chose en vrai. On va donc se servir de tout ce qu'on a pour initialiser notre décodeur AES et décrypter la clé :

>>> master_key = '\xeb\xe4\x2a\x22\x5e\x85\x93\xe4'
>>> master_key += '\x48\xd9\xc5\x45\x73\x81\xaa\xf7'
>>> 
>>> iv = ticket.tit_id[:8] + '\x00' * 8
>>> 
>>> aes = AES.new(master_key, AES.MODE_CBC, iv)
>>> key = aes.decrypt(ticket.enc_tit_key)
>>> key
'U\x84\xfb\x8b\x10\xdfu=B;\xdcyF\xd4G\x9d'

Voilà, nous sommes maintenant en mesure de déchiffrer le contenu de la partition. Les données en elles mêmes commencent après le ticket, à un offset qui se trouve dans ce ticket. À partir de là, on trouve une succession de clusters de 0x8000 octets. Ces clusters sont séparés en tout d'abord 0x400 octets de hashs pour vérifier l'intégrité des données, et 0x7C00 de données chiffrées. La clé de chiffrement est celle que l'on a récupéré précédemment, l'IV se trouve dans le bloc d'intégrité, à l'offset 0x3D0 par rapport au début de la partition. Avec tout ça il est aisé de coder la fonction de lecture d'un cluster :

>>> def read_cluster(idx):
...     data_offset = part.offset + ticket.data_off * 4
...     cluster_offset = data_offset + idx * 0x8000
...     fp.seek(cluster_offset)
...     data_enc = fp.read(0x8000)
...     iv = data_enc[0x3D0:0x3E0]
...     aes = AES.new(key, AES.MODE_CBC, iv)
...     return aes.decrypt(data_enc[0x400:])

Testons ça en décodant les 20 premiers clusters et en regardant leur contenu :

>>> for i in xrange(20):
...     open('/tmp/cluster%d' % i, 'wb').write(read_cluster(i))

$ strings /tmp/cluster* | less
[...]
This Apploader built %s %s for RVL
APPLOADER WARNING >>> Older version of DEVKIT BOOT PROGRAM.
APPLOADER WARNING >>> Use v1.07 or later.
APPLOADER ERROR >>> FSTLength(%d) in BB2 is greater than FSTMaxLength(%d)
APPLOADER ERROR >>> Debug monitor size (%d) should be a multiple of 32
APPLOADER ERROR >>> Simulated memory size (%d) should be a multiple of 32
[...]

Une seule chose à dire : success ! A priori il y a toutes les chances que les données soient bien déchiffrées (ça marche pour les 20 premiers clusters). On va terminer en faisant un dump complet de la partition pour pouvoir étudier ça plus facilement après. On a également la taille de la partition dans le ticket, donc c'est parti !

>>> nclusters = ticket.data_len * 4 / 0x8000
>>> out_fp = open('/path/to/tos-2-dumped.img', 'wb')
>>> for i in xrange(nclusters):
...     print '%f%%' % (i * 100.0 / nclusters)
...     out_fp.write(read_cluster(i))

Et voilà qui conclut cette première partie. Comme dit plus haut, dans la deuxième partie je vais me concentrer sur le système de fichier de la partition que je viens de récupérer. Ça promet :) .