From b34fc37856f0bcea0d9d2763c447a897ccc0e9ca Mon Sep 17 00:00:00 2001 From: David Korth Date: Tue, 6 Dec 2016 20:33:57 -0500 Subject: [PATCH] Import of Wii Ultimate Unscrubber v0.3.2. Source: https://gbatemp.net/threads/wii-ultimate-unscrubber.452110/ v0.3.2 changes * Added support for complex partition tables (the only known game using it "Super Smash Bros. Brawl") * Added support for unscrubbing ISOs with strange UPDATE padding, if the padding was not removed by scrubbing tools. (more info below) v0.3.1 changes * Added support for korean ISOs * Added support for multi-disc games (the only known game "Dragon Quest X: Mezameshi Itsutsu no Shuzoku Online") v0.3 changes * Unscrubber now takes into account "Keep Headers" option in wiiscrubber. v0.2 changes * always try to unscrub, even if the ISO does not seem to be scrubbed. * added "decrypt" switch, to decrypt the ISO instead of unscrub. --- JunkStream.cs | 127 ++++++++++++++++ Program.cs | 290 +++++++++++++++++++++++++++++++++++++ Properties/AssemblyInfo.cs | 36 +++++ UltimateUnscrubber.csproj | 60 ++++++++ UltimateUnscrubber.sln | 20 +++ 5 files changed, 533 insertions(+) create mode 100644 JunkStream.cs create mode 100644 Program.cs create mode 100644 Properties/AssemblyInfo.cs create mode 100644 UltimateUnscrubber.csproj create mode 100644 UltimateUnscrubber.sln diff --git a/JunkStream.cs b/JunkStream.cs new file mode 100644 index 0000000..84be73d --- /dev/null +++ b/JunkStream.cs @@ -0,0 +1,127 @@ +using System; +using System.Text; + +namespace UltimateUnscrubber +{ + public class JunkStream + { + public long Position; + bool hashed; + byte[] id; + byte[] junk = new byte[0x40000]; + int current_junk_index = -1; + uint[] numArray = new uint[0x824]; + long end_offset; + int disc; + + public JunkStream(string ID, int disc, bool hashed, long end_offset) + { + this.hashed = hashed; + this.end_offset = end_offset; + this.disc = disc; + id = Encoding.ASCII.GetBytes(ID); + } + + public void Read(byte[] data, int offset, int size) + { + while (size > 0) + { + var writing_hash = hashed && Position % 0x8000 < 0x400; + var to_write = (writing_hash ? 0x400 : 0x8000) - (int)(Position % 0x8000); + to_write = Math.Min(to_write, size); + if (writing_hash) Array.Clear(data, offset, to_write); + else + { + var unhashed_offset = Unhash(Position); + var junk_index = (int)(unhashed_offset / 0x40000); + var junk_offset = (int)(unhashed_offset % 0x40000); + if (current_junk_index != junk_index) + { + current_junk_index = junk_index; + GetJunkBlock((uint)current_junk_index, id, (byte)disc, junk); + var junk_end_offset = (int)Math.Min(Unhash(end_offset) / 0x8000 * 0x8000 - (unhashed_offset-junk_offset), 0x40000); + junk_end_offset = Math.Max(junk_end_offset, 0); + Array.Clear(junk, junk_end_offset, 0x40000 - junk_end_offset); + } + to_write = Math.Min(to_write, 0x40000 - junk_offset); + Array.Copy(junk, junk_offset, data, offset, to_write); + } + offset += to_write; + size -= to_write; + Position += to_write; + } + } + + long Unhash(long offset) + { + if (hashed) return offset / 0x8000 * 0x7c00 + Math.Max(offset % 0x8000 - 0x400, 0); + else return offset; + } + + void GetJunkBlock(uint block, byte[] ID, byte disc, byte[] buffer) + { + Array.Clear(numArray, 0, numArray.Length); + int num2 = 0; + uint sample = 0; + block = (block * 8) * 0x1ef29123; + for (var i = 0; i < 0x40000; i += 4) + { + if ((i & 0x7fff) == 0) + { + sample = (uint)(((((ID[2] << 8) | ID[1]) << 0x10) | ((ID[3] + ID[2]) << 8)) | (ID[0] + ID[1])); + sample = ((sample ^ disc) * 0x260bcd5) ^ block; + a10002710(sample, numArray); + num2 = 520; + block += 0x1ef29123; + } + num2++; + if (num2 == 0x209) + { + a100026e0(numArray); + num2 = 0; + } + buffer[i] = (byte)(numArray[num2] >> 0x18); + buffer[i + 1] = (byte)(numArray[num2] >> 0x12); + buffer[i + 2] = (byte)(numArray[num2] >> 8); + buffer[i + 3] = (byte)numArray[num2]; + } + } + void a10002710(uint sample, uint[] buffer) + { + int num2; + uint num = 0; + for (num2 = 0; num2 != 0x11; num2++) + { + for (int i = 0; i < 0x20; i++) + { + sample *= 0x5d588b65; + num = (num >> 1) | (++sample & 0x80000000); + } + buffer[num2] = num; + } + buffer[0x10] ^= (buffer[0] >> 9) ^ (buffer[0x10] << 0x17); + for (num2 = 1; num2 != 0x1f9; num2++) + { + buffer[num2 + 0x10] = ((buffer[num2 - 1] << 0x17) ^ (buffer[num2] >> 9)) ^ buffer[num2 + 15]; + } + for (num2 = 0; num2 < 3; num2++) + { + a100026e0(buffer); + } + } + void a100026e0(uint[] buffer) + { + int index = 0; + while (index != 0x20) + { + buffer[index] ^= buffer[index + 0x1e9]; + index++; + } + while (index != 0x209) + { + buffer[index] ^= buffer[index - 0x20]; + index++; + } + } + } +} diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..832f15d --- /dev/null +++ b/Program.cs @@ -0,0 +1,290 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; + +namespace UltimateUnscrubber +{ + class Program + { + static FileStream source; + static FileStream dest; + static bool scrubbed; + static bool decrypted; + static bool original; + static byte[] buffer = new byte[0x8000 * 8 * 8]; + static SHA1 hasher = SHA1.Create(); + static Aes aes = Aes.Create(); + static byte[] IV = new byte[16]; + static bool do_decrypt; + + enum PartitionType { DATA, UPDATE, CHANNEL }; + enum TransformType { Encrypt, Decrypt }; + + static Program() + { + aes.Padding = PaddingMode.None; + } + + static void Main(string[] args) + { + Console.WriteLine("Wii Ultimate Unscrubber v0.3.2\n"); + if (args.Length < 1) + { + Console.WriteLine("Drop the ISO on this exe\n\nOr use:"); + Console.WriteLine("UnltimateUnscrubber [decrypt] "); + Console.ReadKey(); + return; + } + var source_path = args.Length == 1 ? args[0]: args[1]; + Console.WriteLine(source_path+"\n"); + if (!File.Exists(source_path)) + { + Console.WriteLine(source_path + " does not exist"); + Console.ReadKey(); + return; + } + source = File.OpenRead(source_path); + var header = new byte[0x50000]; + if(source.Length >= 0x50000) Read(header.Length); + if (source.Length < 0x50000 || ReadBigEndian(0x18) != 0x5D1C9EA3) + { + Console.WriteLine("Not a Wii ISO"); + Console.ReadKey(); + return; + } + Array.Copy(buffer, 0, header, 0, header.Length); + var partitions_count = (int)ReadBigEndian(0x40000); + var table_offset = (int)ReadBigEndian(0x40004) * 4; + source.Position = (long)ReadBigEndian(table_offset) * 4 + 0x20000; + Read(0x8000); + decrypted = BinaryIsUniform(buffer, 0x26C, 20, 0); + source.Position = source.Length - 16; + Read(16); + scrubbed = BinaryIsUniform(buffer, 0, 16, buffer[0]); + original = !scrubbed && !decrypted; + if (args.Length > 2) + { + Syntaxerror(); + return; + } + if (args.Length == 2) switch (args[0]) + { + case "decrypt": + do_decrypt = true; + break; + default: + Syntaxerror(); + return; + } + + if (!scrubbed && !do_decrypt) Console.WriteLine("The ISO doesn't seem to be scrubbed.\nTrying to unscrub anyway...\n"); + var new_file_path = source_path + ".new"; + dest = File.Create(new_file_path); + CopyUpTo(header.Length); + var disc_id = Encoding.ASCII.GetString(header, 0, 4); + var disc_number = (int)header[6]; + var junk = new JunkStream(disc_id, disc_number, false, source.Length); + var previous_data_end = (long)0x50000; + for (var partition_table_index = 0; partition_table_index < 4; partition_table_index++) + { + header.CopyTo(buffer, 0); + partitions_count = (int)ReadBigEndian(0x40000 + partition_table_index * 8); + if (partitions_count == 0) continue; + table_offset = (int)ReadBigEndian(0x40000 + partition_table_index * 8 + 4) * 4; + for (var partition_index = 0; partition_index < partitions_count; partition_index++) + { + header.CopyTo(buffer, 0); + var partition_offset = (long)ReadBigEndian(table_offset + partition_index * 8) * 4; + var partition_type = (PartitionType)ReadBigEndian(table_offset + partition_index * 8 + 4); + if (partition_table_index == 0 && partition_index == 0 && partition_type != PartitionType.UPDATE) + Console.WriteLine("!!! UPDATE partition is missing. Unscrub will probably fail. !!!\nTrying to unscrub anyway...\n"); + FillSpace(previous_data_end, partition_offset, junk); + Console.WriteLine((do_decrypt ? "decrypting " : "restoring ") + (PartitionType)partition_type + " partition"); + SetPosition(partition_offset); + TransformPartition(); + previous_data_end = source.Position; + } + } + Console.WriteLine("writing last data..."); + FillSpace(previous_data_end, source.Length, junk); + source.Close(); + dest.Close(); + Console.WriteLine("renaming"); + Console.WriteLine(Path.GetFileName(source_path) + " -> " + Path.GetFileName(source_path + ".old")); + if (File.Exists(source_path + ".old")) File.Delete(source_path + ".old"); + File.Move(source_path, source_path + ".old"); + Console.WriteLine(Path.GetFileName(new_file_path) + " -> " + Path.GetFileName(source_path)); + File.Move(new_file_path, source_path); + Console.WriteLine("FINISHED "); + Console.ReadKey(); + } + static void FillSpace(long previous_data_end, long current_offset, JunkStream junk) + { + source.Position = previous_data_end; + Read(32); + if (do_decrypt || current_offset == 0xf800000 && !BinaryIsUniform(buffer, 0, 32, buffer[0])) + { + SetPosition(previous_data_end); + CopyUpTo(current_offset); + } + else if (current_offset != 0xf800000 && current_offset > previous_data_end + 28) + { + SetPosition(previous_data_end + 28); + junk.Position = source.Position; + while (source.Position < current_offset) + { + var to_write = buffer.Length; + to_write = (int)Math.Min(to_write, current_offset - source.Position); + junk.Read(buffer, 0, to_write); + source.Position += to_write; + Write(); + } + } + else SetPosition(current_offset); + } + static void Syntaxerror() + { + Console.WriteLine("Syntax error\n\nUsage\n"); + Console.WriteLine("UnltimateUnscrubber [decrypt] "); + Console.ReadKey(); + } + static void TransformPartition() + { + Read(0x20000); + Write(); + var partition_key = new byte[16]; + Array.Copy(buffer, 0x1bf, partition_key, 0, 16); + var korean = buffer[0x1f1] == 1; + aes.Key = korean ? ParseHex("63b82bb4f4614e2e13f2fefbba4c9b7e") : ParseHex("ebe42a225e8593e448d9c5457381aaf7"); + Array.Clear(IV, 0, 16); + Array.Copy(buffer, 0x1dc, IV, 0, 8); + aes.IV = IV; + using(var cryptor = aes.CreateDecryptor()) cryptor.TransformBlock(partition_key, 0, 16, partition_key, 0); + aes.Key = partition_key; + var size = (long)ReadBigEndian(0x2bc) * 4; + Read(0x8000); + source.Position -= 0x8000; + if (!decrypted) TransformBlock(0, TransformType.Decrypt); + var partition_id = Encoding.ASCII.GetString(buffer, 0x400, 4); + var disc = (int)buffer[0x406]; + var data_offset = source.Position; + var junk = new JunkStream(partition_id, disc, true, size); + var last_progress = 0; + while (source.Position < data_offset + size) + { + Array.Clear(buffer, 0, buffer.Length); + var to_read = 0x8000 * 8 * 8; + to_read = (int)Math.Min(to_read, data_offset + size - source.Position); + Read(to_read); + for (var block_index = 0; block_index < to_read / 0x8000; block_index++) + { + if (!decrypted) TransformBlock(block_index, TransformType.Decrypt); + if (!IsData(block_index)) + { + junk.Position = dest.Position - data_offset + block_index * 0x8000; + junk.Read(buffer, block_index * 0x8000, 0x8000); + } + } + for (var block_index = 0; block_index < 64; block_index++) + { + for (int i = 0; i < 31; i++) hasher.ComputeHash(buffer, block_index * 0x8000 + (i + 1) * 0x400, 0x400).CopyTo(buffer, block_index * 0x8000 + i * 20); + var subgroup_index = block_index / 8; + var block_in_subgroup_index = block_index % 8; + var h1 = hasher.ComputeHash(buffer, block_index * 0x8000, 31 * 20); + for (int block_in_subgroup_index2 = 0; block_in_subgroup_index2 < 8; block_in_subgroup_index2++) + { + var block_index2 = subgroup_index * 8 + block_in_subgroup_index2; + h1.CopyTo(buffer, block_index2 * 0x8000 + 0x280 + block_in_subgroup_index * 20); + } + if (block_in_subgroup_index == 7) + { + var h2 = hasher.ComputeHash(buffer, block_index * 0x8000 + 0x280, 8 * 20); + for (int block_index2 = 0; block_index2 < 64; block_index2++) + h2.CopyTo(buffer, block_index2 * 0x8000 + 0x340 + subgroup_index * 20); + } + } + if (!do_decrypt) for (var block_index = 0; block_index < 64; block_index++) TransformBlock(block_index, TransformType.Encrypt); + Write(); + var progress = (int)(100 - (data_offset + size - source.Position) / (float)size * 100); + if (progress >= last_progress + 5) + { + Console.WriteLine(progress.ToString() + "%"); + last_progress = progress; + } + } + } + + static bool IsData(int block_index) + { + for (int i = 0; i < 31; i++) + if (!BinariesAreEqual(hasher.ComputeHash(buffer, block_index * 0x8000 + (i + 1) * 0x400, 0x400), 0, buffer, block_index * 0x8000 + i * 20, 20)) + return false; + return true; + } + static void SetPosition(long position) + { + source.Position = position; + dest.Position = position; + } + static void TransformBlock(int block_index, TransformType transform_type) + { + Array.Clear(IV, 0, 16); + aes.IV = IV; + if (transform_type == TransformType.Decrypt) Array.Copy(buffer, block_index * 0x8000 + 0x3d0, IV, 0, 16); + using (var cryptor = transform_type == TransformType.Decrypt ? aes.CreateDecryptor() : aes.CreateEncryptor()) + cryptor.TransformBlock(buffer, block_index * 0x8000, 0x400, buffer, block_index * 0x8000); + if (transform_type == TransformType.Encrypt) Array.Copy(buffer, block_index * 0x8000 + 0x3d0, IV, 0, 16); + aes.IV = IV; + using (var cryptor = transform_type == TransformType.Decrypt ? aes.CreateDecryptor() : aes.CreateEncryptor()) + cryptor.TransformBlock(buffer, block_index * 0x8000 + 0x400, 0x7c00, buffer, block_index * 0x8000 + 0x400); + } + static void CopyUpTo(long position) + { + source.Position = dest.Position; + while (position - source.Position > 0) + { + var to_read = buffer.Length; + to_read = (int)Math.Min(to_read, position - source.Position); + Read(to_read); + Write(); + } + } + static void Read(int amount) + { + if (source.Read(buffer, 0, amount) != amount) throw new Exception(); + } + static void Write() + { + dest.Write(buffer, 0, (int)(source.Position - dest.Position)); + } + static bool BinariesAreEqual(byte[] d1, int offset1, byte[] d2, int offset2, int size) + { + for (int i = 0; i < size; i++) if (d1[i + offset1] != d2[i + offset2]) return false; + return true; + } + static bool BinaryIsUniform(byte[] data, int offset, int size, byte value) + { + for (int i = 0; i < size; i++) if (data[i + offset] != value) return false; + return true; + } + static uint ReadBigEndian(int offset) + { + Array.Reverse(buffer, offset, 4); + var num = BitConverter.ToUInt32(buffer, offset); + Array.Reverse(buffer, offset, 4); + return num; + } + static byte[] ParseHex(string hex) + { + hex = hex.Replace(" ", ""); + if (hex.Length % 2 != 0) throw new Exception(); + var buf = new byte[hex.Length / 2]; + for (int i = 0; i < buf.Length; i++) + { + buf[i] = byte.Parse(hex.Substring(i * 2, 2), System.Globalization.NumberStyles.HexNumber); + } + return buf; + } + } +} diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..139041b --- /dev/null +++ b/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("UltimateUnscrubber")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Microsoft")] +[assembly: AssemblyProduct("UltimateUnscrubber")] +[assembly: AssemblyCopyright("Copyright © Microsoft 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("86eea818-c97b-4130-9941-88c0125bb6b5")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/UltimateUnscrubber.csproj b/UltimateUnscrubber.csproj new file mode 100644 index 0000000..2e7b569 --- /dev/null +++ b/UltimateUnscrubber.csproj @@ -0,0 +1,60 @@ + + + + Debug + AnyCPU + 9.0.21022 + 2.0 + {59E28F83-0A53-492A-A678-DF31BC5C4263} + Exe + Properties + UltimateUnscrubber + UltimateUnscrubber + v3.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + 3.5 + + + 3.5 + + + 3.5 + + + + + + + + + + + + \ No newline at end of file diff --git a/UltimateUnscrubber.sln b/UltimateUnscrubber.sln new file mode 100644 index 0000000..681def0 --- /dev/null +++ b/UltimateUnscrubber.sln @@ -0,0 +1,20 @@ + +Microsoft Visual Studio Solution File, Format Version 10.00 +# Visual Studio 2008 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UltimateUnscrubber", "UltimateUnscrubber.csproj", "{59E28F83-0A53-492A-A678-DF31BC5C4263}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {59E28F83-0A53-492A-A678-DF31BC5C4263}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {59E28F83-0A53-492A-A678-DF31BC5C4263}.Debug|Any CPU.Build.0 = Debug|Any CPU + {59E28F83-0A53-492A-A678-DF31BC5C4263}.Release|Any CPU.ActiveCfg = Release|Any CPU + {59E28F83-0A53-492A-A678-DF31BC5C4263}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal