Reverse engineering the R4 DEMON expiry check
I used an R4 card almost daily as a child, but only examined its kernel when I returned to Nintendo hardware last year and found that the same cartridge no longer booted after a particular date. Kynex describes a similar expiry restriction in another R4 kernel. The code and integrity metadata in my build differed enough that I needed to trace its check separately. I wanted to identify what prevented the menu from loading and remove the restriction while keeping the unrelated changes in a community build out of the patch.
The problem
The cartridge loads its menu with an older system date, refuses to load it after 2 September 2024, and works again when the clock is restored to an accepted date. The warning refers to expired firmware, but the check belongs to the menu executable, which reads the DS real time clock. Neither the cartridge firmware nor the microSD data is altered. Changing the clock bypasses the restriction, although other applications then use the wrong date and write incorrect timestamps until it is restored.
The failure screen reports a clock error and refers the user to the former vendor website. Its handler then clears ARM9 memory from 0x02000000, destroying the running menu. This path does not reach the routines that write to the microSD or cartridge flash, so restarting with an accepted date reloads the untouched executable.
Finding the problem
An ordinary string search does not locate the warning because important kernel messages are stored as transformed 16-bit words rather than plain ASCII. The decoder first permutes the bits of the encoded length, applies the same permutation to each character, and subtracts bytes selected from an embedded CRC table. Recovering this transform gave me 42 printable strings, including expiry messages and paths used by the loader and reset stages. Their references led to the error handler at ARM9 address 0x02034B14.
The caller makes the expiry decision before entering the handler, in the startup state machine at 0x02035A14 and before menu initialisation. ARM9 combines the normalised RTC year, month and day fields into a weighted integer, which is neither a Gregorian day count nor a Unix timestamp. Expressing the calculation in C shows how those fields determine whether startup continues:
uint32_t serial =
rtc->month * 0x57
+ rtc->year_since_2000 * 0x2AFB
+ rtc->day
+ 0xA121;
if (serial > 0x4ABBA)
expired_firmware();
At LAB_02035FE8, the decompiler shows a comparison of that weighted RTC value with 0x4ABBA. Exceeding the threshold transfers control to FUN_02034B14:
local_34 = 0;
thunk_FUN_0205aa52(&local_34,0x2000000,0x5004000);
LAB_02035fe8:
if (0x4abba < (uint)*(byte *)(iVar23 + 2) * 0x57 +
(uint)*(byte *)(iVar23 + 1) * 0x2afb + ...)
FUN_02034b14();
The RTC year counts from 2000, so September 2024 contributes 24 for the year and 9 for the month. On 2 September the expression equals 0x4ABBA, which passes because the comparison is strictly greater than the threshold. Advancing by one day gives 0x4ABBB and enters the expiry handler:
2 September 2024 24 * 11003 + 9 * 87 + 2 + 41249 = 306106 = 0x4ABBA
3 September 2024 24 * 11003 + 9 * 87 + 3 + 41249 = 306107 = 0x4ABBB
Reverse engineering it
In the NDS container R4.dat, the ARM9 image begins at file offset 0x200 and loads at runtime base 0x02000000. Subtracting that base from instruction address 0x02037928 and adding the file offset gives container offset 0x37B28. The four bytes there encode an unconditional ARM bl to the expiry handler. This call site is reached from 0x0203604C, and its decoded target agrees with the handler found through the warning strings:
02037910 02 00 80 e2 add r0,r0,#0x2
02037914 70 eb ff eb bl FUN_020326dc
02037918 00 50 a0 e1 mov r5,r0
0203791c c8 fa ff ea b LAB_02036444
LAB_02037920
02037920 41 6f ff eb bl FUN_0201362c
02037924 ae fa ff ea b LAB_020363e4
LAB_02037928
02037928 79 f4 ff eb bl FUN_02034b14
0203792c c7 f9 ff ea b LAB_02036050
Replacing the call with an ARM NOP bypasses expiry while preserving registers and startup state, but the modified menu must also pass a later integrity check. ARM7 checks the ARM9 image against metadata in the secret area at ARM9 address 0x0206B4E4, corresponding to file offset 0x6B6E4. This area contains encoded ARM9, ARM7 and loader checksums followed by a dependent area value.
The checksum resembles reflected CRC16 with initial value 0xFFFF, except that table entry 246 contains 0x4681 where the standard table contains 0x4680. A standard implementation would therefore produce the wrong result. ARM9 checksum coverage excludes the secret area and most of the final image region, and the result undergoes the same 16-bit permutation as the string decoder. The dependent area value is obtained by encrypting the encoded ARM9 and ARM7 fields with two embedded 56-bit DES keys, then adjusting the first output word using the loader checksum.
The community build's metadata describes an ARM9 image with different clone checks, hardware policy and interface behaviour, so it cannot be reused for this patch. Changing only the expiry call gives raw ARM9 checksum 0x7512, encoded checksum 0xA493 and area value 0xDE637BBC. ARM7 and the loader remain unchanged, retaining checksums 0xEFD0 and 0xDBD7.
Bypassing it
Begin with an untouched R4.dat from this exact build. Its header places ARM9 at container offset 0x200, loads it at 0x02000000 and specifies an image length of 0x91D6C bytes. The call at 0x02037928 lies at ARM9 image offset 0x37928 and container offset 0x37B28. Verify that this location contains 79 F4 FF EB before applying the replacements.
Replace the call with mov r0,r0. This four-byte instruction preserves registers, flags and stack state, then falls through to the branch at 0x0203792C. Its encoding is 0xE1A00000, or 00 00 A0 E1 in little endian byte order, leaving the subsequent branch into normal initialisation intact:
02037920 41 6f ff eb bl FUN_0201362c
02037924 ae fa ff ea b LAB_020363e4
02037928 00 00 a0 e1 mov r0,r0
0203792c c7 f9 ff ea b LAB_02036050
Removing the call changes the ARM9 image, so ARM7 will reject it unless its integrity metadata is updated. Recalculate the modified regions' checksum as 0x7512 and store its encoded value 0xA493 at 0x0206B4E8. Store the dependent area value 0xDE637BBC at 0x0206B4EE. The ARM7 checksum 0xEFD0 and loader checksum 0xDBD7 remain valid because their inputs have not changed.
Apply the following three replacements to the extracted ARM9 image after verifying each original byte sequence. Image offsets are relative to ARM9, while container offsets include the 0x200-byte header.
| Runtime address | ARM9 image offset | R4.dat offset | Original bytes | Replacement bytes |
|---|---|---|---|---|
0x02037928 | 0x37928 | 0x37B28 | 79 F4 FF EB | 00 00 A0 E1 |
0x0206B4E8 | 0x6B4E8 | 0x6B6E8 | 59 5F | 93 A4 |
0x0206B4EE | 0x6B4EE | 0x6B6EE | 88 22 24 91 | BC 7B 63 DE |
Before repair, the encoded ARM9 checksum is 59 5F at 0x0206B4E8, and the dependent area value is 88 22 24 91 at 0x0206B4EE. The surrounding secret area bytes are:
0206b4e4 3f 2f 23 40
0206b4e8 59 5f
0206b4ea 3d a7
0206b4ec fe b3
0206b4ee 88 22 24 91
0206b4f2 94 e1 04 00
After repair, the checksum field contains 93 A4 and the area field contains BC 7B 63 DE. The neighbouring ARM7 and loader fields are unchanged:
0206b4e4 3f 2f 23 40
0206b4e8 93 a4
0206b4ea 3d a7
0206b4ec fe b3
0206b4ee bc 7b 63 de
0206b4f2 94 e1 04 00
The patched ARM9 image must remain 0x91D6C bytes long. A different length would shift ARM7, filesystem data or padding within the container. Copying it over the original ARM9 range in a duplicate container preserves the 512-byte header and all other regions, with exactly 10 changed bytes from the three replacements. The PowerShell rebuild below checks the image length, copies it into the container and saves a separate output file:
$container = [System.IO.File]::ReadAllBytes('.\R4.dat')
$arm9 = [System.IO.File]::ReadAllBytes('.\r4_arm9_patched.bin')
if ($arm9.Length -ne 0x91D6C) {
throw 'The patched ARM9 image has the wrong length.'
}
[Array]::Copy($arm9, 0, $container, 0x200, $arm9.Length)
[System.IO.File]::WriteAllBytes('.\R4-notimebomb.dat', $container)
Retain a separate backup of the untouched kernel, then rename the patched output to R4.dat and place it at the microSD root. System dates after 2 September 2024 now follow normal startup, and the repaired metadata satisfies the ARM7 integrity check. Cartridge authentication, game loading, saves, cheats and reset support keep their original routines.