Pwning the Wii: Another Pwn

When I started writing Gecko, my GameCube and Wii emulator, I set myself a goal: find vulnerabilities in Wii games and system software. I wanted to start with a game of my choosing, then tackle my favorite game of all time (Final Fantasy Crystal Chronicles: The Crystal Bearers) and eventually look for a vulnerability in the Wii System Menu. I haven’t started the System Menu work yet (LLE’ing Starlet first with a friend).

This is the first post in that series. I picked Another Code: R because it was already a favorite of mine. The next post covers The Crystal Bearers.

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

Starting with main.dol#

I extracted main.dol from the European RNOP01 build and opened it in IDA Pro using the DOL loader from Gecko’s IDA loaders. Sadly, there were no debug symbols to make my life easier, so I started by tracking down what happens when we select Load Game.

Once FILE 1 has been read, the menu state machine at 0x8056556C calls 0x8006668C. That function rebuilds the game’s runtime objects from the save, so I named it ACR_Save_DeserializeSlot.

The part of the save we’re interested in is an array of 20 entries, which I’ll call records. Each record contains a header, a name, a data buffer and a field giving the length of that data. These are entries within FILE 1, not separate save slots.

The function at 0x80063374 writes these records when saving. When the game loads FILE 1, it copies each record’s name and metadata into a runtime object that also holds a pointer to where the record’s data should go. The saved record and runtime object have different layouts:

typedef struct {
    uint8_t header_before_metadata[0x09];
    int8_t metadata_09;
    int8_t metadata_0a;
    int8_t metadata_0b;
    uint8_t header_tail[0x04];
    char name[0x18];
    uint32_t metadata_28;
    uint32_t metadata_2c;
    uint8_t data[0x6400];
    uint32_t data_length;
} ACR_SaveRecord;       // 0x6434 bytes

typedef struct {
    uint8_t metadata_before_name[0x20];
    char name[0x18];
    uint32_t metadata_38;
    uint32_t metadata_3c;
    uint32_t metadata_40;
    uint32_t metadata_44;
    void* data_destination;
    uint32_t data_length;
    uint8_t rest[0x0c];
} ACR_LoadedRecord;     // 0x5c bytes

Both name fields are 24 bytes long. In the save, the name is followed by 2 metadata words and then the data, so if neither the name nor those words contain a zero byte, nothing stops a string copy at the end of the name :^)

Making sense of the save#

The game reads FILE 1 from save_data_00. The file is 0xEBB00 bytes long and stores a big-endian CRC32 at 0xEBAEC. The game’s checksum routine at 0x801ED6C8 covers every byte before that field, so whenever we modify a record, we have to recalculate the checksum before the game will load the save.

The record array starts at 0x217C8:

FieldOffset
record header+0x00
24 byte name+0x10
metadata words+0x28, +0x2C
data, up to 25’600 bytes+0x30
big-endian data length+0x6430
next record+0x6434

The deserializer handles records from 19 downwards, so record 19 at file offset 0x987A4 is the first one it touches!

A name that doesn’t know when to stop#

Here’s the relevant part of the decompilation, with unrelated code removed:

loaded_record = &g_ACR_LoadedRecords[19];
save_record = &save->records[19];

for (record_index = 19; record_index >= 0; --record_index) {
    // Earlier runtime fields are restored from the header and metadata_28.

    strcpy(loaded_record->name, save_record->name);     // 0x800672AC

    loaded_record->metadata_38 = save_record->metadata_09;
    loaded_record->metadata_3c = save_record->metadata_0a;
    loaded_record->metadata_40 = save_record->metadata_0b;
    loaded_record->metadata_44 = save_record->metadata_2c;

    ACR_Save_CopyBlobAndFlush(                          // 0x800672EC
        loaded_record->data_destination,
        save_record->data,
        save_record->data_length);
    // ACR_Save_CopyBlobAndFlush is effectively a memcpy with extra steps

    loaded_record->data_length = save_record->data_length;
    --loaded_record;
    --save_record;
}

The call at 0x800672AC is an ordinary unbounded strcpy. A normal name ends with a NULL inside its 24 bytes, so the copy stops there. I filled the saved name and the 2 following metadata words with nonzero values, so strcpy continued into the record’s data and kept writing past the runtime object’s name field.

The overflow also runs over the runtime fields at +0x38 through +0x44, but the 4 assignments after strcpy immediately restore them from the save. None of them touch data_destination at +0x48, so the overwritten pointer survives until the loop passes it to ACR_Save_CopyBlobAndFlush as the destination for the record’s data.

The pointer is 40 bytes after the start of the runtime name, while the saved data begins 32 bytes after the saved name starts, so data bytes 8–11 land exactly on data_destination.

Normal record ("Ashley")

save_record
header
name
metadata_28
metadata_2c
data
loaded_record
name
metadata_38
metadata_3c
metadata_40
metadata_44
data_destination
data_length

Crafted record

save_record
header
name
metadata_28
metadata_2c
data
loaded_record
name
metadata_38
metadata_3c
metadata_40
metadata_44
data_destination
data_length
NameDataPointerMetadata / lengthNULL -- outside string copy.. value omitted
Illustrative values showing how the crafted name overwrites the data pointer.

The call at 0x800672EC reaches the helper at 0x80121980, which copies the data and does cache stuff:

void ACR_Save_CopyBlobAndFlush(void* dst, const void* src, uint32_t length) {
    memcpy(dst, src, length);
    ++g_copy_count;
    sub_801E2094(dst, length);      // cache stuff
}

The source data and copy length already come from the save. Now that the destination does too, we can make the game’s own memcpy do this:

memcpy(attacker_address, attacker_data, attacker_length);

That’s an arbitrary write!

There’s one more catch: strcpy must reach the pointer before it encounters a NULL. That constrains both the prefix bytes before the pointer and the address bytes themselves:

Data offsetsContentsConstraint
0-7Prefix bytesMust be nonzero so strcpy reaches the forged pointer
8-11Forged destination addressIts 4 bytes must also be nonzero
12NULL terminatorStops strcpy just after the pointer
13 onwardRemaining dataCan contain anything because strcpy has already stopped

With a copy length of 8 or less, memcpy only writes the prefix bytes I chose. Longer copies also work because memcpy keeps going through the forged pointer and NULL for whatever length the save supplies. If I put a larger block at data offset 16 and set the forged destination to target - 16, 16 junk bytes land just before target, then the whole block lands where I want it, NULL bytes included.

I use that larger form for the stager and the short form when I only need to replace a pointer.

Redirecting the return#

On PowerPC, the link register (LR) holds the return address, which the deserializer saves on the stack immediately after entry:

8006668C  stwu    r1, -0x70(r1)
80066690  mflr    r0
80066694  stw     r0, 0x74(r1)

When FILE 1 is loaded, the entry stack pointer is 0x80560C98. Allocating the 0x70 byte frame sets r1 to 0x80560C98 - 0x70 = 0x80560C28. The saved LR slot at 0x74(r1) is therefore at 0x80560C28 + 0x74 = 0x80560C9C. The arbitrary write can reach that address and its bytes 80 56 0C 9C contain no NULL bytes :)

AddressStack contents
0x80560C28Back chain: the incoming stack pointer, 0x80560C98
0x80560C98Start of the caller’s frame
0x80560C9CSaved LR, overwritten with the stager address 0x817FE140

I used the name overflow to point data_destination at the saved LR slot and gave that record the 4 big-endian bytes 81 7F E1 40 as its data. The copy helper overwrites the saved LR with that address, so when the deserializer restores LR from the stack and returns, it jumps into a small stager I’ll get to in a moment.

Getting the payload into executable memory#

The loader doesn’t need to be written anywhere yet: the game has already loaded the whole save into memory and strcpy never reads past the NULL at data offset 12. The rest of record 19’s 25’600 byte data area can therefore hold both the stager and the loader, NULL bytes included.

When FILE 1 is loaded, the save buffer starts at 0x905A1E40 and the record array begins 0x217C8 bytes into it. Each record occupies 0x6434 bytes, so record 19 is another 19 × 0x6434 = 0x76FDC bytes into the array. Its data starts at record offset 0x30 and the loader sits 0x10C bytes into that data. Adding those offsets gives the loader’s address: 0x905A1E40 + 0x217C8 + 0x76FDC + 0x30 + 0x10C = 0x9063A720.

That address is in MEM2. The Wii has 24 MiB of main memory called MEM1 and another 64 MiB region called MEM2. PowerPC uses Block Address Translation (BAT) register pairs to map large ranges of effective addresses (the addresses a program uses) to physical memory. The MPC750 User’s Manual, Chapter 5 makes a distinction that matters here:

There are separate instruction and data BAT mechanisms

Instruction BATs (IBATs) provide mappings for fetching code, while data BATs (DBATs) provide mappings for reads and writes. The game can read and write the save buffer in MEM2, but its current instruction mappings only cover MEM1, so I can’t simply jump to the loader inside the save.

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

The game's data mappings let it read and write the save buffer in MEM2, but no instruction BAT covers MEM2. Branching to the loader at 0x9063A720 would fail, so the stager copies it to 0x817E0000 in MEM1 first.

A small stager gets around this. Record 19 places the complete 68 byte stager at data offset 16 and copies 84 bytes to 0x817FE130. The first 16 bytes land in scratch space immediately before the stager, while the remaining 68 bytes land at 0x817FE140. Record 18 overwrites the saved LR. The stager calls the game’s own copy helper to move the large loader from 0x9063A720 to 0x817E0000, invalidates the instruction cache through the game’s ICInvalidateRange and branches to the copy.

  1. Save records 18 and 19 write the 68 byte stager and overwrite the saved LR.
  2. MEM1 stager at 0x817FE140 copies the loader from 0x9063A720 in MEM2 to 0x817E0000.
  3. C loader at 0x817E0000 loads boot.elf from the SD card and maps MEM2 as executable.
  4. HackMii starts at the ELF entry point, 0x80004000.

The stager uses LOADER_SIZE, the loader’s size in bytes, for both the copy and cache invalidation:

lis     r3, 0x817E               # destination: executable MEM1
lis     r4, 0x9063
ori     r4, r4, 0xA720           # source: loader inside the save
li      r5, LOADER_SIZE
lis     r12, 0x8012
ori     r12, r12, 0x1980         # ACR_Save_CopyBlobAndFlush
mtctr   r12
bctrl

lis     r3, 0x817E               # copied range
li      r4, LOADER_SIZE
lis     r12, 0x801E
ori     r12, r12, 0x20EC         # ICInvalidateRange
mtctr   r12
bctrl

lis     r12, 0x817E
mtctr   r12
bctr                             # enter the C loader

For the proof of concept in the video, record 19 holds a small renderer instead of the SD loader. It reads the active XFB address from the Video Interface, clears the image to black and draws “pwned by layle” using libogc’s 8×16 font table, writing pixels directly through the XFB’s uncached memory alias:

A black Wii framebuffer with the centered white text pwned by layle.
The proof of concept draws white text into the game’s XFB.

Loading boot.elf from SD#

I linked the freestanding C loader at 0x817E0000. It runs inside the game rather than as a normal homebrew program, so nothing sets up a stack or clears its memory for it.

On PowerPC, r1 points to the current stack frame. The first word of that frame is its back chain, which holds the address of the previous frame. The loader starts on a new stack, so I write 0 there to mark the end of the chain for anything walking the stack. The assembly entry point in entry.s sets this up before entering C:

lis     r1, 0x817D          # stack starts at 0x817D0000
li      r0, 0
stw     r0, 0(r1)           # terminate the stack back chain

It then clears BSS, the loader’s global and static variables that start at zero, before branching to payload_main. For SD access, I called the SDK routines already present in the game. They talk to IOS, the operating system on the Wii’s Starlet coprocessor that handles I/O. Their addresses are defined in payload.ld:

AddressRoutineUse
0x8020EAC4IOS_Openopen /dev/sdio/slot0
0x8020ECA4IOS_Closerelease the SDIO descriptor
0x8020F374IOS_Ioctlhost setup and SD card commands
0x8020F6C4IOS_IoctlvDMA-backed sector reads and SDHC card commands

The loader initializes SD, mounts a FAT16 or FAT32 volume and looks for boot.elf in the root directory. In loader.c, boot_elf holds the 11 byte FAT name BOOT ELF. STAGE_BASE is 0x81200000. Once loader_sd_startup() succeeds, the rest of the loading sequence is short:

disc.readSectors = loader_sd_read_sectors;
image_size = fat_load_root_file(&disc, boot_elf, STAGE_BASE);
if (!image_size)
    for (;;) {}

if (!loader_sd_shutdown())
    for (;;) {}

start = (entrypoint*)elf_install(STAGE_BASE, image_size);
if (!start)
    for (;;) {}

elf_prepare_handoff();
start(0, NULL, NULL, NULL);

The FAT reader uses loader_sd_read_sectors to read the file into STAGE_BASE and returns its size. elf_install validates the ELF header and segment bounds, copies each loadable segment (PT_LOAD) to its target address and zeroes any additional memory reserved at the end of the segment. It then synchronizes the caches and returns the ELF entry point. elf_prepare_handoff sets up the MEM2 instruction mapping before the final call transfers execution to HackMii.

That MEM2 mapping is needed for HackMii itself. Its entry at 0x80004000 soon copies startup code to 0x90010000 and branches to 0x90010068. The game’s existing instruction BATs do not cover that address, so elf_prepare_handoff installs a cached 64 MiB MEM2 mapping in IBAT4. Here’s the inline assembly from elf_loader.c:

sync
isync
li      r0, 0
mtspr   560, r0             # invalidate IBAT4U before changing the mapping
isync
lis     r0, 0x1000
ori     r0, r0, 0x0002
mtspr   561, r0             # IBAT4L: physical base 0x10000000 and access attributes
lis     r0, 0x9000
ori     r0, r0, 0x07FF
mtspr   560, r0             # IBAT4U: effective base 0x90000000, 64 MiB and valid bits
isync

This lets the processor fetch instructions from 0x90000000 through 0x93FFFFFF, covering HackMii’s early MEM2 code.

A loader for both games#

Both Another Pwn and The Haxx Bearers build their FAT and ELF loader from the same libpwn/ sources. The SD reader implements the small part of libogc’s SDIO sequence that the loader needs. In Another Pwn, it sends those requests through the game’s own IOS_* routines.

Wii programs follow the PowerPC embedded ABI (EABI), which groups small global variables into 2 compact areas. Startup code points r13 at the writable one (.sdata and .sbss) and r2 at the read-only one (.sdata2 and .sbss2), then leaves both registers alone for the rest of the program. In Another Code: R, startup sets r13 to 0x805536A0 and r2 to 0x80554F40. The compiler can then reach those globals with a single load or store relative to r13 or r2. Inside the game, both registers still point at the game’s own areas, so I built both loaders with -msdata=none. That makes them address every global through its full address instead.

The memory layout is:

0x81200000  boot.elf staging buffer
    ...
            loader's stack grows toward lower addresses
0x817D0000  initial r1, back-chain word = 0
    ...
0x817E0000  loader code, read-only data and initialized data
            BSS follows, including the SD and FAT buffers
0x81800000  end of MEM1

The Another Code stager still uses the game’s stack while calling its copy and cache helpers, then switches to 0x817D0000 at loader entry. The addresses in the patcher are specific to RNOP01 and the FILE 1 loading path.

HackMii on the real Wii#

I copied the crafted save to my Wii through Data Management, put boot.elf on the SD card and loaded FILE 1. After tons of debugging I’ll spare you, HackMii Installer finally appeared on the console! It works the same way in Dolphin. I released the exploit as Another Pwn in the wii-exploits repository.

HackMii Installer's anti-scam warning and Press 1 to continue prompt after loading the Another Pwn save in Dolphin.
HackMii Installer launched by Another Pwn, captured in Dolphin.

From save file to code execution#

The complete path is now:

  1. Wii Data Management imports the crafted save because the title has no copy restriction.
  2. The game verifies the repaired CRC32 and deserializes FILE 1.
  3. An unbounded strcpy replaces a runtime data pointer.
  4. The following copy helper becomes a constrained arbitrary write primitive.
  5. One record writes a MEM1 stager and another replaces the saved LR.
  6. The stager copies the freestanding loader out of the save buffer.
  7. The loader initializes SD, reads /boot.elf and copies its loadable segments to their target addresses.
  8. IBAT4 makes HackMii’s early MEM2 code executable and the loader enters it at 0x80004000.

Another Code: R is a game about recovering lost memories. This one worked because a record forgot where its name ended :^)

Thank you#

A big thank you to Zayd and Blody for reviewing this post and giving me valuable feedback! Blody also challenged the assumptions behind my first stager, which made the exploit a lot simpler. Zayd is the creator of beanwii and also makes some really cool videos about emulation on his YouTube channel. If you enjoyed this post, go check him out!