diff --git a/Il2CppInterop.Common/AssemblyIATHooker.cs b/Il2CppInterop.Common/AssemblyIATHooker.cs new file mode 100644 index 00000000..93b8f0dc --- /dev/null +++ b/Il2CppInterop.Common/AssemblyIATHooker.cs @@ -0,0 +1,660 @@ +using System.Runtime.InteropServices; + +// ReSharper disable BuiltInTypeReferenceStyle + + +namespace Il2CppInterop.Common +{ + + /// + /// Reads in the header information of the Portable Executable format. + /// Provides information such as the date the assembly was compiled. + /// Also implements ability to create IAT hooks + /// + internal class AssemblyIATHooker + { + #region File Header Structures + + public struct IMAGE_DOS_HEADER + { // DOS .EXE header + public UInt16 e_magic; // Magic number + public UInt16 e_cblp; // Bytes on last page of file + public UInt16 e_cp; // Pages in file + public UInt16 e_crlc; // Relocations + public UInt16 e_cparhdr; // Size of header in paragraphs + public UInt16 e_minalloc; // Minimum extra paragraphs needed + public UInt16 e_maxalloc; // Maximum extra paragraphs needed + public UInt16 e_ss; // Initial (relative) SS value + public UInt16 e_sp; // Initial SP value + public UInt16 e_csum; // Checksum + public UInt16 e_ip; // Initial IP value + public UInt16 e_cs; // Initial (relative) CS value + public UInt16 e_lfarlc; // File address of relocation table + public UInt16 e_ovno; // Overlay number + public UInt16 e_res_0; // Reserved words + public UInt16 e_res_1; // Reserved words + public UInt16 e_res_2; // Reserved words + public UInt16 e_res_3; // Reserved words + public UInt16 e_oemid; // OEM identifier (for e_oeminfo) + public UInt16 e_oeminfo; // OEM information; e_oemid specific + public UInt16 e_res2_0; // Reserved words + public UInt16 e_res2_1; // Reserved words + public UInt16 e_res2_2; // Reserved words + public UInt16 e_res2_3; // Reserved words + public UInt16 e_res2_4; // Reserved words + public UInt16 e_res2_5; // Reserved words + public UInt16 e_res2_6; // Reserved words + public UInt16 e_res2_7; // Reserved words + public UInt16 e_res2_8; // Reserved words + public UInt16 e_res2_9; // Reserved words + public UInt32 e_lfanew; // File address of new exe header + } + + [StructLayout(LayoutKind.Sequential)] + public struct IMAGE_DATA_DIRECTORY + { + public UInt32 VirtualAddress; + public UInt32 Size; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct IMAGE_OPTIONAL_HEADER32 + { + public UInt16 Magic; + public Byte MajorLinkerVersion; + public Byte MinorLinkerVersion; + public UInt32 SizeOfCode; + public UInt32 SizeOfInitializedData; + public UInt32 SizeOfUninitializedData; + public UInt32 AddressOfEntryPoint; + public UInt32 BaseOfCode; + public UInt32 BaseOfData; + public UInt32 ImageBase; + public UInt32 SectionAlignment; + public UInt32 FileAlignment; + public UInt16 MajorOperatingSystemVersion; + public UInt16 MinorOperatingSystemVersion; + public UInt16 MajorImageVersion; + public UInt16 MinorImageVersion; + public UInt16 MajorSubsystemVersion; + public UInt16 MinorSubsystemVersion; + public UInt32 Win32VersionValue; + public UInt32 SizeOfImage; + public UInt32 SizeOfHeaders; + public UInt32 CheckSum; + public UInt16 Subsystem; + public UInt16 DllCharacteristics; + public UInt32 SizeOfStackReserve; + public UInt32 SizeOfStackCommit; + public UInt32 SizeOfHeapReserve; + public UInt32 SizeOfHeapCommit; + public UInt32 LoaderFlags; + public UInt32 NumberOfRvaAndSizes; + + public IMAGE_DATA_DIRECTORY ExportTable; + public IMAGE_DATA_DIRECTORY ImportTable; + public IMAGE_DATA_DIRECTORY ResourceTable; + public IMAGE_DATA_DIRECTORY ExceptionTable; + public IMAGE_DATA_DIRECTORY CertificateTable; + public IMAGE_DATA_DIRECTORY BaseRelocationTable; + public IMAGE_DATA_DIRECTORY Debug; + public IMAGE_DATA_DIRECTORY Architecture; + public IMAGE_DATA_DIRECTORY GlobalPtr; + public IMAGE_DATA_DIRECTORY TLSTable; + public IMAGE_DATA_DIRECTORY LoadConfigTable; + public IMAGE_DATA_DIRECTORY BoundImport; + public IMAGE_DATA_DIRECTORY IAT; + public IMAGE_DATA_DIRECTORY DelayImportDescriptor; + public IMAGE_DATA_DIRECTORY CLRRuntimeHeader; + public IMAGE_DATA_DIRECTORY Reserved; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct IMAGE_OPTIONAL_HEADER64 + { + public UInt16 Magic; + public Byte MajorLinkerVersion; + public Byte MinorLinkerVersion; + public UInt32 SizeOfCode; + public UInt32 SizeOfInitializedData; + public UInt32 SizeOfUninitializedData; + public UInt32 AddressOfEntryPoint; + public UInt32 BaseOfCode; + public UInt64 ImageBase; + public UInt32 SectionAlignment; + public UInt32 FileAlignment; + public UInt16 MajorOperatingSystemVersion; + public UInt16 MinorOperatingSystemVersion; + public UInt16 MajorImageVersion; + public UInt16 MinorImageVersion; + public UInt16 MajorSubsystemVersion; + public UInt16 MinorSubsystemVersion; + public UInt32 Win32VersionValue; + public UInt32 SizeOfImage; + public UInt32 SizeOfHeaders; + public UInt32 CheckSum; + public UInt16 Subsystem; + public UInt16 DllCharacteristics; + public UInt64 SizeOfStackReserve; + public UInt64 SizeOfStackCommit; + public UInt64 SizeOfHeapReserve; + public UInt64 SizeOfHeapCommit; + public UInt32 LoaderFlags; + public UInt32 NumberOfRvaAndSizes; + + public IMAGE_DATA_DIRECTORY ExportTable; + public IMAGE_DATA_DIRECTORY ImportTable; + public IMAGE_DATA_DIRECTORY ResourceTable; + public IMAGE_DATA_DIRECTORY ExceptionTable; + public IMAGE_DATA_DIRECTORY CertificateTable; + public IMAGE_DATA_DIRECTORY BaseRelocationTable; + public IMAGE_DATA_DIRECTORY Debug; + public IMAGE_DATA_DIRECTORY Architecture; + public IMAGE_DATA_DIRECTORY GlobalPtr; + public IMAGE_DATA_DIRECTORY TLSTable; + public IMAGE_DATA_DIRECTORY LoadConfigTable; + public IMAGE_DATA_DIRECTORY BoundImport; + public IMAGE_DATA_DIRECTORY IAT; + public IMAGE_DATA_DIRECTORY DelayImportDescriptor; + public IMAGE_DATA_DIRECTORY CLRRuntimeHeader; + public IMAGE_DATA_DIRECTORY Reserved; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct IMAGE_FILE_HEADER + { + public UInt16 Machine; + public UInt16 NumberOfSections; + public UInt32 TimeDateStamp; + public UInt32 PointerToSymbolTable; + public UInt32 NumberOfSymbols; + public UInt16 SizeOfOptionalHeader; + public UInt16 Characteristics; + } + + // Grabbed the following 2 definitions from http://www.pinvoke.net/default.aspx/Structures/IMAGE_SECTION_HEADER.html + + [StructLayout(LayoutKind.Explicit)] + public struct IMAGE_SECTION_HEADER + { + [FieldOffset(0)] + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + public char[] Name; + [FieldOffset(8)] + public UInt32 VirtualSize; + [FieldOffset(12)] + public UInt32 VirtualAddress; + [FieldOffset(16)] + public UInt32 SizeOfRawData; + [FieldOffset(20)] + public UInt32 PointerToRawData; + [FieldOffset(24)] + public UInt32 PointerToRelocations; + [FieldOffset(28)] + public UInt32 PointerToLinenumbers; + [FieldOffset(32)] + public UInt16 NumberOfRelocations; + [FieldOffset(34)] + public UInt16 NumberOfLinenumbers; + [FieldOffset(36)] + public DataSectionFlags Characteristics; + + public string Section + { + get { return new string(Name); } + } + } + + [Flags] + public enum DataSectionFlags : uint + { + /// + /// Reserved for future use. + /// + TypeReg = 0x00000000, + /// + /// Reserved for future use. + /// + TypeDsect = 0x00000001, + /// + /// Reserved for future use. + /// + TypeNoLoad = 0x00000002, + /// + /// Reserved for future use. + /// + TypeGroup = 0x00000004, + /// + /// The section should not be padded to the next boundary. This flag is obsolete and is replaced by IMAGE_SCN_ALIGN_1BYTES. This is valid only for object files. + /// + TypeNoPadded = 0x00000008, + /// + /// Reserved for future use. + /// + TypeCopy = 0x00000010, + /// + /// The section contains executable code. + /// + ContentCode = 0x00000020, + /// + /// The section contains initialized data. + /// + ContentInitializedData = 0x00000040, + /// + /// The section contains uninitialized data. + /// + ContentUninitializedData = 0x00000080, + /// + /// Reserved for future use. + /// + LinkOther = 0x00000100, + /// + /// The section contains comments or other information. The .drectve section has this type. This is valid for object files only. + /// + LinkInfo = 0x00000200, + /// + /// Reserved for future use. + /// + TypeOver = 0x00000400, + /// + /// The section will not become part of the image. This is valid only for object files. + /// + LinkRemove = 0x00000800, + /// + /// The section contains COMDAT data. For more information, see section 5.5.6, COMDAT Sections (Object Only). This is valid only for object files. + /// + LinkComDat = 0x00001000, + /// + /// Reset speculative exceptions handling bits in the TLB entries for this section. + /// + NoDeferSpecExceptions = 0x00004000, + /// + /// The section contains data referenced through the global pointer (GP). + /// + RelativeGP = 0x00008000, + /// + /// Reserved for future use. + /// + MemPurgeable = 0x00020000, + /// + /// Reserved for future use. + /// + Memory16Bit = 0x00020000, + /// + /// Reserved for future use. + /// + MemoryLocked = 0x00040000, + /// + /// Reserved for future use. + /// + MemoryPreload = 0x00080000, + /// + /// Align data on a 1-byte boundary. Valid only for object files. + /// + Align1Bytes = 0x00100000, + /// + /// Align data on a 2-byte boundary. Valid only for object files. + /// + Align2Bytes = 0x00200000, + /// + /// Align data on a 4-byte boundary. Valid only for object files. + /// + Align4Bytes = 0x00300000, + /// + /// Align data on an 8-byte boundary. Valid only for object files. + /// + Align8Bytes = 0x00400000, + /// + /// Align data on a 16-byte boundary. Valid only for object files. + /// + Align16Bytes = 0x00500000, + /// + /// Align data on a 32-byte boundary. Valid only for object files. + /// + Align32Bytes = 0x00600000, + /// + /// Align data on a 64-byte boundary. Valid only for object files. + /// + Align64Bytes = 0x00700000, + /// + /// Align data on a 128-byte boundary. Valid only for object files. + /// + Align128Bytes = 0x00800000, + /// + /// Align data on a 256-byte boundary. Valid only for object files. + /// + Align256Bytes = 0x00900000, + /// + /// Align data on a 512-byte boundary. Valid only for object files. + /// + Align512Bytes = 0x00A00000, + /// + /// Align data on a 1024-byte boundary. Valid only for object files. + /// + Align1024Bytes = 0x00B00000, + /// + /// Align data on a 2048-byte boundary. Valid only for object files. + /// + Align2048Bytes = 0x00C00000, + /// + /// Align data on a 4096-byte boundary. Valid only for object files. + /// + Align4096Bytes = 0x00D00000, + /// + /// Align data on an 8192-byte boundary. Valid only for object files. + /// + Align8192Bytes = 0x00E00000, + /// + /// The section contains extended relocations. + /// + LinkExtendedRelocationOverflow = 0x01000000, + /// + /// The section can be discarded as needed. + /// + MemoryDiscardable = 0x02000000, + /// + /// The section cannot be cached. + /// + MemoryNotCached = 0x04000000, + /// + /// The section is not pageable. + /// + MemoryNotPaged = 0x08000000, + /// + /// The section can be shared in memory. + /// + MemoryShared = 0x10000000, + /// + /// The section can be executed as code. + /// + MemoryExecute = 0x20000000, + /// + /// The section can be read. + /// + MemoryRead = 0x40000000, + /// + /// The section can be written to. + /// + MemoryWrite = 0x80000000 + } + + [StructLayout(LayoutKind.Explicit)] + public struct IMAGE_IMPORT_DESCRIPTOR + { + [FieldOffset(0)] + public uint Characteristics; + + [FieldOffset(0)] + public uint OriginalFirstThunk; + + [FieldOffset(4)] + public uint TimeDateStamp; + + [FieldOffset(8)] + public uint ForwarderChain; + + [FieldOffset(12)] + public uint Name; + + [FieldOffset(16)] + public uint FirstThunk; + } + + [StructLayout(LayoutKind.Explicit)] + public struct IMAGE_THUNK_DATA + { + [FieldOffset(0)] + public IntPtr ForwarderString; + + [FieldOffset(0)] + public IntPtr Function; + + [FieldOffset(0)] + public IntPtr Ordinal; + + [FieldOffset(0)] + public IntPtr AddressOfData; + } + + public unsafe struct HookData + { + public IMAGE_THUNK_DATA* thunk; + public IntPtr originalFunction; + + public HookData(IMAGE_THUNK_DATA* thunk) + { + this.thunk = thunk; + originalFunction = thunk->Function; + } + } + + #endregion File Header Structures + + #region Private Fields + + private IntPtr imageBase; + + /// + /// The DOS header + /// + private IMAGE_DOS_HEADER dosHeader; + /// + /// The file header + /// + private IMAGE_FILE_HEADER fileHeader; + /// + /// Optional 32 bit file header + /// + private IMAGE_OPTIONAL_HEADER32 optionalHeader32; + /// + /// Optional 64 bit file header + /// + private IMAGE_OPTIONAL_HEADER64 optionalHeader64; + /// + /// Image Section headers. Number of sections is in the file header. + /// + private IMAGE_SECTION_HEADER[] imageSectionHeaders; + + private Dictionary, HookData> hookThunks = new Dictionary, HookData>(); + + #endregion Private Fields + + #region Public Methods + + public unsafe AssemblyIATHooker(IntPtr imageBase) + { + this.imageBase = imageBase; + var stream = new UnmanagedMemoryStream((byte*)imageBase, 5000, 5000, FileAccess.ReadWrite); + Create(stream); + } + + private void Create(Stream stream) + { + BinaryReader reader = new BinaryReader(stream); + dosHeader = FromBinaryReader(reader); + + // Add 4 bytes to the offset + stream.Seek(dosHeader.e_lfanew, SeekOrigin.Begin); + + UInt32 ntHeadersSignature = reader.ReadUInt32(); + fileHeader = FromBinaryReader(reader); + if (Is32BitHeader) + { + optionalHeader32 = FromBinaryReader(reader); + } + else + { + optionalHeader64 = FromBinaryReader(reader); + } + + imageSectionHeaders = new IMAGE_SECTION_HEADER[fileHeader.NumberOfSections]; + for (int headerNo = 0; headerNo < imageSectionHeaders.Length; ++headerNo) + { + imageSectionHeaders[headerNo] = FromBinaryReader(reader); + } + } + + /// + /// Reads in a block from a file and converts it to the struct + /// type specified by the template parameter + /// + /// + /// + /// + public static T FromBinaryReader(BinaryReader reader) + { + // Read in a byte array + byte[] bytes = reader.ReadBytes(Marshal.SizeOf(typeof(T))); + + // Pin the managed memory while, copy it out the data, then unpin it + GCHandle handle = GCHandle.Alloc(bytes, GCHandleType.Pinned); + T theStructure = (T)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(T)); + handle.Free(); + + return theStructure; + } + + /// + /// Gets if the file header is 32 bit or not + /// + public bool Is32BitHeader + { + get + { + UInt16 IMAGE_FILE_32BIT_MACHINE = 0x0100; + return (IMAGE_FILE_32BIT_MACHINE & FileHeader.Characteristics) == IMAGE_FILE_32BIT_MACHINE; + } + } + + /// + /// Gets the file header + /// + public IMAGE_FILE_HEADER FileHeader + { + get + { + return fileHeader; + } + } + + /// + /// Gets the optional header + /// + public IMAGE_OPTIONAL_HEADER32 OptionalHeader32 + { + get + { + return optionalHeader32; + } + } + + /// + /// Gets the optional header + /// + public IMAGE_OPTIONAL_HEADER64 OptionalHeader64 + { + get + { + return optionalHeader64; + } + } + + public IMAGE_SECTION_HEADER[] ImageSectionHeaders + { + get + { + return imageSectionHeaders; + } + } + + public IMAGE_DATA_DIRECTORY ImportsDirectory + { + get + { + if (Is32BitHeader) + { + return optionalHeader32.ImportTable; + } + + return optionalHeader64.ImportTable; + } + } + + public enum ProtectMode + { + PAGE_NOACCESS = 0x1, + PAGE_READONLY = 0x2, + PAGE_READWRITE = 0x4, + PAGE_WRITECOPY = 0x8, + PAGE_EXECUTE = 0x10, + PAGE_EXECUTE_READ = 0x20, + PAGE_EXECUTE_READWRITE = 0x40, + PAGE_EXECUTE_WRITECOPY = 0x80 + } + + [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)] + private static extern bool VirtualProtect(IntPtr lpAddress, IntPtr dwSize, uint flNewProtect, out uint lpflOldProtect); + + private static bool VirtualProtect(IntPtr lpAdress, int dwSize, ProtectMode flNewProtect, out ProtectMode lpflOldProtect) + { + bool result = VirtualProtect(lpAdress, (IntPtr)dwSize, (uint)flNewProtect, out uint oldProtect); + lpflOldProtect = (ProtectMode)oldProtect; + return result; + } + + public unsafe delegate void ApplyAction(IMAGE_THUNK_DATA* thunk); + + public unsafe void CreateIATHook(string targetLibrary, string targetFunction, ApplyAction onFound) + { + var importDescriptor = (IMAGE_IMPORT_DESCRIPTOR*)(imageBase + (int)ImportsDirectory.VirtualAddress); + + while (importDescriptor->Name != 0) + { + var libraryNamePtr = imageBase + (int)importDescriptor->Name; + var libraryName = Marshal.PtrToStringAnsi(libraryNamePtr); + + if (libraryName.Equals(targetLibrary)) + { + var originalFirstThunk = (IMAGE_THUNK_DATA*)(imageBase + (int)importDescriptor->OriginalFirstThunk); + var firstThunk = (IMAGE_THUNK_DATA*)(imageBase + (int)importDescriptor->FirstThunk); + + while (originalFirstThunk->AddressOfData != IntPtr.Zero) + { + var functionNamePtr = (imageBase + (int)originalFirstThunk->AddressOfData); + var functionName = Marshal.PtrToStringAnsi(functionNamePtr + 2); + + if (functionName.Equals(targetFunction)) + { + IntPtr address = (IntPtr)(&firstThunk->Function); + VirtualProtect(address, 8, ProtectMode.PAGE_READWRITE, out ProtectMode oldProtect); + var key = new Tuple(targetLibrary, targetFunction); + + if (!hookThunks.ContainsKey(key)) + hookThunks.Add(key, new HookData(firstThunk)); + onFound.Invoke(firstThunk); + break; + } + ++originalFirstThunk; + ++firstThunk; + } + break; + } + + importDescriptor++; + } + } + + public unsafe void UnpatchIATHook(string targetLibrary, string targetFunction) + { + var key = new Tuple(targetLibrary, targetFunction); + if (hookThunks.ContainsKey(key)) + { + HookData hook = hookThunks[key]; + hook.thunk->Function = hook.originalFunction; + hookThunks.Remove(key); + } + } + + #endregion Properties + } +} diff --git a/Il2CppInterop.Runtime/IL2CPP.cs b/Il2CppInterop.Runtime/IL2CPP.cs index 7604d9c0..04449ad5 100644 --- a/Il2CppInterop.Runtime/IL2CPP.cs +++ b/Il2CppInterop.Runtime/IL2CPP.cs @@ -9,6 +9,7 @@ using System.Text.RegularExpressions; using Il2CppInterop.Common; using Il2CppInterop.Common.Attributes; +using Il2CppInterop.Runtime.Injection; using Il2CppInterop.Runtime.InteropTypes; using Il2CppInterop.Runtime.InteropTypes.Arrays; using Il2CppInterop.Runtime.Runtime; @@ -18,7 +19,7 @@ namespace Il2CppInterop.Runtime; public static unsafe class IL2CPP { - private static readonly Dictionary ourImagesMap = new(); + private static readonly Dictionary il2CppImagesMap = new(); static IL2CPP() { @@ -35,24 +36,24 @@ static IL2CPP() { var image = il2cpp_assembly_get_image(assemblies[i]); var name = Marshal.PtrToStringAnsi(il2cpp_image_get_name(image)); - ourImagesMap[name] = image; + il2CppImagesMap[name] = image; } } internal static IntPtr GetIl2CppImage(string name) { - if (ourImagesMap.ContainsKey(name)) return ourImagesMap[name]; + if (il2CppImagesMap.ContainsKey(name)) return il2CppImagesMap[name]; return IntPtr.Zero; } internal static IntPtr[] GetIl2CppImages() { - return ourImagesMap.Values.ToArray(); + return il2CppImagesMap.Values.ToArray(); } public static IntPtr GetIl2CppClass(string assemblyName, string namespaze, string className) { - if (!ourImagesMap.TryGetValue(assemblyName, out var image)) + if (!il2CppImagesMap.TryGetValue(assemblyName, out var image)) { Logger.Instance.LogError("Assembly {AssemblyName} is not registered in il2cpp", assemblyName); return IntPtr.Zero; diff --git a/Il2CppInterop.Runtime/Injection/AssemblyInjectorComponent.cs b/Il2CppInterop.Runtime/Injection/AssemblyInjectorComponent.cs new file mode 100644 index 00000000..843edce9 --- /dev/null +++ b/Il2CppInterop.Runtime/Injection/AssemblyInjectorComponent.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using Il2CppInterop.Common.Host; +using Il2CppInterop.Runtime.Injection; + +namespace Il2CppInterop.Runtime +{ + internal static class AssemblyListComponentExtensions + { + public static T AddAssemblyInjector(this T host) + where T : BaseHost + where TProvider : IAssemblyListProvider, new() + { + host.AddComponent(new AssemblyInjectorComponent(new TProvider())); + return host; + } + } + + public interface IAssemblyListProvider + { + public IEnumerable GetAssemblyList(); + } + + public class AssemblyInjectorComponent : IHostComponent + { + private static IAssemblyListProvider s_assemblyListProvider; + + public AssemblyInjectorComponent(IAssemblyListProvider assemblyListProvider) => s_assemblyListProvider = assemblyListProvider; + + public static IEnumerable ModAssemblies + { + get + { + if (s_assemblyListProvider == null) + { + throw new InvalidOperationException("Mod Assembly Injector is not initialized! Initialize the host before using Mod Assembly Injector!"); + } + + return s_assemblyListProvider.GetAssemblyList(); + } + } + + public void Dispose() + { + s_assemblyListProvider = null; + } + + public void Start() + { + InjectorHelpers.EarlySetup(); + } + } +} diff --git a/Il2CppInterop.Runtime/Injection/AssemblyList/GameManagersAssemblyListFile.cs b/Il2CppInterop.Runtime/Injection/AssemblyList/GameManagersAssemblyListFile.cs new file mode 100644 index 00000000..b18c5e38 --- /dev/null +++ b/Il2CppInterop.Runtime/Injection/AssemblyList/GameManagersAssemblyListFile.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; + +namespace Il2CppInterop.Runtime.Injection +{ + internal class GameManagersAssemblyListFile : IAssemblyListFile + { + private List _assemblies = new List(); + private string originalFile; + private string newFile; + + public bool IsTargetFile(string originalFilePath) + { + return originalFilePath.Contains("globalgamemanagers"); + } + + public void Setup(string originalFilePath) + { + if (originalFile != null) return; + originalFile = originalFilePath; + } + + public void AddAssembly(string name) + { + _assemblies.Add(name); + } + + public string GetOrCreateNewFile() + { + if (newFile != null) return newFile; + + newFile = Path.GetTempFileName(); + CreateModifiedFile(); + return newFile; + } + + private void CreateModifiedFile() + { + using var outputStream = File.Open(newFile, FileMode.Create); + using var outputWriter = new BinaryWriter(outputStream, Encoding.ASCII, false); + using var inputStream = File.Open(originalFile, FileMode.Open); + using var inputReader = new BinaryReader(inputStream, Encoding.ASCII, false); + + // Assembly list always starts with UnityEngine.dll + var startPos = SeekFirstName(inputStream, inputReader); + if (startPos == -1) + { + throw new Exception("Failed to find start of assembly list in globalgamemanagers file!"); + } + + inputStream.Position = 0; + startPos -= 8; + + for (var i = 0; i < startPos; i++) + { + outputWriter.Write(inputReader.ReadByte()); + } + + var assemblyCount = inputReader.ReadInt32(); + List newAssemblyList = new List(assemblyCount + _assemblies.Count); + List newAssemblyTypes = new List(assemblyCount + _assemblies.Count); + for (var i = 0; i < assemblyCount; i++) + { + newAssemblyList.Add(ReadString(inputReader)); + } + + assemblyCount = inputReader.ReadInt32(); + for (var i = 0; i < assemblyCount; i++) + { + newAssemblyTypes.Add(inputReader.ReadInt32()); + } + + newAssemblyList.AddRange(_assemblies); + newAssemblyTypes.AddRange(_assemblies.Select(_ => 16)); + + outputWriter.Write(newAssemblyList.Count); + foreach (var assemblyName in newAssemblyList) + { + WriteString(outputWriter, assemblyName); + } + + outputWriter.Write(newAssemblyTypes.Count); + foreach (var assemblyType in newAssemblyTypes) + { + outputWriter.Write(assemblyType); + } + + while (inputStream.Position < inputStream.Length) + { + outputWriter.Write(inputReader.ReadByte()); + } + } + + private static void WriteString(BinaryWriter outputWriter, string @string) + { + outputWriter.Write(@string.Length); + var paddedLenth = (int)(Math.Ceiling(@string.Length / 4f) * 4f); + for (int i = 0; i < paddedLenth; i++) + { + if (i < @string.Length) + outputWriter.Write(@string[i]); + else + outputWriter.Write((byte)0); + } + } + + private static string ReadString(BinaryReader inputReader) + { + var length = inputReader.ReadInt32(); + var paddedLenth = (int)(Math.Ceiling(length / 4f) * 4f); + StringBuilder sb = new StringBuilder(length); + for (var j = 0; j < paddedLenth; j++) + { + var c = inputReader.ReadChar(); + if (j < length) + sb.Append(c); + } + + return sb.ToString(); + } + + private static long SeekFirstName(FileStream inputStream, BinaryReader inputReader) + { + while (inputStream.Position < inputStream.Length) + { + var currentPos = inputStream.Position; + var firstChar = inputReader.ReadChar(); + if (firstChar != 'U') continue; + + var nextString = new string(inputReader.ReadChars(14)); + if (!nextString.Equals("nityEngine.dll")) continue; + + return currentPos; + } + + return -1; + } + } +} diff --git a/Il2CppInterop.Runtime/Injection/AssemblyList/IAssemblyListFile.cs b/Il2CppInterop.Runtime/Injection/AssemblyList/IAssemblyListFile.cs new file mode 100644 index 00000000..d46251c0 --- /dev/null +++ b/Il2CppInterop.Runtime/Injection/AssemblyList/IAssemblyListFile.cs @@ -0,0 +1,10 @@ +namespace Il2CppInterop.Runtime.Injection +{ + internal interface IAssemblyListFile + { + public bool IsTargetFile(string originalFilePath); + public void Setup(string originalFilePath); + public void AddAssembly(string name); + public string GetOrCreateNewFile(); + } +} diff --git a/Il2CppInterop.Runtime/Injection/AssemblyList/JSONAssemblyListFile.cs b/Il2CppInterop.Runtime/Injection/AssemblyList/JSONAssemblyListFile.cs new file mode 100644 index 00000000..9485e23d --- /dev/null +++ b/Il2CppInterop.Runtime/Injection/AssemblyList/JSONAssemblyListFile.cs @@ -0,0 +1,46 @@ +using System; +using System.IO; +using System.Text.Json.Nodes; + +namespace Il2CppInterop.Runtime.Injection +{ + internal class JSONAssemblyListFile : IAssemblyListFile + { + private JsonNode node; + private JsonArray names; + private JsonArray types; + + private string newFile; + + public void Setup(string originalFilePath) + { + if (node != null) return; + + node = JsonNode.Parse(File.ReadAllText(originalFilePath)); + names = node["names"].AsArray(); + types = node["types"].AsArray(); + } + + public bool IsTargetFile(string originalFilePath) + { + return originalFilePath.Contains("ScriptingAssemblies.json"); + } + + public void AddAssembly(string name) + { + names.Add(name); + types.Add(16); + } + + public string GetOrCreateNewFile() + { + if (!string.IsNullOrEmpty(newFile)) return newFile; + + var newJson = node.ToJsonString(); + newFile = Path.GetTempFileName(); + + File.WriteAllText(newFile, newJson); + return newFile; + } + } +} diff --git a/Il2CppInterop.Runtime/Injection/ClassInjector.cs b/Il2CppInterop.Runtime/Injection/ClassInjector.cs index 6db08110..2700c53d 100644 --- a/Il2CppInterop.Runtime/Injection/ClassInjector.cs +++ b/Il2CppInterop.Runtime/Injection/ClassInjector.cs @@ -223,7 +223,13 @@ public static void RegisterTypeInIl2Cpp(Type type, RegisterTypeOptions options) var interfaceFunctionCount = interfaces.Sum(i => i.MethodCount); var classPointer = UnityVersionHandler.NewClass(baseClassPointer.VtableCount + interfaceFunctionCount); - classPointer.Image = InjectorHelpers.InjectedImage.ImagePointer; + classPointer.Image = InjectorHelpers.DefaultInjectedImage.ImagePointer; + + if (InjectorHelpers.TryGetInjectedImageForAssembly(type.Assembly, out var imagePtr)) + { + classPointer.Image = (Il2CppImage*)imagePtr; + } + classPointer.Parent = baseClassPointer.ClassPointer; classPointer.ElementClass = classPointer.Class = classPointer.CastClass = classPointer.ClassPointer; classPointer.NativeSize = -1; diff --git a/Il2CppInterop.Runtime/Injection/EnumInjector.cs b/Il2CppInterop.Runtime/Injection/EnumInjector.cs index 71a58ca9..d513ef30 100644 --- a/Il2CppInterop.Runtime/Injection/EnumInjector.cs +++ b/Il2CppInterop.Runtime/Injection/EnumInjector.cs @@ -206,7 +206,13 @@ public static void RegisterEnumInIl2Cpp(Type type, bool logSuccess = true) UnityVersionHandler.Wrap( (Il2CppClass*)Il2CppClassPointerStore.GetNativeClassPointer(Enum.GetUnderlyingType(type))); - il2cppEnum.Image = InjectorHelpers.InjectedImage.ImagePointer; + il2cppEnum.Image = InjectorHelpers.DefaultInjectedImage.ImagePointer; + + if (InjectorHelpers.TryGetInjectedImageForAssembly(type.Assembly, out var imagePtr)) + { + il2cppEnum.Image = (Il2CppImage*)imagePtr; + } + il2cppEnum.Class = il2cppEnum.CastClass = il2cppEnum.ElementClass = elementClass.ClassPointer; il2cppEnum.Parent = baseEnum.ClassPointer; il2cppEnum.ActualSize = il2cppEnum.InstanceSize = diff --git a/Il2CppInterop.Runtime/Injection/Hooks/API_il2cpp_domain_get_assemblies_hook.cs b/Il2CppInterop.Runtime/Injection/Hooks/API_il2cpp_domain_get_assemblies_hook.cs new file mode 100644 index 00000000..09b506d9 --- /dev/null +++ b/Il2CppInterop.Runtime/Injection/Hooks/API_il2cpp_domain_get_assemblies_hook.cs @@ -0,0 +1,67 @@ +using System; +using System.Runtime.InteropServices; +using Il2CppInterop.Runtime.Runtime; + +namespace Il2CppInterop.Runtime.Injection.Hooks +{ + internal unsafe class API_il2cpp_domain_get_assemblies_hook : Hook + { + public override MethodDelegate GetDetour() => Hook; + public override string TargetMethodName => "il2cpp_domain_get_assemblies"; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate IntPtr MethodDelegate(IntPtr domain, long* size); + + private IntPtr currentDataPtr = IntPtr.Zero; + private int lastAssemblyListSize = -1; + + private IntPtr Hook(IntPtr domain, long* size) + { + IntPtr assemblyArrayPtr = Original(domain, size); + + if (InjectorHelpers.InjectedImages.Count > 0) + { + CreateCustomAssemblyList((int)*size, assemblyArrayPtr); + + *size = lastAssemblyListSize; + return currentDataPtr; + } + + return assemblyArrayPtr; + } + + private void CreateCustomAssemblyList(int origSize, IntPtr assemblyArrayPtr) + { + var newSize = origSize + InjectorHelpers.InjectedImages.Count; + if (lastAssemblyListSize == newSize) return; + + Il2CppAssembly** oldArray = (Il2CppAssembly**)assemblyArrayPtr; + + if (currentDataPtr != IntPtr.Zero) + Marshal.FreeHGlobal(currentDataPtr); + + currentDataPtr = Marshal.AllocHGlobal(newSize * sizeof(Il2CppSystem.IntPtr)); + Il2CppAssembly** newArray = (Il2CppAssembly**)currentDataPtr; + + int i; + + for (i = 0; i < origSize; i++) + newArray[i] = oldArray[i]; + + i = origSize; + foreach (IntPtr imagePtr in InjectorHelpers.InjectedImages.Values) + { + var image = UnityVersionHandler.Wrap((Il2CppImage*)imagePtr); + newArray[i] = image.Assembly; + i++; + } + + lastAssemblyListSize = newSize; + } + + public override IntPtr FindTargetMethod() + { + return InjectorHelpers.GetIl2CppExport(nameof(IL2CPP.il2cpp_domain_get_assemblies)); + } + } +} diff --git a/Il2CppInterop.Runtime/Injection/Hooks/AppDomain_GetAssemblies_Hook.cs b/Il2CppInterop.Runtime/Injection/Hooks/AppDomain_GetAssemblies_Hook.cs new file mode 100644 index 00000000..8ae7fc13 --- /dev/null +++ b/Il2CppInterop.Runtime/Injection/Hooks/AppDomain_GetAssemblies_Hook.cs @@ -0,0 +1,75 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using Il2CppInterop.Common; +using Il2CppInterop.Common.Extensions; +using Il2CppInterop.Common.XrefScans; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using Il2CppInterop.Runtime.Runtime; +using Il2CppInterop.Runtime.Startup; +using Microsoft.Extensions.Logging; +using Assembly = Il2CppSystem.Reflection.Assembly; + +namespace Il2CppInterop.Runtime.Injection.Hooks +{ + internal unsafe class AppDomain_GetAssemblies_Hook : Hook + { + public override MethodDelegate GetDetour() => Hook; + public override string TargetMethodName => "AppDomain::GetAssemblies"; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate IntPtr MethodDelegate(IntPtr thisPtr, byte refOnly); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate IntPtr GetAssemblyObject(IntPtr thisPtr); + + private GetAssemblyObject getAssemblyObjectDelegate; + + private IntPtr Hook(IntPtr thisPtr, byte refOnly) + { + IntPtr assemblyArrayPtr = Original(thisPtr, refOnly); + Il2CppReferenceArray assemblyArray = new Il2CppReferenceArray(assemblyArrayPtr); + + if (InjectorHelpers.InjectedImages.Count > 0) + { + var newSize = assemblyArray.Length + InjectorHelpers.InjectedImages.Count; + Il2CppReferenceArray newAssemblyArray = new Il2CppReferenceArray(newSize); + int i; + + for (i = 0; i < assemblyArray.Length; i++) + newAssemblyArray[i] = assemblyArray[i]; + + i = assemblyArray.Length; + foreach (IntPtr imagePtr in InjectorHelpers.InjectedImages.Values) + { + var image = UnityVersionHandler.Wrap((Il2CppImage*)imagePtr); + newAssemblyArray[i] = new Assembly(getAssemblyObjectDelegate((IntPtr)image.Assembly)); + i++; + } + + return newAssemblyArray.Pointer; + } + + return assemblyArrayPtr; + } + + public override IntPtr FindTargetMethod() + { + var appDomain = InjectorHelpers.Il2CppMscorlib.GetTypesSafe().SingleOrDefault((x) => x.Name is "AppDomain"); + if (appDomain == null) + throw new Exception($"Unity {Il2CppInteropRuntime.Instance.UnityVersion} is not supported at the moment: System.AppDomain isn't present in Il2Cppmscorlib.dll for unity version, unable to fetch icall"); + + var GetAssembliesThunk = InjectorHelpers.GetIl2CppMethodPointer(appDomain.GetMethod("GetAssemblies", unchecked((BindingFlags)0xffffffff), new[] { typeof(bool) })); + Logger.Instance.LogTrace("Il2CppSystem.AppDomain::thunk_GetAssemblies: 0x{GetAssembliesThunk}", GetAssembliesThunk.ToInt64().ToString("X2")); + + var myMethod = XrefScannerLowLevel.JumpTargets(GetAssembliesThunk).Single(); + var getAssemblyObject = XrefScannerLowLevel.JumpTargets(myMethod).SkipLast(1).Last(); + Logger.Instance.LogTrace("Reflection::GetAssemblyObject: 0x{getAssemblyObject}", getAssemblyObject.ToInt64().ToString("X2")); + + getAssemblyObjectDelegate = Marshal.GetDelegateForFunctionPointer(getAssemblyObject); + + return myMethod; + } + } +} diff --git a/Il2CppInterop.Runtime/Injection/Hooks/Assembly_GetLoadedAssembly_Hook.cs b/Il2CppInterop.Runtime/Injection/Hooks/Assembly_GetLoadedAssembly_Hook.cs new file mode 100644 index 00000000..246a9255 --- /dev/null +++ b/Il2CppInterop.Runtime/Injection/Hooks/Assembly_GetLoadedAssembly_Hook.cs @@ -0,0 +1,54 @@ +using System; +using System.Linq; +using System.Runtime.InteropServices; +using Il2CppInterop.Common; +using Il2CppInterop.Common.Extensions; +using Il2CppInterop.Common.XrefScans; +using Il2CppInterop.Runtime.Runtime; +using Il2CppInterop.Runtime.Startup; +using Il2CppSystem.Reflection; +using Microsoft.Extensions.Logging; + +namespace Il2CppInterop.Runtime.Injection.Hooks +{ + internal unsafe class Assembly_GetLoadedAssembly_Hook : Hook + { + public override MethodDelegate GetDetour() => Hook; + public override string TargetMethodName => "Assembly::GetLoadedAssembly"; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate Il2CppAssembly* MethodDelegate(IntPtr name); + + private Il2CppAssembly* Hook(IntPtr name) + { + var assemblyName = Marshal.PtrToStringAnsi(name); + Il2CppAssembly* assembly = Original(name); + + if (assembly == null) + { + if (InjectorHelpers.TryGetInjectedImage(assemblyName, out var ptr)) + { + var image = UnityVersionHandler.Wrap((Il2CppImage*)ptr); + assembly = image.Assembly; + } + } + + return assembly; + } + + public override IntPtr FindTargetMethod() + { + var assembly = InjectorHelpers.Il2CppMscorlib.GetTypesSafe().SingleOrDefault((x) => x.Name is "Assembly"); + if (assembly == null) + throw new Exception($"Unity {Il2CppInteropRuntime.Instance.UnityVersion} is not supported at the moment: System.Reflection.Assembly isn't present in Il2Cppmscorlib.dll for unity version, unable to fetch icall"); + + var loadWithPartialNameThunk = InjectorHelpers.GetIl2CppMethodPointer(assembly.GetMethod(nameof(Assembly.load_with_partial_name))); + Logger.Instance.LogTrace("Il2CppSystem.Reflection.Assembly::thunk_load_with_partial_name: 0x{loadWithPartialNameThunk}", loadWithPartialNameThunk.ToInt64().ToString("X2")); + + var loadWithPartialName = XrefScannerLowLevel.JumpTargets(loadWithPartialNameThunk).Last(); + Logger.Instance.LogTrace("Il2CppSystem.Reflection.Assembly::load_with_partial_name: 0x{loadWithPartialName}", loadWithPartialName.ToInt64().ToString("X2")); + + return XrefScannerLowLevel.JumpTargets(loadWithPartialName).ElementAt(1); + } + } +} diff --git a/Il2CppInterop.Runtime/Injection/Hooks/Assembly_Load_Hook.cs b/Il2CppInterop.Runtime/Injection/Hooks/Assembly_Load_Hook.cs new file mode 100644 index 00000000..0c2e1fec --- /dev/null +++ b/Il2CppInterop.Runtime/Injection/Hooks/Assembly_Load_Hook.cs @@ -0,0 +1,47 @@ +using System; +using System.Linq; +using System.Runtime.InteropServices; +using Il2CppInterop.Common; +using Il2CppInterop.Common.XrefScans; +using Il2CppInterop.Runtime.Runtime; +using Microsoft.Extensions.Logging; + +namespace Il2CppInterop.Runtime.Injection.Hooks +{ + internal unsafe class Assembly_Load_Hook : Hook + { + public override MethodDelegate GetDetour() => Hook; + public override string TargetMethodName => "Assembly::Load"; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate Il2CppAssembly* MethodDelegate(IntPtr name); + + private Il2CppAssembly* Hook(IntPtr name) + { + Il2CppAssembly* assembly = Original(name); + InjectorHelpers.UnpatchIATHooks(); + var assemblyName = Marshal.PtrToStringAnsi(name); + + Logger.Instance.LogInformation($"Assembly::Load {assemblyName}"); + if (assembly == null) + { + if (InjectorHelpers.TryGetInjectedImage(assemblyName, out var ptr)) + { + var image = UnityVersionHandler.Wrap((Il2CppImage*)ptr); + assembly = image.Assembly; + } + } + + return assembly; + } + + public override IntPtr FindTargetMethod() + { + var domainAssemblyOpenPtr = InjectorHelpers.GetIl2CppExport(nameof(IL2CPP.il2cpp_domain_assembly_open)); + Logger.Instance.LogTrace("il2cpp_domain_assembly_open: 0x{domainAssemblyOpenPtr}", domainAssemblyOpenPtr.ToInt64().ToString("X2")); + + return XrefScannerLowLevel.JumpTargets(domainAssemblyOpenPtr).Single(); + } + } +} + diff --git a/Il2CppInterop.Runtime/Injection/Hooks/Class_FromName_Hook.cs b/Il2CppInterop.Runtime/Injection/Hooks/Class_FromName_Hook.cs index 09431c93..d87c75bf 100644 --- a/Il2CppInterop.Runtime/Injection/Hooks/Class_FromName_Hook.cs +++ b/Il2CppInterop.Runtime/Injection/Hooks/Class_FromName_Hook.cs @@ -24,6 +24,7 @@ internal unsafe class Class_FromName_Hook : Hook InjectedImages = new Dictionary(); + internal static INativeImageStruct DefaultInjectedImage; + internal static ProcessModule Il2CppModule = Process.GetCurrentProcess() .Modules.OfType() .Single((x) => x.ModuleName is "GameAssembly.dll" or "GameAssembly.so" or "UserAssembly.dll"); internal static IntPtr Il2CppHandle = NativeLibrary.Load("GameAssembly", typeof(InjectorHelpers).Assembly, null); + internal static IntPtr UnityPlayerHandle = NativeLibrary.Load("UnityPlayer", typeof(InjectorHelpers).Assembly, null); + internal static AssemblyIATHooker UnityPlayerIATHooker; + internal static IAssemblyListFile AssemblyListFile; internal static readonly Dictionary StIndOpcodes = new() { @@ -48,18 +52,52 @@ internal static unsafe class InjectorHelpers [typeof(double)] = OpCodes.Stind_R8 }; - private static void CreateInjectedAssembly() + private static void CreateDefaultInjectedAssembly() + { + DefaultInjectedImage ??= CreateInjectedImage(InjectedMonoTypesAssemblyName); + } + + private static INativeImageStruct CreateInjectedImage(string name) { - InjectedAssembly = UnityVersionHandler.NewAssembly(); - InjectedImage = UnityVersionHandler.NewImage(); + Logger.Instance.LogTrace($"Creating injected assembly {name}"); + var assembly = UnityVersionHandler.NewAssembly(); + var image = UnityVersionHandler.NewImage(); + + var nameNoExt = name; + if (nameNoExt.EndsWith(".dll")) + nameNoExt = name.Replace(".dll", ""); - InjectedAssembly.Name.Name = Marshal.StringToHGlobalAnsi("InjectedMonoTypes"); + assembly.Name.Name = Marshal.StringToHGlobalAnsi(nameNoExt); - InjectedImage.Assembly = InjectedAssembly.AssemblyPointer; - InjectedImage.Dynamic = 1; - InjectedImage.Name = InjectedAssembly.Name.Name; - if (InjectedImage.HasNameNoExt) - InjectedImage.NameNoExt = InjectedAssembly.Name.Name; + image.Assembly = assembly.AssemblyPointer; + image.Dynamic = 1; + image.Name = Marshal.StringToHGlobalAnsi(name); + if (image.HasNameNoExt) + image.NameNoExt = assembly.Name.Name; + + assembly.Image = image.ImagePointer; + InjectedImages.Add(name, image.Pointer); + return image; + } + + internal static bool TryGetInjectedImage(string name, out IntPtr ptr) + { + if (!name.EndsWith(".dll")) + name += ".dll"; + return InjectedImages.TryGetValue(name, out ptr); + } + + internal static bool TryGetInjectedImageForAssembly(Assembly assembly, out IntPtr ptr) + { + return TryGetInjectedImage(assembly.GetName().Name, out ptr); + } + + internal static IntPtr GetOrCreateInjectedImage(string name) + { + if (TryGetInjectedImage(name, out var ptr)) + return ptr; + + return CreateInjectedImage(name).Pointer; } private static readonly GenericMethod_GetMethod_Hook GenericMethodGetMethodHook = new(); @@ -69,9 +107,15 @@ private static void CreateInjectedAssembly() private static readonly Class_FromName_Hook FromNameHook = new(); private static readonly GarbageCollector_RunFinalizer_Patch RunFinalizerPatch = new(); + private static readonly Assembly_Load_Hook assemblyLoadHook = new(); + private static readonly API_il2cpp_domain_get_assemblies_hook api_get_assemblies = new(); + + private static readonly Assembly_GetLoadedAssembly_Hook AssemblyGetLoadedAssemblyHook = new(); + private static readonly AppDomain_GetAssemblies_Hook AppDomainGetAssembliesHook = new(); + internal static void Setup() { - if (InjectedAssembly == null) CreateInjectedAssembly(); + CreateDefaultInjectedAssembly(); GenericMethodGetMethodHook.ApplyHook(); GetTypeInfoFromTypeDefinitionIndexHook.ApplyHook(); GetFieldDefaultValueHook.ApplyHook(); @@ -79,6 +123,131 @@ internal static void Setup() FromIl2CppTypeHook.ApplyHook(); FromNameHook.ApplyHook(); RunFinalizerPatch.ApplyHook(); + + //AssemblyGetLoadedAssemblyHook.ApplyHook(); + //AppDomainGetAssembliesHook.ApplyHook(); + } + + [StructLayout(LayoutKind.Sequential)] + public struct WIN32_FILE_ATTRIBUTE_DATA + { + public FileAttributes dwFileAttributes; + public System.Runtime.InteropServices.ComTypes.FILETIME ftCreationTime; + public System.Runtime.InteropServices.ComTypes.FILETIME ftLastAccessTime; + public System.Runtime.InteropServices.ComTypes.FILETIME ftLastWriteTime; + public uint nFileSizeHigh; + public uint nFileSizeLow; + } + + [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + static extern uint GetFinalPathNameByHandle(IntPtr hFile, [MarshalAs(UnmanagedType.LPTStr)] StringBuilder lpszFilePath, uint cchFilePath, uint dwFlags); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + public static extern IntPtr CreateFile( + [MarshalAs(UnmanagedType.LPTStr)] string filename, + [MarshalAs(UnmanagedType.U4)] FileAccess access, + [MarshalAs(UnmanagedType.U4)] FileShare share, + IntPtr securityAttributes, // optional SECURITY_ATTRIBUTES struct or IntPtr.Zero + [MarshalAs(UnmanagedType.U4)] FileMode creationDisposition, + [MarshalAs(UnmanagedType.U4)] FileAttributes flagsAndAttributes, + IntPtr templateFile); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate int d_GetFileAttributesEx(IntPtr lpFileName, int fInfoLevelId, IntPtr lpFileInformation); + + internal static d_GetFileAttributesEx GetFileAttributesEx; + + private static int GetFileAttributesExDetour(IntPtr lpFileName, int fInfoLevelId, IntPtr lpFileInformation) + { + var filePath = Marshal.PtrToStringUni(lpFileName); + filePath = filePath.Replace(@"\\?\", ""); + + if (AssemblyListFile.IsTargetFile(filePath)) + { + AssemblyListFile.Setup(filePath); + foreach (var assemblyName in InjectedImages.Keys) + { + AssemblyListFile.AddAssembly(assemblyName); + } + + var newFile = AssemblyListFile.GetOrCreateNewFile(); + Logger.Instance.LogInformation($"Forcing unity to read assembly list from {newFile}"); + var newlpFileName = Marshal.StringToHGlobalUni(newFile); + + var result = GetFileAttributesEx(newlpFileName, fInfoLevelId, lpFileInformation); + Marshal.FreeHGlobal(newlpFileName); + return result; + } + + return GetFileAttributesEx(lpFileName, fInfoLevelId, lpFileInformation); + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate int d_ReadFile(IntPtr handle, IntPtr bytes, uint numBytesToRead, IntPtr numBytesRead, NativeOverlapped* overlapped); + + internal static d_ReadFile ReadFile; + + private static int ReadFileDetour(IntPtr handle, IntPtr bytes, uint numBytesToRead, IntPtr numBytesRead, NativeOverlapped* overlapped) + { + var sb = new StringBuilder(1024); + var res = GetFinalPathNameByHandle(handle, sb, 1024, 0); + + if (res != 0) + { + var filePath = sb.ToString(); + if (AssemblyListFile.IsTargetFile(filePath)) + { + IntPtr newHandle = CreateFile(AssemblyListFile.GetOrCreateNewFile(), FileAccess.Read, FileShare.Read, IntPtr.Zero, FileMode.Open, FileAttributes.Normal, IntPtr.Zero); + return ReadFile(newHandle, bytes, numBytesToRead, numBytesRead, overlapped); + } + } + + return ReadFile(handle, bytes, numBytesToRead, numBytesRead, overlapped); + } + + internal static void UnpatchIATHooks() + { + if (UnityPlayerIATHooker == null) return; + + Logger.Instance.LogInformation("Unpatching UnityPlayer IAT hooks"); + UnityPlayerIATHooker.UnpatchIATHook("KERNEL32.dll", "ReadFile"); + UnityPlayerIATHooker.UnpatchIATHook("KERNEL32.dll", "GetFileAttributesExW"); + UnityPlayerIATHooker = null; + } + + // Setup before unity loads assembly list + internal static void EarlySetup() + { + CreateDefaultInjectedAssembly(); + var allFiles = AssemblyInjectorComponent.ModAssemblies; + + foreach (var file in allFiles) + { + var extension = Path.GetExtension(file); + if (extension.Equals(".dll")) + { + var assemblyName = Path.GetFileName(file); + CreateInjectedImage(assemblyName); + } + } + + AssemblyListFile = UnityVersionHandler.GetAssemblyListFile(); + + UnityPlayerIATHooker = new AssemblyIATHooker(UnityPlayerHandle); + UnityPlayerIATHooker.CreateIATHook("KERNEL32.dll", "ReadFile", thunk => + { + ReadFile = Marshal.GetDelegateForFunctionPointer(thunk->Function); + thunk->Function = Marshal.GetFunctionPointerForDelegate(ReadFileDetour); + }); + + UnityPlayerIATHooker.CreateIATHook("KERNEL32.dll", "GetFileAttributesExW", thunk => + { + GetFileAttributesEx = Marshal.GetDelegateForFunctionPointer(thunk->Function); + thunk->Function = Marshal.GetFunctionPointerForDelegate(GetFileAttributesExDetour); + }); + + assemblyLoadHook.ApplyHook(); + api_get_assemblies.ApplyHook(); } internal static long CreateClassToken(IntPtr classPointer) @@ -89,12 +258,16 @@ internal static long CreateClassToken(IntPtr classPointer) } internal static void AddTypeToLookup(IntPtr typePointer) where T : class => AddTypeToLookup(typeof(T), typePointer); + internal static void AddTypeToLookup(Type type, IntPtr typePointer) { string klass = type.Name; if (klass == null) return; string namespaze = type.Namespace ?? string.Empty; - var attribute = Attribute.GetCustomAttribute(type, typeof(Il2CppInterop.Runtime.Attributes.ClassInjectionAssemblyTargetAttribute)) as Il2CppInterop.Runtime.Attributes.ClassInjectionAssemblyTargetAttribute; + + var attribute = + Attribute.GetCustomAttribute(type, + typeof(Attributes.ClassInjectionAssemblyTargetAttribute)) as Attributes.ClassInjectionAssemblyTargetAttribute; foreach (IntPtr image in (attribute is null) ? IL2CPP.GetIl2CppImages() : attribute.GetImagePointers()) { @@ -137,28 +310,21 @@ internal static IntPtr GetIl2CppMethodPointer(MethodBase proxyMethod) private static long s_LastInjectedToken = -2; internal static readonly ConcurrentDictionary s_InjectedClasses = new(); + /// (namespace, class, image) : class internal static readonly Dictionary<(string _namespace, string _class, IntPtr imagePtr), IntPtr> s_ClassNameLookup = new(); #region Class::Init + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate void d_ClassInit(Il2CppClass* klass); + internal static d_ClassInit ClassInit; private static readonly MemoryUtils.SignatureDefinition[] s_ClassInitSignatures = { - new MemoryUtils.SignatureDefinition - { - pattern = "\xE8\x00\x00\x00\x00\x0F\xB7\x47\x28\x83", - mask = "x????xxxxx", - xref = true - }, - new MemoryUtils.SignatureDefinition - { - pattern = "\xE8\x00\x00\x00\x00\x0F\xB7\x47\x48\x48", - mask = "x????xxxxx", - xref = true - } + new MemoryUtils.SignatureDefinition { pattern = "\xE8\x00\x00\x00\x00\x0F\xB7\x47\x28\x83", mask = "x????xxxxx", xref = true }, + new MemoryUtils.SignatureDefinition { pattern = "\xE8\x00\x00\x00\x00\x0F\xB7\x47\x48\x48", mask = "x????xxxxx", xref = true } }; private static d_ClassInit FindClassInit() @@ -170,11 +336,13 @@ static nint GetClassInitSubstitute() Logger.Instance.LogTrace("Picked mono_class_instance_size as a Class::Init substitute"); return classInit; } + if (TryGetIl2CppExport("mono_class_setup_vtable", out classInit)) { Logger.Instance.LogTrace("Picked mono_class_setup_vtable as a Class::Init substitute"); return classInit; } + if (TryGetIl2CppExport(nameof(IL2CPP.il2cpp_class_has_references), out classInit)) { Logger.Instance.LogTrace("Picked il2cpp_class_has_references as a Class::Init substitute"); @@ -182,8 +350,10 @@ static nint GetClassInitSubstitute() } Logger.Instance.LogTrace("GameAssembly.dll: 0x{Il2CppModuleAddress}", Il2CppModule.BaseAddress.ToInt64().ToString("X2")); - throw new NotSupportedException("Failed to use signature for Class::Init and a substitute cannot be found, please create an issue and report your unity version & game"); + throw new NotSupportedException( + "Failed to use signature for Class::Init and a substitute cannot be found, please create an issue and report your unity version & game"); } + nint pClassInit = s_ClassInitSignatures .Select(s => MemoryUtils.FindSignatureInModule(Il2CppModule, s)) .FirstOrDefault(p => p != 0); @@ -198,6 +368,7 @@ static nint GetClassInitSubstitute() return Marshal.GetDelegateForFunctionPointer(pClassInit); } + #endregion } } diff --git a/Il2CppInterop.Runtime/Runtime/UnityVersionHandler.cs b/Il2CppInterop.Runtime/Runtime/UnityVersionHandler.cs index 0e4e1f7d..d7e8c2a8 100644 --- a/Il2CppInterop.Runtime/Runtime/UnityVersionHandler.cs +++ b/Il2CppInterop.Runtime/Runtime/UnityVersionHandler.cs @@ -4,6 +4,7 @@ using System.Reflection; using Il2CppInterop.Common; using Il2CppInterop.Common.Extensions; +using Il2CppInterop.Runtime.Injection; using Il2CppInterop.Runtime.Runtime.VersionSpecific.Assembly; using Il2CppInterop.Runtime.Runtime.VersionSpecific.AssemblyName; using Il2CppInterop.Runtime.Runtime.VersionSpecific.Class; @@ -79,6 +80,8 @@ static UnityVersionHandler() public static bool HasShimForGetMethod { get; private set; } public static bool IsMetadataV29OrHigher { get; private set; } + public static bool HasScriptAssembliesFile { get; private set; } + // Version since which extra_arg is set to invoke_multicast, necessitating constructor calls public static bool MustUseDelegateConstructor => IsMetadataV29OrHigher; @@ -98,6 +101,7 @@ internal static void RecalculateHandlers() HasGetMethodFromReflection = unityVersion > new Version(2018, 1, 0); IsMetadataV29OrHigher = unityVersion >= new Version(2021, 2, 0); + HasScriptAssembliesFile = unityVersion >= new Version(2020, 0, 0); HasShimForGetMethod = unityVersion >= new Version(2020, 3, 41) || IsMetadataV29OrHigher; @@ -129,6 +133,14 @@ private static Type[] GetAllTypesSafe() return typeof(UnityVersionHandler).Assembly.GetTypesSafe(); } + internal static IAssemblyListFile GetAssemblyListFile() + { + if (HasScriptAssembliesFile) + return new JSONAssemblyListFile(); + + return new GameManagersAssemblyListFile(); + } + //Assemblies public static INativeAssemblyStruct NewAssembly() { diff --git a/Il2CppInterop.Runtime/Startup/Il2CppInteropRuntime.cs b/Il2CppInterop.Runtime/Startup/Il2CppInteropRuntime.cs index 3147986b..e600b8ff 100644 --- a/Il2CppInterop.Runtime/Startup/Il2CppInteropRuntime.cs +++ b/Il2CppInterop.Runtime/Startup/Il2CppInteropRuntime.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; using Il2CppInterop.Common.Host; using Il2CppInterop.Common.XrefScans; using Il2CppInterop.Runtime.Injection; @@ -37,8 +40,20 @@ public static Il2CppInteropRuntime Create(RuntimeConfiguration configuration) return res; } + public class FakeListProvider : IAssemblyListProvider + { + public IEnumerable GetAssemblyList() + { + var coreFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + var pluginsFolder = Path.Combine(coreFolder, "../plugins/"); + + return Directory.EnumerateFiles(pluginsFolder, "*", SearchOption.AllDirectories); + } + } + public override void Start() { + this.AddAssemblyInjector(); UnityVersionHandler.RecalculateHandlers(); base.Start(); }