Files
BepInEx/BepInEx.IL2CPP/ProxyAssemblyGenerator.cs
T

400 lines
17 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using AssemblyUnhollower;
using BepInEx.Configuration;
using BepInEx.Logging;
using Cpp2IL.Core;
using HarmonyLib;
using Il2CppDumper;
using LibCpp2IL;
using Mono.Cecil;
using UnhollowerBaseLib;
using Logger = BepInEx.Logging.Logger;
namespace BepInEx.IL2CPP;
internal static class ProxyAssemblyGenerator
{
private static readonly ConfigEntry<bool> ConfigUpdateUnhollowedAssemblies =
ConfigFile.CoreConfig.Bind("IL2CPP",
"UpdateUnhollowedAssemblies",
true,
new StringBuilder()
.AppendLine("Whether to run Il2CppAssemblyUnhollower automatically to generate Il2Cpp support assemblies when they are outdated.")
.AppendLine("If disabled assemblies in `BepInEx/unhollowed` won't be updated between game or BepInEx updates!")
.ToString());
private static readonly ConfigEntry<string> ConfigUnityBaseLibrariesSource = ConfigFile.CoreConfig.Bind(
"IL2CPP", "UnityBaseLibrariesSource",
"http://unity.bepinex.dev/libraries/{VERSION}.zip",
new StringBuilder()
.AppendLine("URL to the ZIP of managed Unity base libraries.")
.AppendLine("The base libraries are used by Il2CppUnhollower to generate unhollowed Unity assemblies")
.AppendLine("The URL template MUST use HTTP.")
.AppendLine("The URL can include {VERSION} template which will be replaced with the game's Unity engine version")
.ToString());
private static readonly ConfigEntry<IL2CPPDumperType> ConfigIl2CppDumperType = ConfigFile.CoreConfig.Bind(
"IL2CPP", "Il2CppDumperType",
IL2CPPDumperType.Cpp2IL,
new StringBuilder()
.AppendLine("The IL2CPP metadata dumper tool to use when generating dummy assemblies for Il2CppAssemblyUnhollower.")
.AppendLine("Il2CppDumper - Default. The traditional choice that has been used by BepInEx.")
.AppendLine("Cpp2IL - Experimental, may provide better results than Il2CppDumper. Required for use with BepInEx.MelonLoader.Loader.")
.ToString());
private static readonly ConfigEntry<bool> ConfigDumpDummyAssemblies = ConfigFile.CoreConfig.Bind(
"IL2CPP", "DumpDummyAssemblies",
false,
"If enabled, BepInEx will save dummy assemblies generated by an il2cpp dumper into BepInEx/dummy.");
private static readonly ConfigEntry<string> UnhollowedAssembliesPath = ConfigFile.CoreConfig.Bind(
"IL2CPP", "UnhollowedAssembliesPath",
"{BepInEx}",
new StringBuilder()
.AppendLine("The path to the folder where unhollowed assemblies are stored.")
.AppendLine("Supports the following placeholders:")
.AppendLine("{BepInEx} - Path to the BepInEx folder.")
.AppendLine("{ProcessName} - Name of the current process")
.ToString());
private static ManualLogSource Il2cppDumperLogger;
private static string unhollowedBasePath;
public static string GameAssemblyPath => Path.Combine(Paths.GameRootPath, "GameAssembly.dll");
private static string HashPath => Path.Combine(Preloader.IL2CPPUnhollowedPath, "assembly-hash.txt");
private static string UnhollowedBasePath
{
get
{
if (unhollowedBasePath != null)
return unhollowedBasePath;
var path = Utility.GetCommandLineArgValue("--unhollowed-path") ?? UnhollowedAssembliesPath.Value;
unhollowedBasePath = path.Replace("{BepInEx}", Paths.BepInExRootPath)
.Replace("{ProcessName}", Paths.ProcessName);
return unhollowedBasePath;
}
}
private static string UnityBaseLibsDirectory => Path.Combine(UnhollowedBasePath, "unity-libs");
internal static string IL2CPPUnhollowedPath => Path.Combine(UnhollowedBasePath, "unhollowed");
private static string ComputeHash()
{
using var md5 = MD5.Create();
static void HashFile(ICryptoTransform hash, string file)
{
const int defaultCopyBufferSize = 81920;
using var fs = File.OpenRead(file);
var buffer = new byte[defaultCopyBufferSize];
int read;
while ((read = fs.Read(buffer)) > 0)
hash.TransformBlock(buffer, 0, read, buffer, 0);
}
static void HashString(ICryptoTransform hash, string str)
{
var buffer = Encoding.UTF8.GetBytes(str);
hash.TransformBlock(buffer, 0, buffer.Length, buffer, 0);
}
HashFile(md5, GameAssemblyPath);
if (Directory.Exists(UnityBaseLibsDirectory))
foreach (var file in Directory.EnumerateFiles(UnityBaseLibsDirectory, "*.dll",
SearchOption.TopDirectoryOnly))
{
HashString(md5, Path.GetFileName(file));
HashFile(md5, file);
}
// Hash some common dependencies as they can affect output
HashString(md5, typeof(UnhollowedAssemblyGenerator).Assembly.GetName().Version.ToString());
HashString(md5, typeof(Cpp2IlApi).Assembly.GetName().Version.ToString());
HashString(md5, typeof(Il2CppDumper.Il2CppDumper).Assembly.GetName().Version.ToString());
md5.TransformFinalBlock(new byte[0], 0, 0);
return Utility.ByteArrayToString(md5.Hash);
}
public static bool CheckIfGenerationRequired()
{
static bool NeedGenerationOrSkip()
{
if (!ConfigUpdateUnhollowedAssemblies.Value)
{
var hash = ComputeHash();
Preloader.Log
.Log(LogLevel.Warning,
$"Unhollowed assemblies are possibly out of date. To disable this message, create file {HashPath} with the following contents: {hash}");
return false;
}
return true;
}
if (!Directory.Exists(Preloader.IL2CPPUnhollowedPath))
return true;
if (!File.Exists(HashPath))
return NeedGenerationOrSkip();
if (ComputeHash() != File.ReadAllText(HashPath) && NeedGenerationOrSkip())
{
Preloader.Log.Log(LogLevel.Info, "Detected outdated proxy assemblies, will regenerate them now");
return true;
}
return false;
}
public static void GenerateAssemblies()
{
Il2cppDumperLogger ??= Logger.CreateLogSource(ConfigIl2CppDumperType.Value == IL2CPPDumperType.Il2CppDumper
? "Il2CppDumper"
: "Cpp2IL");
var domain = AppDomainHelper.CreateDomain("GeneratorDomain", new AppDomainHelper.AppDomainSetup
{
ApplicationBase = Paths.BepInExAssemblyDirectory
});
var runner =
(AppDomainRunner) AppDomainHelper.CreateInstanceAndUnwrap(domain,
typeof(AppDomainRunner).Assembly.FullName,
typeof(AppDomainRunner).FullName);
runner.Setup(Paths.ExecutablePath, Paths.BepInExRootPath, Paths.ManagedPath);
var ok = runner.GenerateAssemblies(new AppDomainListener(), Preloader.UnityVersion.ToString(3),
ConfigIl2CppDumperType.Value);
AppDomain.Unload(domain);
if (ok)
File.WriteAllText(HashPath, ComputeHash());
}
internal enum IL2CPPDumperType
{
Il2CppDumper,
Cpp2IL
}
private static class AppDomainHelper
{
static AppDomainHelper()
{
var appDomain = typeof(AppDomain);
var evidenceType = typeof(AppDomain).Assembly.GetType("System.Security.Policy.Evidence");
AppDomainSetupType = typeof(AppDomain).Assembly.GetType("System.AppDomainSetup");
CreateDomainInternal =
MethodInvoker.GetHandler(AccessTools.Method(appDomain, "CreateDomain",
new[]
{
typeof(string), evidenceType, AppDomainSetupType
}));
CreateInstanceAndUnwrap =
AccessTools.MethodDelegate<Func<AppDomain, string, string, object>>(AccessTools.Method(appDomain,
nameof(CreateInstanceAndUnwrap), new[] { typeof(string), typeof(string) }));
}
public static Func<AppDomain, string, string, object> CreateInstanceAndUnwrap { get; }
private static FastInvokeHandler CreateDomainInternal { get; }
private static Type AppDomainSetupType { get; }
public static AppDomain CreateDomain(string name, AppDomainSetup setup)
{
var realSetup = AccessTools.CreateInstance(AppDomainSetupType);
Traverse.IterateProperties(setup, realSetup, (pSrc, pTgt) => pTgt.SetValue(pSrc.GetValue()));
return CreateDomainInternal(null, name, null, realSetup) as AppDomain;
}
public class AppDomainSetup
{
public string ApplicationBase { get; set; }
}
}
[Serializable]
private class AppDomainListener : MarshalByRefObject
{
public void DoPreloaderLog(object data, LogLevel level) => Preloader.Log.Log(level, data);
public void DoDumperLog(object data, LogLevel level) => Il2cppDumperLogger.Log(level, data);
public void DoUnhollowerLog(object data, LogLevel level) => Preloader.UnhollowerLog.Log(level, data);
}
[Serializable]
private class AppDomainRunner : MarshalByRefObject
{
public void Setup(string executablePath, string bepinPath, string managedPath)
{
Paths.SetExecutablePath(executablePath, bepinPath, managedPath);
AppDomain.CurrentDomain.AddCecilPlatformAssemblies(Paths.ManagedPath);
AppDomain.CurrentDomain.AddCecilPlatformAssemblies(UnityBaseLibsDirectory);
}
public bool GenerateAssemblies(AppDomainListener listener,
string unityVersion,
IL2CPPDumperType dumperType)
{
try
{
GenerateAssembliesInternal(listener, unityVersion, dumperType);
return true;
}
catch (Exception e)
{
listener.DoUnhollowerLog($"Failed to generate unhollowed assemblies: {e}", LogLevel.Fatal);
return false;
}
}
private void GenerateAssembliesInternal(AppDomainListener listener,
string unityVersion,
IL2CPPDumperType dumperType)
{
var source =
ConfigUnityBaseLibrariesSource.Value.Replace("{VERSION}", unityVersion);
if (!string.IsNullOrEmpty(source))
{
listener.DoPreloaderLog("Downloading unity base libraries", LogLevel.Message);
Directory.CreateDirectory(UnityBaseLibsDirectory);
Directory.EnumerateFiles(UnityBaseLibsDirectory, "*.dll").Do(File.Delete);
using var httpClient = new HttpClient();
using var zipStream = httpClient.GetStreamAsync(source).GetAwaiter().GetResult();
using var zipArchive = new ZipArchive(zipStream, ZipArchiveMode.Read);
listener.DoPreloaderLog("Extracting downloaded unity base libraries", LogLevel.Message);
zipArchive.ExtractToDirectory(UnityBaseLibsDirectory);
}
listener.DoPreloaderLog("Generating Il2CppUnhollower assemblies", LogLevel.Message);
Directory.CreateDirectory(Preloader.IL2CPPUnhollowedPath);
Directory.EnumerateFiles(Preloader.IL2CPPUnhollowedPath, "*.dll").Do(File.Delete);
var metadataPath = Path.Combine(Paths.GameRootPath,
$"{Paths.ProcessName}_Data",
"il2cpp_data",
"Metadata",
"global-metadata.dat");
List<AssemblyDefinition> sourceAssemblies;
var stopwatch = new Stopwatch();
stopwatch.Start();
if (dumperType == IL2CPPDumperType.Il2CppDumper)
{
listener.DoPreloaderLog("Generating Il2CppDumper intermediate assemblies", LogLevel.Info);
Il2CppDumper.Il2CppDumper.Init(GameAssemblyPath,
metadataPath,
new Config
{
GenerateStruct = false,
GenerateDummyDll = true
},
s => listener.DoDumperLog(s, LogLevel.Debug),
out var metadata,
out var il2Cpp);
var executor = new Il2CppExecutor(metadata, il2Cpp);
var dummy = new DummyAssemblyGenerator(executor, true);
sourceAssemblies = dummy.Assemblies;
}
else // if (dumperType == IL2CPPDumperType.Cpp2IL)
{
Cpp2IL.Core.Logger.VerboseLog += (message, s) =>
listener.DoDumperLog($"[{s}] {message.Trim()}", LogLevel.Debug);
Cpp2IL.Core.Logger.InfoLog += (message, s) =>
listener.DoDumperLog($"[{s}] {message.Trim()}", LogLevel.Info);
Cpp2IL.Core.Logger.WarningLog += (message, s) =>
listener.DoDumperLog($"[{s}] {message.Trim()}", LogLevel.Warning);
Cpp2IL.Core.Logger.ErrorLog += (message, s) =>
listener.DoDumperLog($"[{s}] {message.Trim()}", LogLevel.Error);
var cpp2IlUnityVersion =
Cpp2IlApi.DetermineUnityVersion(Paths.ExecutablePath,
Path.Combine(Paths.GameRootPath, $"{Paths.ProcessName}_Data"));
Cpp2IlApi.InitializeLibCpp2Il(GameAssemblyPath, metadataPath, cpp2IlUnityVersion, false);
sourceAssemblies = Cpp2IlApi.MakeDummyDLLs();
Cpp2IlApi.RunAttributeRestorationForAllAssemblies(null,
parallel: LibCpp2IlMain.MetadataVersion >= 29 ||
LibCpp2IlMain.Binary!.InstructionSet is
InstructionSet.X86_32
or InstructionSet.X86_64);
Cpp2IlApi.DisposeAndCleanupAll();
}
stopwatch.Stop();
listener.DoDumperLog("Total time: " + stopwatch.Elapsed, LogLevel.Info);
if (ConfigDumpDummyAssemblies.Value)
{
var dummyPath = Path.Combine(Paths.BepInExRootPath, "dummy");
Directory.CreateDirectory(dummyPath);
foreach (var assemblyDefinition in sourceAssemblies)
assemblyDefinition.Write(Path.Combine(dummyPath, assemblyDefinition.Name.Name + ".dll"));
}
var unhollowerOptions = new UnhollowerOptions
{
GameAssemblyPath = GameAssemblyPath,
MscorlibPath = Path.Combine(Paths.ManagedPath, "mscorlib.dll"),
Source = sourceAssemblies,
OutputDir = Preloader.IL2CPPUnhollowedPath,
UnityBaseLibsDir = Directory.Exists(UnityBaseLibsDirectory) ? UnityBaseLibsDirectory : null,
NoCopyUnhollowerLibs = true
};
var renameMapLocation = Path.Combine(Paths.BepInExRootPath, "DeobfuscationMap.csv.gz");
if (File.Exists(renameMapLocation))
{
listener.DoPreloaderLog("Parsing deobfuscation rename mappings", LogLevel.Info);
unhollowerOptions.ReadRenameMap(renameMapLocation);
}
listener.DoPreloaderLog("Executing Il2CppUnhollower generator", LogLevel.Info);
LogSupport.InfoHandler += s => listener.DoUnhollowerLog(s.Trim(), LogLevel.Info);
LogSupport.WarningHandler += s => listener.DoUnhollowerLog(s.Trim(), LogLevel.Warning);
LogSupport.TraceHandler += s => listener.DoUnhollowerLog(s.Trim(), LogLevel.Debug);
LogSupport.ErrorHandler += s => listener.DoUnhollowerLog(s.Trim(), LogLevel.Error);
try
{
UnhollowedAssemblyGenerator.GenerateUnhollowedAssemblies(unhollowerOptions);
}
catch (Exception e)
{
listener.DoUnhollowerLog($"Exception while unhollowing: {e}", LogLevel.Error);
}
sourceAssemblies.Do(x => x.Dispose());
}
}
}