Recently I was informed that Datel has released a new version of their Action Replay Cheat Cartridge that allows you to cheat in Nintendo DS games on a Nintendo DSi console. Knowing that Nintendo put quite some effort in blocking any third party cartridges from working on the DSi I was curious to figure out how they did it.
Before jumping in this article, I’ll give you a small warning that what is written here might be quite “techy” to some people. I advise reading lots of GBAtek (And maybe a bit of dsibrew) when you get lost.
Research has shown that getting a DS cart to boot on a DSi requires quite a bit more effort than it did on a (DS)Lite. Cart timings are very important, you cannot eject the cartridge and insert a new one beyond the DSi menu. But most important of all, the DSi menu does additional integrity checking prior to booting the cartridge.
The integrity checking is there to ensure that the cartridge booted is a genuine licensed game cartridge. There is a ‘white list’ stored on the DSi’s NAND flash which has an entry for every DS game ever released. The entries consist of multiple SHA1 (How these hashes are constructed exactly hasn’t been confirmed) checksums for the cartridge header, ARM9 binary and ARM7 binary.
I hear you thinking “what about newly released DS games? How will they ever boot on a DSi without an update to the whitelist?”. Newer DS games come with a RSA signature in the header that is verified instead of checking against the whitelist, and thus they don’t need to be explicitly white-listed.
So back to the Action Replay DSi cartridge, something that I immediately noticed is that when the cartridge is inserted into a DSi the icon and title of the game “Game & Watch Collection” show up. That’s a little weird.. but when selecting the icon you are thrown into a AR DSi menu.
Let’s take a step back, if all game binaries and headers are checked against a whitelist or need to have a valid RSA signature, then how does this Datel cart manage to boot?
I went to find the source code of my cartridge dumper and dumped the AR DSi cartridge on an old DS-Lite. after checking out the resulting binary I was surprised. No signs of the “Game & Watch Collection” ROM header or icon data, or main data for that matter..
$ hexdump -C ardsi.nds | head -n 2
00000000 4d 45 44 49 41 50 4c 41 59 45 52 20 41 53 4d 41 |MEDIAPLAYER ASMA|
00000010 30 31 00 00 07 00 00 00 00 00 00 00 00 00 00 04 |01..............|
This doesn’t match up with what I saw when the cart was booted on a DSi, huh? Neither would this cartheader qualify as anything whitelisted/valid, so there is no way it could boot on a DSi like this.
During a discussion with neimod he showed me a cartridge access trace (of the cartridge bus) he made of a game cartridge inserted in a DSi. Something noticeable was that a DSi would send the ROM ID cart command right after the reset command, before sending the “get header” command. My dumper software was doing it in a bit different order… (The order an actual (DS)Lite would do it in)
This small difference in order of cart commands allows the AR DSi cartridge to distinguish between a DS(-Lite) and DSi console.
For the sake of clarity:
DS-Lite cart initialization sequence:
CMD 9f00000000000000 # reset
CMD 0000000000000000 # get header
CMD 9000000000000000 # get rom_id
DSi cart init sequence:
CMD 9f00000000000000 # reset
CMD 9000000000000000 # get rom_id
CMD 0000000000000000 # get header
After making this small change to my dumper software, I was indeed getting back something that looked like a Game&Watch collection ROM.
$ hexdump -C ardsi2.nds | head -n 2
00000000 47 41 4d 45 20 57 41 54 43 48 20 43 41 57 54 45 |GAME WATCH CAWTE|
00000010 30 31 00 00 06 00 00 00 00 00 00 00 00 00 00 00 |01..............|
Almost there! .. or so I thought. When I started comparing the original game & watch collection ROM against the result I just dumped, I got nothing but mismatches after the initial 0x8000 bytes (header + secure area).
More and more inspection of the dump and head-to-table banging later I noticed something funny: the data in the dump at 0x8000 (right where the mismatches start) was exactly the data that was supposed to be at 0x10C00, which coincidentally matched up with the exact start offset of the icon/title data. What’s going on? Why does this data show up in the wrong location of the dump?
Then a pattern started to emerge, the right data WAS there, just in the wrong order .. then it hit me. The AR DSI cart was completely ignoring the offset field in the read (0xB7) commands I was sending, and simply advancing a pre-constructed buffer that would exactly match the reply data of the commands a DSi console would normally send. This has the advantage that they only need to require a bare-minimum of data of the original game ROM and don’t need any (complex) logic to fully decode those 0xB7 commands.
With this knowledge in hand I tried reordering the dump in order to get a final ROM image. This proved to be quite tricky since I didn’t know when what was exactly read. I then proceeded to hack up DeSmuMe to log all cart reads and match that against a table of the filesystem to figure out in what order the files we’re read. Even now it was hard to get a good reconstruction of the dump, some files are only read partially, etc.
So I decided to build a list of MD5 hashes for each 0x200 bytes sized block in the original ROM dump and the shuffled AR DSi dump. A couple of minutes later I knew in which order to put what blocks back, and was left over with 3 blocks that didn’t match (0x600 bytes total) with *any* of the blocks in the original ROM.
Due to the preceding read commands I could see where in the dump these 0x600 bytes should go (comparing it against the
cart read access log I made using DesMuMe), and I finally had a final image. Now it was time to figure out how this
thing actually worked it’s magic. The answer was obviously in those 3 changed blocks.
To give you a quick idea of exactly how much code/data they borrowed from the original ROM: There are 1984 blocks of 0x200 bytes each from the original ROM, of which 1825 blocks are unique. 1825 * 0x200 = ~912Kb of data from the original ROM.
Sure enough the 3 new changed blocks didn’t fall within the area of the ARM9 or ARM7 binary of the game, else it wouldn’t pass the whitelist. I expected it to be a file-based exploit in one of the few files within the nitro filesystem on the cartridge, but all the offsets of those files weren’t in range.
I had a quick look at the 3 new blocks in a hex editor and noticed there was plain ARM opcodes in there, and a big landpad with 0xEAFFFFFE’s in front (label: b label). I started by disassembling this code, not caring HOW it eventually ends up in memory.
The small amount of code does some basic initialisation, then proceeds to send a special cart command that puts the ARDSi cartridge in a different mode. After this three read (0xB7) commands are executed to read and copy out the header, ARM9 binary and ARM7 binary respectively. By this I mean ofcourse the *actual* AR DSi menu header / binaries.
Knowing what their shellcode did was half of the job, now I had to figure out how it ended up executed in memory. Going back to the ROM I realized I didn’t check the *entire* filesystem range for the offset in which the blocks lie. To be more precise, I had only checked within the boundaries of the filesystem entries that have an entry in the File Name Table (FNT).
Nintendo DS ROMs can feature a thing that is known as “filesystem overlays” or “rom overlays”. These overlays are described in separate tables (pointed to by the NDS cartridge header). Overlay tables consist out of entries that look like this:
/* 0x00 */ u32 id;
/* 0x04 */ u32 ram_addr;
/* 0x08 */ u32 ram_size;
/* 0x0C */ u32 bss_size;
/* 0x10 */ u32 sinit_init;
/* 0x14 */ u32 sinit_init_end;
/* 0x18 */ u32 file_id;
/* 0x1C */ u32 unknown;
These overlays are pieces of code that normally aren’t loaded together with the main binary, but rather can be loaded on-demand by the main binary.
The important fields here are: id – a unique identifier (incrementing number) for the current overlay, ram_addr – the address the overlay is copied to in memory when it is loaded, ram_size – the size in RAM reserved for this overlay, file_id – the index of the overlay file in the filesystem table.
Cross checking the filesystem indexes of the overlays against my card access trace from DeSMuME I determined that the 3 changed were infact the first 3 blocks of overlay 0x01.
To not make this article any more lengthy than it already is, here’s what happens:
- DSi console sends cart initialization sequence
- ARDSi cart determines it’s being ran on a DSi console and starts responding a fixed pattern on every read block request
- Game’s header, ARM9 and ARM7 binary are loaded by the DSi menu and checked for integrity
- Integrity checks pass since all data is 1:1 compared to the original ROM
- Game starts running, starts parsing filename and file allocation tables of filesystem on cartridge
- Game loads overlay 0x01 to 0x020BBF00
- Game does more stuff and eventually branches to code inside loaded overlay @ 0x020BBFE8
- The initial 0xE8 bytes of the datel payload are inifite loop opcodes. The entrypoint is right behind it and payload execution starts.
- Payload sends secret F005000000000000 (Not so secret anymore now, huh?) cart command to put cartridge in secondary mode
- Payload uses normal 0xB7 read commands to read the header, the ARM9 binary and the ARM7 binary.
- Some IPC magic is done to capture execution of the ARM7 cpu
- Finally a softreset SVC/SWI is issued to execute the newly loaded ARM9 code
And here I was hoping datel had a clever technique under their belt to leverage code in DS mode on a DSi. But it seems like everyone else (read: Hong Kong pirate cartridge companies), they as well had to resort to ‘borrowing’ bits of a commercial game ROM. The only clever thing here is storing the actual payload inside a ROM overlay that is loaded early in the game. The overlays don’t seem to be checked by the DSi menu during the integrity check, and they don’t have to rely on a buffer overflow or some other tricky attack(s) in order to get their code running.
EDIT: Normmatt just mentioned on IRC that the DSi menu might be in fact checking the ROM overlays. Comparing the SHA sums in the whitelist for a game that has overlays and for a game that doesn’t shows that one of the hashes is empty for a game that has _no_ ROM overlays. This all still makes sense though: the DSi menu initially reads the overlays to check their integrity. But when the game is ran and it loads the overlay(s) on demand different data is returned and the system is fooled into loading the malicious code in memory. A typical case of “read twice, verify once”.
The thing I’m wondering about most is how Datel was planning to get away with this? Someone mentioned the argument of the ROM data being there “just for interoperability” and that would justify it a bit. But in my opinion borrowing roughly a megabyte of original ROM data is a different ballgame than borrowing a couple of (Ok, 48) bytes for the Nintendo logo. I would be interested to hear you guys’ opinions on the ethical aspect of this whole thing.
Oh well, it made for a nice Sunday of reverse engineering anyway. Hopefully you enjoyed the read as much as I did working this out, ciao! ;-]
P.S. Oh, and before I forget here‘s some bonus picture material for you all.
P.P.S If you are interested in the quick dis-assembly I did: click here. Sorry for not having used the correct base-address in that listing, but the code is position independent, you should get the idea. I hope datel doesn’t mind me ‘borrowing’ something from them.