diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..75a76ed --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ + +*.suo +*.user +bin +obj +_Resharper.* +xextool.exe \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..b98e439 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "Upstream/Cecil"] + path = Upstream/Cecil + url = https://github.com/jbevain/cecil.git diff --git a/AssemblyRewriter.cs b/AssemblyRewriter.cs new file mode 100644 index 0000000..d49b4d2 --- /dev/null +++ b/AssemblyRewriter.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace XexDecryptor { + public class AssemblyRewriter { + public const string AssemblyNameMscorlib = "mscorlib, Version=3.5.0.0, Culture=neutral, PublicKeyToken=1c9e259686f921e0"; + public const string AssemblyNameXnaFramework = "Microsoft.Xna.Framework, Version=3.1.0.0, Culture=neutral, PublicKeyToken=51c3bfb2db46012c"; + + // The XBox 360 versions of MS assemblies have different versions and public key tokens. + // We need to find any references to them and fix them to point to the Win32 versions. + public static readonly Dictionary AssemblyNameReplacements = new Dictionary { + // XNA 3.1 references + + {AssemblyNameMscorlib, + "mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"}, + {"System, Version=3.5.0.0, Culture=neutral, PublicKeyToken=1c9e259686f921e0", + "System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"}, + {"System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=1c9e259686f921e0", + "System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"}, + {"System.Xml.Linq, Version=3.5.0.0, Culture=neutral, PublicKeyToken=1c9e259686f921e0", + "System.Xml.Linq, Version=3.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"}, + {AssemblyNameXnaFramework, + "Microsoft.Xna.Framework, Version=3.1.0.0, Culture=neutral, PublicKeyToken=6d5c3888ef60e27d"}, + {"Microsoft.Xna.Framework.Game, Version=3.1.0.0, Culture=neutral, PublicKeyToken=51c3bfb2db46012c", + "Microsoft.Xna.Framework.Game, Version=3.1.0.0, Culture=neutral, PublicKeyToken=6d5c3888ef60e27d"}, + + // TODO: Add XNA 4.0 references + }; + + private static void KillCallInstruction (MethodBody body, MethodReference invokeTarget, ref int i, ref int c, bool replaceReturnValue) { + // Patch out the call instruction. + int stackEntriesToPop = invokeTarget.Parameters.Count + (invokeTarget.HasThis ? 1 : 0); + body.Instructions.RemoveAt(i); + + c += stackEntriesToPop; + + int insertionPosition = i; + while (stackEntriesToPop > 0) { + body.Instructions.Insert(insertionPosition, Instruction.Create(OpCodes.Pop)); + stackEntriesToPop--; + insertionPosition += 1; + } + + if (!replaceReturnValue || (invokeTarget.ReturnType.FullName == "System.Void")) { + c -= 1; + i = insertionPosition; + } else { + body.Instructions.Insert(insertionPosition, Instruction.Create(OpCodes.Ldnull)); + i = insertionPosition + 1; + } + } + + public static void Rewrite (string executablePath) { + var resolver = new RewriterAssemblyResolver(); + var readerParameters = new ReaderParameters { + AssemblyResolver = resolver, + ReadingMode = ReadingMode.Immediate, + ReadSymbols = false + }; + AssemblyDefinition asm; + try { + asm = Mono.Cecil.AssemblyDefinition.ReadAssembly( + executablePath, readerParameters + ); + } catch (Exception) { + Console.WriteLine("{0} isn't a managed assembly. Not rewriting.", executablePath); + return; + } + + var asmCorlib = resolver.Resolve(AssemblyNameMscorlib, readerParameters); + var asmFramework = resolver.Resolve(AssemblyNameXnaFramework, readerParameters); + + foreach (var module in asm.Modules) { + // Force 32-bit x86 + module.Architecture = TargetArchitecture.I386; + module.Attributes = ((module.Attributes & ~ModuleAttributes.Preferred32Bit) & ~ModuleAttributes.StrongNameSigned) + | ModuleAttributes.Required32Bit; + + // The main module will be console, switch it to GUI + if (module.Kind == ModuleKind.Console) + module.Kind = ModuleKind.Windows; + + for (var i = 0; i < module.AssemblyReferences.Count; i++) { + var ar = module.AssemblyReferences[i]; + Debug.WriteLine(ar.FullName); + + string newFullName; + if (AssemblyNameReplacements.TryGetValue(ar.FullName, out newFullName)) { + var newReference = AssemblyNameReference.Parse(newFullName); + module.AssemblyReferences[i] = newReference; + Console.WriteLine("{0} -> {1}", ar.Name, newFullName); + } else { + Console.WriteLine("Ignoring {0}", ar.Name); + } + } + + for (var i = 0; i < module.Resources.Count; i++) { + var rsrc = module.Resources[i]; + + switch (rsrc.Name) { + case "Microsoft.Xna.Framework.RuntimeProfile": + // FIXME: Detect version of executable and pick correct windows profile + module.Resources[i] = new EmbeddedResource( + rsrc.Name, rsrc.Attributes, + Encoding.ASCII.GetBytes("Windows.v3.1") + ); + + break; + default: + break; + } + } + + foreach (var type in module.GetTypes()) { + string qualifiedName; + + foreach (var method in type.Methods) { + if (!method.HasBody) + continue; + + var body = method.Body; + + // Patch out particular method calls. + for (int i = 0, c = body.Instructions.Count; i < c; i++) { + var instruction = body.Instructions[i]; + MethodReference invokeTarget = null; + + switch (instruction.OpCode.Code) { + case Code.Callvirt: + case Code.Call: + invokeTarget = instruction.Operand as MethodReference; + break; + default: + continue; + } + + if (invokeTarget == null) + continue; + + qualifiedName = invokeTarget.DeclaringType.FullName + "::" + invokeTarget.Name; + + switch (qualifiedName) { + + // XBox 360 only. + case "System.Threading.Thread::SetProcessorAffinity": + case "Microsoft.Xna.Framework.GamerServices.GamerPresence::set_PresenceMode": + KillCallInstruction(body, invokeTarget, ref i, ref c, true); + break; + + // Force windowed mode. + case "Microsoft.Xna.Framework.GraphicsDeviceManager::set_IsFullScreen": + var ldc = body.Instructions[i - 1]; + if (ldc.OpCode.Code == Code.Ldc_I4_1) { + body.Instructions[i - 1] = Instruction.Create( + OpCodes.Ldc_I4_0 + ); + } + + break; + default: + continue; + } + } + + } + } + } + + var writerParameters = new WriterParameters { + WriteSymbols = false + }; + asm.Write(executablePath, writerParameters); + } + } + + public class RewriterAssemblyResolver : BaseAssemblyResolver { + public readonly Dictionary Cache = new Dictionary(); + + public override AssemblyDefinition Resolve (AssemblyNameReference name, ReaderParameters parameters) { + var key = name.FullName; + AssemblyDefinition result; + + string newKey; + if (AssemblyRewriter.AssemblyNameReplacements.TryGetValue(key, out newKey)) + key = newKey; + else + Debugger.Break(); + + if (Cache.TryGetValue(key, out result)) + return result; + + Cache[key] = result = base.Resolve( + AssemblyNameReference.Parse(key), parameters + ); + return result; + } + } +} diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..9239b02 --- /dev/null +++ b/Program.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace XexDecryptor { + public static class Program { + public static void Abort (string format, params object[] args) { + Console.Error.WriteLine(format, args); + Environment.Exit(1); + } + + public static int? FindByteString (byte[] bytes, int startOffset, int? limit, byte[] searchString) { + int i = startOffset; + int end = limit.GetValueOrDefault(bytes.Length - i) + i; + + int matchStart = 0, matchPos = 0; + + while (i < end) { + var current = bytes[i]; + + if (current != searchString[matchPos]) { + if (matchPos != 0) { + matchPos = 0; + continue; + } + } else { + if (matchPos == 0) + matchStart = i; + + matchPos += 1; + if (matchPos == searchString.Length) + return matchStart; + } + + i++; + } + + return 0; + } + + public static int? FindExecutableHeader (byte[] bytes, int startOffset) { + int? offset = FindByteString(bytes, startOffset, null, new byte[] { 0x4D, 0x5A }); + if (!offset.HasValue) + return null; + + int? offset2 = FindByteString(bytes, offset.Value + 2, 512, new byte[] { 0x50, 0x45, 0x00, 0x00 }); + if (!offset2.HasValue) + return null; + + return offset; + } + + public static void Main (string[] args) { + if (args.Length < 1) { + Abort("Usage: XexDecryptor [xex filenames]"); + } + + var filenames = new List(); + foreach (var filename in args) { + if (filename.IndexOfAny(new char[] { '*', '?' }) < 0) { + filenames.Add(filename); + } else { + var dirname = Path.GetDirectoryName(filename); + if (String.IsNullOrWhiteSpace(dirname)) + dirname = Environment.CurrentDirectory; + + filenames.AddRange(Directory.GetFiles(dirname, Path.GetFileName(filename))); + } + } + + foreach (var sourceFile in filenames) { + Console.WriteLine("Decrypting {0}...", sourceFile); + + if (!File.Exists(sourceFile)) { + Abort("File not found: {0}", sourceFile); + } + + string outputFile = Path.GetFullPath(sourceFile); + if (outputFile.ToLower().EndsWith(".xex")) { + outputFile = Path.Combine( + Path.GetDirectoryName(outputFile), + Path.GetFileNameWithoutExtension(outputFile) + ); + } else { + outputFile += ".decrypted"; + } + + var tempFilePath = Path.GetTempFileName(); + File.Delete(tempFilePath); + + string stdError; + byte[] stdOut; + + Util.RunProcess( + "xextool.exe", + String.Format( + "-c u -e u -o \"{0}\" \"{1}\"", + tempFilePath, + sourceFile + ), + null, out stdError, out stdOut + ); + + if (!String.IsNullOrWhiteSpace(stdError)) { + File.Delete(tempFilePath); + Abort("XexTool reported error: {0}", stdError); + } + + var xexBytes = File.ReadAllBytes(tempFilePath); + File.Delete(tempFilePath); + + var firstHeader = FindExecutableHeader(xexBytes, 0); + if (!firstHeader.HasValue) { + Abort("File is not a valid executable."); + } + + var secondHeader = FindExecutableHeader(xexBytes, firstHeader.Value + 128); + if (!secondHeader.HasValue) { + Abort("File does not contain an embedded executable."); + } + + Console.Write("Extracting to '{0}'... ", outputFile); + using (var fs = File.OpenWrite(outputFile)) { + fs.Write(xexBytes, secondHeader.Value, xexBytes.Length - secondHeader.Value); + } + Console.WriteLine("done."); + + Console.WriteLine("Rewriting assembly... "); + AssemblyRewriter.Rewrite(outputFile); + } + + Console.WriteLine("{0} assemblies processed.", filenames.Count); + } + } +} diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..d1ef91a --- /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("XexDecryptor")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("XexDecryptor")] +[assembly: AssemblyCopyright("Copyright © 2012")] +[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("11fd52e2-8757-4f48-9f38-55d250a9fdf7")] + +// 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/Upstream/Cecil b/Upstream/Cecil new file mode 160000 index 0000000..ec8248d --- /dev/null +++ b/Upstream/Cecil @@ -0,0 +1 @@ +Subproject commit ec8248dc5a39f76edf144d0937c866e68608312a diff --git a/Util.cs b/Util.cs new file mode 100644 index 0000000..72704a3 --- /dev/null +++ b/Util.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; + +namespace XexDecryptor { + public static class Util { + public static byte[] ReadEntireStream (Stream stream) { + var result = new List(); + var buffer = new byte[32767]; + + while (true) { + var bytesRead = stream.Read(buffer, 0, buffer.Length); + if (bytesRead < buffer.Length) { + + if (bytesRead > 0) { + result.Capacity = result.Count + bytesRead; + result.AddRange(buffer.Take(bytesRead)); + } + + if (bytesRead <= 0) + break; + } else { + result.AddRange(buffer); + } + } + + return result.ToArray(); + } + + public static void RunProcess (string filename, string parameters, byte[] stdin, out string stderr, out byte[] stdout) { + var psi = new ProcessStartInfo(filename, parameters); + + psi.WorkingDirectory = Path.GetDirectoryName(filename); + psi.UseShellExecute = false; + psi.RedirectStandardInput = true; + psi.RedirectStandardError = true; + psi.RedirectStandardOutput = true; + + using (var process = Process.Start(psi)) { + var stdinStream = process.StandardInput.BaseStream; + var stderrStream = process.StandardError.BaseStream; + + if (stdin != null) { + ThreadPool.QueueUserWorkItem( + (_) => { + if (stdin != null) { + stdinStream.Write( + stdin, 0, stdin.Length + ); + stdinStream.Flush(); + } + + stdinStream.Close(); + }, null + ); + } + + var temp = new string[1] { null }; + ThreadPool.QueueUserWorkItem( + (_) => { + var text = Encoding.ASCII.GetString(ReadEntireStream(stderrStream)); + temp[0] = text; + }, null + ); + + stdout = ReadEntireStream(process.StandardOutput.BaseStream); + + process.WaitForExit(); + stderr = temp[0]; + + process.Close(); + } + } + } +} diff --git a/XexDecryptor.csproj b/XexDecryptor.csproj new file mode 100644 index 0000000..553d78b --- /dev/null +++ b/XexDecryptor.csproj @@ -0,0 +1,70 @@ + + + + Debug + x86 + 8.0.30703 + 2.0 + {42FC16C2-7200-4576-9504-C0EFC58B9DDD} + Exe + Properties + XexDecryptor + XexDecryptor + v4.0 + Client + 512 + + + x86 + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + x86 + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + Always + + + + + {D68133BD-1E63-496E-9EDE-4FBDBF77B486} + Mono.Cecil + + + + + \ No newline at end of file diff --git a/XexDecryptor.sln b/XexDecryptor.sln new file mode 100644 index 0000000..f7e5f68 --- /dev/null +++ b/XexDecryptor.sln @@ -0,0 +1,26 @@ + +Microsoft Visual Studio Solution File, Format Version 11.00 +# Visual Studio 2010 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XexDecryptor", "XexDecryptor.csproj", "{42FC16C2-7200-4576-9504-C0EFC58B9DDD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mono.Cecil", "Upstream\Cecil\Mono.Cecil.csproj", "{D68133BD-1E63-496E-9EDE-4FBDBF77B486}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {42FC16C2-7200-4576-9504-C0EFC58B9DDD}.Debug|Any CPU.ActiveCfg = Debug|x86 + {42FC16C2-7200-4576-9504-C0EFC58B9DDD}.Debug|Any CPU.Build.0 = Debug|x86 + {42FC16C2-7200-4576-9504-C0EFC58B9DDD}.Release|Any CPU.ActiveCfg = Release|x86 + {42FC16C2-7200-4576-9504-C0EFC58B9DDD}.Release|Any CPU.Build.0 = Release|x86 + {D68133BD-1E63-496E-9EDE-4FBDBF77B486}.Debug|Any CPU.ActiveCfg = net_4_0_Debug|Any CPU + {D68133BD-1E63-496E-9EDE-4FBDBF77B486}.Debug|Any CPU.Build.0 = net_4_0_Debug|Any CPU + {D68133BD-1E63-496E-9EDE-4FBDBF77B486}.Release|Any CPU.ActiveCfg = net_4_0_Release|Any CPU + {D68133BD-1E63-496E-9EDE-4FBDBF77B486}.Release|Any CPU.Build.0 = net_4_0_Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/xextool.exe b/xextool.exe new file mode 100644 index 0000000..eb18de4 Binary files /dev/null and b/xextool.exe differ