DSi-enhanced games make use of an extended header together with other data structures which implement additional integrity and obfuscation measures over those already present in NDS games. These measures make it currently impossible to produce a ROM image that will run on unmodded retail hardware, however due to the existence of leaked debug keys, it is possible to resign retail ROMs to make them bootable on dev hardware such as the IS-TWL-DEBUGGER.
A very good description of the TWL header format can be found on GBATEK. It is expanded from the 512 bytes of the DS to 4KiB, and it contains many additional entries, but I will be mainly referring to the following:
- The flags field (0x1BF)
- Digest sector entries (0x1E0 - 0x208)]
- Modcrypt entries (0x220 - 0x230)
- HMAC array (0x300 - 0x3B4)
- RSA signature (0xF80 - 0x1000)
The regular DS has two CPUs, ARM9 and ARM7, and ROMs contain an executable for each of these two processors. To maintain compatibility with NDS consoles, DSi-enhanced games contain ARM9 and ARM7 binaries that do not make use of the additional features of the DSi. Instead, the TWL-exclusive code is located in two other binaries, called ARM9i and ARM7i. These binaries are stored in the high part of the ROM, past the file system. I like to call this part of the ROM the TWL region, while everything before it is the NTR region.
Because they are not needed to boot games in DS mode, most tools completely disregard these binaries or place them wrongly when rebuilding ROMs; the only one which handles them somewhat correctly is TinkeDSi. Unlike the rest of the rom which is aligned to 0x200, TWL binaries are aligned to 0x80000. In fact, at header[0x90] there are two 2-byte values which are set to <start of the TWL region> >> 0x13 and they must match the actual start of the TWL region, otherwise official software will refuse to start the ROM.
The final part of the header contains the first integrity mechanism employed by DSi-enhanced games, which is an array of SHA-1 HMAC digests over several different parts of the ROM. The algorithm used to calculate them is just a regular SHA-1 HMAC with a constant HMAC key that is shared between retail and debug applications. The following HMAC digests are present:
- ARM9 SHA-1 HMAC including encrypted secure area (0x4000 and up)
- ARM7 SHA-1 HMAC
- Digest master hash (see "digest sectors" below)
- Icon/title SHA-1 HMAC
- ARM9i SHA-1 HMAC (decrypted, see "Modcrypt" below)
- ARM7i SHA-1 HMAC (also decrypted)
- ARM9 SHA-1 HMAC excluding the entire secure area, so only from 0x8000 onwards.
In addition to the already existing encrypted secure area at the start of ARM9, which is encrypted using Blowfish, DSi games also introduce a different type of encryption called Modcrypt, which is in essence a slightly modified 128-bit AES-CTR stream cipher. The header points to two different Modcrypt areas, however in my experience only area1 is ever used, and it is always placed at the start of ARM9i. Areas can be of size >= 0x4000, but the minimum size is used most of the time.
A Modcrypt cipher is defined by two values, key and IV. Both of these actually depend on data contained in the ROM itself, therefore Modcrypt does not provide any kind of secrecy, only obfuscation. For debug applications, the key is simply the first 16 bytes of the header. For retail games, the key is defined as follows:
KeyDSi = (((KeyX) XOR KeyY) + FFFEFB4E295902582A680F5F1A4F3E79h) ROL 42
Where KeyX and KeyY are derived as:
KeyX = str.encode("Nintendo", encoding='ascii') + gamecode + emagcode
KeyY = bytes(arm9ihmac[:16])The gamecode is the 4-letter identifier found at header[0xC] while the ARM9i HMAC is the one from the header. The emagcode is just the reversed gamecode.
The IV is the same in both retail and debug, but different for each area. area1 uses the first 16 bytes of the ARM9 HMAC (with encrypted secure area), while area2 uses the first 16 bytes of the ARM7 HMAC.
At the end of the NTR region, two additional data structures are appended which verify the integrity of the entire ROM. GBATEK calls these the "digest sector" and "digest block", but to be more clear I simply call them digest1 and digest2. Their definition is a bit confusing, so I'll describe them in the order in which they are calculated.
First of all, the header points to two digest regions, one for NTR and one for TWL. They usually, but not necessarily, match the entire TWL and NTR regions, minus the digest sector itself obviously. These regions (excluding modcrypt encryption, but including secure area encryption!) are concatenated, then split into sectorSize blocks, where sectorSize is usually 0x400 but otherwise defined in the header. Then each block is hashed and a SHA-1 HMAC is produced. All these hashes are then put together as part of digest1. digest1 is then split again into blocks of size 20 * sectorsPerBlock, which is also defined in the header but usually is 0x20. Each block is hashed again, and the resulting "hashes of hashes" are combined to form digest2. digest1 and digest2 are then placed one after the other at the end of the NTR region. Any extra space at the end of each digest is padded with 0x00 bytes instead of the usual 0xFF used everywhere else.
Finally, the entirety of digest2 is hashed to produce one final SHA-1 HMAC, called the digest master, which is placed together with the other SHA-1 HMACs in the header. This nested structure ensures that the digest master basically commits to the entire rom, becoming invalid if even one single byte is changed.
This is a bit of a weird one, as I don't know exactly how they work and they are basically undocumented, but there is a 0x3000 region right before the start of the ARM9i binary which contains some kind of Blowfish table. This is similar to what is contained in the no-load area of NDS roms (0x1000-0x3000) which is empty in dumps, but in real ROMs and official .SRL files contains some test patterns and Blowfish tables used to decrypt cartridge commands. In DSi ROMs, this part is entirely useless except for one thing, and that is making the System Menu recognize the game as bootable; if they are missing, it will show the "no cartridge" icon. Fortunately, these depend entirely on gamecode for retail games, so we can just leave them untouched; for debug roms, they seem to be always the same, so they can be taken from any debug rom.
The first 0xE00 bytes of the header are signed with a 1024-bit RSA private key, which is different between retail and debug. The retail key was kept secret by Nintendo and currently unobtainable, while the debug key is included in the TWL SDK tools. The public keys are instead included in the BIOS and are used to verify the validity of the signature.
The signature is 0x80 bytes long and is generated as a regular SHA-1 over the header bytes, which is then padded according to the PKCS#1.5 standard, but without ASN.1 encoding. The resulting digest is encrypted by doing a "raw" RSA encryption using the private exponent.
The signature is the final step in the chain of integrity checks within a DSi-enhanced ROM; the signature commits to the header, which contains the HMACs and the master digest, which in turn verifies the digest sectors, which verify the whole ROM. This makes it effectively impossible to modify a single byte without having the firmware notice and abort loading the ROM.
The keys can be found in various places, this is how I recovered them:
- Retail RSA public key: starts with
95 6F 79 0Dand can be found in the DSi BIOS. - Debug RSA keypair: can be extracted from the SDK tool
makerom.TWL. Should be detected by tools likebinwalkas a DER-encoded keypair, and the raw bytes can be extracted as a .der file. The public key starts withAC 93 BB 3C, the private key with95 DC C8 18. - HMAC key: found in the BIOS or launcher. Should also be in
makerom.TWL. Starts with21 06 C0 DEand is 64-byte long. if it's longer, make sure there are no extra 00s interleaved within it. - Blowfish key blob: starts with
99 D5 20 5Fand is 0x1048 bytes long. Found in the BIOS as well asmakerom.TWL.
DER-encoded keypairs can be converted using the openssl CLI tool into a pair of PEM files for easier use. To extract the private key in PEM format, do:
openssl pkcs8 -inform DER -in debug.der -nocrypt -out private.pem
Then to derive the public key use the command:
openssl rsa -in private.pem -pubout -out public.pem
There are two separate checks done on debug ROMs (or, more appropriately, .SRL files). The first one is done by the IS-TWL-DEBUGGER software, which checks bit 7 of header[0x1BF] (developer application flag). If it's 0, it will refuse to send the SRL file over to the debugger. The second check is the integrity check performed by the console itself, over the same structures as retail consoles but using debug keys. If the check fails, the CPU will halt and the bottom screen will show an error message.
To make the IS-TWL-DEBUGGER to load a retail ROM as an SRL file, it must be modified like this:
- Make sure the secure area is encrypted
- Set developer application:
rom[0x1BF] |= 0x80 - Write the NTR Blowfish tables and test patterns at 0x1000-0x4000. They can be taken from a ROM encrypted with
ndstool -se, as long as the gamecode is the same. - Write the TWL Blowfish tables at
[arm9i_start - 0x3000]. They can be copied from any debug rom generated bymakerom.TWL. - Decrypt
area1using the retail key and IV. - Encrypt
area1using the debug key and IV. - Generate the SHA-1 of the first 0xE00 bytes of the header, encode it as PKCS#1.5, and encrypt it with the debug RSA private key, then place it at
header[0xF80].
This is enough if you make no modification to the ROM. Otherwise you will also need to:
- Adjust the header as needed, and fix the header CRC
- Optionally fix the secure area CRC if you modified it
- Regenerate
digest1anddigest2using the decrypted TWL region - Recalculate the SHA-1 HMAC of the regions you changed.
Gen 5 Pokémon games use a different cartridge which includes an IR module. If you try to run them off a dev cartridge without the IR module, this causes the game to not detect the save flash correctly. To fix this, you need to patch the ARM7 binary in this way:
# Disable checking for IR chip when accessing backup memory
# beq #0x40 -> b #0x40
arm7_patch_F170 = b"\x0e\x00\x00\xea"
arm7[0xF170:0xF174] = arm7_patch_F170Below you can find some Python scripts I wrote to resign Pokémon BW and B2W2, I made them for my personal use so they may not work for all ROMs or on all systems, but I think you can easily edit them to suit your needs.
Big thanks to xp for helping me with figuring out some of this stuff, especially the digest sectors.