Introduction

For those that grew up with the Game Boy era consoles, the release of the Nintendo DS (NDS) opened up a completely new world. An innovative dual screen, touchscreen functionality, a microphone and wireless features. And even with all these features, it didn’t forget its predecessor, offering full backwards compatibility for Game Boy Advance (GBA) games. Some Nintendo DS games even introduced additional gameplay features if a GBA game is inserted in the second slot of the console during play.

For those acquainted with the fourth generation of Pokémon games, you may be aware that several features can be unlocked by inserting a generation 3 game in the second slot. This includes encountering Pokémon exclusive to the GBA game and migrate Pokémon through Pal Park. This is a one-way, irreversible process. Once a Pokémon is migrated from Generation 3 to Generation 4, it is permanently removed from the save file on the GBA game. Some of you may already be catching on: we’ll take advantage of this feature to hack GBA save files.

Turning a fun feature into a logistical nightmare

Over the past three years, I’ve uncovered several glitches in Generation 4 Pokémon games giving total control. This can be achieved through Arbitrary Code Execution (ACE) or Remote Code Execution (RCE), exploits that allow you to execute your own code on a system.

The objective is to leverage ACE within a Nintendo DS game to manipulate the code responsible for communicating with the Game Boy Advance (GBA) system, thus enabling us to modify its save file. While it’s possible to develop this functionality from the ground up, leveraging existing functions is easier. Fortunately, Pokémon Diamond and Pearl have some of the most flexible ACE exploits found in any game. Furthermore, due to the Pal Park feature, the necessary code for save file manipulation is already implemented.

Plan of action

Migrating Pokémon is an option only present after booting up the game, on the main menu. After reverse engineering some of the migration code, it quickly became evident that it expects a lot of data to already be initialized properly. I didn’t really feel like dealing with this, so we’ll get a fun little side quest! Hacking the Nintendo DS boot code to maintain ACE through soft resetting the console.

As with most Pokémon games, a soft reset feature exists to quickly reboot the system with a button combo. If we could somehow keep ACE through this process, it could modify the Pal Park migration features after the reboot. Let’s have a quick look at how the reboot process works.

Bizhawk allows me to enable a tracelogger during the rebooting process. I wrote a customized version that automatically creates a call tree from the traceback, using the names of reverse engineered functions in my Ghidra project. After tracing a couple calls, we eventually end up in the following function.

void Sys_Reboot(uint32_t *stack) // FUN_020cd3b4 on English Diamond/Pearl

{
  if (*DAT_020cd42c == 2) {
    Sys_Terminate();
  }

  Sys_LockROM(OS_GetLock());

  Sys_HaltDma(0);
  Sys_HaltDma(1);
  Sys_HaltDma(2);
  Sys_HaltDma(3);

  FUN_0020c9e94(0x40000);
  FUN_020c9de0(0xffffffff);
  *DAT_020cd430 = stack;
  FUN_020cd434(0x10);

  Sys_RebootSystem();
  return;
}

I didn’t take the time to fully reverse-engineer all calls, but following the traceback further leads us to Sys_RebootSystem(). This function is located in Instruction Tightly-Coupled Memory. ITCM is 32kb of dedicated memory, directly connected to the processor. This makes it perfect for performance-critical sections of an application. Sadly Ghidra didn’t automatically decompile this memory region, so I simply copied the bytecode at 0x01ff84a4 (Sys_RebootSystem) and shoved it through shell-storm to get a quick look at the assembly code.


0x01ff84a4:  00 40 2D E9    stmdb sp!, {lr}
0x01ff84a8:  04 D0 4D E2    sub   sp, sp, #4

; get interrupt address
0x01ff84ac:  28 00 9F E5    ldr   r0, [pc, #0x28] => 0x21D37E0

; wait for an interrupt at 0x21D37E0
0x01ff84b0:  B0 10 D0 E1    ldrh  r1, [r0]
0x01ff84b4:  00 00 51 E3    cmp   r1, #0
0x01ff84b8:  FC FF FF 0A    beq   #0x1ff84b0

; write flag to 0x4000208
0x01ff84bc:  1C 00 9F E5    ldr   r0, [pc, #0x1c] => 0x4000208
0x01ff84c0:  00 10 A0 E3    mov   r1, #0
0x01ff84c4:  B0 10 C0 E1    strh  r1, [r0]

; reboot
0x01ff84c8:  BD FF FF EB    bl    #0x1ff83c4 ; Sys_ReloadMemory
0x01ff84cc:  38 FF FF EB    bl    #0x1ff81b4 ; Sys_ForceBoot

After waiting for some interrupt, it calls Sys_ReloadMemory followed by Sys_ForceBoot. From some preliminary testing, it seems the system is not able to boot unless Sys_ReloadMemory has been called. This function copies all static functions from ROM to memory, sets interrupts, and clears/initializes a bunch of memory. Sys_ForceBoot boots you back into the game. Since most memory is reloaded, we’ll need to find a way to keep our custom payload running regardless. Luckily, there is a section of memory near the end of main memory (0x23A8000-0x23E0000), that is unused by the game and therefore not wiped when calling Sys_ReloadMemory. This is perfect to store the payload. From now on, I’ll refer to this payload as Pay_InjectCustomCode.

From plan to execution

To summarize, these steps should be followed to achieve our goals:

  1. Call Sys_ReloadMemory, necessary to get the system in a state to reboot.
  2. Call Pay_InjectCustomCode which modifies code that runs often/during the reboot.
    Should be in placed in unused memory (0x23A8000-0x23E0000).
  3. Call Sys_ForceBoot to boot into the hacked game.

A first test looked as follows:

(...)

; reload, jump to custom code
0x01ff84c8:  BD FF FF EB    bl    #0x1ff83c4 ; Sys_ReloadMemory
0x01ff84cc:  CD DE 0E EB    bl    #0x23B0000 ; Pay_InjectCustomCode

; unused memory with custom code 'Pay_InjectCustomCode'
; hacks a function ran often or early in the booting process
0x023B0000: (...)

; reboot
0x023B0010: 67 20 F1 EB    bl #0x1ff81b4 ; Sys_ForceBoot

Run the code, aaand?! Nothing. No indication the code ran, no crash, nothing.

The payload is still present in unused memory, so it didn’t get wiped by Sys_ReloadMemory. Putting a breakpoint at 0x23B0000 indicates the payload never runs. So what’s going on? Sys_ReloadMemory might not be touching the unused memory, but ITCM definitely isn’t unused, and thus reloaded. In other words, Sys_RebootSystem went back to it’s previous, correct, code. The call to the custom payload is reverted to Sys_ForceBoot.

Well, no big deal. Simply make it so Sys_ReloadMemory itself is also executed from unused memory. When it finishes, execution will resume in unused memory and execute the custom payload.

(...)

; jump to custom code
0x01ff84c8: CC DE 0E EB    bl #0x23B0000 ; unused memory containing payload

; unused memory
0x023B0000: EF 20 F1 EB    bl #0x1ff83c4 ; Sys_ReloadMemory

; custom code 'Pay_InjectCustomCode'
; hacks a function ran often or early in the booting process
0x023B0004:
(...)

0x023B0010: 67 20 F1 EB    bl #0x1ff81b4 ; Sys_ForceBoot

Not quite as they say, but hey, second time’s the charm! The POC payload hijacked MSG_LoadStringIndex (0x0200a9c4 for EN), so it always returns ‘Hello, World!‘.

Now that it’s possible to keep ACE through a soft reboot, it’s time to hack the Pal Park transfer code, and modify GBA save files. It’s important to note however that the Pal Park transfer code is dynamically loaded as an overlay when it’s about to be used. This means that we’ll need to write some code that can detect when this overlay is loaded, and modify it. Luckily, this is a problem I’ve already solved in the past. The concept is simple: just like there is Sys_ReloadMemory, there is a dedicated Sys_LoadOverlay, responsible for loading overlays. We overwrite the return instruction of this function with a jump to a custom payload. Check if the desired overlay has been loaded, and modify it straight away! We’ll store this second payload in the same unused memory, as we don’t want it to be wiped.

original code: Sys_RebootSystem
    ; Jump to ReloadROM, followed by force booting the game
    0x01ff84c8:  BD FF FF EB    bl     #0x1ff83c4 ; Sys_ReloadMemory
    0x01ff84cc:  38 FF FF EB    bl     #0x1ff81b4 ; Sys_ForceBoot


modified code: Sys_RebootSystem
    0x01ff84c8:  CC DE 0E EB    bl #0x23b0000 ; unused memory containing payload

custom payload: Pay_InjectCustomCode
    ; Run Sys_ReloadMemory, since this code is running in unused memory it's not overwritten.
    0x023b0000:  EF 20 F1 EB    bl #0x1ff83c4 ; Sys_ReloadMemory
    
    ; Hack Sys_LoadOverlay at 0x020d74e8, which loads all overlays (dynamic code).
    ; After it loads the overlay, immediately modify desired functions
    0x023b0004:  08 00 9f e5    ldr r0, [0x23b0014] ; load return address of Sys_LoadOverlay
    0x023b0008:  08 10 9f e5    ldr r1, [0x23b0018] ; load new jump instruction (to 0x23b0020)
    0x023b000c:  00 10 00 e5    str r1, [r0,#0x0 ] ; replace return with jump instruction

    ; Reboot the system
    0x023b0010:  67 20 F1 EB    bl #0x1ff81b4 ; Sys_ForceBoot

    ; Data/Immediates
    0x023b0014:  D8 75 0D 02    (address 0x020d75d8, return address of Sys_LoadOverlay)
    0x023b0018:  90 62 0b ea    (jump instruction to 0x23b0020, replaces return instruction)

After this little snippet, if we reboot the game, Sys_LoadOverlay will jump to 0x23B0020 each time it finishes loading an overlay. So let’s implement a method to hack the loaded overlay next.

2. Pay_InjectCustomOverlays

    ; change mode from ARM to Thumb, as it takes less space.
    023b0020  01 50 9f e2    adds       r5, pc, #0x1 => 0x23B0029
    023b0024  15 ff 2f e1    bx         r5

    ; Execute code starting at 0x23B0028 in Thumb mode
    023b0028 ff b5            push      {r0,r1,r2,r3,r4,r5,r6,r7,lr}
    023b002a 17 35            add       r5, #0x17 => (0x23B0040) ; data address

    ; Loop that copies data to from and to a specified addresses.
    ; While meant to modify loaded overlays, it can also modify data or static functions.

    023b002C 07 cd            ldmia      r5!,{r0,r1,r2} (src, dest, size)
    023b002E 00 28            cmp        r0, #0x0
    023b0030 02 d0            beq        return => 0x023B0038 
    023b0032 1e f5 d6 e9      blx        Mem_CopyByteAlligned => (0x20CE3E0)
    023b0036 f9 e7            b          loop => (0x23B002C)

    ; return
    023B0038 ff bd            pop  {r0,r1,r2,r3,r4,r5,r6,r7,pc}

    ; Starting at 0x23B0040, define data to copy. Format is as follows:
    
    uint32_t source_address;
    uint32_t destination_address;
    uint32_t copy_size;

    ; if source_address is 0, the loop will halt.
    

This is a very short code that allows you to easily modify memory. Just be aware that since overlays are dynamic, they replace previous overlays. Since this code runs each time an overlay is loaded, it can also modify unrelated overlays allocated to the same address. To prevent this, the code could be expanded with a checksum algorithm to ensure it only modifies the overlay you want it to.

If you want code to persist indefinitely through multiple soft resets, you can make the code overwrite 0x01ff84c8 in Sys_RebootSystem with the jump to the custom payload, after each reboot.

Now that we have a full system to keep code execution through boot, and modify any functions that get loaded, it’s time to hack the Pal Park Migration code. As a proof of concept, I wrote a function allowing you to edit a Pokémon’s species Id to another. Luckily for us, this turns out to be incredibly simple. Let’s take a quick look at the code to transfer selected Pokémon from the GBA save to the DS game.

void GBA_MigratePokemon(GBA_Data *gbaData) // overlay 83, FUN_02234e6c on English Diamond/Pearl

{
  PalPark *palPark = Save_GetPalParkData(gbaData->save);

  GBAPokeStruct *gbaPokemon;
  PokeStruct *dsPokemon;
  PokeMainData *mDsPokemon = GetPokeMainDataPointer(&dsPokemon);

  // Copy Pokémon from GBA save to DS Pal Park
  int curPokemon, inBoxId, boxId;
  for (int i = 0; i < 6; i++) {
    curPokemon = gbaData->selectedPokemon[i];
    inBoxId = curPokemon.inBoxId;
    boxId = curPokemon.boxId;
    gbaPokemon = &gbaData->boxWrapper->boxdata[bobId][inBoxId];

    ConvertGBAPokeToDSPoke(gbaPokemon, mDsPokemon);
    Save_SetPalParkPokemon(palPark, mDsPokemon, i);
  };
  
  // Wipe Pokémon from GBA save, by setting species Id to 0.
  int SPECIES_ID_PARAM = 0xB;
  for (i = 0; i < 6; i++) {
    curPokemon = gbaData->selectedPokemon[i];
    inBoxId = curPokemon.inBoxId;
    boxId = curPokemon.boxId;
    gbaPokemon = &gbaData->boxWrapper->boxdata[bobId][inBoxId];

    if(inBoxId != -1 && boxId != 0xE){
      GBA_SetPokeParam(&gbaPokemon, SPECIES_ID_PARAM, 0);
    }
  }

  return;
}

I’m not entirely sure why they split this into two loops that practically use the exact same variables, but it does make it easy to explain each loop’s purpose. The first loop goes through all 6 of the Pokémon that are selected for migration, looks for their data in GBA boxdata1, converts it to the DS Pokémon format and stores it in the Pal Park section of the DS save.

The second loop similarly looks for the Pokémon’s data, and simply sets it’s species Id to 0. This is a pattern seen in other places too, where instead of wiping the entire object, it’s identifier is simply set to null/zero and the object is therefore disregarded. This makes it extremely easy to write a custom Pokémon species by changing the null to a desired species Id instead. The rest of the Pokémon’s data will remain in tact.

As a simply proof of concept I wrote some code that writes any Pokémon you want to the gen 3 save file. I also disabled the 24-hour waiting period between migration attempts, and set the required amount of selected Pokémon to transfer to one. The selected Pokémon will have it’s species Id modified.

    < -------------------------------------------------------------------- >
 
    GBA_MigratePokemon (0x02234E6C)
        ; Set Id of Pokémon to write in gen 3
       ::02234eba 1b  4d           ldr        r5,[ DATA_02234f28 ]
       ::02234ebc 10  26           mov        r6,#0x10
       ::02234ebe f5  40           lsr        r5,r6

       ::02234f2A id id            ; the id of the Pokémon you want to write to gen 3

    0x023B0040 ;
        90 00 3B 02    ; source_address = 0x023B0090
        BA 4E 23 02    ; destination_address = 0x02234eba
        06 00 00 00    ; copy_size = 0x6

        96 00 3B 02    ; source_address = 0x023B0096
        2A 4F 23 02    ; destination_address = 0x02234f2A
        02 00 00 00    ; copy_size = 0x2
    
    0x023B0090:
        1B 4D 10 26 F5 40 ; instructions to set id
        id id

    < -------------------------------------------------------------------- >

    GBA_CheckErrorConditions (0x02236548)
        ; return 0, no error (ignore 24 hour limit)
        ::0223654A 00  20           mov        r0,#0x3
        ::0223654C f0  bd           pop        {r4,r5,r6,r7,pc}

    0x023B0058 ;
        98 00 3B 02    ; source_address = 0x023B0098
        4A 65 23 02    ; destination_address = 0x0223654A
        04 00 00 00    ; copy_size = 0x4

    0x023B0098: 
        00 20 F0 BD ; instructions to remove 24-hour wait period

    < -------------------------------------------------------------------- >

    GBA_MainProcess (0x02236804)
        ; set amount of required Pokémon to 1
        ::02236b10 01  28           cmp        r0,#0x1

    0x023B0064 ; GBA_MainProcess
        9C 00 3B 02    ; source_address = 0x023B009C
        10 6B 23 02    ; destination_address = 0x02236B10
        01 00 00 00    ; copy_size = 0x1
    
    0x023B009C:
        01

    < -------------------------------------------------------------------- >
    
    GBA_SelectPokemon (0x02235994)
        ; set amount of required Pokémon to 1
        ::02235a4a 01  28           cmp        r0,#0x1    

    0x023B0070; GBA_SelectPokemon
        9C 00 3B 02    ; source_address = 0x023B009C
        4A 5A 23 02    ; destination_address = 0x02235a4a
        01 00 00 00    ; copy_size = 0x1

    0x023B009C:
        01

    < -------------------------------------------------------------------- >

For completeness sake, I included some code to allow invalid Pokémon to be transferred. This is probably useless to most people, but I used it for my Manaphy injection.

    GBA_isPokemonInvalid (0x02234C74)
        ; return 0, always valid
        ::02234c74 00  20           mov        r0,#0x0 
        ::02234c76 70  47           bx         lr

    0x23B007C:
        A0 00 3B 02    source_address = 0x023B00A0
        74 4C 23 02    destination_address = 0x02234c74
        04 00 00 00    copy_size = 0x4

    0x23B00A0:
        00 20 70 47
  1. GBA boxdata: The game actually copies all GBA save data to main memory. Any edits are performed on this copied data first. Once migration completes the game saves, which writes the data back to the actual GBA save. This also uses the save block rotation code present in the gen 3 games. ↩︎