-
Notifications
You must be signed in to change notification settings - Fork 109
Assembly injection #93
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
7c0ccbf
f4803f7
ad055db
c503551
6674ad5
18a545d
2f23a7b
bbcc921
3a0d621
b574da4
b33cd6d
8784088
af8fb61
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<T, TProvider>(this T host) | ||
| where T : BaseHost | ||
| where TProvider : IAssemblyListProvider, new() | ||
| { | ||
| host.AddComponent(new AssemblyInjectorComponent(new TProvider())); | ||
| return host; | ||
| } | ||
| } | ||
|
|
||
| public interface IAssemblyListProvider | ||
| { | ||
| public IEnumerable<string> GetAssemblyList(); | ||
| } | ||
|
|
||
| public class AssemblyInjectorComponent : IHostComponent | ||
| { | ||
| private static IAssemblyListProvider s_assemblyListProvider; | ||
|
|
||
| public AssemblyInjectorComponent(IAssemblyListProvider assemblyListProvider) => s_assemblyListProvider = assemblyListProvider; | ||
|
|
||
| public static IEnumerable<string> 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(); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<string> _assemblies = new List<string>(); | ||
| private string originalFile; | ||
| private string newFile; | ||
|
|
||
| public bool IsTargetFile(string originalFilePath) | ||
| { | ||
| return originalFilePath.Contains("globalgamemanagers"); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. globalgamemanagers doesn't always exist. |
||
| } | ||
|
|
||
| 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() | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am strongly against a solution that requires modifying game files.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Even if we don't actually touch original files? This PR uses hooks at runtime to provide files when the game requests them. Nothing is modified on disk. Also sadly the only other solution would be patching native Unity code (not lib il2cpp), and according to author of FixPluginSerialization (mono version of this kind of solution), it's an awful idea: https://discord.com/channels/623153565053222947/985596870229385256/1113056578485092382 |
||
| { | ||
| 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<string> newAssemblyList = new List<string>(assemblyCount + _assemblies.Count); | ||
| List<int> newAssemblyTypes = new List<int>(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; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<API_il2cpp_domain_get_assemblies_hook.MethodDelegate> | ||
| { | ||
| 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)); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is an unnecessary using.