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.
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 :^)
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:
| Field | Offset |
|---|---|
| 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!
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.
save_recordheader name metadata_28 metadata_2c data loaded_recordname metadata_38 metadata_3c metadata_40 metadata_44 data_destination data_length save_recordheader name metadata_28 metadata_2c data loaded_recordname metadata_38 metadata_3c metadata_40 metadata_44 data_destination data_length -- outside string copy.. value omitted 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 offsets | Contents | Constraint |
|---|---|---|
0-7 | Prefix bytes | Must be nonzero so strcpy reaches the forged pointer |
8-11 | Forged destination address | Its 4 bytes must also be nonzero |
12 | NULL terminator | Stops strcpy just after the pointer |
13 onward | Remaining data | Can 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.
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 :)
| Address | Stack contents |
|---|---|
0x80560C28 | Back chain: the incoming stack pointer, 0x80560C98 |
0x80560C98 | Start of the caller’s frame |
0x80560C9C | Saved 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.
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.
0x80000000–0x817FFFFF24 MiB0x80004000 HackMii entry point from boot.elf 0x81200000 boot.elf staging buffer 0x817D0000 Initial r1, the loader stack grows toward lower addresses ↑ 0x817E0000 Loader code and data, followed by BSS 0x817FE140 Stager, 68 bytes 0x81800000 End of MEM1 0x90000000–0x93FFFFFF64 MiB0x90010000 HackMii startup code, entered at 0x90010068 can't execute 0x905A1E40 FILE 1 save buffer 0x9063A720 Loader inside record 19 can't execute 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.
0x817FE140 copies the loader from 0x9063A720 in MEM2 to 0x817E0000.0x817E0000 loads boot.elf from the SD card and maps MEM2 as executable.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:

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:
| Address | Routine | Use |
|---|---|---|
0x8020EAC4 | IOS_Open | open /dev/sdio/slot0 |
0x8020ECA4 | IOS_Close | release the SDIO descriptor |
0x8020F374 | IOS_Ioctl | host setup and SD card commands |
0x8020F6C4 | IOS_Ioctlv | DMA-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.
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.
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.

The complete path is now:
strcpy replaces a runtime data pointer./boot.elf and copies its loadable segments to their target addresses.0x80004000.Another Code: R is a game about recovering lost memories. This one worked because a record forgot where its name ended :^)
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!