// 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); }
// 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; }
// 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; }
// 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); }
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; } }
// 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); } } } }
// 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; }
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); }
// 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++; } }
// 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); }
// 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; }