From 21cb7f9f8069b20749fb46ec1bed951e8f16f8c4 Mon Sep 17 00:00:00 2001 From: Katy Coe Date: Sat, 5 Dec 2020 02:10:41 +0100 Subject: [PATCH] IL2CPP: Reconstruct scrambled Il2CppCodeRegistration --- Il2CppInspector.Common/IL2CPP/Arrange.cs | 63 ----- Il2CppInspector.Common/IL2CPP/Il2CppBinary.cs | 3 +- .../IL2CPP/ReconstructMetadata.cs | 215 ++++++++++++++++++ 3 files changed, 217 insertions(+), 64 deletions(-) delete mode 100644 Il2CppInspector.Common/IL2CPP/Arrange.cs create mode 100644 Il2CppInspector.Common/IL2CPP/ReconstructMetadata.cs diff --git a/Il2CppInspector.Common/IL2CPP/Arrange.cs b/Il2CppInspector.Common/IL2CPP/Arrange.cs deleted file mode 100644 index c4ec5cb..0000000 --- a/Il2CppInspector.Common/IL2CPP/Arrange.cs +++ /dev/null @@ -1,63 +0,0 @@ -/* - Copyright 2020 Katy Coe - http://www.djkaty.com - https://github.com/djkaty - - All rights reserved. -*/ - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading; - -namespace Il2CppInspector -{ - // Some IL2CPP applications obfuscate the order of fields in Il2CppCodeRegistration and Il2CppMetadataRegistration - // specifically to defeat IL2CPP reverse engineering tools. We make an imperfect attempt to defeat this below - // by re-arranging the fields back into their original order. This can be greatly improved and more deeply validated. - // Devs: Please don't burn development resources on obfuscation. It's a waste of your time and mine. Spend it making good games instead. - partial class Il2CppBinary - { - private void Arrange() { - // Get number of pointer/count pairs in each structure - var codeItemsCount = Metadata.Sizeof(typeof(Il2CppCodeRegistration), Image.Version, Image.Bits / 8) / (Image.Bits / 8) / 2; - var metaItemsCount = Metadata.Sizeof(typeof(Il2CppMetadataRegistration), Image.Version, Image.Bits / 8) / (Image.Bits / 8) / 2; - - // Read as list of tuples - var codeArray = Image.ReadMappedArray(CodeRegistrationPointer, codeItemsCount * 2); - var codeItems = Enumerable.Range(0, codeArray.Length / 2).Select(i => (Pointer: codeArray[i*2 + 1], Count: codeArray[i*2])); - - var metaArray = Image.ReadMappedArray(MetadataRegistrationPointer, metaItemsCount * 2); - var metaItems = Enumerable.Range(0, metaArray.Length / 2).Select(i => (Pointer: metaArray[i*2 + 1], Count: metaArray[i*2])); - - // Things we need - - // Il2CppCodeRegistration: - // methodPointers (<=24.1) -> list of function pointers - // genericMethodPointers -> list of function pointers (first is zero) - // invokerPointers -> list of function pointers - // customAttributeGenerators -> list of function pointers - // codeGenModules (>=24.2) -> list of Il2CppCodeGenModule* - - if (CodeRegistration.methodPointersCount <= CodeRegistration.genericMethodPointersCount && Image.Version <= 24.1) - throw new Exception("Generic pointers greater than method pointers"); - if (CodeRegistration.genericMethodPointersCount <= CodeRegistration.invokerPointersCount) - throw new Exception("Invoker pointers greater than generic pointers"); - - // Seems to always be true but I'm not sure we can realistically guarantee this - if (CodeRegistration.customAttributeGenerators < CodeRegistration.invokerPointersCount && Image.Version <= 24.1) - throw new Exception("Custom attribute generators less than invoker pointers"); - - // Il2CppMetadataRegistration: - // genericInsts -> list of Il2CppGenericInst* (argc is count of Il2CppType* at data pointer argv; datapoint = GenericParameterIndex) - // genericMethodTable -> list of Il2CppGenericMethodFunctionsDefinitions (genericMethodIndex, methodIndex, invokerIndex) - // types -> list of Il2CppType* - // methodSpecs -> list of Il2CppMethodSpec - // methodReferences (<=16) -> list of uint - // fieldOffsets (fieldOffsetsArePointers) -> either a list of data pointers (some zero, some VAs not mappable) to list of uints, or a list of uints - // metadataUsages (>=19, <27) -> list of unmappable data pointers - } - } -} diff --git a/Il2CppInspector.Common/IL2CPP/Il2CppBinary.cs b/Il2CppInspector.Common/IL2CPP/Il2CppBinary.cs index e754a26..e12b001 100644 --- a/Il2CppInspector.Common/IL2CPP/Il2CppBinary.cs +++ b/Il2CppInspector.Common/IL2CPP/Il2CppBinary.cs @@ -208,7 +208,8 @@ namespace Il2CppInspector CodeRegistration = Image.ReadMappedObject(codeRegistration); MetadataRegistration = Image.ReadMappedObject(metadataRegistration); - Arrange(); + // Restore the field order in CodeRegistration and MetadataRegistration if they have been re-ordered for obfuscation + ReconstructMetadata(); // Do basic validatation that MetadataRegistration and CodeRegistration are sane /* diff --git a/Il2CppInspector.Common/IL2CPP/ReconstructMetadata.cs b/Il2CppInspector.Common/IL2CPP/ReconstructMetadata.cs new file mode 100644 index 0000000..4f3ca43 --- /dev/null +++ b/Il2CppInspector.Common/IL2CPP/ReconstructMetadata.cs @@ -0,0 +1,215 @@ +/* + Copyright 2020 Katy Coe - http://www.djkaty.com - https://github.com/djkaty + + All rights reserved. +*/ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading; + +namespace Il2CppInspector +{ + // Some IL2CPP applications obfuscate the order of fields in Il2CppCodeRegistration and Il2CppMetadataRegistration + // specifically to defeat IL2CPP reverse engineering tools. We make an imperfect attempt to defeat this below + // by re-arranging the fields back into their original order. This can be greatly improved and much more deeply analyzed. + // Devs: Please don't burn development resources on obfuscation. It's a waste of your time and mine. Spend it making good games instead. + partial class Il2CppBinary + { + // Loads all the pointers and counts for the specified IL2CPP metadata type regardless of version or word size into two arrays + // Sorts the pointers and calculates the maximum number of words between each, constrained by the highest count in the IL2CPP metadata type + // and by the end of the nearest section in the image. + // Returns an array of the pointers in sorted order and an array of maximum word counts with corresponding indexes + private (List ptrs, List counts) preparePointerList(Type type, ulong typePtr, IEnumerable
sections) { + // Get number of pointer/count pairs in each structure + var itemsCount = Metadata.Sizeof(type, Image.Version, Image.Bits / 8) / (Image.Bits / 8) / 2; + + // Read pointers and counts as two lists + var itemArray = Image.ReadMappedArray(typePtr, itemsCount * 2); + var itemPtrs = Enumerable.Range(0, itemArray.Length / 2).Select(i => itemArray[i*2 + 1]).ToList(); + var itemCounts = Enumerable.Range(0, itemArray.Length / 2).Select(i => (int) itemArray[i*2]).ToList(); + + // Get maximum count between each pair of pointers + // None of the maximums should be higher than the maximum count specified in the struct + // Rule out zero pointers for no longer used fields (eg. customAttributeGenerators >=27) + var itemMaxCount = itemCounts.Max(); + var itemPtrsOrdered = itemPtrs.Where(p => p != 0).OrderBy(p => p).ToList(); + var itemCountLimits = itemPtrsOrdered + .Zip(itemPtrsOrdered.Skip(1), (a, b) => Math.Min((int) (b - a) / (Image.Bits / 8), itemMaxCount)) + .Append(itemMaxCount) + .ToList(); + + // Prevent a pointer list from overrunning the end of a section + for (var i = 0; i < itemPtrsOrdered.Count; i++) { + var section = sections.FirstOrDefault(s => s.VirtualStart <= itemPtrsOrdered[i] && s.VirtualEnd >= itemPtrsOrdered[i]); + if (section != null) { + var maxSize = (int) (section.VirtualEnd + 1 - itemPtrsOrdered[i]) / (Image.Bits / 8); + itemCountLimits[i] = Math.Min(itemCountLimits[i], maxSize); + } + } + + return (itemPtrsOrdered, itemCountLimits); + } + + // Reconstruct Il2CppCodeRegistration and Il2CppMetadataRegistration into their original, unobfuscated field order + private void ReconstructMetadata() { + // If the section table is not available, give up and do nothing + if (!Image.TryGetSections(out var sections)) + return; + + // Get relevant image sections + var codeSections = sections.Where(s => s.IsExec); + var dataSections = sections.Where(s => s.IsData); + + // Fetch and sanitize our pointer and count lists + var (codePtrsOrdered, codeCountLimits) = preparePointerList(typeof(Il2CppCodeRegistration), CodeRegistrationPointer, dataSections); + var (metaPtrsOrdered, metaCountLimits) = preparePointerList(typeof(Il2CppMetadataRegistration), MetadataRegistrationPointer, dataSections); + + // Things we need from Il2CppCodeRegistration + + // methodPointers (<=24.1) -> list of function pointers (1st count) (non-sequential) + // genericMethodPointers -> list of function pointers (first IS zero) (2nd count) (not sequential) + // customAttributeGenerators (<27) -> list of function pointers (first MAY be zero) (2nd count) (sequential) + // invokerPointers -> list of function pointers (3rd count) (sequential) + // codeGenModules (>=24.2) -> list of Il2CppCodeGenModule* // TODO: We only support <=24.1 currently + // (interopData will probably have 6 sequential pointers since Il2CppInteropData starts with 5 function pointers and a GUID) + + // Let's see how many valid pointers and sequential valid pointers we actually find at each address + // Scan each pointer address for valid list of function pointers and sort into size order + // Consider the sequence to be broken if there is a gap over a certain threshold + var sequenceThreshold = 0x10000; + var fnPtrs = new SortedDictionary(); + var seqFnPtrs = new SortedDictionary(); + for (var i = 0; i < codePtrsOrdered.Count; i++) { + + // Non-sequential valid pointers + var ptrs = Image.ReadMappedArray(codePtrsOrdered[i], codeCountLimits[i]); + var foundCount = ptrs.TakeWhile(p => codeSections.Any(s => p >= s.VirtualStart && p <= s.VirtualEnd || p == 0)).Count(); + + // Prune count of trailing zero pointers + while (foundCount > 0 && ptrs[foundCount - 1] == 0ul) + foundCount--; + + fnPtrs.Add(codePtrsOrdered[i], foundCount); + + // Binaries compiled with MSVC (generally PE files) use /OPT:ICF by default (enable ICF) so this won't work. + // For these binaries, we'll use a different selection strategy below + if (Image is PEReader) + continue; + + // Sequential valid pointers (a subset of non-sequential valid pointers) + foundCount = ptrs.Take(foundCount) + .Zip(ptrs.Take(foundCount).Skip(1), (a, b) => (a, b)) + .TakeWhile(t => ((long) t.b - (long) t.a >= 0 || t.b == 0) + && ((long) t.b - (long) t.a < sequenceThreshold || t.a == 0) + // Disallow two zero pointers in a row + && (t.a != 0 || t.b != 0)) + .Count() + 1; + + // Prune count of trailing zero pointers + while (foundCount > 0 && ptrs[foundCount - 1] == 0ul) + foundCount--; + + seqFnPtrs.Add(codePtrsOrdered[i], foundCount); + } + + KeyValuePair methodPointers, genericMethodPointers, customAttributeGenerators, invokerPointers; + + // Solution without ICF + if (!(Image is PEReader)) { + // The two largest sequential groups are customAttributeGenerators and invokerPointers + var seqLongest = seqFnPtrs.OrderByDescending(kv => kv.Value).Take(2).ToList(); + (customAttributeGenerators, invokerPointers) = (seqLongest[0], seqLongest[1]); + + // For >=27, customAttributeGenerators is zero and so the largest group is invokerPointers + if (Image.Version >= 27) { + invokerPointers = customAttributeGenerators; + customAttributeGenerators = new KeyValuePair(0ul, 0); + } + + // After removing these from the non-sequential list, the largest groups are methodPointers and genericMethodPointers + var longest = fnPtrs.Except(seqLongest).OrderByDescending(kv => kv.Value).Take(2).ToList(); + (methodPointers, genericMethodPointers) = (longest[0], longest[1]); + + // For >=24.2, methodPointers is zero and so the largest group is genericMethodPointers + if (Image.Version >= 24.2) { + genericMethodPointers = methodPointers; + methodPointers = new KeyValuePair(0ul, 0); + } + + // Prune genericMethodPointers at 2nd zero (first pointer is always zero) + var gmPtr = Image.ReadMappedArray(genericMethodPointers.Key, genericMethodPointers.Value); + var gmZero = Array.IndexOf(gmPtr, 0ul, 1); + if (gmZero != -1) + genericMethodPointers = new KeyValuePair(genericMethodPointers.Key, gmZero); + } + + // Solution with ICF + else { + // Take and remove the first item and assume it's methodPointers for <=24.1, otherwise set to zero + var orderedPtrs = fnPtrs.OrderByDescending(kv => kv.Value).ToList(); + if (Image.Version <= 24.1) { + methodPointers = orderedPtrs[0]; + orderedPtrs.RemoveAt(0); + } else + methodPointers = new KeyValuePair(0ul, 0); + + // Assume this order is right most of the time + (genericMethodPointers, customAttributeGenerators, invokerPointers) = (orderedPtrs[0], orderedPtrs[1], orderedPtrs[2]); + + // customAttributeGenerators is removed in metadata >=27 + if (Image.Version >= 27) { + invokerPointers = customAttributeGenerators; + customAttributeGenerators = new KeyValuePair(0ul, 0); + } + } + + #region Debugging validation checks + #if DEBUG + // Used on non-obfuscated binaries during development to confirm the output is correct + if (methodPointers.Key != CodeRegistration.pmethodPointers) + throw new Exception("Method Pointers incorrect"); + if (invokerPointers.Key != CodeRegistration.invokerPointers) + throw new Exception("Invoker Pointers incorrect"); + if (customAttributeGenerators.Key != CodeRegistration.customAttributeGenerators) + throw new Exception("Custom attribute generators incorrect"); + if (genericMethodPointers.Key != CodeRegistration.genericMethodPointers) + throw new Exception("Generic method pointers incorrect"); + + if (methodPointers.Value != (int) CodeRegistration.methodPointersCount) + throw new Exception("Count of Method Pointers incorrect"); + if (invokerPointers.Value != (int) CodeRegistration.invokerPointersCount) + throw new Exception("Count of Invoker Pointers incorrect"); + if (customAttributeGenerators.Value != (int) CodeRegistration.customAttributeCount) + throw new Exception("Count of Custom attribute generators incorrect"); + if (genericMethodPointers.Value != (int) CodeRegistration.genericMethodPointersCount) + throw new Exception("Count of Generic method pointers incorrect"); + #endif + #endregion + + // Perform substitution + CodeRegistration.genericMethodPointers = genericMethodPointers.Key; + CodeRegistration.genericMethodPointersCount = (ulong) genericMethodPointers.Value; + CodeRegistration.customAttributeGenerators = customAttributeGenerators.Key; + CodeRegistration.customAttributeCount = customAttributeGenerators.Value; + CodeRegistration.invokerPointers = invokerPointers.Key; + CodeRegistration.invokerPointersCount = (ulong) invokerPointers.Value; + CodeRegistration.pmethodPointers = methodPointers.Key; + CodeRegistration.methodPointersCount = (ulong) methodPointers.Value; + + // Things we need from Il2CppMetadataRegistration + + // genericInsts -> list of Il2CppGenericInst* (argc is count of Il2CppType* at data pointer argv; datapoint = GenericParameterIndex) + // genericMethodTable -> list of Il2CppGenericMethodFunctionsDefinitions (genericMethodIndex, methodIndex, invokerIndex) + // types -> list of Il2CppType* + // methodSpecs -> list of Il2CppMethodSpec + // methodReferences (<=16) -> list of uint + // fieldOffsets (fieldOffsetsArePointers) -> either a list of data pointers (some zero, some VAs not mappable) to list of uints, or a list of uints + // metadataUsages (>=19, <27) -> list of unmappable data pointers + } + } +} \ No newline at end of file