/// <summary> /// Decrypt the RomFS, if it exists /// </summary> /// <param name="ncsdHeader">NCSD header representing the 3DS file</param> /// <param name="ncchHeader">NCCH header representing the partition</param> /// <param name="reader">BinaryReader representing the input stream</param> /// <param name="writer">BinaryWriter representing the output stream</param> /// TODO: See how much can be extracted into a common method with Encrypt private void DecryptRomFS(NCSDHeader ncsdHeader, NCCHHeader ncchHeader, BinaryReader reader, BinaryWriter writer) { // If the RomFS offset is 0, we log and return if (ncchHeader.RomFSOffsetInMediaUnits == 0) { Console.WriteLine($"Partition {ncchHeader.PartitionNumber} RomFS: No Data... Skipping..."); return; } long romfsSizeM = (int)((long)(ncchHeader.RomFSSizeInMediaUnits * ncsdHeader.MediaUnitSize) / (1024 * 1024)); int romfsSizeB = (int)((long)(ncchHeader.RomFSSizeInMediaUnits * ncsdHeader.MediaUnitSize) % (1024 * 1024)); var cipher = CreateAESCipher(ncchHeader.NormalKey, ncchHeader.RomFSIV, decryptArgs.Encrypt); reader.BaseStream.Seek((ncchHeader.Entry.Offset + ncchHeader.RomFSOffsetInMediaUnits) * ncsdHeader.MediaUnitSize, SeekOrigin.Begin); writer.BaseStream.Seek((ncchHeader.Entry.Offset + ncchHeader.RomFSOffsetInMediaUnits) * ncsdHeader.MediaUnitSize, SeekOrigin.Begin); if (romfsSizeM > 0) { for (int i = 0; i < romfsSizeM; i++) { writer.Write(cipher.ProcessBytes(reader.ReadBytes(1024 * 1024))); writer.Flush(); Console.Write($"\rPartition {ncchHeader.PartitionNumber} RomFS: Decrypting: {i} / {romfsSizeM + 1} mb"); } } if (romfsSizeB > 0) { writer.Write(cipher.DoFinal(reader.ReadBytes(romfsSizeB))); writer.Flush(); } Console.Write($"\rPartition {ncchHeader.PartitionNumber} RomFS: Decrypting: {romfsSizeM + 1} / {romfsSizeM + 1} mb... Done!\r\n"); }
/// <summary> /// Process the ExeFS, if it exists /// </summary> /// <param name="ncsdHeader">NCSD header representing the 3DS file</param> /// <param name="ncchHeader">NCCH header representing the partition</param> /// <param name="reader">BinaryReader representing the input stream</param> /// <param name="writer">BinaryWriter representing the output stream</param> private void ProcessExeFS(NCSDHeader ncsdHeader, NCCHHeader ncchHeader, BinaryReader reader, BinaryWriter writer) { int exefsSizeM = (int)((long)((ncchHeader.ExeFSSizeInMediaUnits - 1) * ncsdHeader.MediaUnitSize) / (1024 * 1024)); int exefsSizeB = (int)((long)((ncchHeader.ExeFSSizeInMediaUnits - 1) * ncsdHeader.MediaUnitSize) % (1024 * 1024)); int ctroffsetE = (int)(ncsdHeader.MediaUnitSize / 0x10); byte[] exefsIVWithOffset = AddToByteArray(ncchHeader.ExeFSIV, ctroffsetE); var exeFS = CreateAESCipher(ncchHeader.NormalKey2C, exefsIVWithOffset, decryptArgs.Encrypt); reader.BaseStream.Seek((ncchHeader.Entry.Offset + ncchHeader.ExeFSOffsetInMediaUnits + 1) * ncsdHeader.MediaUnitSize, SeekOrigin.Begin); writer.BaseStream.Seek((ncchHeader.Entry.Offset + ncchHeader.ExeFSOffsetInMediaUnits + 1) * ncsdHeader.MediaUnitSize, SeekOrigin.Begin); if (exefsSizeM > 0) { for (int i = 0; i < exefsSizeM; i++) { writer.Write(exeFS.ProcessBytes(reader.ReadBytes(1024 * 1024))); writer.Flush(); Console.Write($"\rPartition {ncchHeader.PartitionNumber} ExeFS: " + (decryptArgs.Encrypt ? "Encrypting" : "Decrypting") + $": {i} / {exefsSizeM + 1} mb"); } } if (exefsSizeB > 0) { writer.Write(exeFS.DoFinal(reader.ReadBytes(exefsSizeB))); writer.Flush(); } Console.Write($"\rPartition {ncchHeader.PartitionNumber} ExeFS: " + (decryptArgs.Encrypt ? "Encrypting" : "Decrypting") + $": {exefsSizeM + 1} / {exefsSizeM + 1} mb... Done!\r\n"); }
/// <summary> /// Update the CryptoMethod and BitMasks for the encrypted partition /// </summary> /// <param name="ncsdHeader">NCSD header representing the 3DS file</param> /// <param name="ncchHeader">NCCH header representing the partition</param> /// <param name="writer">BinaryWriter representing the output stream</param> private void UpdateEncryptCryptoAndMasks(NCSDHeader ncsdHeader, NCCHHeader ncchHeader, BinaryWriter writer) { // Write the new CryptoMethod writer.BaseStream.Seek((ncchHeader.Entry.Offset * ncsdHeader.MediaUnitSize) + 0x18B, SeekOrigin.Begin); // For partitions 1 and up, set crypto-method to 0x00 if (ncchHeader.PartitionNumber > 0) { writer.Write((byte)CryptoMethod.Original); } // If partition 0, restore crypto-method from backup flags else { writer.Write((byte)ncsdHeader.BackupHeader.Flags.CryptoMethod); } writer.Flush(); // Write the new BitMasks flag writer.BaseStream.Seek((ncchHeader.Entry.Offset * ncsdHeader.MediaUnitSize) + 0x18F, SeekOrigin.Begin); BitMasks flag = ncchHeader.Flags.BitMasks; flag &= (BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator | BitMasks.NoCrypto) ^ (BitMasks)0xFF; flag |= (BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator) & ncsdHeader.BackupHeader.Flags.BitMasks; writer.Write((byte)flag); writer.Flush(); }
/// <summary> /// Process the extended header, if it exists /// </summary> /// <param name="ncchHeader">NCCH header representing the partition</param> /// <param name="reader">BinaryReader representing the input stream</param> /// <param name="writer">BinaryWriter representing the output stream</param> private bool ProcessExtendedHeader(NCCHHeader ncchHeader, BinaryReader reader, BinaryWriter writer) { // TODO: Determine how to figure out the MediaUnitSize without an NCSD header. Is it a default value? uint mediaUnitSize = 0x200; // mediaUnitSize; if (ncchHeader.ExtendedHeaderSizeInBytes > 0) { reader.BaseStream.Seek((ncchHeader.Entry.Offset * mediaUnitSize) + 0x200, SeekOrigin.Begin); writer.BaseStream.Seek((ncchHeader.Entry.Offset * mediaUnitSize) + 0x200, SeekOrigin.Begin); Console.WriteLine($"Partition {ncchHeader.PartitionNumber} ExeFS: " + (decryptArgs.Encrypt ? "Encrypting" : "Decrypting") + ": ExHeader"); var cipher = CreateAESCipher(ncchHeader.NormalKey2C, ncchHeader.PlainIV, decryptArgs.Encrypt); writer.Write(cipher.ProcessBytes(reader.ReadBytes(Constants.CXTExtendedDataHeaderLength))); writer.Flush(); return(true); } else { Console.WriteLine($"Partition {ncchHeader.PartitionNumber} ExeFS: No Extended Header... Skipping..."); return(false); } return(false); }
/// <summary> /// Update the CryptoMethod and BitMasks for the encrypted partition /// </summary> /// <param name="ciaHeader">CIA header representing the 3DS CIA file</param> /// <param name="ncchHeader">NCCH header representing the partition</param> /// <param name="writer">BinaryWriter representing the output stream</param> private void UpdateEncryptCryptoAndMasks(CIAHeader ciaHeader, NCCHHeader ncchHeader, BinaryWriter writer) { // TODO: Determine how to figure out the MediaUnitSize without an NCSD header. Is it a default value? uint mediaUnitSize = 0x200; // ncsdHeader.MediaUnitSize; // Write the new CryptoMethod writer.BaseStream.Seek((ncchHeader.Entry.Offset * mediaUnitSize) + 0x18B, SeekOrigin.Begin); // For partitions 1 and up, set crypto-method to 0x00 if (ncchHeader.PartitionNumber > 0) { writer.Write((byte)CryptoMethod.Original); } // TODO: Determine how to figure out the original crypto method, if possible // If partition 0, restore crypto-method from backup flags //else // writer.Write((byte)ciaHeader.BackupHeader.Flags.CryptoMethod); writer.Flush(); // Write the new BitMasks flag writer.BaseStream.Seek((ncchHeader.Entry.Offset * mediaUnitSize) + 0x18F, SeekOrigin.Begin); BitMasks flag = ncchHeader.Flags.BitMasks; flag &= (BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator | BitMasks.NoCrypto) ^ (BitMasks)0xFF; // TODO: Determine how to figure out the original crypto method, if possible //flag |= (BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator) & ciaHeader.BackupHeader.Flags.BitMasks; writer.Write((byte)flag); writer.Flush(); }
public void ExtractNCCHFromFile(string NCCH_PATH, string outputDirectory, RichTextBox TB_Progress = null, ProgressBar PB_Show = null) { if (!Directory.Exists(outputDirectory)) { Directory.CreateDirectory(outputDirectory); } byte[] headerBytes = new byte[0x200]; using (FileStream fs = new FileStream(NCCH_PATH, FileMode.Open, FileAccess.Read)) { fs.Read(headerBytes, 0, headerBytes.Length); Header = new NCCHHeader(); Header.BuildHeaderFromBytes(headerBytes); logo = new byte[Header.LogoSize * MEDIA_UNIT_SIZE]; fs.Seek(Convert.ToInt32(Header.LogoOffset * MEDIA_UNIT_SIZE), SeekOrigin.Begin); fs.Read(logo, 0, logo.Length); plainregion = new byte[Header.PlainRegionSize * MEDIA_UNIT_SIZE]; fs.Seek(Convert.ToInt32(Header.PlainRegionOffset * MEDIA_UNIT_SIZE), SeekOrigin.Begin); fs.Read(plainregion, 0, plainregion.Length); } ExtractExheader(NCCH_PATH, outputDirectory, TB_Progress); ExtractExeFS(NCCH_PATH, outputDirectory, TB_Progress); ExtractRomFS(NCCH_PATH, outputDirectory, TB_Progress, PB_Show); }
/// <summary> /// Process all partitions in the content file data of a CIA header /// </summary> /// <param name="ciaHeader">CIA header representing the 3DS CIA file</param> /// <param name="reader">BinaryReader representing the input stream</param> /// <param name="writer">BinaryWriter representing the output stream</param> private void ProcessAllPartitions(CIAHeader ciaHeader, BinaryReader reader, BinaryWriter writer) { // Iterate over all NCCH partitions for (int p = 0; p < ciaHeader.Partitions.Length; p++) { NCCHHeader ncchHeader = ciaHeader.Partitions[0]; ProcessPartition(ciaHeader, ncchHeader, reader, writer); } }
/// <summary> /// Process the extended header, if it exists /// </summary> /// <param name="ncchHeader">NCCH header representing the partition</param> /// <param name="reader">BinaryReader representing the input stream</param> /// <param name="writer">BinaryWriter representing the output stream</param> private void ProcessExeFSFileEntries(NCCHHeader ncchHeader, BinaryReader reader, BinaryWriter writer) { // TODO: Determine how to figure out the MediaUnitSize without an NCSD header. Is it a default value? uint mediaUnitSize = 0x200; // mediaUnitSize; reader.BaseStream.Seek((ncchHeader.Entry.Offset + ncchHeader.ExeFSOffsetInMediaUnits) * mediaUnitSize, SeekOrigin.Begin); ExeFSHeader exefsHeader = ExeFSHeader.Read(reader); // If the header failed to read, log and return if (exefsHeader == null) { Console.WriteLine($"Partition {ncchHeader.PartitionNumber} ExeFS header could not be read. Skipping..."); return; } foreach (ExeFSFileHeader fileHeader in exefsHeader.FileHeaders) { // Only decrypt a file if it's a code binary if (!fileHeader.IsCodeBinary) { continue; } uint datalenM = ((fileHeader.FileSize) / (1024 * 1024)); uint datalenB = ((fileHeader.FileSize) % (1024 * 1024)); uint ctroffset = ((fileHeader.FileOffset + mediaUnitSize) / 0x10); byte[] exefsIVWithOffsetForHeader = AddToByteArray(ncchHeader.ExeFSIV, (int)ctroffset); var firstCipher = CreateAESCipher(ncchHeader.NormalKey, exefsIVWithOffsetForHeader, decryptArgs.Encrypt); var secondCipher = CreateAESCipher(ncchHeader.NormalKey2C, exefsIVWithOffsetForHeader, !decryptArgs.Encrypt); reader.BaseStream.Seek((((ncchHeader.Entry.Offset + ncchHeader.ExeFSOffsetInMediaUnits) + 1) * mediaUnitSize) + fileHeader.FileOffset, SeekOrigin.Begin); writer.BaseStream.Seek((((ncchHeader.Entry.Offset + ncchHeader.ExeFSOffsetInMediaUnits) + 1) * mediaUnitSize) + fileHeader.FileOffset, SeekOrigin.Begin); if (datalenM > 0) { for (int i = 0; i < datalenM; i++) { writer.Write(secondCipher.ProcessBytes(firstCipher.ProcessBytes(reader.ReadBytes(1024 * 1024)))); writer.Flush(); Console.Write($"\rPartition {ncchHeader.PartitionNumber} ExeFS: " + (decryptArgs.Encrypt ? "Encrypting" : "Decrypting") + $": {fileHeader.ReadableFileName}... {i} / {datalenM + 1} mb..."); } } if (datalenB > 0) { writer.Write(secondCipher.DoFinal(firstCipher.DoFinal(reader.ReadBytes((int)datalenB)))); writer.Flush(); } Console.Write($"\rPartition {ncchHeader.PartitionNumber} ExeFS: " + (decryptArgs.Encrypt ? "Encrypting" : "Decrypting") + $": {fileHeader.ReadableFileName}... {datalenM + 1} / {datalenM + 1} mb... Done!\r\n"); } }
/// <summary> /// Process the ExeFS Filename Table /// </summary> /// <param name="ncsdHeader">NCSD header representing the 3DS file</param> /// <param name="ncchHeader">NCCH header representing the partition</param> /// <param name="reader">BinaryReader representing the input stream</param> /// <param name="writer">BinaryWriter representing the output stream</param> private void ProcessExeFSFilenameTable(NCSDHeader ncsdHeader, NCCHHeader ncchHeader, BinaryReader reader, BinaryWriter writer) { reader.BaseStream.Seek((ncchHeader.Entry.Offset + ncchHeader.ExeFSOffsetInMediaUnits) * ncsdHeader.MediaUnitSize, SeekOrigin.Begin); writer.BaseStream.Seek((ncchHeader.Entry.Offset + ncchHeader.ExeFSOffsetInMediaUnits) * ncsdHeader.MediaUnitSize, SeekOrigin.Begin); Console.WriteLine($"Partition {ncchHeader.PartitionNumber} ExeFS: " + (decryptArgs.Encrypt ? "Encrypting" : "Decrypting") + $": ExeFS Filename Table"); var exeFSFilenameTable = CreateAESCipher(ncchHeader.NormalKey2C, ncchHeader.ExeFSIV, decryptArgs.Encrypt); writer.Write(exeFSFilenameTable.ProcessBytes(reader.ReadBytes((int)ncsdHeader.MediaUnitSize))); writer.Flush(); }
/// <summary> /// Encrypt the RomFS, if it exists /// </summary> /// <param name="ncchHeader">NCCH header representing the partition</param> /// <param name="reader">BinaryReader representing the input stream</param> /// <param name="writer">BinaryWriter representing the output stream</param> /// TODO: See how much can be extracted into a common method with Decrypt private void EncryptRomFS(NCCHHeader ncchHeader, BinaryReader reader, BinaryWriter writer) { // TODO: Determine how to figure out the MediaUnitSize without an NCSD header. Is it a default value? uint mediaUnitSize = 0x200; // ncsdHeader.MediaUnitSize; // If the RomFS offset is 0, we log and return if (ncchHeader.RomFSOffsetInMediaUnits == 0) { Console.WriteLine($"Partition {ncchHeader.PartitionNumber} RomFS: No Data... Skipping..."); return; } long romfsSizeM = (int)((long)(ncchHeader.RomFSSizeInMediaUnits * mediaUnitSize) / (1024 * 1024)); int romfsSizeB = (int)((long)(ncchHeader.RomFSSizeInMediaUnits * mediaUnitSize) % (1024 * 1024)); // Encrypting RomFS for partitions 1 and up always use Key0x2C if (ncchHeader.PartitionNumber > 0) { // TODO: Determine how to figure out the original crypto method, if possible //if (ciaHeader.BackupHeader.Flags?.BitMasks.HasFlag(BitMasks.FixedCryptoKey) == true) // except if using zero-key //{ // ncchHeader.NormalKey = 0x00; //} //else //{ ncchHeader.KeyX = (decryptArgs.Development ? decryptArgs.DevKeyX0x2C : decryptArgs.KeyX0x2C); ncchHeader.NormalKey = RotateLeft((RotateLeft(ncchHeader.KeyX, 2, 128) ^ ncchHeader.KeyY) + decryptArgs.AESHardwareConstant, 87, 128); //} } var cipher = CreateAESCipher(ncchHeader.NormalKey, ncchHeader.RomFSIV, decryptArgs.Encrypt); reader.BaseStream.Seek((ncchHeader.Entry.Offset + ncchHeader.RomFSOffsetInMediaUnits) * mediaUnitSize, SeekOrigin.Begin); writer.BaseStream.Seek((ncchHeader.Entry.Offset + ncchHeader.RomFSOffsetInMediaUnits) * mediaUnitSize, SeekOrigin.Begin); if (romfsSizeM > 0) { for (int i = 0; i < romfsSizeM; i++) { writer.Write(cipher.ProcessBytes(reader.ReadBytes(1024 * 1024))); writer.Flush(); Console.Write($"\rPartition {ncchHeader.PartitionNumber} RomFS: Encrypting: {i} / {romfsSizeM + 1} mb"); } } if (romfsSizeB > 0) { writer.Write(cipher.DoFinal(reader.ReadBytes(romfsSizeB))); writer.Flush(); } Console.Write($"\rPartition {ncchHeader.PartitionNumber} RomFS: Encrypting: {romfsSizeM + 1} / {romfsSizeM + 1} mb... Done!\r\n"); }
/// <summary> /// Process all partitions in the partition table of an NCSD header /// </summary> /// <param name="ncsdHeader">NCSD header representing the 3DS file</param> /// <param name="reader">BinaryReader representing the input stream</param> /// <param name="writer">BinaryWriter representing the output stream</param> private void ProcessAllPartitions(NCSDHeader ncsdHeader, BinaryReader reader, BinaryWriter writer) { // Iterate over all 8 NCCH partitions for (int p = 0; p < 8; p++) { NCCHHeader ncchHeader = GetPartitionHeader(ncsdHeader, reader, p); if (ncchHeader == null) { continue; } ProcessPartition(ncsdHeader, ncchHeader, reader, writer); } }
/// <summary> /// Process the ExeFS Filename Table /// </summary> /// <param name="ncchHeader">NCCH header representing the partition</param> /// <param name="reader">BinaryReader representing the input stream</param> /// <param name="writer">BinaryWriter representing the output stream</param> private void ProcessExeFSFilenameTable(NCCHHeader ncchHeader, BinaryReader reader, BinaryWriter writer) { // TODO: Determine how to figure out the MediaUnitSize without an NCSD header. Is it a default value? uint mediaUnitSize = 0x200; // mediaUnitSize; reader.BaseStream.Seek((ncchHeader.Entry.Offset + ncchHeader.ExeFSOffsetInMediaUnits) * mediaUnitSize, SeekOrigin.Begin); writer.BaseStream.Seek((ncchHeader.Entry.Offset + ncchHeader.ExeFSOffsetInMediaUnits) * mediaUnitSize, SeekOrigin.Begin); Console.WriteLine($"Partition {ncchHeader.PartitionNumber} ExeFS: " + (decryptArgs.Encrypt ? "Encrypting" : "Decrypting") + $": ExeFS Filename Table"); var exeFSFilenameTable = CreateAESCipher(ncchHeader.NormalKey2C, ncchHeader.ExeFSIV, decryptArgs.Encrypt); writer.Write(exeFSFilenameTable.ProcessBytes(reader.ReadBytes((int)mediaUnitSize))); writer.Flush(); }
/// <summary> /// Update the CryptoMethod and BitMasks for the decrypted partition /// </summary> /// <param name="ncsdHeader">NCSD header representing the 3DS file</param> /// <param name="ncchHeader">NCCH header representing the partition</param> /// <param name="writer">BinaryWriter representing the output stream</param> private void UpdateDecryptCryptoAndMasks(NCSDHeader ncsdHeader, NCCHHeader ncchHeader, BinaryWriter writer) { // Write the new CryptoMethod writer.BaseStream.Seek((ncchHeader.Entry.Offset * ncsdHeader.MediaUnitSize) + 0x18B, SeekOrigin.Begin); writer.Write((byte)CryptoMethod.Original); writer.Flush(); // Write the new BitMasks flag writer.BaseStream.Seek((ncchHeader.Entry.Offset * ncsdHeader.MediaUnitSize) + 0x18F, SeekOrigin.Begin); BitMasks flag = ncchHeader.Flags.BitMasks; flag &= (BitMasks)((byte)(BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator) ^ 0xFF); flag |= BitMasks.NoCrypto; writer.Write((byte)flag); writer.Flush(); }
/// <summary> /// Encrypt the RomFS, if it exists /// </summary> /// <param name="ncsdHeader">NCSD header representing the 3DS file</param> /// <param name="ncchHeader">NCCH header representing the partition</param> /// <param name="reader">BinaryReader representing the input stream</param> /// <param name="writer">BinaryWriter representing the output stream</param> /// TODO: See how much can be extracted into a common method with Decrypt private void EncryptRomFS(NCSDHeader ncsdHeader, NCCHHeader ncchHeader, BinaryReader reader, BinaryWriter writer) { // If the RomFS offset is 0, we log and return if (ncchHeader.RomFSOffsetInMediaUnits == 0) { Console.WriteLine($"Partition {ncchHeader.PartitionNumber} RomFS: No Data... Skipping..."); return; } long romfsSizeM = (int)((long)(ncchHeader.RomFSSizeInMediaUnits * ncsdHeader.MediaUnitSize) / (1024 * 1024)); int romfsSizeB = (int)((long)(ncchHeader.RomFSSizeInMediaUnits * ncsdHeader.MediaUnitSize) % (1024 * 1024)); // Encrypting RomFS for partitions 1 and up always use Key0x2C if (ncchHeader.PartitionNumber > 0) { if (ncsdHeader.BackupHeader.Flags?.BitMasks.HasFlag(BitMasks.FixedCryptoKey) == true) // except if using zero-key { ncchHeader.NormalKey = 0x00; } else { ncchHeader.KeyX = (development ? Constants.DevKeyX0x2C : Constants.KeyX0x2C); ncchHeader.NormalKey = Helper.RotateLeft((Helper.RotateLeft(ncchHeader.KeyX, 2, 128) ^ ncchHeader.KeyY) + Constants.AESHardwareConstant, 87, 128); } } var cipher = Helper.CreateAESCipher(ncchHeader.NormalKey, ncchHeader.RomFSIV, encrypt); reader.BaseStream.Seek((ncchHeader.Entry.Offset + ncchHeader.RomFSOffsetInMediaUnits) * ncsdHeader.MediaUnitSize, SeekOrigin.Begin); writer.BaseStream.Seek((ncchHeader.Entry.Offset + ncchHeader.RomFSOffsetInMediaUnits) * ncsdHeader.MediaUnitSize, SeekOrigin.Begin); if (romfsSizeM > 0) { for (int i = 0; i < romfsSizeM; i++) { writer.Write(cipher.ProcessBytes(reader.ReadBytes(1024 * 1024))); writer.Flush(); Console.Write($"\rPartition {ncchHeader.PartitionNumber} RomFS: Encrypting: {i} / {romfsSizeM + 1} mb"); } } if (romfsSizeB > 0) { writer.Write(cipher.DoFinal(reader.ReadBytes(romfsSizeB))); writer.Flush(); } Console.Write($"\rPartition {ncchHeader.PartitionNumber} RomFS: Encrypting: {romfsSizeM + 1} / {romfsSizeM + 1} mb... Done!\r\n"); }
static void Main(string[] args) { // string path = @"D:\Games\3DS\LEGO Star Wars III (USA).3ds"; string path = args[0]; // string path = @"D:\Games\3DS\Mario Kart 7 (USA).3ds"; FileStream fs = File.Open(path, FileMode.Open); NCSDHeader ncsdHeader = new NCSDHeader(IO.ReadData(fs, 512)); System.Console.WriteLine(ncsdHeader); int partition = 1; if (args.Length > 1) { partition = int.Parse(args[1]); } else { System.Console.Write("Partition: "); partition = int.Parse(System.Console.ReadLine()); } long offset = ncsdHeader.PartitionTable.partitions[partition].Offset; fs.Seek(offset, SeekOrigin.Begin); NCCHHeader ncchHeader = new NCCHHeader(IO.ReadData(fs, 512), offset); System.Console.WriteLine(ncchHeader); fs.Seek(ncchHeader.ExeFSOffset, SeekOrigin.Begin); ExeFSHeader exeFSHeader = new ExeFSHeader(IO.ReadData(fs, 512), ncchHeader.ExeFSOffset); System.Console.WriteLine($"\nExeFS Header\n{exeFSHeader}"); fs.Seek(ncchHeader.RomFSOffset, SeekOrigin.Begin); IVFCHeader ivfcHeader = new IVFCHeader(IO.ReadData(fs, 92), ncchHeader.RomFSOffset); System.Console.WriteLine($"\nIVFC Header (RomFS)\n{ivfcHeader}"); L3Partition l3Partition = new L3Partition(fs, ncchHeader.RomFSOffset + 0x1000); System.Console.WriteLine($"\nL3 Header (RomFS)\n{l3Partition.Header}"); // System.Console.WriteLine($"\nL3 Partition Directory Metadata Table (RomFS)\n{l3Partition.DirectoryMetadataTable}"); // System.Console.WriteLine($"\nL3 Partition Files Metadata Table (RomFS)\n{l3Partition.FileMetadataTable}"); System.Console.WriteLine($"\nL3 Partition Structure (RomFS)\n{l3Partition}"); }
/// <summary> /// Update the CryptoMethod and BitMasks for the decrypted partition /// </summary> /// <param name="ncchHeader">NCCH header representing the partition</param> /// <param name="writer">BinaryWriter representing the output stream</param> private void UpdateDecryptCryptoAndMasks(NCCHHeader ncchHeader, BinaryWriter writer) { // TODO: Determine how to figure out the MediaUnitSize without an NCSD header. Is it a default value? uint mediaUnitSize = 0x200; // ncsdHeader.MediaUnitSize; // Write the new CryptoMethod writer.BaseStream.Seek((ncchHeader.Entry.Offset * mediaUnitSize) + 0x18B, SeekOrigin.Begin); writer.Write((byte)CryptoMethod.Original); writer.Flush(); // Write the new BitMasks flag writer.BaseStream.Seek((ncchHeader.Entry.Offset * mediaUnitSize) + 0x18F, SeekOrigin.Begin); BitMasks flag = ncchHeader.Flags.BitMasks; flag &= (BitMasks)((byte)(BitMasks.FixedCryptoKey | BitMasks.NewKeyYGenerator) ^ 0xFF); flag |= BitMasks.NoCrypto; writer.Write((byte)flag); writer.Flush(); }
/// <summary> /// Encrypt the ExeFS, if it exists /// </summary> /// <param name="ncchHeader">NCCH header representing the partition</param> /// <param name="reader">BinaryReader representing the input stream</param> /// <param name="writer">BinaryWriter representing the output stream</param> private void EncryptExeFS(NCCHHeader ncchHeader, BinaryReader reader, BinaryWriter writer) { // If the ExeFS size is 0, we log and return if (ncchHeader.ExeFSSizeInMediaUnits == 0) { Console.WriteLine($"Partition {ncchHeader.PartitionNumber} ExeFS: No Data... Skipping..."); return; } // TODO: Determine how to figure out the original crypto method, if possible // For all but the original crypto method, process each of the files in the table //if (ciaHeader.BackupHeader.Flags.CryptoMethod != CryptoMethod.Original) // ProcessExeFSFileEntries(ncchHeader, reader, writer); // Encrypt the filename table ProcessExeFSFilenameTable(ncchHeader, reader, writer); // Encrypt the rest of the ExeFS ProcessExeFS(ncchHeader, reader, writer); }
/// <summary> /// Process the extended header, if it exists /// </summary> /// <param name="ncsdHeader">NCSD header representing the 3DS file</param> /// <param name="ncchHeader">NCCH header representing the partition</param> /// <param name="reader">BinaryReader representing the input stream</param> /// <param name="writer">BinaryWriter representing the output stream</param> private bool ProcessExtendedHeader(NCSDHeader ncsdHeader, NCCHHeader ncchHeader, BinaryReader reader, BinaryWriter writer) { if (ncchHeader.ExtendedHeaderSizeInBytes > 0) { reader.BaseStream.Seek((ncchHeader.Entry.Offset * ncsdHeader.MediaUnitSize) + 0x200, SeekOrigin.Begin); writer.BaseStream.Seek((ncchHeader.Entry.Offset * ncsdHeader.MediaUnitSize) + 0x200, SeekOrigin.Begin); Console.WriteLine($"Partition {ncchHeader.PartitionNumber} ExeFS: " + (decryptArgs.Encrypt ? "Encrypting" : "Decrypting") + ": ExHeader"); var cipher = CreateAESCipher(ncchHeader.NormalKey2C, ncchHeader.PlainIV, decryptArgs.Encrypt); writer.Write(cipher.ProcessBytes(reader.ReadBytes(Constants.CXTExtendedDataHeaderLength))); writer.Flush(); return(true); } else { Console.WriteLine($"Partition {ncchHeader.PartitionNumber} ExeFS: No Extended Header... Skipping..."); return(false); } }
/// <summary> /// Decrypt the ExeFS, if it exists /// </summary> /// <param name="ncsdHeader">NCSD header representing the 3DS file</param> /// <param name="ncchHeader">NCCH header representing the partition</param> /// <param name="reader">BinaryReader representing the input stream</param> /// <param name="writer">BinaryWriter representing the output stream</param> private void DecryptExeFS(NCSDHeader ncsdHeader, NCCHHeader ncchHeader, BinaryReader reader, BinaryWriter writer) { // If the ExeFS size is 0, we log and return if (ncchHeader.ExeFSSizeInMediaUnits == 0) { Console.WriteLine($"Partition {ncchHeader.PartitionNumber} ExeFS: No Data... Skipping..."); return; } // Decrypt the filename table ProcessExeFSFilenameTable(ncsdHeader, ncchHeader, reader, writer); // For all but the original crypto method, process each of the files in the table if (ncchHeader.Flags.CryptoMethod != CryptoMethod.Original) { ProcessExeFSFileEntries(ncsdHeader, ncchHeader, reader, writer); } // Decrypt the rest of the ExeFS ProcessExeFS(ncsdHeader, ncchHeader, reader, writer); }
private byte[] getNcchAesCounter(NCCHHeader header, NCCHSection type) //Function based on code from ctrtool's source: https://github.com/Relys/Project_CTR { byte[] counter = new byte[16]; if (header.formatVersion == 2 || header.formatVersion == 0) { for (int i = 0; i < 8; ++i) { counter[i] = header.titleId[header.titleId.Length - 1 - i]; } counter[8] = (byte)type; } else if (header.formatVersion == 1) { UInt32 x = 0; switch (type) { case NCCHSection.ExHeader: x = 0x200; //ExHeader is always 0x200 bytes into the NCCH break; case NCCHSection.ExeFS: x = header.exefsOffset * MEDIAUNITSIZE; break; case NCCHSection.RomFS: x = header.romfsOffset * MEDIAUNITSIZE; break; } for (int i = 0; i < 8; ++i) { counter[i] = header.titleId[i]; } counter[12] = (byte)((x >> 24) & 0xFF); counter[13] = (byte)((x >> 16) & 0xFF); counter[14] = (byte)((x >> 8) & 0xFF); counter[15] = (byte)(x & 0xFF); } return(counter); }
/// <summary> /// Get a specific partition header from the partition table /// </summary> /// <param name="ncsdHeader">NCSD header representing the 3DS file</param> /// <param name="reader">BinaryReader representing the input stream</param> /// <param name="partitionNumber">Partition number to attempt to retrieve</param> /// <returns>NCCH header for the partition requested, null on error</returns> private NCCHHeader GetPartitionHeader(NCSDHeader ncsdHeader, BinaryReader reader, int partitionNumber) { if (!ncsdHeader.PartitionsTable[partitionNumber].IsValid()) { Console.WriteLine($"Partition {partitionNumber} Not found... Skipping..."); return(null); } // Seek to the beginning of the NCCH partition reader.BaseStream.Seek((ncsdHeader.PartitionsTable[partitionNumber].Offset * ncsdHeader.MediaUnitSize), SeekOrigin.Begin); NCCHHeader partitionHeader = NCCHHeader.Read(reader, readSignature: true); if (partitionHeader == null) { Console.WriteLine($"Partition {partitionNumber} Unable to read NCCH header"); return(null); } partitionHeader.PartitionNumber = partitionNumber; partitionHeader.Entry = ncsdHeader.PartitionsTable[partitionNumber]; return(partitionHeader); }
/// <summary> /// Process a single partition /// </summary> /// <param name="ncsdHeader">NCSD header representing the 3DS file</param> /// <param name="ncchHeader">NCCH header representing the partition</param> /// <param name="reader">BinaryReader representing the input stream</param> /// <param name="writer">BinaryWriter representing the output stream</param> private void ProcessPartition(NCSDHeader ncsdHeader, NCCHHeader ncchHeader, BinaryReader reader, BinaryWriter writer) { // If we're forcing the operation, tell the user if (decryptArgs.Force) { Console.WriteLine($"Partition {ncchHeader.PartitionNumber} is not verified due to force flag being set."); } // If we're not forcing the operation, check if the 'NoCrypto' bit is set else if (ncchHeader.Flags.PossblyDecrypted ^ decryptArgs.Encrypt) { Console.WriteLine($"Partition {ncchHeader.PartitionNumber}: Already " + (decryptArgs.Encrypt ? "Encrypted" : "Decrypted") + "?..."); return; } // Determine the Keys to be used SetEncryptionKeys(ncsdHeader, ncchHeader); // Process the extended header ProcessExtendedHeader(ncsdHeader, ncchHeader, reader, writer); // If we're encrypting, encrypt the filesystems and update the flags if (decryptArgs.Encrypt) { EncryptExeFS(ncsdHeader, ncchHeader, reader, writer); EncryptRomFS(ncsdHeader, ncchHeader, reader, writer); UpdateEncryptCryptoAndMasks(ncsdHeader, ncchHeader, writer); } // If we're decrypting, decrypt the filesystems and update the flags else { DecryptExeFS(ncsdHeader, ncchHeader, reader, writer); DecryptRomFS(ncsdHeader, ncchHeader, reader, writer); UpdateDecryptCryptoAndMasks(ncsdHeader, ncchHeader, writer); } }
//Function based on code from ctrtool's source: https://github.com/Relys/Project_CTR private byte[] getNcchAesCounter(NCCHHeader header, NCCHSection type) { byte[] counter = new byte[16]; if(header.formatVersion == 2 || header.formatVersion == 0) { for(int i = 0; i < 8; ++i) { counter[i] = header.titleId[header.titleId.Length - 1 - i]; } counter[8] = (byte) type; } else if(header.formatVersion == 1) { UInt32 x = 0; switch(type) { case NCCHSection.ExHeader: x = 0x200; //ExHeader is always 0x200 bytes into the NCCH break; case NCCHSection.ExeFS: x = header.exefsOffset * MEDIAUNITSIZE; break; case NCCHSection.RomFS: x = header.romfsOffset * MEDIAUNITSIZE; break; } for(int i = 0; i < 8; ++i) { counter[i] = header.titleId[i]; } counter[12] = (byte) ((x >> 24) & 0xFF); counter[13] = (byte) ((x >> 16) & 0xFF); counter[14] = (byte) ((x >> 8) & 0xFF); counter[15] = (byte) (x & 0xFF); } return counter; }
private NCCHInfoEntry parseNCCHSection(NCCHHeader header, NCCHSection type, byte[] keyY, bool uses7xCrypto, bool usesSeedCrypto, bool print, string indent) { NCCHInfoEntry entry = new NCCHInfoEntry(); entry.keyY = keyY; entry.reserved = 0x00000000; entry.uses7xcrypto = (UInt32) (uses7xCrypto ? 0x00000001 : 0x00000000); entry.uses9xcrypto = (UInt32) (usesSeedCrypto ? 0x00000001 : 0x00000000); entry.outputname = new byte[112]; string sectionname; uint offset; uint sectionsize; switch(type) { case NCCHSection.ExHeader: sectionname = "ExHeader"; offset = 0x200; //Always 0x200 sectionsize = header.exhdrSize * MEDIAUNITSIZE; break; case NCCHSection.ExeFS: sectionname = "ExeFS"; offset = header.exefsOffset * MEDIAUNITSIZE; sectionsize = header.exefsSize * MEDIAUNITSIZE; break; case NCCHSection.RomFS: sectionname = "RomFS"; offset = header.romfsOffset * MEDIAUNITSIZE; sectionsize = header.romfsSize * MEDIAUNITSIZE; break; default: Console.Error.WriteLine("Illegal NCCH Section type provided!"); Environment.Exit(1); return entry; //Needed to compile } entry.counter = getNcchAesCounter(header, type); entry.titleID = new byte[8]; Array.Copy(header.programId, 0, entry.titleID, 0, 8); //Compute section size in MB, rounding up to the next MB uint sectionMb = sectionsize / BYTES_PER_MEGABYTE; //Rounding down in this step uint remainder = sectionsize % BYTES_PER_MEGABYTE; if(remainder != 0) { ++sectionMb; } entry.size = sectionMb; if(print) { Console.WriteLine(String.Format(indent + "{0} offset: {1:X8}", sectionname, offset)); Console.WriteLine(String.Format(indent + "{0} counter: {1}", sectionname, ToHexString(entry.counter))); Console.WriteLine(String.Format(indent + "{0} Megabytes(rounded up): {1}", sectionname, sectionMb)); } //TODO //return struct.pack('<16s16sIIIIQ', str(counter), str(keyY), sectionMb, 0, usesSeedCrypto, uses7xCrypto, titleId) return entry; }
private List <NCCHInfoEntry> parseNCCH(BinaryReader reader, uint offset = 0, uint idx = 0, string titleID = "", bool standalone = true) { string indent = standalone ? " " : " "; if (standalone) { Console.WriteLine("Parsing NCCH in file " + (reader.BaseStream as FileStream).Name); } else { Console.WriteLine(" Parsing {0} NCCH", GetPartitionName(idx)); } //Read the NCCH header reader.BaseStream.Position = offset; byte[] headerbytes = reader.ReadBytes(Marshal.SizeOf(typeof(NCCHHeader))); NCCHHeader header = StructMarshaling.BytesToStruct <NCCHHeader>(headerbytes); if (titleID.Equals("")) { titleID = ByteArrayTo3DSIdentifier(header.titleId); } //Find keyY byte[] keyY = new byte[16]; Array.Copy(header.signature, 0, keyY, 0, keyY.Length); //Print info if (!standalone) { Console.WriteLine(indent + "NCCH offset: {0:X8}", offset); } Console.WriteLine(indent + "Product code: " + Encoding.ASCII.GetString(header.productCode).TrimEnd((char)0x00)); if (!standalone) { Console.WriteLine(indent + "Partition number: {0}", idx); } Console.WriteLine(indent + "KeyY: {0}", ToHexString(keyY)); Console.WriteLine(indent + "Title ID: {0}", ByteArrayTo3DSIdentifier(header.titleId)); Console.WriteLine(indent + "Format version: {0}", header.formatVersion); bool uses7xCrypto = (header.flags[3] != 0x00); bool usesSeedCrypto = (header.flags[3] == 0x20); if (uses7xCrypto) { Console.WriteLine(indent + "Uses 7.x NCCH crypto"); } if (usesSeedCrypto) { Console.WriteLine(indent + "Uses 9.x SEED crypto"); } Console.WriteLine(); List <NCCHInfoEntry> entries = new List <NCCHInfoEntry>(); NCCHInfoEntry entry; if (header.exhdrSize != 0) { entry = parseNCCHSection(header, NCCHSection.ExHeader, keyY, false, false, true, indent); byte[] namebytes = Encoding.UTF8.GetBytes(genOutName(titleID, GetPartitionName(idx), "exheader")); Array.Copy(namebytes, 0, entry.outputname, 0, namebytes.Length); entries.Add(entry); Console.WriteLine(); } if (header.exefsSize != 0) { //We need generate two xorpads for exefs if it uses 7.x crypto, since only a part of it uses the new crypto. entry = parseNCCHSection(header, NCCHSection.ExeFS, keyY, false, false, true, indent); byte[] namebytes = Encoding.UTF8.GetBytes(genOutName(titleID, GetPartitionName(idx), "exefs_norm")); Array.Copy(namebytes, 0, entry.outputname, 0, namebytes.Length); entries.Add(entry); if (uses7xCrypto) { entry = parseNCCHSection(header, NCCHSection.ExeFS, keyY, uses7xCrypto, usesSeedCrypto, false, indent); namebytes = Encoding.UTF8.GetBytes(genOutName(titleID, GetPartitionName(idx), "exefs_7x")); Array.Copy(namebytes, 0, entry.outputname, 0, namebytes.Length); entries.Add(entry); } Console.WriteLine(); } if (header.romfsSize != 0) { entry = parseNCCHSection(header, NCCHSection.RomFS, keyY, uses7xCrypto, usesSeedCrypto, true, indent); byte[] namebytes = Encoding.UTF8.GetBytes(genOutName(titleID, GetPartitionName(idx), "romfs")); Array.Copy(namebytes, 0, entry.outputname, 0, namebytes.Length); entries.Add(entry); Console.WriteLine(); } Console.WriteLine(); return(entries); }
private NCCHInfoEntry parseNCCHSection(NCCHHeader header, NCCHSection type, byte[] keyY, bool uses7xCrypto, bool usesSeedCrypto, bool print, string indent) { NCCHInfoEntry entry = new NCCHInfoEntry(); entry.keyY = keyY; entry.reserved = 0x00000000; entry.uses7xcrypto = (UInt32)(uses7xCrypto ? 0x00000001 : 0x00000000); entry.uses9xcrypto = (UInt32)(usesSeedCrypto ? 0x00000001 : 0x00000000); entry.outputname = new byte[112]; string sectionname; uint offset; uint sectionsize; switch (type) { case NCCHSection.ExHeader: sectionname = "ExHeader"; offset = 0x200; //Always 0x200 sectionsize = header.exhdrSize * MEDIAUNITSIZE; break; case NCCHSection.ExeFS: sectionname = "ExeFS"; offset = header.exefsOffset * MEDIAUNITSIZE; sectionsize = header.exefsSize * MEDIAUNITSIZE; break; case NCCHSection.RomFS: sectionname = "RomFS"; offset = header.romfsOffset * MEDIAUNITSIZE; sectionsize = header.romfsSize * MEDIAUNITSIZE; break; default: Console.Error.WriteLine("Illegal NCCH Section type provided!"); Environment.Exit(1); return(entry); //Needed to compile } entry.counter = getNcchAesCounter(header, type); entry.titleID = new byte[8]; Array.Copy(header.programId, 0, entry.titleID, 0, 8); //Compute section size in MB, rounding up to the next MB uint sectionMb = sectionsize / BYTES_PER_MEGABYTE; //Rounding down in this step uint remainder = sectionsize % BYTES_PER_MEGABYTE; if (remainder != 0) { ++sectionMb; } entry.size = sectionMb; if (print) { Console.WriteLine(String.Format(indent + "{0} offset: {1:X8}", sectionname, offset)); Console.WriteLine(String.Format(indent + "{0} counter: {1}", sectionname, ToHexString(entry.counter))); Console.WriteLine(String.Format(indent + "{0} Megabytes(rounded up): {1}", sectionname, sectionMb)); } //TODO //return struct.pack('<16s16sIIIIQ', str(counter), str(keyY), sectionMb, 0, usesSeedCrypto, uses7xCrypto, titleId) return(entry); }
/// <summary> /// Determine the set of keys to be used for encryption or decryption /// </summary> /// <param name="ncsdHeader">NCSD header representing the 3DS file</param> /// <param name="ncchHeader">NCCH header representing the partition</param> private void SetEncryptionKeys(NCSDHeader ncsdHeader, NCCHHeader ncchHeader) { ncchHeader.KeyX = 0; ncchHeader.KeyX2C = decryptArgs.Development ? decryptArgs.DevKeyX0x2C : decryptArgs.KeyX0x2C; // Backup headers can't have a KeyY value set if (ncchHeader.RSA2048Signature != null) { ncchHeader.KeyY = new BigInteger(ncchHeader.RSA2048Signature.Take(16).Reverse().ToArray()); } else { ncchHeader.KeyY = new BigInteger(0); } ncchHeader.NormalKey = 0; ncchHeader.NormalKey2C = RotateLeft((RotateLeft(ncchHeader.KeyX2C, 2, 128) ^ ncchHeader.KeyY) + decryptArgs.AESHardwareConstant, 87, 128); // Set the header to use based on mode BitMasks masks; CryptoMethod method; if (decryptArgs.Encrypt) { masks = ncsdHeader.BackupHeader.Flags.BitMasks; method = ncsdHeader.BackupHeader.Flags.CryptoMethod; } else { masks = ncchHeader.Flags.BitMasks; method = ncchHeader.Flags.CryptoMethod; } if (masks.HasFlag(BitMasks.FixedCryptoKey)) { ncchHeader.NormalKey = 0x00; ncchHeader.NormalKey2C = 0x00; Console.WriteLine("Encryption Method: Zero Key"); } else { if (method == CryptoMethod.Original) { ncchHeader.KeyX = decryptArgs.Development ? decryptArgs.DevKeyX0x2C : decryptArgs.KeyX0x2C; Console.WriteLine("Encryption Method: Key 0x2C"); } else if (method == CryptoMethod.Seven) { ncchHeader.KeyX = decryptArgs.Development ? decryptArgs.DevKeyX0x25 : decryptArgs.KeyX0x25; Console.WriteLine("Encryption Method: Key 0x25"); } else if (method == CryptoMethod.NineThree) { ncchHeader.KeyX = decryptArgs.Development ? decryptArgs.DevKeyX0x18 : decryptArgs.KeyX0x18; Console.WriteLine("Encryption Method: Key 0x18"); } else if (method == CryptoMethod.NineSix) { ncchHeader.KeyX = decryptArgs.Development ? decryptArgs.DevKeyX0x1B : decryptArgs.KeyX0x1B; Console.WriteLine("Encryption Method: Key 0x1B"); } ncchHeader.NormalKey = RotateLeft((RotateLeft(ncchHeader.KeyX, 2, 128) ^ ncchHeader.KeyY) + decryptArgs.AESHardwareConstant, 87, 128); } }