diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 539faac..bc98e9c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,6 +26,9 @@ jobs: with: dotnet-version: '8.0.x' + - name: Add samboy's nuget source + run: dotnet nuget add source https://nuget.samboy.dev/v3/index.json + - name: Build protodec run: dotnet publish --configuration Release --runtime ${{ matrix.runtime }} /p:Version=${{ github.ref_name }} /p:ContinuousIntegrationBuild=true diff --git a/README.md b/README.md index b350840..03f7d98 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ protodec ======== -A tool to decompile protobuf classes compiled by [protoc](https://github.com/protocolbuffers/protobuf), from CIL assemblies back into .proto definitions. +A tool to decompile [protoc](https://github.com/protocolbuffers/protobuf) compiled protobuf classes back into .proto definitions. Usage ----- ``` -Usage: [arguments...] [options...] [-h|--help] [--version] +Usage: [command] [arguments...] [options...] [-h|--help] [--version] +Use reflection backend to load target CIL assembly and its dependants. Arguments: [0] Either the path to the target assembly or a directory of assemblies, all of which be parsed. @@ -18,14 +19,19 @@ Options: --parse-service-servers Parses gRPC service definitions from server classes. (Optional) --parse-service-clients Parses gRPC service definitions from client classes. (Optional) --log-level Logging severity level. (Default: Information) + +Commands: + il2cpp Use LibCpp2IL backend to directly load Il2Cpp compiled game assembly. EXPERIMENTAL. ``` +See per-command help message for more info. Limitations ----------- - Integers are assumed to be (u)int32/64 as CIL doesn't differentiate between them and sint32/64 and (s)fixed32/64. - Package names are not preserved in protobuf compilation so naturally we cannot recover them during decompilation, which may result in naming conflicts. -- When decompiling from [Il2CppDumper](https://github.com/Perfare/Il2CppDumper) DummyDLLs - - The `Name` parameter of `OriginalNameAttribute` is not dumped. In this case, the CIL enum field names are used after conforming them to protobuf conventions +- When decompiling from [Il2CppDumper](https://github.com/Perfare/Il2CppDumper) DummyDLLs or from an Il2Cpp assembly older than metadata version 29, due to the development branch of [LibCpp2Il](https://github.com/SamboyCoding/Cpp2IL/tree/development/LibCpp2IL) not yet recovering method bodies + - The `Name` parameter of `OriginalNameAttribute` is not parsed. In this case, the CIL enum field names are used after conforming them to protobuf conventions. + - The `Tool` parameter of `GeneratedCodeAttribute` is not compared against when parsing gRPC service methods, which may cause false positives in the event that another tool has generated methods in the service class. License ------- diff --git a/protodec.sln b/protodec.sln index d47bba3..1693280 100644 --- a/protodec.sln +++ b/protodec.sln @@ -10,6 +10,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{22F217C3-0FC2-4D06-B5F3-AA1E3AFC402E}" ProjectSection(SolutionItems) = preProject README.md = README.md + .github\workflows\release.yml = .github\workflows\release.yml EndProjectSection EndProject Global diff --git a/src/LibProtodec/LibProtodec.csproj b/src/LibProtodec/LibProtodec.csproj index 2bf839e..a846f36 100644 --- a/src/LibProtodec/LibProtodec.csproj +++ b/src/LibProtodec/LibProtodec.csproj @@ -20,6 +20,7 @@ + diff --git a/src/LibProtodec/Loaders/CilAssemblyLoader.cs b/src/LibProtodec/Loaders/CilAssemblyLoader.cs index da01b6c..eae71ee 100644 --- a/src/LibProtodec/Loaders/CilAssemblyLoader.cs +++ b/src/LibProtodec/Loaders/CilAssemblyLoader.cs @@ -11,7 +11,7 @@ using LibProtodec.Models.Cil; namespace LibProtodec.Loaders; -public abstract class CilAssemblyLoader : IDisposable +public abstract class CilAssemblyLoader { public IReadOnlyList LoadedTypes { get; protected init; } @@ -43,7 +43,5 @@ public abstract class CilAssemblyLoader : IDisposable && type.CustomAttributes.Any(attribute => attribute.Type == bindServiceMethodAttribute)); } - public virtual void Dispose() { } - protected abstract ICilType FindType(string typeFullName, string assemblySimpleName); } \ No newline at end of file diff --git a/src/LibProtodec/Loaders/ClrAssemblyLoader.cs b/src/LibProtodec/Loaders/ClrAssemblyLoader.cs index 0aa0f7f..04ea50e 100644 --- a/src/LibProtodec/Loaders/ClrAssemblyLoader.cs +++ b/src/LibProtodec/Loaders/ClrAssemblyLoader.cs @@ -16,7 +16,7 @@ using Microsoft.Extensions.Logging; namespace LibProtodec.Loaders; -public sealed class ClrAssemblyLoader : CilAssemblyLoader +public sealed class ClrAssemblyLoader : CilAssemblyLoader, IDisposable { public readonly MetadataLoadContext LoadContext; @@ -55,7 +55,7 @@ public sealed class ClrAssemblyLoader : CilAssemblyLoader return ClrType.GetOrCreate(clrType); } - public override void Dispose() => + public void Dispose() => LoadContext.Dispose(); /// diff --git a/src/LibProtodec/Loaders/Il2CppAssemblyLoader.cs b/src/LibProtodec/Loaders/Il2CppAssemblyLoader.cs new file mode 100644 index 0000000..e4d960d --- /dev/null +++ b/src/LibProtodec/Loaders/Il2CppAssemblyLoader.cs @@ -0,0 +1,64 @@ +// Copyright © 2024 Xpl0itR +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +using System; +using System.Linq; +using AssetRipper.Primitives; +using CommunityToolkit.Diagnostics; +using LibCpp2IL; +using LibCpp2IL.Logging; +using LibCpp2IL.Metadata; +using LibCpp2IL.Reflection; +using LibProtodec.Models.Cil; +using LibProtodec.Models.Cil.Il2Cpp; +using Microsoft.Extensions.Logging; + +namespace LibProtodec.Loaders; + +public sealed class Il2CppAssemblyLoader : CilAssemblyLoader +{ + public Il2CppAssemblyLoader(string assemblyPath, string metadataPath, UnityVersion unityVersion, ILoggerFactory? loggerFactory = null) + { + if (loggerFactory is not null) + { + LibLogger.Writer = new LibCpp2IlLogger( + loggerFactory.CreateLogger(nameof(LibCpp2IL))); + } + + if (!LibCpp2IlMain.LoadFromFile(assemblyPath, metadataPath, unityVersion)) + ThrowHelper.ThrowInvalidDataException("Failed to load IL2Cpp assembly!"); + + this.LoadedTypes = LibCpp2IlMain.TheMetadata!.typeDefs.Select(Il2CppType.GetOrCreate).ToList(); + + loggerFactory?.CreateLogger() + .LogLoadedTypeAndAssemblyCount(this.LoadedTypes.Count, LibCpp2IlMain.TheMetadata.imageDefinitions.Length); + } + + protected override ICilType FindType(string typeFullName, string assemblySimpleName) + { + Il2CppTypeDefinition? type = LibCpp2IlReflection.GetTypeByFullName(typeFullName); + Guard.IsNotNull(type); + + return Il2CppType.GetOrCreate(type); + } + + private sealed class LibCpp2IlLogger(ILogger logger) : LogWriter + { + private static readonly Func MessageFormatter = (message, _) => message.Trim(); + + public override void Info(string message) => + logger.Log(LogLevel.Information, default(EventId), message, null, MessageFormatter); + + public override void Warn(string message) => + logger.Log(LogLevel.Warning, default(EventId), message, null, MessageFormatter); + + public override void Error(string message) => + logger.Log(LogLevel.Error, default(EventId), message, null, MessageFormatter); + + public override void Verbose(string message) => + logger.Log(LogLevel.Debug, default(EventId), message, null, MessageFormatter); + } +} \ No newline at end of file diff --git a/src/LibProtodec/Models/Cil/Clr/ClrAttribute.cs b/src/LibProtodec/Models/Cil/Clr/ClrAttribute.cs index 493ae88..485a0ed 100644 --- a/src/LibProtodec/Models/Cil/Clr/ClrAttribute.cs +++ b/src/LibProtodec/Models/Cil/Clr/ClrAttribute.cs @@ -15,6 +15,9 @@ public sealed class ClrAttribute(CustomAttributeData clrAttribute) : ICilAttribu private ICilType? _type; private object?[]? _constructorArgumentValues; + public bool CanReadConstructorArgumentValues => + true; + public ICilType Type => _type ??= ClrType.GetOrCreate( clrAttribute.AttributeType); diff --git a/src/LibProtodec/Models/Cil/ICilAttribute.cs b/src/LibProtodec/Models/Cil/ICilAttribute.cs index 4bbdaac..7d44a3a 100644 --- a/src/LibProtodec/Models/Cil/ICilAttribute.cs +++ b/src/LibProtodec/Models/Cil/ICilAttribute.cs @@ -12,5 +12,7 @@ public interface ICilAttribute { ICilType Type { get; } + bool CanReadConstructorArgumentValues { get; } + IList ConstructorArgumentValues { get; } } \ No newline at end of file diff --git a/src/LibProtodec/Models/Cil/Il2Cpp/Il2CppAttribute.cs b/src/LibProtodec/Models/Cil/Il2Cpp/Il2CppAttribute.cs new file mode 100644 index 0000000..ee05c75 --- /dev/null +++ b/src/LibProtodec/Models/Cil/Il2Cpp/Il2CppAttribute.cs @@ -0,0 +1,25 @@ +// Copyright © 2024 Xpl0itR +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; +using CommunityToolkit.Diagnostics; +using LibCpp2IL.Metadata; + +namespace LibProtodec.Models.Cil.Il2Cpp; + +public sealed class Il2CppAttribute(Il2CppTypeDefinition il2CppAttrType, object?[]? ctorArgValues) : ICilAttribute +{ + public ICilType Type => + Il2CppType.GetOrCreate(il2CppAttrType); + + public bool CanReadConstructorArgumentValues => + ctorArgValues is not null; + + public IList ConstructorArgumentValues => + ctorArgValues + ?? ThrowHelper.ThrowNotSupportedException>( + "Attribute constructor argument parsing is only available on Il2Cpp metadata version 29 or greater."); +} \ No newline at end of file diff --git a/src/LibProtodec/Models/Cil/Il2Cpp/Il2CppField.cs b/src/LibProtodec/Models/Cil/Il2Cpp/Il2CppField.cs new file mode 100644 index 0000000..7f75411 --- /dev/null +++ b/src/LibProtodec/Models/Cil/Il2Cpp/Il2CppField.cs @@ -0,0 +1,40 @@ +// Copyright © 2024 Xpl0itR +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +using System.Reflection; +using LibCpp2IL.Metadata; + +namespace LibProtodec.Models.Cil.Il2Cpp; + +public sealed class Il2CppField(Il2CppFieldDefinition il2CppField) : Il2CppMember, ICilField +{ + private readonly FieldAttributes _attributes = + (FieldAttributes)il2CppField.RawFieldType!.Attrs; + + public string Name => + il2CppField.Name!; + + public object? ConstantValue => + il2CppField.DefaultValue!.Value; + + public bool IsLiteral => + (_attributes & FieldAttributes.Literal) != 0; + + public bool IsPublic => + (_attributes & FieldAttributes.Public) != 0; + + public bool IsStatic => + (_attributes & FieldAttributes.Static) != 0; + + protected override Il2CppImageDefinition DeclaringAssembly => + il2CppField.FieldType!.baseType!.DeclaringAssembly!; + + protected override int CustomAttributeIndex => + il2CppField.customAttributeIndex; + + protected override uint Token => + il2CppField.token; +} \ No newline at end of file diff --git a/src/LibProtodec/Models/Cil/Il2Cpp/Il2CppMember.cs b/src/LibProtodec/Models/Cil/Il2Cpp/Il2CppMember.cs new file mode 100644 index 0000000..aee5dca --- /dev/null +++ b/src/LibProtodec/Models/Cil/Il2Cpp/Il2CppMember.cs @@ -0,0 +1,306 @@ +// Copyright © 2024 Xpl0itR +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; +using System.Text; +using SystemEx.Memory; +using CommunityToolkit.Diagnostics; +using LibCpp2IL; +using LibCpp2IL.BinaryStructures; +using LibCpp2IL.Metadata; + +namespace LibProtodec.Models.Cil.Il2Cpp; + +public abstract class Il2CppMember +{ + private ICilAttribute[]? _customAttributes; + + public IList CustomAttributes + { + get + { + if (_customAttributes is null) + { + if (LibCpp2IlMain.MetadataVersion < 29f) + { + int attrTypeRngIdx = LibCpp2IlMain.MetadataVersion <= 24f + ? CustomAttributeIndex + : BinarySearchToken( + LibCpp2IlMain.TheMetadata!.attributeTypeRanges, + Token, + DeclaringAssembly.customAttributeStart, + (int)DeclaringAssembly.customAttributeCount); + + if (attrTypeRngIdx < 0) + return _customAttributes = Array.Empty(); + + Il2CppCustomAttributeTypeRange attrTypeRng = LibCpp2IlMain.TheMetadata!.attributeTypeRanges[attrTypeRngIdx]; + + _customAttributes = new ICilAttribute[attrTypeRng.count]; + for (int attrTypeIdx = 0; attrTypeIdx < attrTypeRng.count; attrTypeIdx++) + { + int typeIndex = LibCpp2IlMain.TheMetadata.attributeTypes[attrTypeRng.start + attrTypeIdx]; + var type = LibCpp2IlMain.Binary!.GetType(typeIndex); + var typeDef = LibCpp2IlMain.TheMetadata.typeDefs[type.Data.ClassIndex]; + + _customAttributes[attrTypeIdx] = new Il2CppAttribute(typeDef, null); + } + } + else + { + int attrDataRngIdx = BinarySearchToken( + LibCpp2IlMain.TheMetadata!.AttributeDataRanges, + Token, + DeclaringAssembly.customAttributeStart, + (int)DeclaringAssembly.customAttributeCount); + + if (attrDataRngIdx < 0) + return _customAttributes = Array.Empty(); + + Il2CppCustomAttributeDataRange attrDataRange = LibCpp2IlMain.TheMetadata.AttributeDataRanges[attrDataRngIdx]; + Il2CppCustomAttributeDataRange attrDataRngNext = LibCpp2IlMain.TheMetadata.AttributeDataRanges[attrDataRngIdx + 1]; + + long attrDataStart = LibCpp2IlMain.TheMetadata.metadataHeader.attributeDataOffset + attrDataRange.startOffset; + long attrDataEnd = LibCpp2IlMain.TheMetadata.metadataHeader.attributeDataOffset + attrDataRngNext.startOffset; + byte[] attrData = LibCpp2IlMain.TheMetadata.ReadByteArrayAtRawAddress(attrDataStart, (int)(attrDataEnd - attrDataStart)); + + MemoryReader reader = new(attrData); + int attributeCount = (int)ReadUnityCompressedUInt32(ref reader); + + Span ctorIndices = stackalloc uint[attributeCount]; + for (int i = 0; i < attributeCount; i++) + ctorIndices[i] = reader.ReadUInt32LittleEndian(); + + _customAttributes = new ICilAttribute[attributeCount]; + for (int i = 0; i < attributeCount; i++) + { + uint ctorArgCount = ReadUnityCompressedUInt32(ref reader); + uint fieldCount = ReadUnityCompressedUInt32(ref reader); + uint propCount = ReadUnityCompressedUInt32(ref reader); + + object?[] ctorArgValues = ctorArgCount > 0 + ? new object[ctorArgCount] + : Array.Empty(); + + for (int j = 0; j < ctorArgCount; j++) + { + ctorArgValues[j] = ReadValue(ref reader); + } + + for (uint j = 0; j < fieldCount; j++) + { + ReadValue(ref reader); + ResolveMember(ref reader); + } + + for (uint j = 0; j < propCount; j++) + { + ReadValue(ref reader); + ResolveMember(ref reader); + } + + Il2CppMethodDefinition attrCtor = LibCpp2IlMain.TheMetadata.methodDefs[ctorIndices[i]]; + Il2CppTypeDefinition attrType = LibCpp2IlMain.TheMetadata.typeDefs[attrCtor.declaringTypeIdx]; + + _customAttributes[i] = new Il2CppAttribute(attrType, ctorArgValues); + } + } + } + + return _customAttributes; + } + } + + protected abstract Il2CppImageDefinition DeclaringAssembly { get; } + + protected abstract int CustomAttributeIndex { get; } + + protected abstract uint Token { get; } + + private static int BinarySearchToken(IReadOnlyList source, uint token, int start, int count) + { + int lo = start; + int hi = start + count - 1; + while (lo <= hi) + { + int i = lo + ((hi - lo) >> 1); + + switch (source[i].Token.CompareTo(token)) + { + case 0: + return i; + case < 0: + lo = i + 1; + break; + default: + hi = i - 1; + break; + } + } + + return ~lo; + } + + private static uint ReadUnityCompressedUInt32(ref MemoryReader reader) + { + byte byt = reader.ReadByte(); + + switch (byt) + { + case < 128: + return byt; + case 240: + return reader.ReadUInt32LittleEndian(); + case 254: + return uint.MaxValue - 1; + case byte.MaxValue: + return uint.MaxValue; + } + + if ((byt & 192) == 192) + { + return (byt & ~192U) << 24 + | ((uint)reader.ReadByte() << 16) + | ((uint)reader.ReadByte() << 8) + | reader.ReadByte(); + } + + if ((byt & 128) == 128) + { + return (byt & ~128U) << 8 + | reader.ReadByte(); + } + + return ThrowHelper.ThrowInvalidDataException(); + } + + private static int ReadUnityCompressedInt32(ref MemoryReader reader) + { + uint unsigned = ReadUnityCompressedUInt32(ref reader); + if (unsigned == uint.MaxValue) + return int.MinValue; + + bool isNegative = (unsigned & 1) == 1; + unsigned >>= 1; + + return isNegative + ? -(int)(unsigned + 1) + : (int)unsigned; + } + + private static object? ReadValue(ref MemoryReader reader) + { + Il2CppTypeEnum type = (Il2CppTypeEnum)reader.ReadByte(); + return ReadValue(ref reader, type); + } + + private static object? ReadValue(ref MemoryReader reader, Il2CppTypeEnum type) + { + switch (type) + { + case Il2CppTypeEnum.IL2CPP_TYPE_ENUM: + Il2CppTypeEnum underlyingType = ReadEnumUnderlyingType(ref reader); + return ReadValue(ref reader, underlyingType); + case Il2CppTypeEnum.IL2CPP_TYPE_SZARRAY: + return ReadSzArray(ref reader); + case Il2CppTypeEnum.IL2CPP_TYPE_IL2CPP_TYPE_INDEX: + int typeIndex = ReadUnityCompressedInt32(ref reader); + return LibCpp2IlMain.Binary!.GetType(typeIndex); + case Il2CppTypeEnum.IL2CPP_TYPE_BOOLEAN: + return reader.ReadBoolean(); + case Il2CppTypeEnum.IL2CPP_TYPE_CHAR: + return (char)reader.ReadInt16LittleEndian(); + case Il2CppTypeEnum.IL2CPP_TYPE_I1: + return reader.ReadSByte(); + case Il2CppTypeEnum.IL2CPP_TYPE_U1: + return reader.ReadByte(); + case Il2CppTypeEnum.IL2CPP_TYPE_I2: + return reader.ReadInt16LittleEndian(); + case Il2CppTypeEnum.IL2CPP_TYPE_U2: + return reader.ReadUInt16LittleEndian(); + case Il2CppTypeEnum.IL2CPP_TYPE_I4: + return ReadUnityCompressedInt32(ref reader); + case Il2CppTypeEnum.IL2CPP_TYPE_U4: + return ReadUnityCompressedUInt32(ref reader); + case Il2CppTypeEnum.IL2CPP_TYPE_I8: + return reader.ReadInt64LittleEndian(); + case Il2CppTypeEnum.IL2CPP_TYPE_U8: + return reader.ReadUInt64LittleEndian(); + case Il2CppTypeEnum.IL2CPP_TYPE_R4: + return reader.ReadSingleLittleEndian(); + case Il2CppTypeEnum.IL2CPP_TYPE_R8: + return reader.ReadDoubleLittleEndian(); + case Il2CppTypeEnum.IL2CPP_TYPE_STRING: + return ReadString(ref reader); + case Il2CppTypeEnum.IL2CPP_TYPE_CLASS: + case Il2CppTypeEnum.IL2CPP_TYPE_OBJECT: + case Il2CppTypeEnum.IL2CPP_TYPE_GENERICINST: + default: + return ThrowHelper.ThrowNotSupportedException(); + } + } + + private static Il2CppTypeEnum ReadEnumUnderlyingType(ref MemoryReader reader) + { + int typeIdx = ReadUnityCompressedInt32(ref reader); + var enumType = LibCpp2IlMain.Binary!.GetType(typeIdx); + var underlyingType = LibCpp2IlMain.Binary.GetType( + enumType.AsClass().ElementTypeIndex); + + return underlyingType.Type; + } + + private static object?[]? ReadSzArray(ref MemoryReader reader) + { + int arrayLength = ReadUnityCompressedInt32(ref reader); + if (arrayLength == -1) + return null; + + Il2CppTypeEnum arrayType = (Il2CppTypeEnum)reader.ReadByte(); + if (arrayType == Il2CppTypeEnum.IL2CPP_TYPE_ENUM) + arrayType = ReadEnumUnderlyingType(ref reader); + + bool typePrefixed = reader.ReadBoolean(); + if (typePrefixed && arrayType != Il2CppTypeEnum.IL2CPP_TYPE_OBJECT) + ThrowHelper.ThrowInvalidDataException("Array elements are type-prefixed, but the array type is not object"); + + object?[] array = new object?[arrayLength]; + for (int i = 0; i < arrayLength; i++) + { + Il2CppTypeEnum elementType = typePrefixed + ? (Il2CppTypeEnum)reader.ReadByte() + : arrayType; + + array[i] = ReadValue(ref reader, elementType); + } + + return array; + } + + private static string? ReadString(ref MemoryReader reader) + { + int length = ReadUnityCompressedInt32(ref reader); + if (length == -1) + return null; + + ReadOnlySpan bytes = reader.ReadBytes(length); + return Encoding.UTF8.GetString(bytes); + } + + private static void ResolveMember(ref MemoryReader reader) + { + // We don't care about attribute properties or fields, + // so we just read enough to exhaust the stream + + int memberIndex = ReadUnityCompressedInt32(ref reader); + if (memberIndex < 0) + { + uint typeIndex = ReadUnityCompressedUInt32(ref reader); + memberIndex = -(memberIndex + 1); + } + } +} \ No newline at end of file diff --git a/src/LibProtodec/Models/Cil/Il2Cpp/Il2CppMethod.cs b/src/LibProtodec/Models/Cil/Il2Cpp/Il2CppMethod.cs new file mode 100644 index 0000000..9a08194 --- /dev/null +++ b/src/LibProtodec/Models/Cil/Il2Cpp/Il2CppMethod.cs @@ -0,0 +1,57 @@ +// Copyright © 2024 Xpl0itR +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; +using System.Reflection; +using LibCpp2IL; +using LibCpp2IL.Metadata; +using LibCpp2IL.Reflection; + +namespace LibProtodec.Models.Cil.Il2Cpp; + +public sealed class Il2CppMethod(Il2CppMethodDefinition il2CppMethod) : Il2CppMember, ICilMethod +{ + public string Name => + il2CppMethod.Name!; + + public bool IsInherited => + false; + + public bool IsConstructor => + Name is ".ctor" or ".cctor"; + + public bool IsPublic => + (il2CppMethod.Attributes & MethodAttributes.Public) != 0; + + public bool IsStatic => + (il2CppMethod.Attributes & MethodAttributes.Static) != 0; + + public bool IsVirtual => + (il2CppMethod.Attributes & MethodAttributes.Virtual) != 0; + + public ICilType ReturnType => + Il2CppType.GetOrCreate( + LibCpp2ILUtils.GetTypeReflectionData( + LibCpp2IlMain.Binary!.GetType( + il2CppMethod.returnTypeIdx))); + + public IEnumerable GetParameterTypes() + { + foreach (Il2CppParameterReflectionData parameter in il2CppMethod.Parameters!) + { + yield return Il2CppType.GetOrCreate(parameter.Type); + } + } + + protected override Il2CppImageDefinition DeclaringAssembly => + il2CppMethod.DeclaringType!.DeclaringAssembly!; + + protected override int CustomAttributeIndex => + il2CppMethod.customAttributeIndex; + + protected override uint Token => + il2CppMethod.token; +} \ No newline at end of file diff --git a/src/LibProtodec/Models/Cil/Il2Cpp/Il2CppProperty.cs b/src/LibProtodec/Models/Cil/Il2Cpp/Il2CppProperty.cs new file mode 100644 index 0000000..abc004d --- /dev/null +++ b/src/LibProtodec/Models/Cil/Il2Cpp/Il2CppProperty.cs @@ -0,0 +1,56 @@ +// Copyright © 2024 Xpl0itR +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +using System.Linq; +using LibCpp2IL; +using LibCpp2IL.Metadata; + +namespace LibProtodec.Models.Cil.Il2Cpp; + +public sealed class Il2CppProperty(Il2CppPropertyDefinition il2CppProperty, Il2CppTypeDefinition declaringType) : Il2CppMember, ICilProperty +{ + private Il2CppMethod? _getter; + private Il2CppMethod? _setter; + + public string Name => + il2CppProperty.Name!; + + public bool IsInherited => + false; + + public bool CanRead => + il2CppProperty.get >= 0; + + public bool CanWrite => + il2CppProperty.set >= 0; + + public ICilMethod? Getter => + CanRead + ? _getter ??= new Il2CppMethod( + LibCpp2IlMain.TheMetadata!.methodDefs[ + declaringType.FirstMethodIdx + il2CppProperty.get]) + : null; + + public ICilMethod? Setter => + CanWrite + ? _setter ??= new Il2CppMethod( + LibCpp2IlMain.TheMetadata!.methodDefs[ + declaringType.FirstMethodIdx + il2CppProperty.set]) + : null; + + public ICilType Type => + Getter?.ReturnType + ?? Setter!.GetParameterTypes().First(); + + protected override Il2CppImageDefinition DeclaringAssembly => + declaringType.DeclaringAssembly!; + + protected override int CustomAttributeIndex => + il2CppProperty.customAttributeIndex; + + protected override uint Token => + il2CppProperty.token; +} \ No newline at end of file diff --git a/src/LibProtodec/Models/Cil/Il2Cpp/Il2CppType.cs b/src/LibProtodec/Models/Cil/Il2Cpp/Il2CppType.cs new file mode 100644 index 0000000..621a630 --- /dev/null +++ b/src/LibProtodec/Models/Cil/Il2Cpp/Il2CppType.cs @@ -0,0 +1,197 @@ +// Copyright © 2024 Xpl0itR +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Reflection; +using CommunityToolkit.Diagnostics; +using LibCpp2IL; +using LibCpp2IL.Metadata; +using LibCpp2IL.Reflection; + +namespace LibProtodec.Models.Cil.Il2Cpp; + +public sealed class Il2CppType : Il2CppMember, ICilType +{ + private readonly Il2CppTypeDefinition _il2CppType; + private readonly Il2CppTypeReflectionData[] _genericArgs; + private ICilType[]? _genericTypeArguments; + + private Il2CppType(Il2CppTypeDefinition il2CppType, Il2CppTypeReflectionData[] genericArgs) => + (_il2CppType, _genericArgs) = (il2CppType, genericArgs); + + public string Name => + _il2CppType.Name!; + + public string FullName => + _il2CppType.FullName!; + + public string? Namespace => + _il2CppType.Namespace; + + public string DeclaringAssemblyName => + LibCpp2IlMain.TheMetadata!.GetStringFromIndex( + DeclaringAssembly.nameIndex); + + public ICilType? DeclaringType => + IsNested + ? GetOrCreate( + LibCpp2ILUtils.GetTypeReflectionData( + LibCpp2IlMain.Binary!.GetType( + _il2CppType.DeclaringTypeIndex))) + : null; + + public ICilType? BaseType => + _il2CppType.ParentIndex == -1 + ? null + : GetOrCreate( + LibCpp2ILUtils.GetTypeReflectionData( + LibCpp2IlMain.Binary!.GetType( + _il2CppType.ParentIndex))); + + public bool IsAbstract => + _il2CppType.IsAbstract; + + public bool IsClass => + (_il2CppType.Attributes & TypeAttributes.ClassSemanticsMask) == TypeAttributes.Class + && !_il2CppType.IsValueType; + + public bool IsEnum => + _il2CppType.IsEnumType; + + public bool IsNested => + _il2CppType.DeclaringTypeIndex >= 0; + + public bool IsSealed => + (_il2CppType.Attributes & TypeAttributes.Sealed) != 0; + + public IList GenericTypeArguments + { + get + { + if (_genericTypeArguments is null) + { + _genericTypeArguments = _genericArgs.Length < 1 + ? Array.Empty() + : new ICilType[_genericArgs.Length]; + + for (int i = 0; i < _genericArgs.Length; i++) + { + _genericTypeArguments[i] = GetOrCreate(_genericArgs[i]); + } + } + + return _genericTypeArguments; + } + } + + public IEnumerable GetFields() + { + for (int i = 0; i < _il2CppType.FieldCount; i++) + { + yield return new Il2CppField( + LibCpp2IlMain.TheMetadata!.fieldDefs[ + _il2CppType.FirstFieldIdx + i]); + } + } + + public IEnumerable GetMethods() + { + for (int i = 0; i < _il2CppType.MethodCount; i++) + { + yield return new Il2CppMethod( + LibCpp2IlMain.TheMetadata!.methodDefs[ + _il2CppType.FirstMethodIdx + i]); + } + } + + public IEnumerable GetNestedTypes() + { + for (int i = 0; i < _il2CppType.NestedTypeCount; i++) + { + yield return GetOrCreate( + LibCpp2IlMain.TheMetadata!.typeDefs[ + LibCpp2IlMain.TheMetadata.nestedTypeIndices[ + _il2CppType.NestedTypesStart + i]]); + } + } + + public IEnumerable GetProperties() + { + for (int i = 0; i < _il2CppType.PropertyCount; i++) + { + yield return new Il2CppProperty( + LibCpp2IlMain.TheMetadata!.propertyDefs[ + _il2CppType.FirstPropertyId + i], + _il2CppType); + } + } + + public bool IsAssignableTo(ICilType type) + { + if (type is Il2CppType il2CppType) + { + return IsAssignableTo(_il2CppType, il2CppType._il2CppType); + } + + return ThrowHelper.ThrowNotSupportedException(); + } + + protected override Il2CppImageDefinition DeclaringAssembly => + _il2CppType.DeclaringAssembly!; + + protected override int CustomAttributeIndex => + _il2CppType.CustomAttributeIndex; + + protected override uint Token => + _il2CppType.Token; + + + private static readonly ConcurrentDictionary TypeLookup = []; + + public static ICilType GetOrCreate(Il2CppTypeDefinition il2CppType) => + TypeLookup.GetOrAdd( + il2CppType.FullName!, + static (_, il2CppType) => + new Il2CppType(il2CppType, Array.Empty()), + il2CppType); + + public static ICilType GetOrCreate(Il2CppTypeReflectionData il2CppTypeData) + { + Guard.IsTrue(il2CppTypeData.isType); + + return TypeLookup.GetOrAdd( + il2CppTypeData.ToString(), + static (_, il2CppTypeData) => + new Il2CppType(il2CppTypeData.baseType!, il2CppTypeData.genericParams), + il2CppTypeData); + } + + private static bool IsAssignableTo(Il2CppTypeDefinition thisType, Il2CppTypeDefinition baseType) + { + if (baseType.IsInterface) + { + foreach (Il2CppTypeReflectionData @interface in thisType.Interfaces!) + { + if (@interface.baseType == baseType) + { + return true; + } + } + } + + if (thisType == baseType) + { + return true; + } + + Il2CppTypeDefinition? thisTypeBaseType = thisType.BaseType?.baseType; + + return thisTypeBaseType is not null + && IsAssignableTo(thisTypeBaseType, baseType); + } +} \ No newline at end of file diff --git a/src/LibProtodec/ProtodecContext.cs b/src/LibProtodec/ProtodecContext.cs index ef0d09b..71f7f71 100644 --- a/src/LibProtodec/ProtodecContext.cs +++ b/src/LibProtodec/ProtodecContext.cs @@ -572,7 +572,7 @@ public class ProtodecContext protected string TranslateEnumFieldName(IEnumerable attributes, string fieldName, string enumName) { - if (attributes.SingleOrDefault(static attr => attr.Type.Name == "OriginalNameAttribute") + if (attributes.SingleOrDefault(static attr => attr is { CanReadConstructorArgumentValues: true, Type.Name: "OriginalNameAttribute" }) ?.ConstructorArgumentValues[0] is string originalName) { return originalName; @@ -627,8 +627,9 @@ public class ProtodecContext name.Length == 11 && name.CountUpper() == 11; protected static bool HasGeneratedCodeAttribute(IEnumerable attributes, string tool) => - attributes.Any(attr => attr.Type.Name == nameof(GeneratedCodeAttribute) - && attr.ConstructorArgumentValues[0] as string == tool); + attributes.Any(attr => attr.Type.Name == nameof(GeneratedCodeAttribute) + && (!attr.CanReadConstructorArgumentValues + || attr.ConstructorArgumentValues[0] as string == tool)); protected static bool HasNonUserCodeAttribute(IEnumerable attributes) => attributes.Any(static attr => attr.Type.Name == nameof(DebuggerNonUserCodeAttribute)); diff --git a/src/protodec/Program.cs b/src/protodec/Program.cs index 9e46bb0..792d6de 100644 --- a/src/protodec/Program.cs +++ b/src/protodec/Program.cs @@ -7,7 +7,9 @@ using System.CodeDom.Compiler; using System.Collections.Generic; using System.IO; +using AssetRipper.Primitives; using ConsoleAppFramework; +using LibCpp2IL; using LibProtodec; using LibProtodec.Loaders; using LibProtodec.Models.Cil; @@ -21,7 +23,7 @@ app.Run(args); internal sealed class Commands { /// - /// A tool to decompile protobuf classes compiled by protoc, from CIL assemblies back into .proto definitions. + /// Use reflection backend to load target CIL assembly and its dependants. /// /// Either the path to the target assembly or a directory of assemblies, all of which be parsed. /// An existing directory to output into individual files, otherwise output to a single file. @@ -41,6 +43,88 @@ internal sealed class Commands bool parseServiceServers, bool parseServiceClients, LogLevel logLevel = LogLevel.Information) + { + using ILoggerFactory loggerFactory = CreateLoggerFactory(logLevel); + ILogger logger = CreateProtodecLogger(loggerFactory); + + logger.LogInformation("Loading target assemblies..."); + using ClrAssemblyLoader loader = new(targetPath, loggerFactory.CreateLogger()); + + Handle( + loader, + outPath, + skipEnums, + includePropertiesWithoutNonUserCodeAttribute, + includeServiceMethodsWithoutGeneratedCodeAttribute, + parseServiceServers, + parseServiceClients, + loggerFactory, + logger); + } + + /// + /// Use LibCpp2IL backend to directly load Il2Cpp compiled game assembly. EXPERIMENTAL. + /// + /// The path to the game assembly DLL. + /// The path to the global-metadata.dat file. + /// The version of Unity which was used to create the metadata file or alternatively, the path to the globalgamemanagers or the data.unity3d file. + /// An existing directory to output into individual files, otherwise output to a single file. + /// Logging severity level. + /// Parses gRPC service definitions from server classes. + /// Parses gRPC service definitions from client classes. + /// Skip parsing enums and replace references to them with int32. + /// Includes properties that aren't decorated with `DebuggerNonUserCode` when parsing. + /// Includes methods that aren't decorated with `GeneratedCode("grpc_csharp_plugin")` when parsing gRPC services. + [Command("il2cpp")] + public void Il2Cpp( + [Argument] string gameAssembly, + [Argument] string globalMetadata, + [Argument] string unityVersion, + [Argument] string outPath, + bool skipEnums, + bool includePropertiesWithoutNonUserCodeAttribute, + bool includeServiceMethodsWithoutGeneratedCodeAttribute, + bool parseServiceServers, + bool parseServiceClients, + LogLevel logLevel = LogLevel.Information) + { + if (!UnityVersion.TryParse(unityVersion, out UnityVersion unityVer, out _)) + { + unityVer = unityVersion.EndsWith("globalgamemanagers") + ? LibCpp2IlMain.GetVersionFromGlobalGameManagers( + File.ReadAllBytes(unityVersion)) + : LibCpp2IlMain.GetVersionFromDataUnity3D( + File.OpenRead(unityVersion)); + } + + using ILoggerFactory loggerFactory = CreateLoggerFactory(logLevel); + ILogger logger = CreateProtodecLogger(loggerFactory); + + logger.LogInformation("Loading target assemblies..."); + Il2CppAssemblyLoader loader = new(gameAssembly, globalMetadata, unityVer, loggerFactory); + + Handle( + loader, + outPath, + skipEnums, + includePropertiesWithoutNonUserCodeAttribute, + includeServiceMethodsWithoutGeneratedCodeAttribute, + parseServiceServers, + parseServiceClients, + loggerFactory, + logger); + } + + private static void Handle( + CilAssemblyLoader loader, + string outPath, + bool skipEnums, + bool includePropertiesWithoutNonUserCodeAttribute, + bool includeServiceMethodsWithoutGeneratedCodeAttribute, + bool parseServiceServers, + bool parseServiceClients, + ILoggerFactory loggerFactory, + ILogger logger) { ParserOptions options = ParserOptions.None; if (skipEnums) @@ -50,18 +134,6 @@ internal sealed class Commands if (includeServiceMethodsWithoutGeneratedCodeAttribute) options |= ParserOptions.IncludeServiceMethodsWithoutGeneratedCodeAttribute; - using ILoggerFactory loggerFactory = LoggerFactory.Create( - builder => builder.AddSimpleConsole(static console => console.IncludeScopes = true) - .SetMinimumLevel(logLevel)); - - ILogger logger = loggerFactory.CreateLogger("protodec"); - ConsoleApp.LogError = msg => logger.LogError(msg); - - logger.LogInformation("Loading target assemblies..."); - using CilAssemblyLoader loader = new ClrAssemblyLoader( - targetPath, - loggerFactory.CreateLogger()); - ProtodecContext ctx = new() { Logger = loggerFactory.CreateLogger() @@ -125,4 +197,17 @@ internal sealed class Commands ctx.WriteAllTo(indentWriter); } } + + private static ILoggerFactory CreateLoggerFactory(LogLevel logLevel) => + LoggerFactory.Create( + builder => builder.AddSimpleConsole(static console => console.IncludeScopes = true) + .SetMinimumLevel(logLevel)); + + private static ILogger CreateProtodecLogger(ILoggerFactory loggerFactory) + { + ILogger logger = loggerFactory.CreateLogger("protodec"); + ConsoleApp.LogError = msg => logger.LogError(msg); + + return logger; + } } \ No newline at end of file