mirror of
https://github.com/AssetRipper/Cpp2IL.git
synced 2026-11-13 17:07:41 -10:00
422 lines
20 KiB
C#
422 lines
20 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using LibCpp2IL.BinaryStructures;
|
|
using LibCpp2IL.Logging;
|
|
using LibCpp2IL.Metadata;
|
|
using LibCpp2IL.NintendoSwitch;
|
|
|
|
namespace LibCpp2IL;
|
|
|
|
public class BinarySearcher(Il2CppBinary binary, Il2CppMetadata metadata, int methodCount, int typeDefinitionsCount)
|
|
{
|
|
//Used for codereg location pre-2019
|
|
//Used for metadata reg location in 24.5+
|
|
|
|
private static int FindSequence(ReadOnlySpan<byte> haystack, ReadOnlySpan<byte> needle, int requiredAlignment = 1, int startOffset = 0)
|
|
{
|
|
var currentHaystack = haystack[startOffset..];
|
|
while (true)
|
|
{
|
|
var relativeIdx = currentHaystack.IndexOf(needle);
|
|
|
|
//No match found
|
|
if (relativeIdx == -1)
|
|
return -1;
|
|
|
|
var absoluteIdx = haystack.Length - currentHaystack.Length + relativeIdx;
|
|
|
|
if (requiredAlignment <= 1 || absoluteIdx % requiredAlignment == 0)
|
|
return absoluteIdx;
|
|
|
|
// If not aligned, slice the haystack to start searching just after this failed match
|
|
currentHaystack = currentHaystack[(relativeIdx + 1)..];
|
|
}
|
|
}
|
|
|
|
// Find all occurrences of a sequence of bytes, using word alignment by default
|
|
private IEnumerable<uint> FindAllBytes(byte[] signature, int alignment = 0)
|
|
{
|
|
LibLogger.VerboseNewline($"\t\t\tLooking for bytes: {string.Join(" ", signature.Select(b => b.ToString("x2")))}");
|
|
var offset = 0;
|
|
var ptrSize = binary.is32Bit ? 4 : 8;
|
|
while (offset != -1)
|
|
{
|
|
offset = FindSequence(binary.GetRawBinaryContent(), signature, alignment != 0 ? alignment : ptrSize, offset);
|
|
if (offset != -1)
|
|
{
|
|
yield return (uint)offset;
|
|
offset += ptrSize;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find strings
|
|
public IEnumerable<uint> FindAllStrings(string str) => FindAllBytes(Encoding.ASCII.GetBytes(str), 1);
|
|
|
|
// Find 32-bit words
|
|
private IEnumerable<uint> FindAllDWords(uint word) => FindAllBytes(BitConverter.GetBytes(word), binary is NsoFile ? 1 : 4);
|
|
|
|
// Find 64-bit words
|
|
private IEnumerable<uint> FindAllQWords(ulong word) => FindAllBytes(BitConverter.GetBytes(word), binary is NsoFile ? 1 : 8);
|
|
|
|
// Find words for the current binary size
|
|
private IEnumerable<uint> FindAllWords(ulong word)
|
|
=> binary.is32Bit ? FindAllDWords((uint)word) : FindAllQWords(word);
|
|
|
|
private IEnumerable<ulong> MapOffsetsToVirt(IEnumerable<uint> offsets)
|
|
{
|
|
foreach (var offset in offsets)
|
|
if (binary.TryMapRawAddressToVirtual(offset, out var word))
|
|
yield return word;
|
|
}
|
|
|
|
// Find all valid virtual address pointers to a virtual address
|
|
public IEnumerable<ulong> FindAllMappedWords(ulong word)
|
|
{
|
|
var fileOffsets = FindAllWords(word).ToList();
|
|
return MapOffsetsToVirt(fileOffsets);
|
|
}
|
|
|
|
// Find all valid virtual address pointers to a set of virtual addresses
|
|
public IEnumerable<ulong> FindAllMappedWords(IEnumerable<ulong> va) => va.SelectMany(FindAllMappedWords);
|
|
|
|
public IEnumerable<ulong> FindAllMappedWords(IEnumerable<uint> va) => va.SelectMany(a => FindAllMappedWords(a));
|
|
|
|
public ulong FindCodeRegistrationPre2019()
|
|
{
|
|
//First item in the CodeRegistration is the number of methods.
|
|
var vas = MapOffsetsToVirt(FindAllBytes(BitConverter.GetBytes(methodCount), 1)).ToList();
|
|
|
|
LibLogger.VerboseNewline($"\t\t\tFound {vas.Count} instances of the method count {methodCount}, as bytes {string.Join(", ", BitConverter.GetBytes((ulong)methodCount).Select(x => $"0x{x:X}"))}");
|
|
|
|
if (vas.Count == 0)
|
|
return 0;
|
|
|
|
foreach (var va in vas)
|
|
{
|
|
LibLogger.VerboseNewline($"\t\t\tChecking for CodeRegistration at virtual address 0x{va:x}...");
|
|
var cr = binary.ReadReadableAtVirtualAddress<Il2CppCodeRegistration>(va);
|
|
|
|
if ((long)cr.customAttributeCount == metadata.attributeTypeRanges!.Count)
|
|
return va;
|
|
|
|
LibLogger.VerboseNewline($"\t\t\t\tNot a valid CodeRegistration - custom attribute count is {cr.customAttributeCount}, expecting {metadata.attributeTypeRanges.Count}");
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
[SuppressMessage("ReSharper", "PossibleMultipleEnumeration")]
|
|
internal ulong FindCodeRegistrationPost2019()
|
|
{
|
|
//Works only on >=24.2
|
|
var mscorlibs = FindAllStrings("mscorlib.dll\0").Select(idx => binary.MapRawAddressToVirtual(idx)).ToList();
|
|
|
|
LibLogger.VerboseNewline($"\t\t\tFound {mscorlibs.Count} occurrences of mscorlib.dll: [{string.Join(", ", mscorlibs.Select(p => p.ToString("X")))}]");
|
|
|
|
var pMscorlibCodegenModule = FindAllMappedWords(mscorlibs).ToList(); //CodeGenModule address will be in here
|
|
|
|
LibLogger.VerboseNewline($"\t\t\tFound {pMscorlibCodegenModule.Count} potential codegen modules for mscorlib: [{string.Join(", ", pMscorlibCodegenModule.Select(p => p.ToString("X")))}]");
|
|
|
|
var pMscorlibCodegenEntryInCodegenModulesList = FindAllMappedWords(pMscorlibCodegenModule).ToList(); //CodeGenModules list address will be in here
|
|
|
|
LibLogger.VerboseNewline($"\t\t\tFound {pMscorlibCodegenEntryInCodegenModulesList.Count} address for potential codegen modules in potential codegen module lists: [{string.Join(", ", pMscorlibCodegenEntryInCodegenModulesList.Select(p => p.ToString("X")))}]");
|
|
|
|
if (pMscorlibCodegenEntryInCodegenModulesList.Count == 0)
|
|
{
|
|
LibLogger.ErrorNewline("\t\t\tNo codegen modules found for mscorlib! Aborting search.");
|
|
return 0;
|
|
}
|
|
|
|
var ptrSize = (binary.is32Bit ? 4u : 8u);
|
|
|
|
List<ulong>? pCodegenModules = null;
|
|
if (metadata.MetadataVersion < 27f)
|
|
{
|
|
//Pre-v27, mscorlib is the first codegen module, so *MscorlibCodegenEntryInCodegenModulesList == g_CodegenModules, so we can just find a pointer to this.
|
|
if (pMscorlibCodegenEntryInCodegenModulesList.Count == 1)
|
|
//Small optimisation in the case of only one ptr
|
|
pCodegenModules = FindAllMappedWords(pMscorlibCodegenEntryInCodegenModulesList[0]).ToList();
|
|
else
|
|
pCodegenModules = FindAllMappedWords(pMscorlibCodegenEntryInCodegenModulesList).ToList();
|
|
}
|
|
else
|
|
{
|
|
//but in v27 it's close to the LAST codegen module (winrt.dll is an exception), so we need to work back until we find an xref.
|
|
var sanityCheckNumberOfModules = Math.Min(400, metadata.imageDefinitions.Length);
|
|
var pSomewhereInCodegenModules = pMscorlibCodegenEntryInCodegenModulesList.AsEnumerable();
|
|
var numModuleDefs = metadata.imageDefinitions.Length;
|
|
var initialBacktrack = numModuleDefs - 10;
|
|
|
|
pSomewhereInCodegenModules = pSomewhereInCodegenModules.Select(va => va - ptrSize * (ulong)initialBacktrack);
|
|
|
|
//Slightly experimental, but we're gonna try backtracking most of the way through the number of modules. Not all the way because we don't want to overshoot.
|
|
|
|
List<ulong> FindCodegenModules(int initialBacktrackParam)
|
|
{
|
|
List<ulong> foundModules = [];
|
|
|
|
for (int backtrack = initialBacktrackParam; backtrack < sanityCheckNumberOfModules && foundModules.Count != 1; backtrack++)
|
|
{
|
|
foundModules = FindAllMappedWords(pSomewhereInCodegenModules).ToList();
|
|
|
|
//Sanity check the count, which is one pointer back
|
|
if (foundModules.Count == 1)
|
|
{
|
|
if (foundModules.Any())
|
|
{
|
|
binary.Reader.Position = binary.MapVirtualAddressToRaw(foundModules.First() - ptrSize);
|
|
var moduleCount = binary.Reader.ReadInt32();
|
|
|
|
if (moduleCount < 0 || moduleCount > sanityCheckNumberOfModules)
|
|
foundModules = [];
|
|
else
|
|
LibLogger.VerboseNewline($"\t\t\tFound valid address for pCodegenModules after a backtrack of {backtrack}/{metadata.imageDefinitions.Length}: {foundModules[0]:X}");
|
|
}
|
|
}
|
|
else if (foundModules.Count > 1)
|
|
{
|
|
LibLogger.VerboseNewline($"\t\t\tFound {foundModules.Count} potential pCodegenModules addresses after a backtrack of {backtrack}, which is too many (> 1). Will try backtracking further.");
|
|
}
|
|
pSomewhereInCodegenModules = pSomewhereInCodegenModules.Select(va => va - ptrSize);
|
|
}
|
|
|
|
return foundModules;
|
|
}
|
|
|
|
var backtrackedModules = FindCodegenModules(initialBacktrack);
|
|
if (backtrackedModules.Count < 1)
|
|
{
|
|
LibLogger.WarnNewline($"Hit backtrack limit of {sanityCheckNumberOfModules} modules and still didn't find a valid pCodegenModules pointer. Switching to the fallback non-backtracked search");
|
|
pSomewhereInCodegenModules = pMscorlibCodegenEntryInCodegenModulesList.AsEnumerable();
|
|
var unbacktrackedModules = FindCodegenModules(0);
|
|
if (unbacktrackedModules.Count < 1)
|
|
{
|
|
LibLogger.WarnNewline("Fallback search failed to find a valid pCodegen modules pointer.");
|
|
return 0;
|
|
}
|
|
else
|
|
{
|
|
pCodegenModules = unbacktrackedModules;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
pCodegenModules = backtrackedModules;
|
|
}
|
|
|
|
if (pCodegenModules?.Any() != true)
|
|
throw new Exception("Failed to find pCodegenModules");
|
|
|
|
if (pCodegenModules.Count() > 1)
|
|
throw new Exception("Found more than 1 pointer as pCodegenModules");
|
|
}
|
|
|
|
LibLogger.VerboseNewline($"\t\t\tFound {pCodegenModules.Count} potential pCodegenModules addresses: [{string.Join(", ", pCodegenModules.Select(p => p.ToString("X")))}]");
|
|
|
|
//We have pCodegenModules which *should* be x-reffed in the last pointer of Il2CppCodeRegistration.
|
|
//So, subtract the size of one pointer from that...
|
|
var bytesToGoBack = (ulong)Il2CppCodeRegistration.GetStructSize(binary.is32Bit, metadata.MetadataVersion) - ptrSize;
|
|
|
|
LibLogger.VerboseNewline($"\t\t\tpCodegenModules is the second-to-last field of the codereg struct. Therefore on this version and architecture, we need to subtract {bytesToGoBack} bytes from its address to get pCodeReg");
|
|
|
|
var fields = typeof(Il2CppCodeRegistration).GetFields();
|
|
var fieldsByName = fields.ToDictionary(f => f.Name);
|
|
|
|
foreach (var pCodegenModule in pCodegenModules)
|
|
{
|
|
//...and subtract that from our pointer.
|
|
var address = pCodegenModule - bytesToGoBack;
|
|
|
|
if (pCodegenModules.Count == 1)
|
|
{
|
|
LibLogger.VerboseNewline($"\t\t\tOnly found one codegen module pointer, so assuming it's correct and returning pCodeReg = 0x{address:X}");
|
|
return address;
|
|
}
|
|
|
|
LibLogger.Verbose($"\t\t\tConsidering potential code registration at 0x{address:X}...");
|
|
|
|
var codeReg = binary.ReadReadableAtVirtualAddress<Il2CppCodeRegistration>(address);
|
|
|
|
var success = ValidateCodeRegistration(codeReg, fieldsByName, binary);
|
|
|
|
if (success)
|
|
{
|
|
LibLogger.VerboseNewline("Looks good!");
|
|
return address;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
public static bool ValidateCodeRegistration(Il2CppCodeRegistration codeReg, Dictionary<string, FieldInfo> fieldsByName, Il2CppBinary binaryToValidate)
|
|
{
|
|
var success = true;
|
|
foreach (var keyValuePair in fieldsByName)
|
|
{
|
|
var fieldValue = (ulong)keyValuePair.Value.GetValue(codeReg)!;
|
|
|
|
if (fieldValue == 0)
|
|
continue; //Allow zeroes
|
|
|
|
if (keyValuePair.Key.EndsWith("count", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
//19 Apr 2022: Upped to 0x70000 due to Zenith (which has genericMethodPointersCount = 0x6007B)
|
|
if (fieldValue > 0x70_000)
|
|
{
|
|
LibLogger.VerboseNewline($"Rejected due to unreasonable count field 0x{fieldValue:X} for field {keyValuePair.Key}");
|
|
success = false;
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//Pointer
|
|
if (!binaryToValidate.TryMapVirtualAddressToRaw(fieldValue, out _))
|
|
{
|
|
LibLogger.VerboseNewline($"Rejected due to invalid pointer 0x{fieldValue:X} for field {keyValuePair.Key}");
|
|
success = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
public ulong FindMetadataRegistrationPre24_5()
|
|
{
|
|
//We're looking for TypeDefinitionsSizesCount, which is the 4th-to-last field
|
|
var sizeOfMr = (ulong)Il2CppMetadataRegistration.GetStructSize(binary.is32Bit, metadata.MetadataVersion);
|
|
var ptrSize = binary.is32Bit ? 4ul : 8ul;
|
|
|
|
var bytesToSubtract = sizeOfMr - ptrSize * 4;
|
|
|
|
var potentialMetaRegPointers = MapOffsetsToVirt(FindAllBytes(BitConverter.GetBytes(metadata.TypeDefinitionCount), 1)).ToList();
|
|
|
|
LibLogger.VerboseNewline($"\t\t\tFound {potentialMetaRegPointers.Count} instances of the number of type defs, {metadata.TypeDefinitionCount}");
|
|
|
|
potentialMetaRegPointers = potentialMetaRegPointers.Select(p => p - bytesToSubtract).ToList();
|
|
|
|
foreach (var potentialMetaRegPointer in potentialMetaRegPointers)
|
|
{
|
|
var mr = binary.ReadReadableAtVirtualAddress<Il2CppMetadataRegistration>(potentialMetaRegPointer);
|
|
|
|
if (mr.metadataUsagesCount == (ulong)metadata.metadataUsageLists!.Length)
|
|
{
|
|
LibLogger.VerboseNewline($"\t\t\tFound and selected probably valid metadata registration at 0x{potentialMetaRegPointer:X}.");
|
|
return potentialMetaRegPointer;
|
|
}
|
|
else
|
|
LibLogger.VerboseNewline($"\t\t\tSkipping 0x{potentialMetaRegPointer:X} as the metadata reg, metadata usage count was 0x{mr.metadataUsagesCount:X}, expecting 0x{metadata.metadataUsageLists.Length:X}");
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
public ulong FindMetadataRegistrationPost24_5()
|
|
{
|
|
var ptrSize = binary.is32Bit ? 4ul : 8ul;
|
|
var sizeOfMr = (uint)Il2CppMetadataRegistration.GetStructSize(binary.is32Bit, metadata.MetadataVersion);
|
|
|
|
LibLogger.VerboseNewline($"\t\t\tLooking for the number of type definitions, 0x{typeDefinitionsCount:X}");
|
|
var ptrsToNumberOfTypes = FindAllMappedWords((ulong)typeDefinitionsCount).ToList();
|
|
|
|
//typeDefinitionsSizesCount sits 4 fields from the end of the struct, or 6 on >= 106.1 which added the two alwaysInitMetadataUsages fields after it
|
|
var typeDefSizesCountFieldsFromEnd = metadata.MetadataVersion >= 106.1f ? 6ul : 4ul;
|
|
|
|
LibLogger.VerboseNewline($"\t\t\tFound {ptrsToNumberOfTypes.Count} instances of the number of type definitions: [{string.Join(", ", ptrsToNumberOfTypes.Select(p => p.ToString("X")))}]");
|
|
var possibleMetadataUsages = ptrsToNumberOfTypes.Select(a => a - sizeOfMr + ptrSize * typeDefSizesCountFieldsFromEnd).ToList();
|
|
|
|
LibLogger.VerboseNewline($"\t\t\tFound {possibleMetadataUsages.Count} potential metadata registrations: [{string.Join(", ", possibleMetadataUsages.Select(p => p.ToString("X")))}]");
|
|
|
|
var mrFieldCount = sizeOfMr / ptrSize;
|
|
foreach (var va in possibleMetadataUsages)
|
|
{
|
|
try
|
|
{
|
|
var mrWords = binary.ReadNUintArrayAtVirtualAddress(va, (int)mrFieldCount);
|
|
|
|
// Even field indices are counts, odd field indices are pointers
|
|
var ok = true;
|
|
for (var i = 0; i < mrWords.Length && ok; i++)
|
|
{
|
|
if (i % 2 == 0)
|
|
{
|
|
//Count
|
|
ok = mrWords[i] < 0xC_0000;
|
|
|
|
if (!ok)
|
|
LibLogger.VerboseNewline($"\t\t\tRejected Metadata registration at 0x{va:X}, because it has a count field 0x{mrWords[i]:X} at offset {i} which is above sanity limit of 0xC0000. If metadata registration detection fails, may need to bump up the limit.");
|
|
}
|
|
else
|
|
{
|
|
//Pointer
|
|
if (mrWords[i] == 0)
|
|
{
|
|
//Maybe need an investigation here, but metadataUsages can be (always is?) a null ptr on v27. It's at index 11 on v108+ (which dropped two count/pointer pairs), 14 otherwise.
|
|
ok = i >= (metadata.MetadataVersion >= 108 ? 11 : 14);
|
|
ok |= i == 1; //genericClasses can rarely be null on v39?
|
|
if (!ok)
|
|
LibLogger.VerboseNewline($"\t\t\tRejecting metadata registration 0x{va:X} because the pointer at index {i} is 0.");
|
|
}
|
|
else
|
|
{
|
|
ok = binary.TryMapVirtualAddressToRaw((ulong)mrWords[i], out _); //Can be mapped successfully to the binary.
|
|
if (!ok)
|
|
LibLogger.VerboseNewline($"\t\t\tRejecting metadata registration 0x{va:X} because the pointer at index {i}, which is 0x{mrWords[i]:X}, can't be mapped to the binary.");
|
|
}
|
|
}
|
|
|
|
if (!ok)
|
|
break;
|
|
}
|
|
|
|
if (ok)
|
|
{
|
|
var metaReg = binary.ReadReadableAtVirtualAddress<Il2CppMetadataRegistration>(va);
|
|
if (metadata.MetadataVersion >= 27f && (metaReg.metadataUsagesCount != 0 || metaReg.metadataUsages != 0))
|
|
{
|
|
//Too many metadata usages - should be 0 on v27
|
|
LibLogger.VerboseNewline($"\t\t\tWarning: metadata registration 0x{va:X} has {metaReg.metadataUsagesCount} metadata usages at a pointer of 0x{metaReg.metadataUsages:X}. We're on v27, these should be 0.");
|
|
// continue;
|
|
}
|
|
|
|
if (metaReg.typeDefinitionsSizesCount != metadata.TypeDefinitionCount)
|
|
{
|
|
LibLogger.VerboseNewline($"\t\t\tRejecting metadata registration 0x{va:X} because it has {metaReg.typeDefinitionsSizesCount} type def sizes, while metadata file defines {metadata.TypeDefinitionCount} type defs");
|
|
continue;
|
|
}
|
|
|
|
if (metaReg.numTypes < metadata.TypeDefinitionCount)
|
|
{
|
|
LibLogger.VerboseNewline($"\t\t\tRejecting metadata registration 0x{va:X} because it has {metaReg.numTypes} types, which is less than metadata-file-defined type def count of {metadata.TypeDefinitionCount}");
|
|
continue;
|
|
}
|
|
|
|
if (metaReg.fieldOffsetsCount != metadata.TypeDefinitionCount)
|
|
{
|
|
//If we see any cases of failing to find meta reg and this line is in verbose log, maybe the assumption (num field offsets == num type defs) is wrong.
|
|
LibLogger.VerboseNewline($"\t\t\tRejecting metadata registration 0x{va:X} because it has {metaReg.fieldOffsetsCount} field offsets, while metadata file defines {metadata.TypeDefinitionCount} type defs");
|
|
continue;
|
|
}
|
|
|
|
LibLogger.VerboseNewline($"\t\t\tAccepting metadata reg as VA 0x{va:X}");
|
|
return va;
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
LibLogger.VerboseNewline($"\t\t\tRejecting metadata registration at 0x{va:X} because it threw an exception of type {e.GetType().FullName}");
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
}
|