diff --git a/Il2CppInspector.CLI/Program.cs b/Il2CppInspector.CLI/Program.cs index 38191ba..0566109 100644 --- a/Il2CppInspector.CLI/Program.cs +++ b/Il2CppInspector.CLI/Program.cs @@ -20,8 +20,8 @@ namespace Il2CppInspector.CLI { private class Options { - [Option('i', "bin", Required = false, HelpText = "IL2CPP binary, APK or IPA input file", Default = "libil2cpp.so")] - public string BinaryFile { get; set; } + [Option('i', "bin", Required = false, Separator = ',', HelpText = "IL2CPP binary, APK or IPA input file(s) (single file or comma-separated list for split APKs)", Default = new[] { "libil2cpp.so" })] + public IEnumerable BinaryFiles { get; set; } [Option('m', "metadata", Required = false, HelpText = "IL2CPP metadata file input (ignored for APK/IPA)", Default = "global-metadata.dat")] public string MetadataFile { get; set; } @@ -171,17 +171,19 @@ namespace Il2CppInspector.CLI Console.WriteLine("Using Unity assemblies at " + unityAssembliesPath); } + // Check that specified binary files exist + foreach (var file in options.BinaryFiles) + if (!File.Exists(file)) { + Console.Error.WriteLine($"File {file} does not exist"); + return 1; + } + // Check files exist and determine whether they're archives or not List il2cppInspectors; using (new Benchmark("Analyze IL2CPP data")) { - if (!File.Exists(options.BinaryFile)) { - Console.Error.WriteLine($"File {options.BinaryFile} does not exist"); - return 1; - } - try { - il2cppInspectors = Il2CppInspector.LoadFromPackage(options.BinaryFile); + il2cppInspectors = Il2CppInspector.LoadFromPackage(options.BinaryFiles); } catch (Exception ex) { Console.Error.WriteLine(ex.Message); @@ -194,7 +196,7 @@ namespace Il2CppInspector.CLI return 1; } - il2cppInspectors = Il2CppInspector.LoadFromFile(options.BinaryFile, options.MetadataFile); + il2cppInspectors = Il2CppInspector.LoadFromFile(options.BinaryFiles.First(), options.MetadataFile); } } diff --git a/Il2CppInspector.Common/FileFormatReaders/APKReader.cs b/Il2CppInspector.Common/FileFormatReaders/APKReader.cs index ba74323..eb47d91 100644 --- a/Il2CppInspector.Common/FileFormatReaders/APKReader.cs +++ b/Il2CppInspector.Common/FileFormatReaders/APKReader.cs @@ -23,20 +23,19 @@ namespace Il2CppInspector protected override bool Init() { // Check if it's a zip file first because ZipFile.OpenRead is extremely slow if it isn't - if (ReadUInt32() != 0x04034B50) + // 0x04034B50 = magic file header + // 0x02014B50 = central directory file header (will appear if we merged a split APK in memory) + var magic = ReadUInt32(); + if (magic != 0x04034B50 && magic != 0x02014B50) return false; try { zip = new ZipArchive(BaseStream); - // Check for existence of global-metadata.dat - if (!zip.Entries.Any(f => f.FullName == "assets/bin/Data/Managed/Metadata/global-metadata.dat")) - return false; - // Get list of binary files binaryFiles = zip.Entries.Where(f => f.FullName.StartsWith("lib/") && f.Name == "libil2cpp.so").ToArray(); - // This package doesn't contain an IL2CPP application + // This package doesn't contain an IL2CPP binary if (!binaryFiles.Any()) return false; } diff --git a/Il2CppInspector.Common/IL2CPP/Il2CppInspector.cs b/Il2CppInspector.Common/IL2CPP/Il2CppInspector.cs index 2f09665..07a0158 100644 --- a/Il2CppInspector.Common/IL2CPP/Il2CppInspector.cs +++ b/Il2CppInspector.Common/IL2CPP/Il2CppInspector.cs @@ -380,65 +380,98 @@ namespace Il2CppInspector return res; } - // Finds and extracts the metadata and IL2CPP binary from an APK or IPA file into MemoryStreams + #region Loaders + // Finds and extracts the metadata and IL2CPP binary from one or more APK files, or one IPA file into MemoryStreams // Returns null if package not recognized or does not contain an IL2CPP application - public static (MemoryStream Metadata, MemoryStream Binary)? GetStreamsFromPackage(string packageFile, bool silent = false) { + public static (MemoryStream Metadata, MemoryStream Binary)? GetStreamsFromPackage(IEnumerable packageFiles, bool silent = false) { try { - // Check if it's a zip file first because ZipFile.OpenRead is extremely slow if it isn't - using (BinaryReader zipTest = new BinaryReader(File.Open(packageFile, FileMode.Open))) { - if (zipTest.ReadUInt32() != 0x04034B50) - return null; - } + // Check every item is a zip file first because ZipFile.OpenRead is extremely slow if it isn't + foreach (var file in packageFiles) + using (BinaryReader zipTest = new BinaryReader(File.Open(file, FileMode.Open))) { + if (zipTest.ReadUInt32() != 0x04034B50) + return null; + } - using ZipArchive zip = ZipFile.OpenRead(packageFile); + MemoryStream metadataMemoryStream = null, binaryMemoryStream = null; + ZipArchiveEntry ipaBinaryFolder = null; + var binaryFiles = new List(); - Stream metadataStream, binaryStream; + // Iterate over each archive looking for the wanted files + // There are three possibilities: + // - A single IPA file containing global-metadata.dat and a single binary supporting one or more architectures + // (we return the binary inside the IPA to be loaded by MachOReader for single arch or UBReader for multi arch) + // - A single APK file containing global-metadata.dat and one or more binaries (one per architecture) + // (we return the entire APK to be loaded by APKReader) + // - Multiple APK files, one of which contains global-metadadata.dat and the others contain one binary each + // (we return all of the binaries re-packed in memory to a new Zip file, to be loaded by APKReader) + foreach (var file in packageFiles) { + // We can't close the files because we might have to read from them after the foreach + var zip = ZipFile.OpenRead(file); - // Check for Android APK - var metadataFile = zip.Entries.FirstOrDefault(f => f.FullName == "assets/bin/Data/Managed/Metadata/global-metadata.dat"); - var binaryFiles = zip.Entries.Where(f => f.FullName.StartsWith("lib/") && f.Name == "libil2cpp.so"); + // Check for Android APK (split APKs will only fill one of these two variables) + var metadataFile = zip.Entries.FirstOrDefault(f => f.FullName == "assets/bin/Data/Managed/Metadata/global-metadata.dat"); + binaryFiles.AddRange(zip.Entries.Where(f => f.FullName.StartsWith("lib/") && f.Name == "libil2cpp.so")); - // Check for iOS IPA - var ipaBinaryFolder = zip.Entries.FirstOrDefault(f => f.FullName.StartsWith("Payload/") && f.FullName.EndsWith(".app/") && f.FullName.Count(x => x == '/') == 2); + // Check for iOS IPA + ipaBinaryFolder = zip.Entries.FirstOrDefault(f => f.FullName.StartsWith("Payload/") && f.FullName.EndsWith(".app/") && f.FullName.Count(x => x == '/') == 2); - if (ipaBinaryFolder != null) { - var ipaBinaryName = ipaBinaryFolder.FullName[8..^5]; - metadataFile = zip.Entries.FirstOrDefault(f => f.FullName == $"Payload/{ipaBinaryName}.app/Data/Managed/Metadata/global-metadata.dat"); - binaryFiles = zip.Entries.Where(f => f.FullName == $"Payload/{ipaBinaryName}.app/{ipaBinaryName}"); + if (ipaBinaryFolder != null) { + var ipaBinaryName = ipaBinaryFolder.FullName[8..^5]; + metadataFile = zip.Entries.FirstOrDefault(f => f.FullName == $"Payload/{ipaBinaryName}.app/Data/Managed/Metadata/global-metadata.dat"); + binaryFiles.AddRange(zip.Entries.Where(f => f.FullName == $"Payload/{ipaBinaryName}.app/{ipaBinaryName}")); + } + + // Found metadata? + if (metadataFile != null) { + // Extract the metadata file to memory + if (!silent) + Console.WriteLine($"Extracting metadata from {file}{Path.DirectorySeparatorChar}{metadataFile.FullName}"); + + metadataMemoryStream = new MemoryStream(); + using var metadataStream = metadataFile.Open(); + metadataStream.CopyTo(metadataMemoryStream); + metadataMemoryStream.Position = 0; + } } // This package doesn't contain an IL2CPP application - if (metadataFile == null || !binaryFiles.Any()) { - Console.Error.WriteLine($"Package {packageFile} does not contain an IL2CPP application"); + if (metadataMemoryStream == null || !binaryFiles.Any()) { + Console.Error.WriteLine($"Package does not contain a complete IL2CPP application"); return null; } - // Extract the metadata file to memory - if (!silent) - Console.WriteLine($"Extracting metadata from {packageFile}{Path.DirectorySeparatorChar}{metadataFile.FullName}"); - - var metadataMemoryStream = new MemoryStream(); - metadataStream = metadataFile.Open(); - metadataStream.CopyTo(metadataMemoryStream); - metadataMemoryStream.Position = 0; - - // Extract the binary file or package to memory - var binaryMemoryStream = new MemoryStream(); - // IPAs will only have one binary (which may or may not be a UB covering multiple architectures) if (ipaBinaryFolder != null) { if (!silent) - Console.WriteLine($"Extracting binary from {packageFile}{Path.DirectorySeparatorChar}{binaryFiles.First().FullName}"); + Console.WriteLine($"Extracting binary from {packageFiles.First()}{Path.DirectorySeparatorChar}{binaryFiles.First().FullName}"); - binaryStream = binaryFiles.First().Open(); + // Extract the binary file or package to memory + binaryMemoryStream = new MemoryStream(); + using var binaryStream = binaryFiles.First().Open(); binaryStream.CopyTo(binaryMemoryStream); binaryMemoryStream.Position = 0; } - // APKs may have one or more binaries, one per architecture + // Single APKs may have one or more binaries, one per architecture // We'll read the entire APK and load those via APKReader + else if (packageFiles.Count() == 1) { + binaryMemoryStream = new MemoryStream(File.ReadAllBytes(packageFiles.First())); + } + + // Split APKs will have one binary per APK + // Roll them up into a new in-memory zip file and load it via APKReader else { - binaryMemoryStream = new MemoryStream(File.ReadAllBytes(packageFile)); + binaryMemoryStream = new MemoryStream(); + using (var apkArchive = new ZipArchive(binaryMemoryStream, ZipArchiveMode.Create, true)) { + foreach (var binary in binaryFiles) { + // Don't waste time re-compressing data we just uncompressed + var archiveFile = apkArchive.CreateEntry(binary.FullName, CompressionLevel.NoCompression); + using var archiveFileStream = archiveFile.Open(); + using var binarySourceStream = binary.Open(); + binarySourceStream.CopyTo(archiveFileStream); + } + } + binaryMemoryStream.Position = 0; } return (metadataMemoryStream, binaryMemoryStream); @@ -450,9 +483,9 @@ namespace Il2CppInspector } } - // Load from an APK or IPA file - public static List LoadFromPackage(string packageFile, bool silent = false) { - var streams = GetStreamsFromPackage(packageFile, silent); + // Load from an IPA or one or more APK files + public static List LoadFromPackage(IEnumerable packageFiles, bool silent = false) { + var streams = GetStreamsFromPackage(packageFiles, silent); if (!streams.HasValue) return null; return LoadFromStream(streams.Value.Binary, streams.Value.Metadata, silent); @@ -530,5 +563,6 @@ namespace Il2CppInspector Console.SetOut(stdout); return processors; } + #endregion } } diff --git a/Il2CppInspector.GUI/App.xaml.cs b/Il2CppInspector.GUI/App.xaml.cs index 325b3c4..50732a4 100644 --- a/Il2CppInspector.GUI/App.xaml.cs +++ b/Il2CppInspector.GUI/App.xaml.cs @@ -35,7 +35,8 @@ namespace Il2CppInspectorGUI try { OnStatusUpdate?.Invoke(this, "Extracting package"); - var streams = Inspector.GetStreamsFromPackage(packageFile); + // TODO: Accept multiple APKs + var streams = Inspector.GetStreamsFromPackage(new string[] { packageFile }); if (streams == null) throw new InvalidOperationException("The supplied package is not an APK or IPA file, or does not contain an IL2CPP application");