diff --git a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/NDS/MelonDS.cs b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/NDS/MelonDS.cs index 27bd5c91f8..7d7bd8c4dc 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/NDS/MelonDS.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/NDS/MelonDS.cs @@ -1,10 +1,12 @@ using System.Buffers.Binary; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Numerics; using System.Runtime.InteropServices; +using System.Security.Cryptography; using System.Text; using System.Threading; @@ -81,6 +83,9 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.NDS return new(x, y); } + private static readonly BigInteger GENERATOR_CONSTANT = BigInteger.Parse("0FFFEFB4E295902582A680F5F1A4F3E79", NumberStyles.HexNumber, CultureInfo.InvariantCulture); + private static readonly BigInteger U128_MAX = BigInteger.Parse("0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", NumberStyles.HexNumber, CultureInfo.InvariantCulture); + [CoreConstructor(VSystemID.Raw.NDS)] public NDS(CoreLoadParameters lp) : base(lp.Comm, new() @@ -106,7 +111,190 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.NDS var roms = lp.Roms.Select(r => r.FileData).ToList(); - DSiTitleId = GetDSiTitleId(roms[0]); + // make sure we have a valid header before doing any parsing + if (roms[0].Length < 0x1000) + { + throw new InvalidOperationException("ROM is too small to be a valid NDS ROM!"); + } + + ReadOnlySpan romHeader = roms[0].AsSpan(0, 0x1000); + ReadOnlySpan dsiWare = [ ], bios7i = [ ]; + if (!IsRomValid(roms[0])) + { + // if the ROM isn't valid, this could potentially be a backup TAD the user is attempting to load + // backup TADs are DSiWare exported to the SD Card + // https://problemkaputt.de/gbatek.htm#dsisdmmcdsiwarefilesonexternalsdcardbinakatadfiles + bios7i = CoreComm.CoreFileProvider.GetFirmwareOrThrow(new("NDS", "bios7i")); + var fixTadKeyX = bios7i.Slice(0xB588, 0x10); + var fixTadKeyY = bios7i.Slice(0x8328, 0x10); + var varTadKeyY = bios7i.Slice(0x8318, 0x10); + + // CCM is used to encrypt backup TAD files + // For purposes of decryption, this is just CTR really, as we can more or less ignore authentication + // CTR isn't implemented by C# AES of course... so have to reimplement it + // Note: Not a copy paste of N3DSHasher! DSi is weird and transforms each block in little endian rather than big endian + static void AesCtrTransform(Aes aes, byte[] iv, Span inputOutput) + { + // ECB encryptor is used for both CTR encryption and decryption + using var encryptor = aes.CreateEncryptor(); + var blockSize = aes.BlockSize / 8; + var outputBlockBuffer = new byte[blockSize]; + + // mostly copied from tiny-AES-c (public domain) + for (int i = 0, bi = blockSize; i < inputOutput.Length; ++i, ++bi) + { + if (bi == blockSize) + { + encryptor.TransformBlock(iv, 0, iv.Length, outputBlockBuffer, 0); + for (bi = blockSize - 1; bi >= 0; --bi) + { + if (iv[bi] == 0xFF) + { + iv[bi] = 0; + continue; + } + + ++iv[bi]; + break; + } + + bi = 0; + } + + inputOutput[i] ^= outputBlockBuffer[blockSize - 1 - bi]; + } + } + + static byte[] DeriveNormalKey(ReadOnlySpan keyX, ReadOnlySpan keyY) + { + static BigInteger LeftRot128(BigInteger v, int rot) + { + var l = (v << rot) & U128_MAX; + var r = v >> (128 - rot); + return l | r; + } + + static BigInteger Add128(BigInteger v1, BigInteger v2) + => (v1 + v2) & U128_MAX; + + var keyBytes = new byte[17]; + keyX.CopyTo(keyBytes); + var keyXBigInteger = new BigInteger(keyBytes); + + keyY.CopyTo(keyBytes); + var keyYBigInteger = new BigInteger(keyBytes); + + var normalKey = LeftRot128(Add128(keyXBigInteger ^ keyYBigInteger, GENERATOR_CONSTANT), 42); + var normalKeyBytes = normalKey.ToByteArray(); + if (normalKeyBytes.Length > 17) + { + // this shoudn't ever happen + throw new InvalidOperationException(); + } + + Array.Resize(ref normalKeyBytes, 16); + Array.Reverse(normalKeyBytes); + return normalKeyBytes; + } + + static void InitIv(Span iv, ReadOnlySpan nonce) + { + iv[0] = 0x02; + // ES block nonce + for (var i = 0; i < 12; i++) + { + iv[1 + i] = nonce[11 - i]; + } + + iv[13] = 0x00; + iv[14] = 0x00; + iv[15] = 0x01; + } + + using var aes = Aes.Create(); + aes.Mode = CipherMode.ECB; + aes.Padding = PaddingMode.None; + aes.BlockSize = 128; + aes.KeySize = 128; + var iv = new byte[16]; + + // first decrypt the header (mainly to verify this is in fact a backup TAD) + aes.Key = DeriveNormalKey(fixTadKeyX, fixTadKeyY); + InitIv(iv, roms[0].AsSpan(0x4020 + 0xB4 + 0x11, 12)); + AesCtrTransform(aes, iv, roms[0].AsSpan(0x4020, 0x30)); + + if (!roms[0].AsSpan(0x4020, 4).SequenceEqual("4ANT"u8)) + { + // not a backup TAD, this is a garbage NDS anyways + throw new InvalidOperationException("Invalid ROM"); + } + + // these include the ES block footer + var tmdSize = BinaryPrimitives.ReadUInt32LittleEndian(roms[0].AsSpan(0x4020 + 0x28, 4)); + var appSize = BinaryPrimitives.ReadUInt32LittleEndian(roms[0].AsSpan(0x4020 + 0x2C, 4)); + + if (appSize % 0x20020 < 0x20) + { + throw new InvalidOperationException("Invalid ROM"); + } + + // decrypt cert area now (to fetch the console unique id, which Nintendo mistakenly includes in here) + InitIv(iv, roms[0].AsSpan(0x40F4 + 0x440 + 0x11, 12)); + AesCtrTransform(aes, iv, roms[0].AsSpan(0x40F4, 0x440)); + + var consoleIdStr = Encoding.ASCII.GetString(roms[0].AsSpan(0x40F4 + 0x38F, 16)); + if (!ulong.TryParse(consoleIdStr, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var consoleId)) + { + throw new InvalidOperationException("Failed to parse console ID in TAD TWL cert!"); + } + + Span varTadKeyX = stackalloc byte[16]; + BinaryPrimitives.WriteUInt32LittleEndian(varTadKeyX, 0x4E00004A); + BinaryPrimitives.WriteUInt32LittleEndian(varTadKeyX[4..], 0x4A00004E); + BinaryPrimitives.WriteUInt32LittleEndian(varTadKeyX[8..], (uint)(consoleId >> 32) ^ 0xC80C4B72); + BinaryPrimitives.WriteUInt32LittleEndian(varTadKeyX[12..], (uint)(consoleId & 0xFFFFFFFF)); + + // decrypt app area now + aes.Key = DeriveNormalKey(varTadKeyX, varTadKeyY); + var appStart = (int)(0x4554 + tmdSize); + var appEnd = appStart + (int)appSize; + var appOffset = 0; + var appDataOffset = 0; + while (appStart + appOffset < appEnd) + { + var esBlockFooterOffset = (int)Math.Min(0x20020, appSize - appOffset) - 0x20; + InitIv(iv, roms[0].AsSpan(appStart + appOffset + esBlockFooterOffset + 0x11, 12)); + AesCtrTransform(aes, iv, roms[0].AsSpan(appStart + appOffset, esBlockFooterOffset)); + appOffset += esBlockFooterOffset + 0x20; + appDataOffset += esBlockFooterOffset; + } + + var appData = new byte[appDataOffset]; + appOffset = 0; + appDataOffset = 0; + while (appStart + appOffset < appEnd) + { + var esBlockFooterOffset = (int)Math.Min(0x20020, appSize - appOffset) - 0x20; + roms[0].AsSpan(appStart + appOffset, esBlockFooterOffset).CopyTo(appData.AsSpan(appDataOffset)); + appOffset += esBlockFooterOffset + 0x20; + appDataOffset += esBlockFooterOffset; + } + + dsiWare = appData; + if (dsiWare.Length < 0x1000 || !IsRomValid(dsiWare)) + { + throw new InvalidOperationException("Invalid ROM in TAD"); + } + + romHeader = dsiWare[..0x1000]; + DSiTitleId = GetDSiTitleId(romHeader); + if (!IsDSiWare) + { + throw new InvalidOperationException("Backup TAD did not have DSiWare"); + } + } + + DSiTitleId = GetDSiTitleId(romHeader); IsDSi |= IsDSiWare; if (roms.Count > (IsDSi ? 1 : 2)) @@ -203,17 +391,17 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.NDS if (!_activeSyncSettings.UseRealBIOS) { - var arm9RomOffset = BinaryPrimitives.ReadInt32LittleEndian(roms[0].AsSpan(0x20, 4)); + var arm9RomOffset = BinaryPrimitives.ReadInt32LittleEndian(romHeader.Slice(0x20, 4)); if (arm9RomOffset is >= 0x4000 and < 0x8000) { // check if the user is using an encrypted rom // if they are, they need to be using real bios files - var secureAreaId = BinaryPrimitives.ReadUInt64LittleEndian(roms[0].AsSpan(arm9RomOffset, 8)); + var secureAreaId = BinaryPrimitives.ReadUInt64LittleEndian(romHeader.Slice(arm9RomOffset, 8)); _activeSyncSettings.UseRealBIOS = secureAreaId != 0xE7FFDEFF_E7FFDEFF; } } - byte[] bios9 = null, bios7 = null, firmware = null; + ReadOnlySpan bios9 = [ ], bios7 = [ ], firmware = [ ]; if (_activeSyncSettings.UseRealBIOS) { bios9 = CoreComm.CoreFileProvider.GetFirmwareOrThrow(new("NDS", "bios9")); @@ -228,19 +416,19 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.NDS NDSFirmware.MaybeWarnIfBadFw(firmware, CoreComm.ShowMessage); } - byte[] bios9i = null, bios7i = null, nand = null; + ReadOnlySpan bios9i = [ ], nand = [ ]; if (IsDSi) { bios9i = CoreComm.CoreFileProvider.GetFirmwareOrThrow(new("NDS", "bios9i")); - bios7i = CoreComm.CoreFileProvider.GetFirmwareOrThrow(new("NDS", "bios7i")); - nand = DecideNAND(CoreComm.CoreFileProvider, (DSiTitleId.Upper & ~0xFF) == 0x00030000, roms[0][0x1B0]); + bios7i = bios7i.IsEmpty ? CoreComm.CoreFileProvider.GetFirmwareOrThrow(new("NDS", "bios7i")) : bios7i; + nand = DecideNAND(CoreComm.CoreFileProvider, (DSiTitleId.Upper & ~0xFF) == 0x00030000, romHeader[0x1B0]); } - byte[] ndsRom = null, gbaRom = null, dsiWare = null, tmd = null; + ReadOnlySpan ndsRom = [ ], gbaRom = [ ], tmd = [ ]; if (IsDSiWare) { tmd = GetTMDData(DSiTitleId.Full); - dsiWare = roms[0]; + dsiWare = dsiWare.IsEmpty ? roms[0] : dsiWare; } else { @@ -264,16 +452,16 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.NDS LibMelonDS.ConsoleCreationArgs consoleCreationArgs; - consoleCreationArgs.NdsRomLength = ndsRom?.Length ?? 0; - consoleCreationArgs.GbaRomLength = gbaRom?.Length ?? 0; - consoleCreationArgs.Arm9BiosLength = bios9?.Length ?? 0; - consoleCreationArgs.Arm7BiosLength = bios7?.Length ?? 0; - consoleCreationArgs.FirmwareLength = firmware?.Length ?? 0; - consoleCreationArgs.Arm9iBiosLength = bios9i?.Length ?? 0; - consoleCreationArgs.Arm7iBiosLength = bios7i?.Length ?? 0; - consoleCreationArgs.NandLength = nand?.Length ?? 0; - consoleCreationArgs.DsiWareLength = dsiWare?.Length ?? 0; - consoleCreationArgs.TmdLength = tmd?.Length ?? 0; + consoleCreationArgs.NdsRomLength = ndsRom.Length; + consoleCreationArgs.GbaRomLength = gbaRom.Length; + consoleCreationArgs.Arm9BiosLength = bios9.Length; + consoleCreationArgs.Arm7BiosLength = bios7.Length; + consoleCreationArgs.FirmwareLength = firmware.Length; + consoleCreationArgs.Arm9iBiosLength = bios9i.Length; + consoleCreationArgs.Arm7iBiosLength = bios7i.Length; + consoleCreationArgs.NandLength = nand.Length; + consoleCreationArgs.DsiWareLength = dsiWare.Length; + consoleCreationArgs.TmdLength = tmd.Length; consoleCreationArgs.DSi = IsDSi; consoleCreationArgs.ClearNAND = _activeSyncSettings.ClearNAND || lp.DeterministicEmulationRequested; @@ -377,14 +565,47 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.NDS } } - private static (ulong Full, uint Upper, uint Lower) GetDSiTitleId(ReadOnlySpan file) + private static bool IsRomValid(ReadOnlySpan rom) { - ulong titleId = 0; - for (var i = 0; i < 8; i++) + // check ARM7/ARM9 (and maybe ARM7i/ARM9i) binary offsets/sizes to see if they're sane + // if these are wildly wrong, the ROM may crash the core + var unitCode = rom[0x12]; + var isDsiExclusive = (unitCode & 0x03) == 3; + var arm9RomOffset = BinaryPrimitives.ReadUInt32LittleEndian(rom.Slice(0x20, 4)); + var arm9Size = BinaryPrimitives.ReadUInt32LittleEndian(rom.Slice(0x2C, 4)); + var arm7RomOffset = BinaryPrimitives.ReadUInt32LittleEndian(rom.Slice(0x30, 4)); + var arm7Size = BinaryPrimitives.ReadUInt32LittleEndian(rom.Slice(0x3C, 4)); + if (arm9RomOffset > rom.Length || + (arm9Size > 0x3BFE00 && !isDsiExclusive) || + arm9RomOffset + arm9Size > rom.Length || + arm7RomOffset > rom.Length || + (arm7Size > 0x3BFE00 && !isDsiExclusive) || + arm7RomOffset + arm7Size > rom.Length) { - titleId <<= 8; - titleId |= file[0x237 - i]; + return false; } + + if ((unitCode & 0x02) != 0) + { + var arm9iRomOffset = BinaryPrimitives.ReadUInt32LittleEndian(rom.Slice(0x1C0, 4)); + var arm9iSize = BinaryPrimitives.ReadUInt32LittleEndian(rom.Slice(0x1CC, 4)); + var arm7iRomOffset = BinaryPrimitives.ReadUInt32LittleEndian(rom.Slice(0x1D0, 4)); + var arm7iSize = BinaryPrimitives.ReadUInt32LittleEndian(rom.Slice(0x1DC, 4)); + if (arm9iRomOffset > rom.Length || + arm9iRomOffset + arm9iSize > rom.Length || + arm7iRomOffset > rom.Length || + arm7iRomOffset + arm7iSize > rom.Length) + { + return false; + } + } + + return true; + } + + private static (ulong Full, uint Upper, uint Lower) GetDSiTitleId(ReadOnlySpan romHeader) + { + var titleId = BinaryPrimitives.ReadUInt64LittleEndian(romHeader.Slice(0x230, 8)); return (titleId, (uint)(titleId >> 32), (uint)(titleId & 0xFFFFFFFFU)); } diff --git a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/NDS/NDSFirmware.cs b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/NDS/NDSFirmware.cs index 67a6cea547..afb63a9020 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/NDS/NDSFirmware.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/NDS/NDSFirmware.cs @@ -9,7 +9,7 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.NDS // TODO: a lot of this has been removed as it's not really needed anymore (our c++ code forces correctness everywhere) internal static class NDSFirmware { - public static void MaybeWarnIfBadFw(byte[] fw, Action warningCallback) + public static void MaybeWarnIfBadFw(ReadOnlySpan fw, Action warningCallback) { if (fw[0x17C] != 0xFF) { @@ -25,7 +25,8 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.NDS } [DllImport("libfwunpack", CallingConvention = CallingConvention.Cdecl)] - private static extern bool GetDecryptedFirmware(byte[] fw, int fwlen, out IntPtr decryptedFw, out int decryptedlen); + [return: MarshalAs(UnmanagedType.U1)] + private static extern bool GetDecryptedFirmware(IntPtr fw, int fwLen, out IntPtr decryptedFw, out int decryptedLen); [DllImport("libfwunpack", CallingConvention = CallingConvention.Cdecl)] private static extern void FreeDecryptedFirmware(IntPtr decryptedFw); @@ -46,21 +47,30 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.NDS "BFBC33D996AA73A050F1951529327D5844461A00", // MACi DS Lite (Korean v5, 2006-11-09) }; - private static void CheckDecryptedCodeChecksum(byte[] fw, Action warningCallback) + private static unsafe void CheckDecryptedCodeChecksum(ReadOnlySpan fw, Action warningCallback) { - if (!GetDecryptedFirmware(fw, fw.Length, out var decryptedfw, out var decrypedfwlen)) + IntPtr decryptedFw; + int decrypedFwLen; + fixed (byte* fwPtr = fw) { - warningCallback("Firmware could not be decryped for verification! This firmware might be not work!"); - return; + if (!GetDecryptedFirmware((IntPtr)fwPtr, fw.Length, out decryptedFw, out decrypedFwLen)) + { + warningCallback("Firmware could not be decryped for verification! This firmware might be not work!"); + return; + } } - var decryptedFirmware = new byte[decrypedfwlen]; - Marshal.Copy(decryptedfw, decryptedFirmware, 0, decrypedfwlen); - FreeDecryptedFirmware(decryptedfw); - var hash = SHA1Checksum.ComputeDigestHex(decryptedFirmware); - if (!_goodHashes.Contains(hash)) + try { - warningCallback("Potentially bad firmware dump! Decrypted hash " + hash + " does not match known good dumps."); + var hash = SHA1Checksum.ComputeDigestHex(Util.UnsafeSpanFromPointer(decryptedFw, decrypedFwLen)); + if (!_goodHashes.Contains(hash)) + { + warningCallback($"Potentially bad firmware dump! Decrypted hash {hash} does not match known good dumps."); + } + } + finally + { + FreeDecryptedFirmware(decryptedFw); } } }