Ejemplo n.º 1
0
        // Detect and defeat various kinds of XOR encryption
        public void PostProcessImage <T>(FileFormatStream <T> stream, PluginPostProcessImageEventInfo data) where T : FileFormatStream <T>
        {
            if (stream is ElfReader32 stream32)
            {
                elf32 = stream32;
            }
            else if (stream is ElfReader64 stream64)
            {
                elf64 = stream64;
            }
            else
            {
                return;
            }

            PluginServices.For(this).StatusUpdate("Detecting encryption");

            this.stream = stream;
            sections    = stream.GetSections().GroupBy(s => s.Name).ToDictionary(s => s.Key, s => s.First());

            if (HasDynamicEntry(Elf.DT_INIT) && sections.ContainsKey(".rodata"))
            {
                // Use the data section to determine some possible keys
                // If the data section uses striped encryption, bucketing the whole section will not give the correct key
                var roDataBytes            = stream.ReadBytes(sections[".rodata"].ImageStart, sections[".rodata"].ImageLength);
                var xorKeyCandidateStriped = roDataBytes.Take(1024).GroupBy(b => b).OrderByDescending(f => f.Count()).First().Key;
                var xorKeyCandidateFull    = roDataBytes.GroupBy(b => b).OrderByDescending(f => f.Count()).First().Key;

                // Select test nibbles and values for ARM instructions depending on architecture (ARMv7 / AArch64)
                var testValues = new Dictionary <int, (int, int, int, int)> {
        public LookupModule Process(TypeModel model, BeebyteDeobfuscatorPlugin plugin)
        {
            PluginServices services = PluginServices.For(plugin);

            services.StatusUpdate("Creating model for Mono dll");
            if (plugin.CompilerType.Value != CppCompilerType.MSVC)
            {
                throw new System.ArgumentException("Cross compiler deobfuscation has not been implemented yet");
            }

            MonoDecompiler.MonoDecompiler monoDecompiler = MonoDecompiler.MonoDecompiler.FromFile(plugin.MonoPath);
            if (monoDecompiler == null)
            {
                throw new System.ArgumentException("Could not load unobfuscated application");
            }

            services.StatusUpdate("Creating LookupModel for obfuscated application");

            LookupModule lookupModel = new LookupModule(plugin.NamingRegex);

            lookupModel.Init(model.ToLookupModel(statusCallback: services.StatusUpdate), LookupModel.FromModuleDef(monoDecompiler.Module, statusCallback: services.StatusUpdate), statusCallback: services.StatusUpdate);
            services.StatusUpdate("Deobfuscating binary");
            lookupModel.TranslateTypes(statusCallback: services.StatusUpdate);

            plugin.CompilerType = null;
            return(lookupModel);
        }
Ejemplo n.º 3
0
        // This implements IPostProcessMetadata
        // This hook is executed after global-metadata.dat is fully loaded
        public void PostProcessMetadata(Metadata metadata, PluginPostProcessMetadataEventInfo info)
        {
            // This displays a progress update for our plugin in the CLI or GUI
            PluginServices.For(this).StatusUpdate("Decrypting strings");

            // Go through every string literal (string[] metadata.StringLiterals) and ROT each string
            for (var i = 0; i < metadata.StringLiterals.Length; i++)
            {
                metadata.StringLiterals[i] = string.Join("", metadata.StringLiterals[i].Select(x => (char)(x >= 'a' && x <= 'z' ? (x - 'a' + rotKey.Value) % 26 + 'a' : x)));
            }

            // Report back that we modified the metadata
            // Note: we do not set info.FullyProcessed in order to allow other plugins to do further processing
            info.IsDataModified = true;
        }
Ejemplo n.º 4
0
        // This executes just as Il2CppInspector is about to read all of the .NET identifier strings (eg. type names).
        // We can use this to acquire the strings ourselves instead
        public void GetStrings(Metadata metadata, PluginGetStringsEventInfo data)
        {
            // Don't do anything if this isn't for us
            if (!IsOurs)
            {
                return;
            }

            // Tell the user what is happening in case it takes a while
            PluginServices.For(this).StatusUpdate("Decrypting strings");

            // miHoYo workloads use encrypted strings, and we need to know the correct string indexes
            // to pass to UnityPlayer.dll's GetStringFromIndex function.

            // To find them, we scan every definition in the metadata that refers to a string index,
            // combine them, put them in order and remove duplicates
            var stringIndexes =
                metadata.Images.Select(x => x.nameIndex)
                .Concat(metadata.Assemblies.Select(x => x.aname.nameIndex))
                .Concat(metadata.Assemblies.Select(x => x.aname.cultureIndex))
                .Concat(metadata.Assemblies.Select(x => x.aname.hashValueIndex))
                .Concat(metadata.Assemblies.Select(x => x.aname.publicKeyIndex))
                .Concat(metadata.Events.Select(x => x.nameIndex))
                .Concat(metadata.Fields.Select(x => x.nameIndex))
                .Concat(metadata.Methods.Select(x => x.nameIndex))
                .Concat(metadata.Params.Select(x => x.nameIndex))
                .Concat(metadata.Properties.Select(x => x.nameIndex))
                .Concat(metadata.Types.Select(x => x.nameIndex))
                .Concat(metadata.Types.Select(x => x.namespaceIndex))
                .Concat(metadata.GenericParameters.Select(x => x.nameIndex))
                .OrderBy(x => x)
                .Distinct()
                .ToList();

            // Create a delegate which internally is a function pointer to the GetStringFromIndex function in the DLL
            var pGetStringFromIndex = (GetStringFromIndex)
                                      Marshal.GetDelegateForFunctionPointer(ModuleBase + Offsets[game.Value].GetStringFromIndex, typeof(GetStringFromIndex));

            // For each index, call the delegate with the decrypted metadata byte array and index as arguments
            foreach (var index in stringIndexes)
            {
                data.Strings.Add(index, Marshal.PtrToStringAnsi(pGetStringFromIndex(metadataBlob, (uint)index)));
            }

            // This tells Il2CppInspector we have handled the strings and not to attempt to read them itself
            // The strings will be copied from data.Strings to metadata.Strings automatically
            data.IsDataModified = true;
        }
Ejemplo n.º 5
0
        // A "package" is the combination of global-metadata.dat, the application binary,
        // and some analysis which links them both together into a single unit, the Il2CppInspector object.

        // This executes after all the low-level processing and analysis of the application is completed,
        // but before any higher-level abstractions are created, such as the .NET type model or C++ application model.

        // Therefore this is a good place to make any final changes to the data that the high level models and output modules will rely on
        // In this case, we are going to acquire all of the string literals that we deferred earlier
        public void PostProcessPackage(Il2CppInspector.Il2CppInspector package, PluginPostProcessPackageEventInfo data)
        {
            // Don't do anything if this isn't for us
            if (!IsOurs)
            {
                return;
            }

            // Tell the user what is happening in case it takes a while
            PluginServices.For(this).StatusUpdate("Decrypting string literals");

            // Calculate the number of string literals
            // This calculation depends on being able to scan MetadataUsages for all of the StringLiteral uses
            // and finding the one with the highest index. The creation of MetadataUsages requires data
            // from both global-metadata.dat and the application binary; this data is merged together
            // when the Il2CppInspector object is initialized, so this is the earliest opportunity we have to examine it
            var stringLiteralCount = package.MetadataUsages.Where(u => u.Type == MetadataUsageType.StringLiteral).Max(u => u.SourceIndex) + 1;

            // Create a delegate which internally is a function pointer to the GetStringLiteralFromIndex function in the DLL
            var pGetStringLiteralFromIndex = (GetStringLiteralFromIndex)
                                             Marshal.GetDelegateForFunctionPointer(ModuleBase + Offsets[game.Value].GetStringLiteralFromIndex, typeof(GetStringLiteralFromIndex));

            var stringLiterals = new List <string>();
            var length         = 0;

            // For each index, call the delegate with the decrypted metadata byte array, index and a pointer as arguments
            // In this case, the function returns an array of UTF8-encoded characters,
            // and populates 'length' with the number of bytes returned
            for (uint index = 0; index < stringLiteralCount; index++)
            {
                var decryptedBytesUnmanaged = pGetStringLiteralFromIndex(metadataBlob, index, ref length);
                var str = new byte[length];
                Marshal.Copy(decryptedBytesUnmanaged, str, 0, length);

                stringLiterals.Add(Encoding.UTF8.GetString(str));
            }

            // If we had used IGetStringLiterals above, we would have set data.StringLiterals,
            // but here we modify the package (the Il2CppInspector object) directly instead
            package.Metadata.StringLiterals = stringLiterals.ToArray();

            // We don't set FullyProcessed so that other plugins can perform further post-processing modifications
            // IsDataModified tells Il2CppInspector that the contents of its internal data structures have been changed
            // Note this is different from IsStreamModified; changing the data in memory
            // does not automatically rewrite the stream.
            data.IsDataModified = true;
        }
        public LookupModel Process(TypeModel model, BeebyteDeobfuscatorPlugin plugin)
        {
            if (!plugin.CompilerType.HasValue)
            {
                return(null);
            }

            PluginServices services = PluginServices.For(plugin);

            services.StatusUpdate("Loading unobfuscated application");
            var il2cppClean = Il2CppInspector.Il2CppInspector.LoadFromPackage(new[] { plugin.BinaryPath }, statusCallback: services.StatusUpdate);

            if (il2cppClean == null)
            {
                il2cppClean = Il2CppInspector.Il2CppInspector.LoadFromFile(plugin.BinaryPath, plugin.MetadataPath, statusCallback: services.StatusUpdate);
            }

            if (il2cppClean == null)
            {
                throw new System.ArgumentException("Could not load unobfuscated application");
            }

            if (plugin.CompilerType.Value != CppCompiler.GuessFromImage(il2cppClean[0].BinaryImage))
            {
                throw new System.ArgumentException("Cross compiler deobfuscation has not been implemented yet");
            }
            services.StatusUpdate("Creating type model for unobfuscated application");
            var modelClean = new TypeModel(il2cppClean[0]);

            if (modelClean == null)
            {
                throw new System.ArgumentException("Could not create type model for unobfuscated application");
            }

            services.StatusUpdate("Creating LookupModel for obfuscated application");
            LookupModel lookupModel = new LookupModel(plugin.NamingRegex);

            lookupModel.Init(model.ToLookupModule(lookupModel, statusCallback: services.StatusUpdate), modelClean.ToLookupModule(lookupModel, statusCallback: services.StatusUpdate));
            services.StatusUpdate("Deobfuscating binary");
            lookupModel.TranslateTypes(true, statusCallback: services.StatusUpdate);

            plugin.CompilerType = null;
            return(lookupModel);
        }
Ejemplo n.º 7
0
        public static void Export(BeebyteDeobfuscatorPlugin plugin, LookupModel lookupModel)
        {
            PluginServices services = PluginServices.For(plugin);

            services.StatusUpdate("Generating output..");
            if (!lookupModel.Translations.Any(t => t.CleanName != t.ObfName))
            {
                return;
            }
            switch (plugin.Export)
            {
            case ExportType.PlainText:
                ExportPlainText(plugin.ExportPath, lookupModel);
                break;

            case ExportType.Classes:
                ExportClasses(plugin.ExportPath, plugin.PluginName, lookupModel, statusCallback: services.StatusUpdate);
                break;
            }
        }
Ejemplo n.º 8
0
        // Handle ROT name encryption found in some binaries
        public void PreProcessBinary(Il2CppBinary binary, PluginPreProcessBinaryEventInfo data)
        {
            // Get all exports
            var exports = binary.Image.GetExports()?.ToList();

            if (exports == null)
            {
                return;
            }

            // Try every ROT possibility (except 0 - these will already be added to APIExports)
            var exportRgx = new Regex(@"^_+");

            for (var rotKey = 1; rotKey <= 25; rotKey++)
            {
                var possibleExports = exports.Select(e => new {
                    Name           = string.Join("", e.Name.Select(x => (char)(x >= 'a' && x <= 'z'? (x - 'a' + rotKey) % 26 + 'a' : x))),
                    VirtualAddress = e.VirtualAddress
                }).ToList();

                var foundExports = possibleExports
                                   .Where(e => (e.Name.StartsWith("il2cpp_") || e.Name.StartsWith("_il2cpp_") || e.Name.StartsWith("__il2cpp_")) &&
                                          !e.Name.Contains("il2cpp_z_"))
                                   .Select(e => e);

                if (foundExports.Any() && !data.IsDataModified)
                {
                    PluginServices.For(this).StatusUpdate("Decrypting API export names");
                    data.IsDataModified = true;
                }

                foreach (var export in foundExports)
                {
                    if (binary.Image.TryMapVATR(export.VirtualAddress, out _))
                    {
                        binary.APIExports.Add(exportRgx.Replace(export.Name, ""), export.VirtualAddress);
                    }
                }
            }
        }
Ejemplo n.º 9
0
        // This executes as soon as the raw global-metadata.dat has been read from storage,
        // before any attempt is made to analyze its contents
        public void PreProcessMetadata(BinaryObjectStream stream, PluginPreProcessMetadataEventInfo info)
        {
            stream.Position = 21;

            var key = presetKey.Value == "custom"? customKey.Value : PresetKeys[presetKey.Value];

            if (string.IsNullOrEmpty(key))
            {
                throw new ArgumentException("Game version or decryption key must be specified");
            }

            var sig = stream.ReadBytes(4);

            sig = sig.Select((b, i) => (byte)(b - key[i % key.Length])).ToArray();
            if (BitConverter.ToUInt32(sig) != Il2CppConstants.MetadataSignature)
            {
                return;
            }

            PluginServices.For(this).StatusUpdate("Decrypting metadata");

            // Subtract key bytes from metadata bytes, skipping the first bytes,
            // and cycling through the key bytes repeatedly
            var bytesToSkipAtStart = 21;
            var length             = stream.Length;

            var bytes    = stream.ToArray();
            var keyBytes = Encoding.ASCII.GetBytes(key);

            for (int index = 0, pos = bytesToSkipAtStart; pos < length; index = (index + 1) % key.Length, pos++)
            {
                bytes[pos] -= keyBytes[index];
            }

            // We replace the loaded global-metadata.dat with the newly decrypted version,
            // allowing Il2CppInspector to analyze it as normal
            stream.Write(0, bytes.Skip(bytesToSkipAtStart).ToArray());

            info.IsStreamModified = true;
        }
Ejemplo n.º 10
0
        public static void Export(BeebyteDeobfuscatorPlugin plugin, LookupModule lookupModule)
        {
            PluginServices services = PluginServices.For(plugin);

            services.StatusUpdate("Generating output..");
            if (!lookupModule.Translations.Any(t => t.CleanName != t.ObfName))
            {
                return;
            }
            List <Translation> filteredTranslations = lookupModule.Translations
                                                      .Where(t => !t.CleanName.EndsWith('&'))
                                                      .GroupBy(t => t.CleanName)
                                                      .Select(t => t.First())
                                                      .GroupBy(t => t.ObfName)
                                                      .Select(t => t.First())
                                                      .ToList();

            lookupModule.Translations.Clear();
            lookupModule.Translations.AddRange(filteredTranslations);

            IGenerator.GetGenerator(plugin).Generate(plugin, lookupModule);
        }
Ejemplo n.º 11
0
        // Decrypt XOR-encrypted strings in global-metadata.dat
        public void PostProcessMetadata(Metadata metadata, PluginPostProcessMetadataEventInfo data)
        {
            // To check for encryption, find every single string start position by scanning all of the definitions
            var stringOffsets = metadata.Images.Select(x => x.nameIndex)
                                .Concat(metadata.Assemblies.Select(x => x.aname.nameIndex))
                                .Concat(metadata.Assemblies.Select(x => x.aname.cultureIndex))
                                .Concat(metadata.Assemblies.Select(x => x.aname.hashValueIndex)) // <=24.3
                                .Concat(metadata.Assemblies.Select(x => x.aname.publicKeyIndex))
                                .Concat(metadata.Events.Select(x => x.nameIndex))
                                .Concat(metadata.Fields.Select(x => x.nameIndex))
                                .Concat(metadata.Methods.Select(x => x.nameIndex))
                                .Concat(metadata.Params.Select(x => x.nameIndex))
                                .Concat(metadata.Properties.Select(x => x.nameIndex))
                                .Concat(metadata.Types.Select(x => x.nameIndex))
                                .Concat(metadata.Types.Select(x => x.namespaceIndex))
                                .Concat(metadata.GenericParameters.Select(x => x.nameIndex))
                                .OrderBy(x => x)
                                .Distinct()
                                .ToList();

            // Now confirm that all the keys are present in the string dictionary
            if (metadata.Header.stringCount == 0 || !stringOffsets.Except(metadata.Strings.Keys).Any())
            {
                return;
            }

            // If they aren't, that means one or more of the null terminators wasn't null, indicating potential encryption
            // Only do this if we need to because it's very slow
            PluginServices.For(this).StatusUpdate("Decrypting strings");

            // There may be zero-padding at the end of the last string since counts seem to be word-aligned
            // Find the true location one byte after the final character of the final string
            var endOfStrings = metadata.Header.stringCount;

            while (metadata.ReadByte(metadata.Header.stringOffset + endOfStrings - 1) == 0)
            {
                endOfStrings--;
            }

            // Start again
            metadata.Strings.Clear();
            metadata.Position = metadata.Header.stringOffset;

            // Read in all of the strings as if they are fixed length rather than null-terminated
            foreach (var offset in stringOffsets.Zip(stringOffsets.Skip(1).Append(endOfStrings), (a, b) => (current: a, next: b)))
            {
                var encryptedString = metadata.ReadBytes(offset.next - offset.current - 1);

                // The null terminator is the XOR key
                var xorKey = metadata.ReadByte();

                var decryptedString = metadata.Encoding.GetString(encryptedString.Select(b => (byte)(b ^ xorKey)).ToArray());
                metadata.Strings.Add(offset.current, decryptedString);
            }

            // Write changes back in case the user wants to save the metadata file
            metadata.Position = metadata.Header.stringOffset;
            foreach (var str in metadata.Strings.OrderBy(s => s.Key))
            {
                metadata.WriteNullTerminatedString(str.Value);
            }
            metadata.Flush();

            data.IsDataModified   = true;
            data.IsStreamModified = true;
        }
        public void Generate(BeebyteDeobfuscatorPlugin plugin, LookupModule module)
        {
            IEnumerable <Translation> translations = module.Translations.Where(t =>
                                                                               t.Type == TranslationType.TypeTranslation &&
                                                                               !Regex.IsMatch(t.CleanName, @"\+<.*(?:>).*__[1-9]{0,4}|[A-z]*=.{1,4}|<.*>") &&
                                                                               !Regex.IsMatch(t.CleanName, module.NamingRegex) &&
                                                                               (t._type?.DeclaringType.IsEmpty ?? false) &&
                                                                               !(t._type?.IsArray ?? false) &&
                                                                               !(t._type?.IsGenericType ?? false) &&
                                                                               !(t._type?.IsNested ?? false) &&
                                                                               !(t._type?.Namespace.Contains("System") ?? false) &&
                                                                               !(t._type?.Namespace.Contains("MS") ?? false)
                                                                               );
            int            current  = 0;
            int            total    = translations.Count();
            PluginServices services = PluginServices.For(plugin);

            foreach (Translation translation in translations)
            {
                services.StatusUpdate(translations, $"Exported {current}/{total} classes");

                FileStream exportFile = null;
                if (!translation.CleanName.Contains("+"))
                {
                    exportFile = new FileStream(plugin.ExportPath +
                                                Path.DirectorySeparatorChar +
                                                $"{Helpers.SanitizeFileName(translation.CleanName)}.cs",
                                                FileMode.Create);
                }
                else
                {
                    if (!File.Exists($"{Helpers.SanitizeFileName(translation.CleanName.Split("+")[0])}.cs"))
                    {
                        continue;
                    }
                    var lines = File.ReadAllLines($"{Helpers.SanitizeFileName(translation.CleanName.Split("+")[0])}.cs");
                    File.WriteAllLines($"{Helpers.SanitizeFileName(translation.CleanName.Split("+")[0])}.cs", lines.Take(lines.Length - 1).ToArray());
                    exportFile = new FileStream(plugin.ExportPath +
                                                Path.DirectorySeparatorChar +
                                                $"{Helpers.SanitizeFileName(translation.CleanName.Split("+")[0])}.cs",
                                                FileMode.Open);
                }

                StreamWriter output = new StreamWriter(exportFile);

                if (!translation.CleanName.Contains("+"))
                {
                    string start = Output.ClassOutputTop;
                    start = start.Replace("#PLUGINNAME#", plugin.PluginName);
                    output.Write(start);
                    output.Write($"    [Translator]\n    public struct {translation.CleanName}\n    {{\n");
                }
                else
                {
                    var names = translation.CleanName.Split("+").ToList();
                    names.RemoveAt(0);
                    output.Write($"    [Translator]\n    public struct {string.Join('.', names)}\n    {{\n");
                }
                foreach (LookupField f in translation._type.Fields)
                {
                    output.WriteLine(f.ToFieldExport());
                }
                output.Write("    }\n}");

                output.Close();
                current++;
            }
        }
Ejemplo n.º 13
0
        // Generate analytics from binary image
        public void PostProcessImage <T>(FileFormatStream <T> stream, PluginPostProcessImageEventInfo info) where T : FileFormatStream <T>
        {
            // Report to the user what is happening
            PluginServices.For(this).StatusUpdate("Generating analytics");

            // Get the section we would like to investigate
            Section section;

            try {
                section = stream.GetSections().Single(s => s.Name == sectionName.Value);
            }
            // Not all binaries have a section with this name, or any sections at all
            catch {
                return;
            }

            // Get contents of section
            var bytes = stream.ReadBytes(section.ImageStart, (int)(section.ImageEnd - section.ImageStart));

            // Produce frequency graph of bytes
            // This will produce a dictionary where the key is the byte value and the value is the number of times it occurred
            var freq = bytes.GroupBy(b => b).OrderBy(g => g.Key).ToDictionary(g => g.Key, g => (double)g.Count() * 100 / bytes.Length);

            // Write as CSV file
            if (Path.GetExtension(outputPath.Value) == ".csv")
            {
                var csv = new StringBuilder();
                csv.AppendLine("Byte,Count");
                csv.Append(string.Join(Environment.NewLine, freq.Select(f => f.Key + "," + f.Value)));

                File.WriteAllText(outputPath.Value, csv.ToString());
                return;
            }

            // Write as XLSX file (using nuget package Aspose.Cells)
            var wb    = new Workbook();
            var sheet = wb.Worksheets[0];

            // Add headers
            sheet.Cells["A1"].PutValue("Byte");
            sheet.Cells["B1"].PutValue("Count");

            // Create number format style
            var style = new CellsFactory().CreateStyle();

            style.Custom = "@"; // Text for hex bytes column

            // Add data
            for (var row = 0; row < freq.Count; row++)
            {
                sheet.Cells["A" + (row + 2)].PutValue($"{row:X2}");
                sheet.Cells["A" + (row + 2)].SetStyle(style, true);
                sheet.Cells["B" + (row + 2)].PutValue(freq[(byte)row]);
            }

            // Create table
            var list = sheet.ListObjects[sheet.ListObjects.Add("A1", "B257", hasHeaders: true)];

            list.TableStyleType = TableStyleType.TableStyleMedium6;

            // Create chart
            var chart = sheet.Charts[sheet.Charts.Add(ChartType.Column, 3, 3, 40, 26)];

            chart.Title.Text = "Frequency graph of data bytes";
            chart.Style      = 3;
            chart.ValueAxis.IsLogarithmic       = true;
            chart.ValueAxis.CrossAt             = 0.001;
            chart.ValueAxis.IsAutomaticMaxValue = false;
            chart.ValueAxis.MaxValue            = 100;
            chart.ValueAxis.LogBase             = 2;

            // Set chart data
            chart.SetChartDataRange("A1:B257", isVertical: true);

            chart.NSeries.CategoryData = "A2:A257";
            chart.NSeries[0].GapWidth  = 0; // 'Count' data series is at index 0 now that category has been set
            chart.ShowLegend           = false;

            // Save workbook
            wb.Save(outputPath.Value, SaveFormat.Xlsx);
        }
Ejemplo n.º 14
0
        // This executes as soon as the raw global-metadata.dat has been read from storage,
        // before any attempt is made to analyze its contents
        // We use it to call UnityPlayer.dll to decrypt the file in memory
        public void PreProcessMetadata(BinaryObjectStream stream, PluginPreProcessMetadataEventInfo info)
        {
            // Assume that your plugin is enabled regardless of what is loading
            // Therefore, we don't want to process global-metadata.dat files that are not for us!
            // miHoYo metadata has an invalid signature at the start of the file so we use that as the criteria
            IsOurs = stream.ReadUInt32(0) != Il2CppConstants.MetadataSignature;
            if (!IsOurs)
            {
                return;
            }

            // The DWORD 0x4008 bytes from the end should be an offset to itself
            var lastBlockPointer = stream.Length - 0x4008;

            IsOurs = stream.ReadUInt32(lastBlockPointer) == lastBlockPointer;
            if (!IsOurs)
            {
                return;
            }

            // Tell the user what is happening in case it takes a while
            PluginServices.For(this).StatusUpdate("Decrypting metadata");

            // Create a delegate which internally is a function pointer to the DecryptMetadata function in the DLL
            var pDecryptMetadata = (DecryptMetadata)Marshal.GetDelegateForFunctionPointer(ModuleBase + Offsets[game.Value].DecryptMetadata, typeof(DecryptMetadata));

            // Call the delegate with the encrypted metadata byte array and length as arguments
            var decryptedBytesUnmanaged = pDecryptMetadata(stream.ToArray(), (int)stream.Length);

            // Copy the decrypted data back from unmanaged memory to a byte array
            metadataBlob = new byte[stream.Length];
            Marshal.Copy(decryptedBytesUnmanaged, metadataBlob, 0, (int)stream.Length);

            // Genshin Impact adds some spice by encrypting 0x10 bytes for every 'step' bytes of the metadata
            // with a simple XOR key, so we need to apply that too
            if (game.Value.StartsWith("genshin-impact"))
            {
                var key = new byte [] { 0xAD, 0x2F, 0x42, 0x30, 0x67, 0x04, 0xB0, 0x9C, 0x9D, 0x2A, 0xC0, 0xBA, 0x0E, 0xBF, 0xA5, 0x68 };

                // The step is based on the file size
                var step = (int)(stream.Length >> 14) << 6;

                for (var pos = 0; pos < metadataBlob.Length; pos += step)
                {
                    for (var b = 0; b < 0x10; b++)
                    {
                        metadataBlob[pos + b] ^= key[b];
                    }
                }
            }

            // We replace the loaded global-metadata.dat with the newly decrypted version,
            // allowing Il2CppInspector to analyze it as normal
            stream.Write(0, metadataBlob);

            // Some types have reordered fields - these calls tell Il2CppInspector what the correct field order is
            // See MappedTypes.cs for details
            stream.AddObjectMapping(typeof(Il2CppInspector.Il2CppGlobalMetadataHeader), typeof(Il2CppGlobalMetadataHeader));
            stream.AddObjectMapping(typeof(Il2CppInspector.Il2CppTypeDefinition), typeof(Il2CppTypeDefinition));
            stream.AddObjectMapping(typeof(Il2CppInspector.Il2CppMethodDefinition), typeof(Il2CppMethodDefinition));
            stream.AddObjectMapping(typeof(Il2CppInspector.Il2CppFieldDefinition), typeof(Il2CppFieldDefinition));
            stream.AddObjectMapping(typeof(Il2CppInspector.Il2CppPropertyDefinition), typeof(Il2CppPropertyDefinition));

            // We tell Il2CppInspector that we have taken care of the metadata
            // IsStreamModified marks the original data stream as modified so that the user is able to save the changes
            // SkipValidation tells Il2CppInspector not to check this global-metadata.dat for validity;
            // if we don't set this, it will report that the metadata is invalid
            info.IsStreamModified = true;
            info.SkipValidation   = true;
        }