Pwning the Wii: The Haxx Bearers

When I started writing Gecko, my GameCube and Wii emulator, I set myself a goal: find vulnerabilities in Wii games and system software. The plan was to find an exploit in a game of my choosing, then in my favorite game and eventually in the Wii System Menu. I haven’t started the System Menu work yet.

Another Code: R was my first target. This second post is about the game I had wanted to tackle from the start: Final Fantasy Crystal Chronicles: The Crystal Bearers, my favorite game of all time!

Loading User 1’s Auto Save runs a proof of concept that draws “pwned by layle” directly into the game’s external framebuffer (XFB).

Mapping out the save#

I started with main.dol from the US RFCEGD version and 2 saves: 1 made before and 1 made after the clock sequence at the very start of the game. I opened the executable in IDA using the DOL loader from Gecko’s IDA loaders. Unless specified otherwise, the addresses below refer to this version.

The game has no symbols, so I followed its NAND operations and string references to the savegame reads and writes, then worked my way to the code that actually restores the game state. The breakthrough was sub_80256834, which constructs a serializer from 5 component objects. Each component vtable has a size method at +0x0C, a serialize method at +0x14 and a deserialize method at +0x18, so each component is responsible for saving and restoring its own part of the game state. Names like “component”, “tag” and “manager” are my own labels based on how the code uses them, since I don’t know what the developers called these things.

sub_80256D5C writes the header and components and sub_80256F04 reads them back. The constructor inserts each component at the front of a linked list, which explains why their “on-disk” order is reversed:

RegionFile offsetSizeRuntime form
header0x00000x0020slot metadata
component 50x00200x0640inline object data
component 40x06600x05C8inline object data
component 30x0C280x2010separate heap buffer
component 20x2C380x3BA0manager object at +0x200
component 10x67D80x088Cinline object data

Each save slot is stored in a separate file. The exploit modifies usr0_0, which holds User 1’s Auto Save and is loaded when we choose Continue. The serialized content ends at 0x7068 and the save file is padded out to 0x7080.

The save contains 2 CRC32 values. The header CRC at 0x000C covers the first 12 bytes, while the content CRC at 0x7064 covers bytes from offset 0x0000 through 0x7063. Both are stored big-endian and must match the modified slot for the game to accept it.

Following the save into event scripts#

With the component boundaries mapped, I compared the 2 saves. Only 44 bytes differ! Among those changes is a new tag in component 2.

Component 2 restores 3 lookup tables that work much like dictionaries. Each entry has a 4 byte key and an 8 byte value I’ll call a pair, since it spans 2 words. Keys are 4 character tags such as s012 and cp15, while pairs often form 8 character resource identifiers such as e_01_012. Unlike a typical dictionary, these tables use arrays rather than a hash map and can contain duplicate keys, which will matter later! During a state transition, the game can look up a key and use its pair to select which resource to load next.

The tables are identical in both saves, whereas a field just after them changes from zero to the tag cp10. The tag relevant to the exploit is cp15, which the game uses as a key in these tables. When no resource is selected for cp15, the game falls back to loading the event script cpt1500s.

cpt1500s contains event bytecode with an EEVB header (should I make a decompiler for this stuff?). Each command stores a 16 bit opcode, an argument count and the size of its argument data. The opcode is just the number identifying which command the script wants to run. The game interprets those commands by looking up their handler functions in a separate table of function pointers, which I’ll call the callback table.

For example, when the script reaches opcode 67 (lol), the dispatcher looks up callbacks[67] and calls that function. This gives us something useful to target: if I can change that function pointer, the game’s existing script will call my code when it reaches command 67.

3 tables and a missing bounds check#

Component 2 is copied as a single 0x3BA0 byte block to manager + 0x200, where the manager object lives at 0x80BB71E0. It contains 3 related tables with room for 50 entries each and all 3 counts come directly from the save:

TableCount at manager offsetComponent offsetFile offset
0+0x224+0x240x2C5C
1+0x228+0x280x2C60
2+0x22C+0x2C0x2C64

The 3 tables are interleaved by index. The keys for index i sit next to each other in 1 row of 12 bytes and their pairs in 1 row of 24 bytes, so the relevant table 0 and table 2 addresses are:

table0.key[i]  = manager + 0x3C0 + 12*i
table0.pair[i] = manager + 0x618 + 24*i
table2.key[i]  = manager + 0x3C8 + 12*i
table2.pair[i] = manager + 0x628 + 24*i

The interesting function is sub_80048364. After tidying up how the 64 bit pair is passed in registers, the pseudocode looks like this:

void update_table(Manager* m, uint32_t key, Pair pair, unsigned table) {
    uint32_t count = m->count[table];

    // Search: trust the saved count without checking the table's capacity.
    for (int i = count - 1; i >= 0; --i) {
        if (m->key[i][table] != key)
            continue;

        if (pair != 0) {
            // Update: overwrite the pair at the matching index.
            m->pair[i][table] = pair;       // 8 byte write
        } else {
            // Removal: move the last entry here if needed, then reduce the count.
            if (i != count - 1) {
                m->key[i][table] = m->key[count - 1][table];
                m->pair[i][table] = m->pair[count - 1][table];
            }
            --m->count[table];
        }
        return;
    }

    // Append: add a new entry only if the table has room.
    if (pair != 0 && count < 50) {
        m->key[count][table] = key;
        m->pair[count][table] = pair;
        ++m->count[table];
    }
}

The append path checks the capacity, but the search, update and removal paths don’t! A saved count above 50 makes the loop treat neighboring memory as more table entries. If an out-of-bounds key matches, the existing-entry path writes the supplied pair without ever reaching count < 50. That unchecked update gives us an 8 byte out-of-bounds write primitive. Its destination depends on the matching index and the table’s 24 byte stride.

Making the game perform the write#

The game already has a transition in sub_80047304 that moves an entry from table 0 to table 2. It takes the pair belonging to the requested key from table 0, removes that entry and passes the same key and pair to update_table for table 2. In the path I use, that requested key is cp15.

I control the saved entries and counts, so I can choose the 8 bytes in the source pair, increase table 2’s count and place cp15 at an index beyond its 50 allocated entries. When the normal transition runs, the updater finds that key and writes my pair to the corresponding destination without checking whether the index fits in the table!

The next step is to turn that write into code execution. If I overwrite a callback with the address of my payload, the game will jump there when it calls the callback. I therefore need an index whose pair overlaps a callback the game will use, while its key still lies in save data I can modify.

Finding a useful callback#

I inspected MEM1 around the manager object and checked where writes at different indices would land, looking for function pointers I could overwrite. A neighboring object at 0x80BBB4E0 holds 1’090 script callback pointers.

The callback table belongs to the event script interpreter described above. IDA shows its dispatcher at 0x80369F4C accepting command numbers below 0x442 (1’090), loading the corresponding function pointer and calling it:

lwzx    r12,r4,r7       # callbacks[opcode]
cmpwi   r12,0
beq     failure
...
mtctr   r12
bctrl                   # 0x80369FC4

The pair for forged index 660 in table 2 lands at 0x80BBB5E8, exactly over callback entries 66 and 67:

0x80BB71E0 + 0x628 + 24 × 660 = 0x80BBB5E8
0x80BBB4E0 + 4 × 66           = 0x80BBB5E8
0x80BBB4E0 + 4 × 67           = 0x80BBB5EC

I set table 2’s count to 661, so the backward search starts at index 660, where I placed cp15. The updater then copies my table 0 pair to manager + 0x628 + 24 * 660, overwriting those 2 callbacks.

Because keys are spaced 12 bytes apart while pairs are spaced 24 bytes apart, they drift apart as the index grows. At index 660, the key is still inside the serialized component, where I can place cp15, but the corresponding pair overlaps the neighboring callback table:

count = 661
Component 2: KeysPairsOther stateTailCallbacks
table2.key[660]0x80BB9498
KeysPairsComponent 2 tailCallbacks
0x80BB71E0 + 0x3C8 + 12 * 660 = 0x80BB9498
table2.pair[660]0x80BBB5E8
KeysPairsComponent 2 tailCallbacks
0x80BB71E0 + 0x628 + 24 * 660 = 0x80BBB5E8
search matches
key[660] = cp15 in the component 2 tail at +0x20B8
update writes
pair[660] over callbacks[66]callbacks[67] in the callback table

The save places cp15 at this key, so the update copies the table 0 pair over both callbacks.

The saved count controls how far the search goes, the forged key selects the entry to update and the table 0 pair supplies the 8 bytes to write. When the script dispatcher calls one of the overwritten callbacks, it jumps to my payload.

I traced the cpt1500s event script loaded by the cp15 fallback and saw commands 126, 137, 684 and then 67. That last command is the useful one: its dispatcher path loads callbacks[67] and reaches the bctrl above. The write covers both entries 66 and 67, so I put the payload address in both.

Why 1 cp15 isn’t enough#

A single cp15 entry holding the payload addresses doesn’t work, because an earlier transition looks up that key and uses its pair as an 8 character resource identifier. If it gets my addresses instead, the resource lookup fails before we reach the callback overwrite. Since the table search runs backward, I put 2 entries with the same key in table 0:

IndexKeyPairUsed for
0cp150x80BB96E0, 0x80BB96E0later callback overwrite
1cp150x6C6F636D, 0x6573636Bearly resource lookup

The pair at index 1 spells locmesck, an identifier I found in the game’s resource list. I also added a cp15 entry to table 1 with the pair (1, 1). Those values only have to pass a nonzero check in sub_80047304, which tests whether (word0 | word1) != 0.

With that setup, the game looks up cp15 3 times:

  1. The early transition finds index 1 first and removes it from table 0, reducing the count from 2 to 1. Like every transition out of table 0, it also copies the pair to table 2, so locmesck briefly overwrites callbacks 66 and 67 before the game uses it for the resource lookup.
  2. The later transition takes the payload addresses from index 0 and copies them to index 660 of table 2 the same way, replacing locmesck in callbacks 66 and 67. It also looks the pair up as a resource identifier, but unlike the early transition, it skips the load when nothing matches.
  3. The next transition finds the nonzero pair in table 1. It removes cp15 from tables 1 and 2 and clears the selected resource pair, so the game uses the normal resource fallback. Without this entry, it would look up cp15 in table 2 and try to use the payload addresses as a resource identifier.

Removing cp15 from table 2 removes forged index 660. Because the declared count is exactly 661, index 660 is the last entry, so removal only decrements the count without copying over or clearing its pair. Those bytes are also callbacks 66 and 67, so the overwrite survives.

Payload addresses Resource identifier Nonzero pair Removed
Table 0count = 2
  1. 0 cp15 0x80BB96E0, 0x80BB96E0
  2. 1 cp15 locmesck
Table 1count = 1
  1. 0 cp15 1, 1
Table 2count = 661
  1. 660 cp15 0x00000000, 0x00000000 callbacks[66], callbacks[67]
Resource lookup
none

The save declares 2 entries in table 0, 1 in table 1 and 661 in table 2. Searches run backward from the last entry, so each lookup finds the highest matching index first.

The fallback loads the cpt1500s event script. When that script reaches opcode 67, the dispatcher calls the overwritten callback and jumps to my payload.

Putting code in the save#

For the proof of concept, I used the same payload as in Another Pwn: it clears the game’s XFB to black and draws “pwned by layle” directly into it. The renderer is written in freestanding C and linked at component offset 0x2300, runtime address 0x80BB96E0.

The normal component 2 tail is filled with 0x00 bytes from offset 0x0E38 through almost the end of the 0x3BA0 byte block. The forged key is at 0x20B8. Placing the renderer at 0x2300 keeps it clear of the forged key and leaves 6’296 bytes of space. The finished payload occupies 4’440 bytes.

extern uint8_t console_font_8x16[256 * 16];
static const char phrase[] = "pwned by layle";

__attribute__((section(".text.payload_entry"), noreturn)) void payload_entry() {
    const uint32_t physical = vi_xfb_address(VI32(0x1c));

    render((volatile uint32_t*)(uintptr_t)(physical | 0xc0000000u), 640u, 480u, 640u, 180u);

    __asm__ volatile("sync" ::: "memory");

    for (;;) {
        __asm__ volatile("nop");
    }
}

The renderer writes YCbCr 4:2:2 pixels directly through the uncached alias of the external framebuffer (XFB) that the Video Interface (VI) is already displaying. The build links console_font_8x16 from libogc.a, but it doesn’t initialize libogc’s console, heap or video code inside the game.

The important save fields are:

File offsetValuePurpose
0x2C5C2table 0 count
0x2C601table 1 count
0x2C64661forged table 2 count
0x2DF8, 0x2E04cp15duplicate table 0 keys
0x2DFCcp15table 1 fallback key
0x3050payload address twiceattacker pair
0x3058(1, 1)nonzero pair for the fallback path
0x3068locm / esckvalid sacrificial pair
0x4CF0cp15forged table 2 key 660
0x4F38..0x608Frenderer and complete 4 KiB fontpayload
0x70640x39B46203repaired content CRC32

Making the jump to HackMii#

The 4’440 byte renderer fits in the free space after the forged key, but the 6’180 byte SD loader doesn’t. The HackMii build therefore changes the overwritten callbacks to 0x80BBA8E0, where component 2 holds a stager at offset 0x3500. The stager reconstructs the loader in upper MEM1 at 0x817E0000, flushes the data cache, invalidates the instruction cache and branches to it.

The copy is split into 4 pieces because the forged cp15 key occupies component offset 0x20B8 and the game rewrites 2 more component words at 0x0E9C and 0x1728 after loading:

Component 20x80BB73E0
A Rewritten game field at +0x0E9C B Rewritten game field at +0x1728 C Forged cp15 key at +0x20B8 D
MEM1 loader0x817E0000
Component 2MEM1 loader

The stager copies the 6'180 byte loader in 4 pieces around the 3 reserved words, then joins them at 0x817E0000.

The final range holds only the remaining loader bytes. For a loader large enough to use all 4 ranges, these are the copy operations in src/stager.s, with the size conditionals omitted:

COPY_WORDS (COMPONENT_ADDRESS + 0x0E40), 0x0000, (0x005c / 4)
COPY_WORDS (COMPONENT_ADDRESS + 0x0EA0), 0x005c, (0x0888 / 4)
COPY_WORDS (COMPONENT_ADDRESS + 0x172C), 0x08e4, (0x098c / 4)
COPY_WORDS (COMPONENT_ADDRESS + 0x20BC), 0x1270, ((LOADER_SIZE - 0x1270) / 4)

COMPONENT_ADDRESS is 0x80BB73E0 for NTSC-U and LOADER_SIZE is the loader’s size in bytes. The COPY_WORDS macro takes a source address, a destination offset relative to 0x817E0000 and a count of 32 bit words. The first 3 copies reconstruct 0x1270 bytes, then the last copy appends the remaining loader bytes. The source addresses skip the 3 words reserved for the game’s fields and the forged key, while the destination offsets place the pieces directly after one another.

The stager first disables external interrupts so the game’s handlers can’t race the loader for the IPC registers. It then sets r1 to 0x817D0000 and writes 0 there to mark the end of the new stack’s back chain, as explained in Another Pwn. The stack grows downward, while the reconstructed loader and its BSS start above it at 0x817E0000. Because this takeover uses a script callback, it doesn’t need to replace a saved LR on the game’s stack.

Both exploits share the FAT and ELF parts of libpwn/, described in the Another Pwn writeup, but they talk to IOS differently. Another Pwn calls the IOS routines already present in its game, whereas on a real Wii FFCC’s very first IOS_Open("/dev/sdio/slot0") failed before the loader could read a single sector. The same path worked in Dolphin.

The FFCC build therefore takes ownership of IPC instead of calling back through the game’s wrapper. It clears pending IPC state, closes the inherited IOS file descriptors and talks to Starlet through a small polling transport in ios_direct.c. Once SD is ready, the shared FAT reader loads boot.elf and the ELF loader copies its segments into memory.

Before jumping to HackMii, the loader sets up IBAT4, an instruction Block Address Translation register pair that maps program addresses to physical memory for fetching code. As in Another Pwn, it maps the 64 MiB range starting at 0x90000000 so HackMii can execute its startup code after moving it into MEM2.

Read and write Execute No instruction mapping
MEM10x80000000–0x817FFFFF24 MiB
  1. 0x80004000 HackMii entry point from boot.elf
  2. 0x80BB73E0 Component 2 save data
    0x80BBA8E0 Stager
  3. 0x81200000 boot.elf staging buffer
  4. 0x817D0000 Initial r1, the loader stack grows toward lower addresses ↑
  5. 0x817E0000 Loader code and data, followed by BSS
  6. 0x81800000 End of MEM1
MEM20x90000000–0x93FFFFFF64 MiB
  1. 0x90010000 HackMii startup code, entered at 0x90010068 can't execute
  2. 0x94000000 End of MEM2

Component 2 lives in MEM1, so the stager at 0x80BBA8E0 runs in place. HackMii later moves its startup code to 0x90010000 in MEM2, where the game has no instruction mapping.

With boot.elf on the SD root, the loader reaches the final handoff and HackMii Installer v1.2 appears:

HackMii Installer v1.2 showing its anti-scam warning after being launched by the Crystal Bearers save exploit.
HackMii Installer v1.2 launched by the crafted save.

Taking the exploit to PAL#

The PAL release uses the same save layout, table counts, forged index and cp15 event path. What changes is where component 2 is allocated, which also moves the renderer and stager:

RegionTitleComponent 2Renderer addressStager address
NTSC-URFCE0x80BB73E00x80BB96E00x80BBA8E0
PALRFCP0x80BB74E00x80BB97E00x80BBA9E0

The build selects the component base through regions/ntsc-u.mk or regions/pal.mk. Keeping the callback targets relative to that base means the patching logic itself stays identical, while the direct IPC transport is region independent. The PAL build follows the same Data Management, User 1 and Continue path and reaches HackMii in Dolphin and on a stock Wii.

The complete chain#

I released the exploit as The Haxx Bearers in the wii-exploits repository. With boot.elf on the SD root, import the save through Wii Data Management, load User 1 and choose Continue to launch HackMii. It works both in Dolphin and on a stock Wii.

The completed chain is:

  1. The unchecked saved count makes the table search reach forged table 2 entry 660.
  2. Updating that entry performs a controlled 8 byte out-of-bounds write.
  3. The write replaces callbacks 66 and 67 with 0x80BBA8E0.
  4. The normal cp15 fallback loads cpt1500s.
  5. Opcode 67 dispatches through the overwritten callback.
  6. The stager reconstructs the loader at 0x817E0000.
  7. The direct IPC SD reader clears FFCC’s inherited IOS state and loads boot.elf at 0x81200000.
  8. The ELF loader starts HackMii Installer v1.2.

I borrowed Layle’s name as a kid. Years later, I got the game to run my code :^)

Thank you#

A big thank you to Zayd for reviewing this post and giving me valuable feedback. He’s the creator of beanwii and also makes some really cool videos about emulation on his YouTube channel. If you enjoyed this post, go check them out!