Oracle of Ages & Seasons Link Cable Vulnerabilities (CVEs pending)

I have uncovered multiple vulnerabilities, including a severe Remote Code Execution (RCE) vulnerability, in the link cable communication module of “The Legend of Zelda: Oracle of Seasons” for the Game Boy Color. The vulnerability is caused by a failure to sanitize inputs received over the link cable.

Oracle of Ages, the sister game to Oracle of Seasons, is also affected by these vulnerabilities, and remote code execution is believed to be possible. However, the scope of the vulnerability’s impact has not been investigated as deeply, given that this software already has an unpatched Code Execution vulnerability which does not involve use of a Link Cable. By contrast, this is the first known code execution vulnerability in Oracle of Seasons.

In this report, I will demonstrate how the previously-known code execution exploit in Oracle of Ages can be used to achieve Remote Code Execution in Oracle of Seasons by sending corrupted file data over the link cable. I will demonstrate a proof-of-concept exploitation by warping directly to the credits in Oracle of Seasons within seconds of opening the file for the first time. Speedrunners of the “Linked Any%” category will find this to be either an alarming development or a welcome one, depending on their attitude towards Oracle of Seasons.

Background

The Legend of Zelda: Oracle of Ages and Seasons are a pair of video games for the Game Boy Color released in 2001. As they were released simultaneously, they have a unique “linking” system which must be used to unlock the “true” ending of the two games. Because not all players had a Gameboy Link Cable, the games could alternatively communicate via a series of “passwords”. I have previously described non-exploitable bugs in this password system; however, this is the first time the Link Cable feature has been thoroughly audited.

Oracle of Ages (left) and Seasons (right).
Oracle of Ages (left) and Seasons (right).

While these two games share the same code base, surprisingly, very few exploitable bugs have been discovered in Oracle of Seasons when compared to Oracle of Ages. (In this context, “exploitable bug” means a bug that can be used to beat the game more quickly). The only known exploitable bug of note is the so-called “Rooster Adventure” bug, and even this was patched in the Europe update.

The Rooster Adventure bug in action.
The Rooster Adventure bug in action.

By contrast, Oracle of Ages has a number of routinely exploited bugs including shovel duplication, text warping, and the infamous Veran Warp. More recently, Arbitrary Code Execution (ACE) has become so easy that it is routinely performed by speedrunners to warp directly to the credits.

Veran
Warp has many buggy side-effects.
Veran Warp has many buggy side-effects.

Arbitrary Code Execution (ACE) in Oracle of Ages

Because achieving Remote Code Execution in Seasons first requires Arbitrary Code Execution in Ages, I will describe that first.

In recent months, new ACE techniques have been developed allowing one to write any byte to almost any memory address, meaning that one can achieve “total control” over the game. Given that our only input device is the buttons on the Gameboy itself (the link cable was not used for this), this is no small feat. In the proof-of-concept video shown below, this technique is used to disable all collisions in the game. Note that the Japanese version is almost always used when performing ACE, because the larger character set used for name input is very useful when executed as code (but it is still possible in principle on other versions).

A slightly modified version of this ACE technique was used in order to prepare the Link Cable exploit. Here is how it works:

First, when creating the file, the player inputs the name “ズモゲフフ” (zumogefufu). There is also a nameable child NPC who is given the name “ぇヌフコオ” (enufukoo). This will be executed as code later. Then, the player must play the game “normally” up until obtaining the “Tune of Ages” item. At this point, the player will have the equipment necessary to perform the “text warp” glitch in a specific area and reach an out-of-bounds map tile.

At this point, if the player opens the map, they will find that their cursor is out-of-bounds. If they select a specific out-of-bounds tile, the program counter (pc register) will attempt to jump to an undefined location; what this means is that it puts some value which is not supposed to be an address into the pc register. Ultimately, pc ends up jumping to address FAD5, which is in the middle of object memory. Then, by manipulating the objects on-screen, we can create a “jp” instruction in order to jump to address E602, which stores Link’s name. Earlier research by SBD goes into more detail on how this is achieved.

Link’s name (ズモゲフフ) corresponds to the opcodes “xor a,D2; ld (cbcb),a”. Since the value of the ‘a’ register is D5 before this, we are actually putting value “07” into address “cbcb”. (We are not able to input values between 0x00-0x5f with the name, so we need to work around that sometimes).

Address “cbcb” corresponds to a variable tracking what type of menu is opened. This is normally “02” for the map menu, but by changing it to “07”, we change the menu to the name input screen for the child NPC.

Graphics are corrupted, but it works just fine.
Graphics are corrupted, but it works just fine.

This is convenient, because in fact, the child’s name buffer is stored directly next to Link’s name. This means that the child’s name is what will get executed next. More specifically, because of the bytes surrounding that name, it will execute the opcode “ld bc,XXXX”, where “XXXX” are characters in the child’s name; this is followed by 3 more controllable bytes, and then the opcode “ld (bc),a”.

To recap: We have an Arbitrary Code Execution setup which opens the child renaming menu, and which also executes the child’s name as code; this means we can keep changing the code we want to execute. Even with only 5 bytes to work with, and even without being able to input bytes 00-5f, this is enough to write any value to many memory addresses (as long as each byte of the address is between 60-ff).

There is just one remaining problem: If we want to use all 5 bytes in the child’s name, we need some kind of “return” opcode. In this case, the “rst 38” opcode effectively functions as that. The child’s initial name, “ぇヌフコオ”, writes this opcode (0xFF) to address C611 (E611 is a mirror of C611).

Note that address E611 now has opcode 'rst 38'.
Note that address E611 now has opcode 'rst 38'.

We now have the ability to write bytes to a very large range of memory addresses. Now that I’ve shown how to gain this ability in Oracle of Ages, I will show how it can be used to exploit vulnerabilities in Oracle of Seasons over the link cable.

In the file select screen, there is an option to “link” with another game if it is connected through the link cable. For example, after completing an Oracle of Ages file, Oracle of Seasons can link to it to initialize a new file based on the Ages file. If this process is successful, a 0x16-byte “file header” is transferred across the link cable, and this data is used to initialize the file. This data includes, but is not limited to:

  • Link’s name
  • Child’s name
  • Animal companion (you can choose one of 3 “companions” to help in the game)
  • File properties, such as whether it’s marked as completed

The 0x16-byte headers for each of the three files (0x42 bytes total) are stored at address 4:d98d. They correspond to the bytes from addresses c600-c615 during normal gameplay.

There are two boolean variables from the file header that are key to this vulnerability, the “linked game” and “hero’s game” variables. The important thing to understand is that these are supposed to be either 0 or 1, but we can modify them with ACE in the Ages file to be any value. This is problematic when the following code gets run (code snippet from the disassembly):

    ; Load in a: wFileIsHeroGame (bit 1),
    ; wFileIsLinkedGame (bit 0)
    ld hl,wFileIsHeroGame
    ldd a,(hl)
    add a
    add (hl) ; wFileIsLinkedGame
    push af
    
    ; Initialize data differently based on whether
    ; it's a linked or hero game
    ld hl,initialFileVariablesTable
    rst_addDoubleIndex ; hl += a * 2
    ldi a,(hl)
    ld h,(hl)
    ld l,a
    call _initializeFileVariables

[...]

initialFileVariablesTable:
	.dw _initialFileVariables_standardGame
	.dw _initialFileVariables_linkedGame
	.dw _initialFileVariables_heroGame
	.dw _initialFileVariables_linkedGame

The above code is assuming that the variables “wFileIsLinkedGame” and “wFileIsHeroGame” are 0 or 1, but they can be anything. By setting them to value 4 or greater, it will attempt to read a nonexistent entry from “initialFileVariablesTable”. The result: it uses completely garbage data to initialize the file.

This can only be used to corrupt values within a 256-byte range (address c6xx during normal gameplay), but this is a highly critical region of save data. It holds the 0x16-byte file header described earlier, the current room Link is in, his inventory items, many game progression flags… there is a lot to work with.

Setting the “wFileIsLinkedGame” variable (c612) to an invalid value in Oracle of Ages will trigger this corruption in Oracle of Seasons when they are linked. In most cases, the results are interesting, but not very controllable. There are two values of particular note, however.

The first value is 0x2b, which causes the game to read the initial file data from address 2312. This points to ROM, so the results are 100% consistent; it creates a strange file where Link starts in a completely different room from normal, Link seems to have a corrupt / non-existent item, and attempting to talk to a nearby NPC crashes the game. This by itself isn’t exactly useful, but it has some properties that make it helpful when used in combination with the 2nd link cable vulnerability, described later.

The second value is 0x4f, which reads initial file data from address 4:f9c9. This is a mirror of address 4:d9c9, which is part of the “file header” data transferred over the link cable; in particular, this points to the last 6 bytes of file 3’s header. Of course, this is fully controllable from Oracle of Ages. Having 6 bytes is enough for us to overwrite any 3 addresses in the c6xx range with any value we want.

This seems like it should be very powerful, but there are limitations. First of all, if we want to change the room Link spawns in, we’ll also need to overwrite an event flag that will disable a cutscene at the start of the game, otherwise Link will be unable to move. We may also want to overwrite another event flag to enable the inventory or map menus to be opened; and already, we’ve used up all 3 address writes. Still, this precise control does allow for some very interesting setups, such as this file where Link spawned into the final dungeon equipped with a level 0 sword. (Sadly this isn’t enough equipment to actually defeat the final bosses.)

Given that many files corrupted in this way have a tendency to crash the game, remote code execution could very likely be achieved with this vulnerability alone; but, it was unnecessary, since the vulnerability described below achieved RCE easily and reliably.

As mentioned earlier, the “animal companion” is an animal who helps the player throughout the game. Expected values for this variable are 0x0b (Ricky the kangaroo), 0x0c (Dimitri the dodongo), and 0x0d (Moosh the flying bear).

Of course, this can be corrupted in the Oracle of Ages file prior to linking. In my previous research on vulnerabilities in the password system, I described how the companion could be set to any value between 0x00-0x0f. However, when the Link Cable is used, we can set it to any value between 0x00-0xff.

In both Oracle of Ages and Oracle of Seasons, this can trigger a vulnerability in the map screen. Because the layout of the map actually changes based on which animal companion was selected, some code is run which overwrites a rectangular section on the map with different tiles based on who the animal companion is. As it turns out, setting the animal companion to a high value can instead cause it to overwrite unrelated sections of memory. This happens because, again, the game attempts to read undefined values from a table, as shown in the following code:

	; If the companion is not ricky, perform
	; appropriate minimap tile substitutions.
	ld a,(wAnimalCompanion)
	sub $0c ; Dimitri
	call nc,mapMenu_performTileSubstitutions

[...]

mapMenu_performTileSubstitutions:
	ld hl,mapMenu_tileSubstitutionTable
	rst_addAToHl
	ld a,(hl)
	rst_addAToHl

@nextSubstitution:
	ldi a,(hl)
	or a
	ret z

	ld b,a

	; de = destination
	ldi a,(hl)
	ld e,a
	ldi a,(hl)
	ld d,a

	; hl = src
	ldi a,(hl)
	ld c,a
	ldi a,(hl)
	push hl
	ld h,a
	ld l,c

	; b = height, c = width
	ld a,b
	and $0f
	ld c,a
	ld a,b
	and $f0
	swap a
	ld b,a

	; Code after this copies a rectangular area
	; of data from "hl" to "de".

[...]

; This is a table of tile substitutions to perform on
; the overworld map in various situations.
mapMenu_tileSubstitutionTable:
    .db @subst0 - CADDR
    .db @subst1 - CADDR
    .db @subst2 - CADDR
    .db @subst3 - CADDR
    .db @subst4 - CADDR
    .db @subst5 - CADDR
    .db @subst6 - CADDR
    .db @subst7 - CADDR
    ; Reading values past this point will cause
    ; unrelated memory to get corrupted!

For the Oracle of Seasons exploit, we will use animal companion value 0x44 (there are several other values that behave similarly). This will cause the animal companion to write to an address in the range 0x2000-0x2fff, which is the switchable ROM bank register. Because this occurs while code is executing from the switchable ROM bank, the code that is being executed suddenly changes under its nose.

Before bank switch
Before bank switch
After bank switch (one opcode later)
After bank switch (one opcode later)

This could easily cause the game to crash, and normally, it would. But what happens at first is, luckily, consistent; the program counter ultimately jumps to address F81A, which is a mirror of D81A. In this situation, switchable RAM bank 4 is loaded, meaning that this is actually address 4:F81A - which is remarkably close to our file header buffer stored at 4:D98D (or 4:F98D), a buffer which is completely controllable from the link cable transfer!

The question that remains, then, is whether it’s possible for the program counter to reach our payload at F98D. There are two annoying obstacles in the way. The first is a buffer at 4:F900, which stores a temporary copy of Link’s current sprite when a menu is opened. After some experimentation, I discovered that the sprite of Link facing up while holding an item was one of the simplest sprites we could use that would not cause crashes or other undesireable effects, because it contained a useful “jr” opcode (relative jump) that would skip over most of the resulting code.

The second obstacle is variable 4:F98C, one byte before our payload. This appears to be an unused and uninitialized byte, which means its value depends entirely on what it happens to be when the hardware powers on. Based on testing in the BGB emulator, this value is usually 0xff, or the “rst 38” opcode, which ruins our attempt at exploitation; but it can sometimes be something else, and we will need to count on this for the exploit to work. However, it’s unclear whether real hardware behaves the same as the BGB emulator, or even if all hardware is consistent in this regard.

This is where we will combine vulnerability 2 with vulnerability 1 to bypass this problem. A corrupted file initialized with value 0x2b for the “linked game” flag sets the two critical flags which, respectively, bypass Link’s initial “unconscious” state, and enable the map menu to be opened; this effectively bypasses the 2-minute intro. Also, for reasons that are unclear, Link initially appears as if he is holding a shield, even though he is not.

This particular sprite is extremely convenient when executed as code - it happens to contain the instruction “jr nz,F98E”, which bypasses the uninitialized byte at 4:F98C by jumping directly to our payload. This makes the exploit 100% consistent.

The payload

Now that we have a way to execute a region of memory controllable by the link cable, we need to decide on a payload. In order to safely resume execution of the game, we must do two things first:

  • Restore the stack pointer to value C218, because it was corrupted.
  • Turn the screen back on (it was briefly turned off when opening the menu). For some reason, the game softlocks if this is not done.

In addition, we will write value 0x0A to address C2EF; this will trigger the credits sequence. So, the payload will look like this:

ld sp,c218
ld a,0a
ld (c2ef),a
jp 02ea     (LCD enable function)

Putting the exploit together

Now, I will explain how to prepare this exploit in an Oracle of Ages file prior to linking with Oracle of Seasons.

First, we will set up ACE in Oracle of Ages by setting Link’s name to “ズモゲフフ” (zumogefufu) and the child’s name to “ぇヌフコオ” (enufukoo), as explained in the earlier section. This gives us the ability to write to many memory addresses by repeatedly modifying the child’s name.

The strategy will then be to corrupt our file by writing directly to the data for file slot 1 in the Save RAM. Normally, it would be easier to corrupt our file by writing to the copy stored in the c6xx memory range, and then saving the game normally, but there are two reasons why we can’t do this:

  • We cannot reliably write to addresses like c610, because “10” is not a value that corresponds to an enterable character.
  • The data we want to corrupt for the Seasons payload is also the data that we are using for our ACE setup in Ages. If we overwrote it, we would lose the ability to do arbitrary memory writes, and we wouldn’t be able to finish the process.

Writing directly to the save file bypasses these problems, but introduces a new problem: the file checksum. The game would automatically fix the file checksum if we modified the c6xx memory range, but this is not the case if we write directly to the save data. So, we must ensure that the checksum does not change, to prevent the game from considering the file as “corrupted” and restoring the backup file.

Luckily, the checksum algorithm is simple: it is the sum of all 16-bit words in the file. So long as the difference between the initial value of a byte and the new value is always the same, the difference in the checksum will always be the same; so, we can fix the checksum by countering that difference in another variable. We make the following assumptions to ensure that the variables’ initial values are consistent:

  • Link’s name is “ズモゲフフ” and the child’s name is “ぇヌフコオ”.
  • The game has been saved sometime after initially naming the child.
  • If the child’s name has been changed, the game has not been saved since then.
  • The animal companion is Dimitri (value 0x0C).
  • The file is not a linked game.
  • The file has not been completed.

In order to write to Save RAM, the first thing we must do is write value 0x0A to any address in the range 0x0000-0x1FFF. This will unlock the Save RAM between addresses A000-BFFF and allow us to write to it. This can be accomplished by setting the child’s name to “ろろフケの” (rorofukeno):

Then, we will begin writing the payload to file 1, in addition to the corrupted “Animal Companion” (A070) and “Linked Game” (A072) values necessary to corrupt the Oracle of Seasons file as described in the above sections. We must also write a nonzero value to the “Completed File” (A074) variable in order to be able to perform the link at all; this variable will double as one of the two checksum fixers. The table below shows all names that must be entered, the address they modify, and what the value is changed to. Note that the “SRAM” address is the one that is actually written to, but the “WRAM” copy is also noted for reference.

WRAM SRAM Initial New Chksm Name Comment
c604 a064 ea 31 -00b9 おぞララ ld sp,c218
c605 a065 cb 18 -b300 かぞラゼ  
c606 a066 cb c2 -0009 きぞヌシ  
c607 a067 00 3e +3e00 くぞラハ ld a,0a
c608 a068 01 0a +0009 けぞラプ  
c609 a069 91 ea +5900 こぞヌッ ld (c2ef),a
c60a a06a c6 ef +0029 さぞヌギ  
c60b a06b cb c2 -0900 しぞヌシ  
c60c a06c b9 -     cp c (do nothing)
c60d a06d b4 c3 +0f00 せぞヌス jp 02ea
c60e a06e 00 ea +00ea そぞヌッ  
c60f a06f 02 -      
c610 a070 0c 44 +0038 ちぞラト Animal Companion
c612 a072 00 2b +002b てぞラヲ Linked Game
c614 a074 00 43 +0043 なぞラナ Completed File
c681 a0e1 00 15 +1500 ェぞラヂ Fix Checksum

To speed up the process of entering names, they can be rearranged into the following order, to take advantage of repeated characters:

おぞララ
かぞラゼ
くぞラハ
けぞラプ
ちぞラト
てぞラヲ
なぞラナ
ェぞラヂ
きぞヌシ
しぞヌシ
こぞヌッ
そぞヌッ
さぞヌギ
せぞヌス

After all the names in this table have been entered and executed, the player must reset or turn off the game without saving it (otherwise they will undo everything they just did). If it was done correctly, the file’s name should have changed (it will now include characters that are normally impossible to enter such as the right arrow sign). If done incorrectly, the checksum will be incorrect, and the file will appear unchanged due to the backup being restored.

An Oracle of Seasons file initialized by linking with this file can then warp to the credits simply by opening the map as soon as the file is loaded; this will trigger the Animal Companion bug and execute the file header from Oracle of Ages that was sent over the link cable. The Oracle of Ages file should be in file slot 1 in order for this to work. This exploit can be seen in action here.

Conclusion

It is essential that software developers sanitize inputs received from external sources in order to avoid security flaws such as those discussed in this report. Nintendo may have thought that nobody would think to send corrupted data over a Link Cable, but they were clearly mistaken.

The Remote Code Execution exploit demonstrated here could allow a malicious actor to gain full control over your Nintendo Game Boy. Theat actors may compromise your Game Boy in order to run cryptominers, to encrypt your save data for ransom, or to beat a game far more quickly than Miyamoto intended. I advise that you do not connect your Game Boy to any untrusted devices in order to prevent your system from being compromised.

Acknowledgements

The following researchers have helped contribute to the discovery of these vulnerabilities either directly or indirectly:

  • Scorpianman42: Discovered the “Veran Warp” bug in Oracle of Ages.
  • Sockfolder: Discovered the first known Oracle of Ages code execution exploit while researching the “Veran Warp” bug.
  • SBD: Discovered an easier and more consistent method of achieving ACE in Oracle of Ages with small payloads.
  • The Paper Mario speedrunning community: For the idea of using one game to break another.
  • Stewmat (me): Discovered “Total Control” ACE in Oracle of Ages; discovered the Link Cable vulnerabilities and wrote this report.
Written on April 4, 2021