In the process of finding ways to activate the developer menu left in Animal Crossing, including the game selection menu for the NES emulator, I discovered an interesting feature that exists in the original game and was constantly active, but never used by Nintendo.
In addition to the NES / Famicom games that can be obtained inside the game, you can download new NES games from a memory card.
I also managed to find a way to use this ROM loader to patch my code and data into the game, which allows you to run the code through a memory card.
')
Introduction - NES Console Objects
Conventional NES games that can be obtained at Animal Crossing are separate pieces of furniture in the form of an NES console with a cartridge on it.
By placing this object in your home and interacting with it, you can run this single game. The picture below shows Excitebike and Golf.
There is also a common “NES Console” object, in which there are no embedded games. You can buy it from Redd, and sometimes get through random events, for example, by reading on the city bulletin board that the console is buried at a random point in the city.
This object looks like a NES console with no cartridges on it.
The problem with this object is that it was conceived as unplayable. Every time you interact with it, you simply see a message stating that you do not have gaming software.
It turned out that this object is actually trying to scan the memory card for the presence of specially designed files containing ROM images for NES! The NES emulator used to run embedded games seems to be the full standard GameCube NES emulator, and is able to run most games.
Before demonstrating these functions, I will explain the process of their reverse development.
Search ROM loader on a memory card
Looking for developer menu
Initially, I wanted to find the code that activates various developer menus, such as the map selection menu or the game selection menu for the NES emulator. The Forest Map Select menu, which makes it easy to instantly download different locations of the game, was quite easy to find - I just searched for the FOREST MAP SELECT line that appears at the top of the screen (you can see it in various videos and screenshots on the Internet ).
In FOREST MAP SELECT, there are cross-references of data to the select_print_wait function, which leads to a bunch of other functions that also have the select_* prefix, including the select_init function. These turned out to be functions that control the card selection menu.
The select_init function leads to another interesting function called game_get_next_game_dlftbl . This function links together all the other menus and “scenes” that can be launched: a screen with the Nintendo logo, the main screen, the map selection menu, the NES (Famicom) emulator menu, and so on. It starts at the beginning of the main game procedure, finds which scene initialisation function should be launched, and finds its entry in the table data structure called game_dlftbls . This table contains links to various scene processing functions, as well as some other data.
A careful study of the first block of the function showed that it loads the “next game init” function, and then begins to compare it with a series of known init functions:
first_game_init
select_init
play_init
second_game_init
trademark_init
player_select_init
save_menu_init
famicom_emu_init
prenmi_init
One of the pointers to the functions it is looking for is famicom_emu_init , which is responsible for running the NES / Famicom emulator. game_get_next_game_init Dolphin debugger to the game_get_next_game_init result to be famicom_emu_init or select_init , I was able to display special menus. The next step is to determine how these pointers are set in the normal way during program execution. The only thing that the game_get_next_game_init function game_get_next_game_init is loading the value at the 0xC offset of the first argument to game_get_next_game_dlftbl .
Keeping track of these values ​​in various data structures was a bit boring, so I’ll go straight to the main one. The most important thing I found:
When a game is launched in the usual way, it performs the following sequence of actions:
first_game_init
second_game_init
trademark_init
play_init
player_select_init sets the next init value to select_init . This screen should allow you to select a player immediately after selecting a card, but it looks like it is not working properly.
I also found one nameless function that sets the em function init, but I didn’t find anything assigning the init functions the init values ​​of the player or card selection.
At that point, I realized that I had another stupid problem with how I loaded the function names in IDA: because of the regular expression used to cut lines in the debug symbol file, I missed all the function names starting with a capital letter . The function that set famicom_emu_init looked similar to the transition between scenes, and, of course, was called Game_play_fbdemo_wipe_proc .
Game_play_fbdemo_wipe_proc handles scene transitions, such as clearing the screen and dimming.
Under certain conditions, the transition of the screen was carried out from the usual gameplay to the display of the emulator. It was he who set the em function init.
Processing console objects
In fact, switching to the screen transition handler emulator is forced by the function-handlers of furniture objects for NES consoles. When a player interacts with one of the consoles, aMR_FamicomEmuCommonMove is aMR_FamicomEmuCommonMove .
When calling the function r6 contains an index value corresponding to the numbers in the names of the NES game files in famicom.arc :
01_nes_cluclu3.bin.szs
02_usa_balloon.nes.szs
03_nes_donkey1_3.bin.szs
04_usa_jr_math.nes.szs
05_pinball_1.nes.szs
06_nes_tennis3.bin.szs
07_usa_golf.nes.szs
08_punch_wh.nes.szs
09_usa_baseball_1.nes.szs
10_cluclu_1.qd.szs
11_usa_donkey3.nes.szs
12_donkeyjr_1.nes.szs
13_soccer.nes.szs
14_exbike.nes.szs
15_usa_wario.nes.szs
16_usa_icecl.nes.szs
17_nes_mario1_2.bin.szs
18_smario_0.nes.szs
19_usa_zelda1_1.nes.szs
( .arc is the proprietary format of file archives.)
When r6 non-zero, it is passed in the aMR_RequestStartEmu call. This triggers the transition to the emulator.
However, if r6 is zero, the function aMR_RequestStartEmu_MemoryC is called aMR_RequestStartEmu_MemoryC . Having assigned the value 0 in the debugger, I received the message "I don't have any software". I didn’t immediately remember that I had to check the “NES Console” object to make sure that it r6 value, but it turned out that the zero index is used for the console object without a cartridge.
Although aMR_RequestStartEmu simply saves the index value in some kind of data structure, aMR_RequestStartEmu_MemoryC performs much more complex operations ...
This third block of code calls aMR_GetCardFamicomCount and checks for a non-zero result, otherwise it misses most of the interesting things on the left side of the function graph.
aMR_GetCardFamicomCount calls famicom_get_disksystem_titles , which then calls memcard_game_list , and then everything becomes very interesting.
memcard_game_list mounts the memory card and starts memcard_game_list around in the file recording cycle, checking each one for some values. Having traced the function in the debugger, I was able to understand that it compared the values ​​with each of my files on the memory card.
The function decides whether to load or not to load the file, depending on the results of checking several lines. First, it checks for the presence of the “GAFE” and “01” strings, which are game and company identifiers. 01 stands for Nintendo, “GAFE” is Animal Crossing. I think it stands for GameCube Animal Forest English.
She then checks the strings “DobutsunomoriP_F_” and “SAVE”. In this case, the first line must match, but not the second. It turned out that “DobutsunomoriP_F_SAVE” is the name of the file in which the data of the embedded NES games are stored. Therefore, all files will be loaded, except for this, with the prefix "DobutsunomoriP_F_".
Using the Dolphin debugger to skip the string comparison with “SAVE” and forcing the game to assume that my SAVE file can be safely downloaded, I received this menu after using the NES console:
I answered “Yes” and tried to load the save file as a game, after which I first saw the built-in game crash screen:
Fine! Now I know that she is actually trying to load games from a memory card, and I can start analyzing the format for save files to see if you can load a real ROM.
The first thing I tried to do was try to find where the name of the game is read from the memory card file. Searching for the string “FEFSC”, which was present in the message “Would you like to play <name>?”, I found the offset at which it was read from the file: 0x642 . I copied the save file, changed the file name to “DobutsunomoriP_F_TEST”, changed the bytes at offset 0x642 to “TESTING” and imported the changed save, after which the name I needed was displayed in the menu.
After adding several more files in this format, several more choices appeared in the menu:
ROM download
If aMR_GetCardFamicomCount returns non-zero, then memory is allocated on the heap, famicom_get_disksystem_titles is called again directly, and then a bunch of random offsets are specified in the data structure. Instead of deciphering where these values ​​would be read, I began to study the list of famicom functions.
It turned out that I needed famicom_rom_load . It manages the loading of a ROM, or from a memory card, or from the game’s internal resources.
The most important thing in this “boot from memory card” block is that it causes
memcard_game_load . She again mounts the file on the memory card, reads it and parsit. This is where the most important file format options become apparent.
Checksum value
The first thing that happens after downloading a file is the calculation of the checksum. calcSum function is calcSum , which is a very simple algorithm that summarizes the values ​​of all bytes in the data from the memory card. The bottom eight bits of the result must be zero. That is, to pass this test, you need to sum up the values ​​of all bytes in the source file, calculate the value that needs to be added so that the bottom eight bits become zero, and then assign a value to the checksum byte in the file.
If the check fails, then you receive a message that the memory card cannot be correctly read, and nothing happens. During debugging, I just need to skip this check.
Copy ROM
Toward the end of memcard_game_load , another interesting thing happens. There are some more interesting blocks of code between it and the checksum, but none of them leads to branching that skips the execution of this behavior.
If the particular 16-bit integer value read from the memory card is not zero, then a function is called that checks the compression header in the buffer. It checks for the presence of Nintendo's proprietary compression formats, looking for “Yay0” or “Yaz0” at the beginning of the buffer. If one of these lines is found, then the unpack function is called. Otherwise, a simple memory copy function is performed. In any of the cases, the variable named nesinfo_data_size then updated.
Another hint of context here is that ROM files for NES embedded games use “Yaz0” compression, and this line is present in the headers of their files.
Having observed the value that is checked for zero, and the buffer transferred to the compression check functions, I quickly found out where the game was read in the file on the memory card. A zero check is performed for a portion of the 32-byte buffer copied from offset 0x640 in the file, which is most likely a ROM header. Also this function checks other parts of the file, and it is in them that the name of the game is located (starting with the third byte of the header).
In the code execution path I found, the ROM buffer is located immediately after this 32-byte header buffer.
This information is enough to try to create a working ROM file. I just took one of the other Animal Crossing save files and edited it in a hex editor to replace the file name with DobutsunomoriP_F_TEST and clear all areas where I wanted to insert data.
For the test run, I used the ROM game Pinball, which is already in the game, and pasted its contents after the 32-byte header. Instead of calculating the checksum value, I set breakpoints just to skip calcSum and also watch the results of other checks that could lead to a branch skipping the ROM boot process.
Finally, I imported a new file through the Dolphin memory card manager, restarted the game, and tried to start the console.
It worked! There were small graphical bugs related to Dolphin parameters, which affected the graphics mode used by the NES emulator, but in general the game was just perfect. (In newer Dolphin builds, it should work by default.)
To make sure that other games will also start, I tried to record several other ROMs that were not in the game. Battletoads was launched, but stopped working after the text of the screen saver (after further settings, I managed to make it playable). On the other hand, Mega Man worked perfectly:
To learn how to generate new ROM files that could be loaded without debugging intervention, I had to start writing code and more deeply understand the parsing of the file format.
External ROM file format
The most important part of parsing files takes place in memcard_game_load . There are six main sections of the code parsing block in this function:
Check sum
Save file name
ROM file header
Unknown buffer copied without any processing
Text comment, icon and banner uploader (for creating a new save file)
ROM bootloader
Check sum
The bottom eight bits of the sum of all byte values ​​in the save file must be zero. Here is a simple Python code that generates the required checksum byte:
checksum = 0for byte_val in new_data_tmp: checksum += byte_val checksum = checksum % (2**32) # keep it 32 bit checkbyte = 256 - (checksum % 256) new_data_tmp[-1] = checkbyte
There is probably a special place to store the checksum byte, but adding it to the empty space at the very end of the save file works fine.
File name
Again, the name of the save file must begin with “DobutsunomoriP_F_” and end with something that does not contain “SAVE”. This file name is copied a couple of times, and in one case the letter “F” is replaced with “S”. This will be the name of the save files for the NES game (“DobutsunomoriP_S_NAME”).
ROM header
An immediate copy of the 32-byte header is loaded into memory. Some of the values ​​in this header are used to determine how to handle subsequent sections. These are mainly some 16-bit size values ​​and packed parameter bits.
If you trace the pointer, copied by the header, all the way to the beginning of the function, and find the position of its argument, the function signature below will show that it actually has the type MemcardGameHeader_t* .
The 16-bit value of the size from the header is checked. If it is not zero, then the corresponding number of bytes is directly copied from the file buffer to the new block of allocated memory. This moves the data pointer in the file buffer so that further copying can proceed from the next section.
Banner, Icon and Comment
Another size value is checked in the header, and if it is not zero, the file compression check function is called. If necessary, the decompression algorithm will be launched, after which SetupExternCommentImage is SetupExternCommentImage .
This function deals with three things: a “comment”, a banner image and an icon. For each of them in the ROM header there is a code showing how they should be processed. There are the following options:
Use default
Copy from banner / icon / comment section in ROM file
Copy from alternate buffer
The default code values ​​cause the icon or banner to be loaded from the resource on disk, and the name of the save file and comment (text description of the file) are assigned the values ​​Animal Crossing and NES Cassette Save Data. Here's what it looks like:
The second code value simply copies the name of the game from the ROM file (some alternative to “Animal Crossing”), and then tries to find the line "] ROM" in the file comments and replace it with "] SAVE". Apparently, the files that Nintendo wanted to release should have been named “Game Name [NES] ROM” or something like that.
For an icon and banner, the code tries to determine the image format, obtain a fixed-size value corresponding to this format, and then copy the image.
At the last code value, the file name and description are copied unchanged from the buffer, and the icon and banner are also loaded from the alternative buffer.
ROM
If you take a close look at the screenshot of the memcard_game_load copy memcard_game_load , you will notice that the 16-bit value, checked for equality to zero, is shifted to the left by 4 bits (multiplied by 16), and then used as the size of the memcpy function, if compression is not detected. This is another size value present in the header.
If the size is not zero, then the ROM data is checked for compression and then copied.
Unknown buffer and bug search
Although loading a new ROM is quite curious, the most interesting thing about this ROM loader for me was that in fact it is the only part of the game that receives user input of variable size and copies it to different places in memory. Almost everything else uses buffers of constant size. Things like names and letter texts may seem different in length, but in fact the empty space is just filled with spaces. Zero-terminated strings are used infrequently, thus avoiding common memory corruption bugs, such as using strcpy with a buffer that is too small to copy strings into it.
I was very interested in the possibility of finding a game exploit based on save files, and it seemed that this was the best option.
Most of the ROM file operations described above use copies of a constant size, with the exception of an unknown buffer and ROM data. Unfortunately, the code processing this buffer allocates just as much space as is necessary to copy it, so no overflow occurs, and specifying very large sizes of ROM files was not very useful.
But I still wanted to know what was happening with this buffer, which is copied without any processing.
NES Information Tag Handlers
I am back to famicom_rom_load . After loading a ROM from a memory card or disk, several functions are called:
nesinfo_tag_process1
nesinfo_tag_process2
nesinfo_tag_process3
Tracing the place where the unknown buffer is copied, I made sure that this task is performed by these functions. They begin with a call to nesinfo_next_tag , which performs a simple algorithm:
Checks whether the specified pointer nesinfo_tags_end pointer in nesinfo_tags_end . If it is less than nesinfo_tags_end or nesinfo_tags_end is zero, then it checks the presence of the string "END" in the header of the pointer.
If END is reached, or the pointer has risen to or above nesinfo_tags_end , then the function returns zero (null).
Otherwise, the byte at offset 0x3 pointer is added to 4 and to the current pointer, after which the value is returned.
This tells us that there is some sort of label format of the three-letter name, the data size values, and the data itself. The result is a pointer to the next label, because the current label is skipped ( cur_ptr + 4 skips the three-letter name and one byte, and size_byte skips the data).
If the result is not zero, then the label processing function performs a series of string comparisons to figure out which label to process. Some of the label names checked in nesinfo_tag_process1 : VEQ, VNE, GID, GNO, BBR and QDS.
If a match is found for a tag, some handler code is executed. Some of the handlers do nothing but output the label in the debug message. Other handlers are more complex. After processing the label, the function tries to get the next label and continue processing.
Fortunately, there are many detailed debugging messages that appear when tags are detected.They are all in Japanese, so they must first be decoded from Shift-JIS and translated. For example, a message for QDS might read "Loading disk save area" or "Since this is the first run, create a disk save area." The messages for BBR read "load backup battery" or "since this is the first run, perform the cleanup."
Both of these codes also load some values ​​from the data section of their label and use them to calculate the offset in the ROM data, and then perform copy operations. Obviously, they are responsible for determining the parts in the ROM memory associated with the preservation of the state.
There is also a “HSC” tag with a debug message saying that it handles point records. It receives the ROM offset from its tag data, as well as the initial value of the record points. These tags can be used to indicate a place in the memory of a NES game for storing record points, possibly for storing and restoring them later.
These tags create a rather complex system of loading metadata about ROM. Moreover, many of them lead to calls memcpybased on the values ​​passed to these tags.
Hunt for bugs
, , , , 16- . 16- NES, , 32- GameCube.
, , memcpy , 0xFFFF .
QDS
QDS 24- , 16- .
, . c — , ROM , 16- .
24- 0xFFFFFF , ROM. …
, 0xFFFF , . ( 0x1000 ), «QDS» .
And therein lies the problem, because it nesinfo_tag_process1is actually called twice. For the first time, she receives some information about the space she needs to prepare for the save data. The QDS and BBR tags are not fully processed on the first run. After the first execution, a place is prepared for these saves, and the function is called again. This time, the QDS and BBR labels are processed completely, but if the lines of the label names are cleared from memory, then it is impossible to match the labels again!
This can be avoided by setting a smaller size value. Another problem is that the offset value can only move forward in memory, and the ROM NES data is located in a heap rather close to the end of the available memory.
, , .
, malloc , , malloc . . free .
malloc ( 0x7373 ) , free. If she does not find these bytes, then it causes OSPanicand the game freezes.
Without having the ability to influence these bytes in some target location, it is not possible to write here. In other words, it is impossible to write something in an arbitrary place without being able to record something near this place. There may be some way to make the value 0x73730000stored on the stack directly in front of the return address and the place to which the value we want to write is assigned to the destination address (it will also be checked as if it were a pointer to the heap block), but this difficult to achieve and use in an exploit.
nesinfo_update_highscore
Another feature related to QDS, BBR and HSC tags is nesinfo_update_highscore. The sizes of the QDS, BBR and OFS labels (offset, offset) are used to calculate the offset to which the recording is to be performed, and the HSC label includes the recording to this location. This function is performed for each frame processed by the NES emulator.
The maximum offset value for each label in this case, even for QDS, is 0xFFFF. However, during the label processing cycle, the size values ​​from the BBR and QDS labels actually accumulate . This means that multiple labels can be used to calculate almost any offset value. The limit is the number of tags that can fit in the ROM data section of a file on a memory card, and it also has a maximum size 0xFFFF.
The base address to which the offset is added is the 0x800C3180save data buffer. This address is much lower than the ROM data, which gives us more freedom in choosing a place to write. For example, it will be enough just to rewrite the return address in the stack at 0x812F95DC.
Unfortunately, that didn't work either. It turns out that it nesinfo_tag_process1also checks the accumulated size of the offsets from these labels, and uses this size to initialize the space:
With the offset value that I was trying to calculate, this led to the fact that 0x48D91EC(76,386,796) memory bytes were cleared , which is why the game failed to perform spectacularly.
PAT Mark
I have already begun to lose hope, because all these tags that performed unprotected calls memcpyled to failure even before I had time to use them. I decided to just do the documentation of the purpose of each tag, and gradually got to the mark in nesinfo_tag_process2.
Most tag handlers nesinfo_tag_process2never run, because they only work when the pointer is nesinfo_rom_startnon-zero. Nothing in the code assigns a non-zero value to this pointer. It is initialized with a zero value and is never used again. When loading ROM, it is set only nesinfo_data_start, therefore it looks like a dead code.
However, there is one label that can still work with non-zero nesinfo_rom_start: PAT. This is the most difficult label in the function nesinfo_tag_process2.
She also uses as a pointer nesinfo_rom_start, but never checks it for zero. The PAT label reads its own label data buffer, processing codes that calculate offsets. These offsets are added to the pointer nesinfo_rom_startto calculate the destination address, and then the bytes are copied from the patch buffer to this location. This copying is done with instructions for loading and saving bytes, and not with the help memcpy, so I did not notice it before.
Each PAT tag data buffer has an 8-bit type code, an 8-bit patch size, and a 16-bit offset value, followed by the patch data.
If the code is 2, then the offset value is added to the current sum of offsets.
If the code is 9, then the offset is shifted up by 4 bits and added to the current sum of offsets.
If the code is 3, then the sum of the offsets is reset to 0.
The maximum size of the NES information label is 255, that is, the largest PAT patch size is 251 bytes. However, it is allowed to use several PAT tags, that is, you can patch more than 251 bytes, as well as patch non-adjacent places.
As long as we have a series of PAT subtags with code 2 or code 9, the offset of the destination pointer continues to accumulate. When copying patch data, it is reset to zero, but if you use a zero patch size, this can be avoided. It is clear that this can be used to calculate an arbitrary offset with a null pointer nesinfo_rom_startusing a variety of PAT labels.
However, there are two more checks for code values ​​...
0x800xFF , 0x7F80 , 16 . 16- .
0x800000000x807FFFFF ! Animal Crossing. , Animal Crossing ROM .
- .
, «zuru mode 2» ( , ) when the user loads the ROM from the game card. It turned out that the cheat combo of the keys only activates the “zuru mode 1” mode, which does not have access to the functions that mode 2. With this patch, thanks to the memory card, we can get full access to the developer mode on the real hardware.
The patch labels will be processed when the ROM is loaded.
After downloading the ROM, you need to exit the NES emulator to see the result.
Works!
The format of the informational labels patch
Information labels in the save file that execute this patch are as follows:
ZZZ \x00: ignored start label. 0x00- this is the size of its data buffer: zero.
PAT \x08 \xA0 \x04 \x6F\x9C \x00\x00\x00\x7D: patch 0x80206F9Cc 0x0000007D.
0x08 Is the size of the label buffer.
0xA0when added to 0x7F80becomes 0x8020, that is, the top 16 bits of the destination address.
0x04- This is the size of the patch data ( 0x0000007D).
0x6F9C - This is the bottom 16 bits of the destination address.
0x0000007D - this is patch data.
END \x00 : end marker.
If you want to experiment with the creation of a patcher or ROM save files, then I posted a very simple code for generating files at https://github.com/jamchamb/ac-nesrom-save-generator . A patch like the one shown above can be generated with the following command:
With this tag you can achieve the execution of arbitrary code in Animal Crossing.
But here the last obstacle arises: the use of patches for data works well, but there are problems when patching code instructions.
When patches are recorded, the game continues to carry out the old instructions that were in its place. This seems to be a caching problem, and in fact it is. The GameCube CPU has instruction caches, as written in the specifications .
To understand how to clear the cache, I began to examine the cache-related functions from the GameCube SDK documentation, and discovered ICInvalidateRange. This function invalidates cached blocks of instructions at a specified address in memory, which allows the modified instruction memory to be executed with updated code.
However, without the ability to run the original code, we still can not call ICInvalidateRange. For the successful execution of the code, we need another trick.
Studying the implementation mallocfor the possibility of using the heap overflow exploit, I learned that the implementation functions malloccan be turned off dynamically using a data structure called my_malloc. my_mallocloads a pointer to the current implementation mallocor freefrom a static memory location, and then calls this function, passing all the arguments passed to my_malloc.
NES emulator actively usesmy_malloc ROM NES , , , PAT.
my_malloc , , , mallocfree . , my_malloc .
- DĹŤbutsu no Mori e+ Cuyler PowerPC : https://www.youtube.com/watch?v=BdxN7gP6WIc. (DĹŤbutsu no Mori e + was the last iteration of Animal Crossing on the GameCube, which had the most updates. Released only in Japan.) The patch loads some code that allows the player to create any objects by typing their ID by letter and pressing the Z button.
Thanks to this, you can download mods, cheats and homebrew in a regular copy of Animal