-
-
[原创]什么?IL2CPP APP分析这一篇就够啦!
-
发表于: 2024-8-9 15:07 2678
-
本文作者:SWDD@360SRC
前言
近年来,由U3D开发的游戏越来越多,诸如最近很火的手游版“永劫无间”等等,因此针对于U3D游戏安全的保护也越来越高级,目前大多数厂商都会选择IL2CPP来编译游戏。即便如此,只使用简单的IL2CPP虽然在反编译上极大的增加了难度,但是由于C#+.Net的特性,无法像传统的ELF,EXE文件等完全抹除符号,所以还是给破解者留下了很大的操作空间,破解者可以通过对内存的读写来绕过游戏内一些机制的判定,使得游戏运营防遭受损失,因此本篇文章将针对于IL2CPP这一技术探究一下逆向与破解的过程。
最近刷抖音又发现了一款小爆款游戏,叫做我们是战士,后续调查发现抖音上那一款估计是套壳游戏,而原本这个游戏的名字叫做《We Are Warriors!》,但是由于这个游戏某些恶心的设定,于是决定逆向这个游戏看看到底是怎么回事,解压APK发现了该程序妥妥的U3D结构,并且使用了IL2CPP,于是便诞生了这篇总结文章。注意本文中不会涉及到对该游戏的逆向,而是利用demo程序等做同等的操作替换。
Unity3D项目结构
在开始分析il2cpp之前,首先我们了解一下一个unity app的项目结构:
1 2 3 4 5 6 7 8 9 10 11 | AppName/ ├── Assets/ # 包含所有游戏资源和脚本文件 ├── Library/ # Unity的库文件和缓存,自动生成 ├── ProjectSettings/ # Unity项目设置文件夹 ├── Packages/ # Unity Package Manager (UPM) 的依赖包 ├── obj/ # 中间对象文件夹,用于编译时 ├── Temp/ # 临时文件夹,包括临时生成的资源 ├── Build/ # 打包输出目录,包括生成的App文件和数据 ├── Logs/ # 日志文件夹 ├── Packages/ # Unity Package Manager (UPM) 的依赖包 └── ProjectSettings/ # Unity项目设置文件夹 |
在生成的时候\Temp\StagingArea目录下则会产生安卓编译时所需要的内容。
生成APK后的结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 | AppName/ ├── AndroidManifest.xml # Android应用清单文件 ├── assets/ # 包含资源文件夹,如图像和声音 ├── res/ # 包含资源文件夹,如布局和字符串 ├── lib/ # 包含原生库文件夹,如armeabi-v7a和arm64-v8a ├── META-INF/ # 包含APK签名的META-INF文件夹 ├── classes.dex # Android应用的主要DEX文件 ├── resources.arsc # 包含资源表文件 ├── AndroidManifest.xml # Android清单文件 ├── res/ # 包含资源文件夹,如布局和字符串 ├── assets/ # 包含资源文件夹,如图像和声音 ├── lib/ # 包含原生库文件夹,如armeabi-v7a和arm64-v8a |
打包成APK的unity项目实际上和正常的unity项目是一致的,其中的Java代码主要用于实现unity和Android平台的交互,是由unity自己生成的代码,因此我们在对Unity项目分析时主要关注的还是在assets目录下储存的项目信息。
Assets目录结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | │ bin\ │ │ Data\ │ │ │ ├── boot.config │ │ │ ├── data.unity3d │ │ │ ├── platform_native_link.xml │ │ │ ├── resources.resource │ │ │ ├── RuntimeInitializeOnLoads.json │ │ │ ├── ScriptingAssemblies.json │ │ │ └── unity default resources │ │ │ Managed\ │ │ │ │ dll文件\ │ │ Managed\ │ │ │ dll文件\ │ Data\ │ │ ├── boot.config │ │ ├── data.unity3d │ │ ├── platform_native_link.xml │ │ ├── resources.resource │ │ ├── RuntimeInitializeOnLoads.json │ │ ├── ScriptingAssemblies.json │ │ └── unity default resources │ │ Managed\ │ │ │ dll文件\ │ Managed\ │ │ dll文件\ bin\ │ Data\ │ │ ├── boot.config │ │ ├── data.unity3d │ │ ├── platform_native_link.xml │ │ ├── resources.resource │ │ ├── RuntimeInitializeOnLoads.json │ │ ├── ScriptingAssemblies.json │ │ └── unity default resources │ │ Managed\ │ │ │ dll文件\ │ Managed\ │ │ dll文件\ Data\ │ ├── boot.config │ ├── data.unity3d │ ├── platform_native_link.xml │ ├── resources.resource │ ├── RuntimeInitializeOnLoads.json │ ├── ScriptingAssemblies.json │ └── unity default resources │ Managed\ │ │ dll文件\ Managed\ │ dll文件\ |
游戏的主要脚本逻辑就在assets\bin\Data\Managed\Assembly-CSharp.dll中,因此针对于使用Mono打包的Unity 3D项目直接使用ILSPY或者使用DNSPY就可以实现反编译,并且阅读性与源码基本无异。因为如此,导致了Mono打包的Unity App存在着极高的被破解风险,由此现在大多数的Unity 游戏都不使用Mono打包了,都开始使用iL2cpp,那么iL2cpp是如何提升逆向破解难度的呢?请继续往下看。
IL2CPP分析
IL2CPP是Unity引入的一种新的脚本后处理方式,用于加强对编译后代码的保护。在Unity开发中,人物操作、攻击、伤害和死亡判断通常通过C#脚本实现。IL2CPP通过将C#脚本编译为C++代码,然后再编译为本地代码,提供了额外的安全层。这种方式使得反编译和逆向工程变得更加困难,有助于保护知识产权和游戏内容的安全性。
IL2CPP生成分析
IL2CPP的代码位于\Editor\Data\il2cpp\libil2cpp\codegen,通过分析可以发现,IL2CPP采用了一种类似于虚拟机的机制。它通过将C#代码编译成中间语言(IL),然后再将IL代码转换成C++代码,最终编译为本地机器码。在这个过程中,IL2CPP对代码进行了多层次的优化和处理。
IL2CPP在将C#代码编译成IL代码时,会对代码进行一定程度的优化,例如移除不必要的代码和进行常量折叠。接着,IL代码会被转化为C++代码。在这个阶段,IL2CPP生成的C++代码不仅包含了原始C#代码的逻辑,还加入了一些辅助的代码,用于实现运行时环境和垃圾回收等功能。生成的C++代码会被编译为本地机器码。这一步通常会使用平台相关的编译器,以确保生成的代码在目标平台上具有最佳的性能和兼容性。由于C++代码在编译后变成了本地机器码,反编译的难度大大增加,从而增强了代码的安全性。IL2CPP还通过各种手段来优化代码执行的效率。它会对频繁执行的代码路径进行优化,以减少运行时的开销;它还会使用高效的数据结构和算法,以提高整体性能。
iL2cpp编译过程首先是将C#的脚本,还有Unity引擎的代码,Boo 代码通过各自的编译器编译为IL指令代码,然后还有一些其他的IL代码一起通过iL2cpp转化成C++代码,然后通过C++编译成libIL2cpp.so,再由Il2cpp提供的虚拟机对代码进行解释和运行。在其源码的结构中,也可以发现其两个部分的代码。
将流程转换成线性图则如下图所示:
IL2CPP加载分析
根据IL2CPP的生成过程,我们会发现游戏的逻辑都到了Native运行,那么C#的语言特性需要如何继续实现呢,我们可以看vm中的代码。
在\Unity Edit\2021.3.22f1c1\Editor\Data\il2cpp\libil2cpp\vm\GlobalMetadata.cpp中我们可以发现如下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | bool il2cpp::vm::GlobalMetadata::Initialize(int32_t* imagesCount, int32_t* assembliesCount) { s_GlobalMetadata = vm::MetadataLoader::LoadMetadataFile( "global-metadata.dat" ); if (!s_GlobalMetadata) return false ; s_GlobalMetadataHeader = ( const Il2CppGlobalMetadataHeader*)s_GlobalMetadata; IL2CPP_ASSERT(s_GlobalMetadataHeader->sanity == 0xFAB11BAF); IL2CPP_ASSERT(s_GlobalMetadataHeader->version == 29); IL2CPP_ASSERT(s_GlobalMetadataHeader->stringLiteralOffset == sizeof (Il2CppGlobalMetadataHeader)); s_MetadataImagesCount = *imagesCount = s_GlobalMetadataHeader->imagesSize / sizeof (Il2CppImageDefinition); *assembliesCount = s_GlobalMetadataHeader->assembliesSize / sizeof (Il2CppAssemblyDefinition); // Pre-allocate these arrays so we don't need to lock when reading later. // These arrays hold the runtime metadata representation for metadata explicitly // referenced during conversion. There is a corresponding table of same size // in the converted metadata, giving a description of runtime metadata to construct. s_MetadataImagesTable = (Il2CppImageGlobalMetadata*)IL2CPP_CALLOC(s_MetadataImagesCount, sizeof (Il2CppImageGlobalMetadata)); s_TypeInfoTable = (Il2CppClass**)IL2CPP_CALLOC(s_Il2CppMetadataRegistration->typesCount, sizeof (Il2CppClass*)); s_TypeInfoDefinitionTable = (Il2CppClass**)IL2CPP_CALLOC(s_GlobalMetadataHeader->typeDefinitionsSize / sizeof (Il2CppTypeDefinition), sizeof (Il2CppClass*)); s_MethodInfoDefinitionTable = ( const MethodInfo**)IL2CPP_CALLOC(s_GlobalMetadataHeader->methodsSize / sizeof (Il2CppMethodDefinition), sizeof (MethodInfo*)); s_GenericMethodTable = ( const Il2CppGenericMethod**)IL2CPP_CALLOC(s_Il2CppMetadataRegistration->methodSpecsCount, sizeof (Il2CppGenericMethod*)); ProcessIl2CppTypeDefinitions(InitializeTypeHandle, InitializeGenericParameterHandle); return true ; } |
这个代码在加载global-metadata.dat,并且对其做了合法性判断。继续阅读后我们还会发现其使用了GetStringLiteralFromIndex(StringLiteralIndex index)等函数加载了字符信息,函数指针信息等一系列内容。
为了更好的分析,我们可以通过010导入UnityMetadata.bt的模板文件,使得文件的结构更加清晰。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 | //------------------------------------------------ //--- 010 Editor v13.0.1 Binary Template // // File: UnityMetadata.bt // Authors: xia0 // Version: 0.2 // Purpose: Parse unity3d metadata file // Category: Game // File Mask: *.dat // ID Bytes: FA B1 1B AF // History: // 0.2 2023-03-24 avan: Automatically generate the string content of all StringLiterals based on the offset value of the StringLiteral in GlobalMetadataHeader. // 0.1 2019-10-31 xia0: init basic unity3d metadata info version //------------------------------------------------ // Blog: https://4ch12dy.site // Github: https://github.com/4ch12dy // https://www.sweetscape.com/010editor/manual/DataTypes.htm // http://www.sweetscape.com/010editor/repository/templates/ typedef int32 TypeIndex; typedef int32 TypeDefinitionIndex; typedef int32 FieldIndex; typedef int32 DefaultValueIndex; typedef int32 DefaultValueDataIndex; typedef int32 CustomAttributeIndex; typedef int32 ParameterIndex; typedef int32 MethodIndex; typedef int32 GenericMethodIndex; typedef int32 PropertyIndex; typedef int32 EventIndex; typedef int32 GenericContainerIndex; typedef int32 GenericParameterIndex; typedef int16 GenericParameterConstraintIndex; typedef int32 NestedTypeIndex; typedef int32 InterfacesIndex; typedef int32 VTableIndex; typedef int32 InterfaceOffsetIndex; typedef int32 RGCTXIndex; typedef int32 StringIndex; typedef int32 StringLiteralIndex; typedef int32 GenericInstIndex; typedef int32 ImageIndex; typedef int32 AssemblyIndex; typedef int32 InteropDataIndex; typedef struct Il2CppGlobalMetadataHeader { int32 sanity <format=hex>; int32 version; int32 stringLiteralOffset <comment= "string data for managed code" >; int32 stringLiteralCount; int32 stringLiteralDataOffset; int32 stringLiteralDataCount; int32 stringOffset <comment= "string data for metadata" >; int32 stringCount; int32 eventsOffset <comment= "Il2CppEventDefinition" >; int32 eventsCount; int32 propertiesOffset <comment= "Il2CppPropertyDefinition" >; int32 propertiesCount; int32 methodsOffset <comment= "Il2CppMethodDefinition" >; int32 methodsCount; int32 parameterDefaultValuesOffset <comment= "Il2CppParameterDefaultValue" >; int32 parameterDefaultValuesCount; int32 fieldDefaultValuesOffset <comment= "Il2CppFieldDefaultValue" >; int32 fieldDefaultValuesCount; int32 fieldAndParameterDefaultValueDataOffset; //uint8_t int32 fieldAndParameterDefaultValueDataCount; int32 fieldMarshaledSizesOffset <comment= "Il2CppFieldMarshaledSize" >; int32 fieldMarshaledSizesCount; int32 parametersOffset <comment= "Il2CppParameterDefinition" >; int32 parametersCount; int32 fieldsOffset <comment= "Il2CppFieldDefinition" >; int32 fieldsCount; int32 genericParametersOffset <comment= "Il2CppGenericParameter" >; int32 genericParametersCount; int32 genericParameterConstraintsOffset <comment= "TypeIndex" >; int32 genericParameterConstraintsCount; int32 genericContainersOffset <comment= "Il2CppGenericContainer" >; int32 genericContainersCount; int32 nestedTypesOffset <comment= "TypeDefinitionIndex" >; int32 nestedTypesCount; int32 interfacesOffset <comment= "TypeIndex" >; int32 interfacesCount; int32 vtableMethodsOffset <comment= "EncodedMethodIndex" >; int32 vtableMethodsCount; int32 interfaceOffsetsOffset <comment= "Il2CppInterfaceOffsetPair" >; int32 interfaceOffsetsCount; int32 typeDefinitionsOffset <comment= "Il2CppTypeDefinition" >; int32 typeDefinitionsCount; int32 rgctxEntriesOffset <comment= "Il2CppRGCTXDefinition" >; int32 rgctxEntriesCount; int32 imagesOffset <comment= "Il2CppImageDefinition" >; int32 imagesCount; int32 assembliesOffset <comment= "Il2CppAssemblyDefinition" >; int32 assembliesCount; int32 metadataUsageListsOffset <comment= "Il2CppMetadataUsageList" >; int32 metadataUsageListsCount; int32 metadataUsagePairsOffset <comment= "Il2CppMetadataUsagePair" >; int32 metadataUsagePairsCount; int32 fieldRefsOffset <comment= "Il2CppFieldRef" >; int32 fieldRefsCount; int32 referencedAssembliesOffset; // int32 int32 referencedAssembliesCount; int32 attributesInfoOffset <comment= "Il2CppCustomAttributeTypeRange" >; int32 attributesInfoCount; int32 attributeTypesOffset <comment= "TypeIndex" >; int32 attributeTypesCount; int32 unresolvedVirtualCallParameterTypesOffset <comment= "TypeIndex" >; int32 unresolvedVirtualCallParameterTypesCount; int32 unresolvedVirtualCallParameterRangesOffset <comment= "Il2CppRange" >; int32 unresolvedVirtualCallParameterRangesCount; int32 windowsRuntimeTypeNamesOffset <comment= "Il2CppWindowsRuntimeTypeNamePair" >; int32 windowsRuntimeTypeNamesSize; int32 exportedTypeDefinitionsOffset <comment= "TypeDefinitionIndex" >; int32 exportedTypeDefinitionsCount; } Il2CppGlobalMetadataHeader; typedef struct Il2CppStringLiteralInfoDefinition { uint32 Length; uint32 Offset; } Il2CppStringLiteralInfoDefinition; typedef struct (uint infoSize, uint stringLiteralDataOffset, Il2CppStringLiteralInfoDefinition StringLiteralInfos[]) { typedef struct (uint stringLiteralDataOffset, uint index, Il2CppStringLiteralInfoDefinition StringLiteralInfos[]) { local uint infoOffset = StringLiteralInfos[index].Offset; local uint infoLength = StringLiteralInfos[index].Length; FSeek(stringLiteralDataOffset + infoOffset); if (infoLength > 0) char data[infoLength] <optimize= false >; } StringLiteralDefinition <read=(infoLength > 0 ? data : "null" )>; local uint index = 0; while (index + 1 < infoSize) StringLiteralDefinition StringLiteralDefinitions(stringLiteralDataOffset, index++, StringLiteralInfos); } Il2CppStringLiteralDefinition; typedef struct Il2CppImageDefinition { StringIndex nameIndex; AssemblyIndex assemblyIndex; TypeDefinitionIndex typeStart; uint32 typeCount; TypeDefinitionIndex exportedTypeStart; uint32 exportedTypeCount; MethodIndex entryPointIndex; uint32 token; CustomAttributeIndex customAttributeStart; uint32 customAttributeCount; } Il2CppImageDefinition; #define PUBLIC_KEY_BYTE_LENGTH 8 typedef struct Il2CppAssemblyNameDefinition { StringIndex nameIndex; StringIndex cultureIndex; StringIndex hashValueIndex; StringIndex publicKeyIndex; uint32 hash_alg; int32 hash_len; uint32 flags; int32 major; int32 minor; int32 build; int32 revision; ubyte public_key_token[PUBLIC_KEY_BYTE_LENGTH]; } Il2CppAssemblyNameDefinition; typedef struct Il2CppAssemblyDefinition { ImageIndex imageIndex; uint32 token; int32 referencedAssemblyStart; int32 referencedAssemblyCount; Il2CppAssemblyNameDefinition aname; } Il2CppAssemblyDefinition; typedef struct Il2CppTypeDefinition { StringIndex nameIndex; StringIndex namespaceIndex; TypeIndex byvalTypeIndex; TypeIndex byrefTypeIndex; TypeIndex declaringTypeIndex; TypeIndex parentIndex; TypeIndex elementTypeIndex; // we can probably remove this one. Only used for enums RGCTXIndex rgctxStartIndex; int32 rgctxCount; GenericContainerIndex genericContainerIndex; uint32 flags; FieldIndex fieldStart; MethodIndex methodStart; EventIndex eventStart; PropertyIndex propertyStart; NestedTypeIndex nestedTypesStart; InterfacesIndex interfacesStart; VTableIndex vtableStart; InterfacesIndex interfaceOffsetsStart; uint16 method_count; uint16 property_count; uint16 field_count; uint16 event_count; uint16 nested_type_count; uint16 vtable_count; uint16 interfaces_count; uint16 interface_offsets_count; // bitfield to portably encode boolean values as single bits // 01 - valuetype; // 02 - enumtype; // 03 - has_finalize; // 04 - has_cctor; // 05 - is_blittable; // 06 - is_import_or_windows_runtime; // 07-10 - One of nine possible PackingSize values (0, 1, 2, 4, 8, 16, 32, 64, or 128) uint32 bitfield; uint32 token; } Il2CppTypeDefinition; typedef struct Il2CppMetadataUsageList { uint32 start; uint32 count; } Il2CppMetadataUsageList; typedef struct Il2CppMetadataUsagePair { uint32 destinationIndex; uint32 encodedSourceIndex; } Il2CppMetadataUsagePair; Il2CppGlobalMetadataHeader metadataHeader <comment= "metadata header information" >; local uint infoSize = metadataHeader.stringLiteralCount / sizeof (Il2CppStringLiteralInfoDefinition); FSeek(metadataHeader.stringLiteralOffset); Il2CppStringLiteralInfoDefinition StringLiteralInfoDefinitions[infoSize] <comment= "metadata define StringLiteralInfo" >; Il2CppStringLiteralDefinition StringLiteralDefinitions(infoSize, metadataHeader.stringLiteralDataOffset, StringLiteralInfoDefinitions) <comment= "metadata define StringLiteralDefinitions" >; FSeek(metadataHeader.imagesOffset); Il2CppImageDefinition imagesDefinitions[metadataHeader.imagesCount / sizeof (Il2CppImageDefinition)] <comment= "metadata define images" >; FSeek(metadataHeader.assembliesOffset); Il2CppAssemblyDefinition assemblyDefinitions[metadataHeader.imagesCount / sizeof (Il2CppImageDefinition)] <comment= "metadata define assemblys" >; FSeek(metadataHeader.typeDefinitionsOffset); Il2CppTypeDefinition typeDefinitions[metadataHeader.assembliesCount / sizeof (Il2CppAssemblyDefinition)] <comment= "metadata define types" >; FSeek(metadataHeader.metadataUsagePairsOffset); Il2CppMetadataUsagePair metadataUsagePair[metadataHeader.metadataUsagePairsCount / sizeof (Il2CppMetadataUsagePair)] <comment= "metadata metadata usage pair" >; FSeek(metadataHeader.metadataUsageListsOffset); Il2CppMetadataUsageList metadataUsageList[metadataHeader.metadataUsageListsCount / sizeof (Il2CppMetadataUsageList)] <comment= "metadata metadata usage list" >; |
在游戏被打包成APK后metadata被储存的路径位于\assets\bin\Data\Managed\Metadata中,我们使用010对其分析,当我们导入并且运行模板的时候就可以在检查器中查找到各个结构体了
)
既然如此,我们则可以通过解析global-metadata.dat的信息来获取函数指针,并且通过偏移去查找libil2cpp中的游戏逻辑,当然仅凭这些,我们无法得知具体的字符串的,还需要利用到ilbil2cpp.so进行寻址的操作。
IL2CPP逆向分析
由于在libil2cpp中我们是无法直接看到符号信息的,因此我们如果直接逆向il2cpp的话十分困难。但是我们可以通过global-metadata.dat以及il2cpp来恢复libil2cpp的符号信息。
Il2cppDumper使用以及分析
源码分析
这里介绍一下Il2cppDumper的原理,并对其源码做一定的解释。
)
在执行dump之前,Il2cppDumper会对il2cpp进行解析。
在Init方法中加载了il2cpp。
之后,根据不同的magic number对二进制文件进行解析。
)
后在dump中继续解析,完成dll的构建,其中用到了Il2CppDecompiler等反编译il2cpp的方法,用于提取其符号信息等等,这里就不过多赘述了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | private static void Dump(Metadata metadata, Il2Cpp il2Cpp, string outputDir) { Console.WriteLine( "Dumping..." ); var executor = new Il2CppExecutor(metadata, il2Cpp); var decompiler = new Il2CppDecompiler(executor); decompiler.Decompile(config, outputDir); Console.WriteLine( "Done!" ); if (config.GenerateStruct) { Console.WriteLine( "Generate struct..." ); var scriptGenerator = new StructGenerator(executor); scriptGenerator.WriteScript(outputDir); Console.WriteLine( "Done!" ); } if (config.GenerateDummyDll) { Console.WriteLine( "Generate dummy dll..." ); DummyAssemblyExporter.Export(executor, outputDir, config.DummyDllAddToken); Console.WriteLine( "Done!" ); } } |
使用il2cppDumper获取Assembly-Csharp.dll
下载地址:Perfare/Il2CppDumper: Unity il2cpp reverse engineer (github.com)
根据刚刚的源码分析,我们也能发现iL2cppDumper的操作,运行Il2CppDumper.exe依次选择libil2cpp.so以及global-metadata.dat,运行完成后显示如下界面(其实可以发现其输出界面和我们刚刚的流程是一致的)
则代表解密成功,如果报错,则可能是global-metadata.dat被加密了,这个时候我们首先需要解密global-metadata.dat。解密该文件的方法有很多,可以通过分析起加密过程或者通过Hook动态dump等方式解密原本的dll,可以使用Perfare/Zygisk-Il2CppDumper: Using Zygisk to dump il2cpp data at runtime (github.com)
现在介绍正常dump下来的dll,dump下来之后我们需要关注几个关键文件:
这几个文件中我们首先需要看到的是DummyDll文件
其中可以找到我们在做U3D逆向时熟悉的dll,但是需要注意的是这个dll并不会储存源码,我们能在这个dll文件中看到原本程序的方法名和变量名,还有程序的结构。
U3D iL2cpp游戏逆向实战
初步分析
这里用到的是一个坦克大战小游戏的demo,游戏主界面如下:
游戏可以通过购买坦克来增加自己的实力,那么我们就从白嫖他的坦克开始分析这个程序,首先还是用Dnspy或者使用ILspy去解析这个Assembly-Csharp.dll获取方法的参数和偏移。
既然要分析购买功能,那我们可以通过方法的关键词去搜索,类似于Buy,Price,Coins之类的,这里我们搜索Buy。
我们使用搜索功能(CTRL+F),这里还需要注意的是我们需要选择非泛型类型,这个其实就是找类了,熟悉开发的同学应该可以明白,很多功能都是在类中实现的。那么对于这个demo我们很快就搜索到了BuyTank类,我们可以点进去看看类里面有一些什么方法。
哎,局势瞬间明了,购买逻辑是通过Buy()方法实现的,然后其中涉及到了一些成员变量,比如price和textPrice什么的,点开Buy方法,查看方法的一些基本信息。
这里可以发现,Buy方法没有返回值,没有参数,那么购买逻辑肯定是在内部判断的,也就是意味着Buy方法内部存在获取用户金钱数量的逻辑,那么我们通过修改这个逻辑就可以白嫖他的坦克了。
逻辑修改
不难发现的是dll程序中是没有实现逻辑的,逻辑存在于il2cpp.dll中,初步打开里面啥也没有,符号也没有什么都没有,因此我们需要恢复符号表。其实在我们dump下dll的时候恢复符号表的脚本就已经在il2cppdumper的目录下产生了,我们只需要使用其准备好的脚本即可。
在IDA中按alt+f7进入加载脚本界面,然后选择ida_py3.py,选择scripts.json就可以恢复符号表了,值得注意的是产生的ida database文件大小可能会是以G为单位,需要预留足够的空间。
恢复符号表后我们可以直接在IDA中搜索Buy,或者使用Dnspy提供的便宜寻找,这里我们已经知道偏移是0x19a2928,使用IDA 的G键跳转过去即可。
逻辑比较杂乱,那么我们依旧需要借助Dnspy中的信息来分析
对于Buy逻辑,price肯定是一个重要的变量,那么我们就可以在Dnspy中查找到他的偏移地址。
这里可以发现偏移地址是0x48
那么这个v1+0x48则是我们的坦克的价格了。
于此同时我们还可以在gameDataBase中找到money的偏移。
正好是0x18,那么就可以确定 这是在判断是否买得起了。
那么我们只需要trace到FCMP S0, S1这一行并且保证 S0>S1 就可以了,Frida代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | function main() { Java.perform( function () { var module = Process.getModuleByName( "libil2cpp.so" ); var addr = module.base.add( "0x19A299C" ); var func = new NativePointer(addr.toString()); Interceptor.attach(func, { onEnter: function (args) { this .context.s0 = 50000; }, onLeave: function (retval) { console.log( '[+] Method onLeave : ' , retval); } }); }); } setTimeout(main, 250); |
当我们购买时可以发现钱数量减少成复数,并且成功购买。
扩展
当然上文中有提到Perfare/Zygisk-Il2CppDumper: Using Zygisk to dump il2cpp data at runtime (github.com)项目,这是一个动态的dump的Magisk模块,通常在上面的Il2cppDumper无法静态的获取符号信息的时候我们用这个来处理。
首先下载源码之后使用Android Studio 打开:
修改game.h中的包名之后,选择Gradle中的编译模式。后再Build中Make Project即可:
导入模块运行之后app私有目录下file文件夹中就会存在dump.cs
总结
在逆向工程中,IL2CPP为Unity应用程序引入了额外的复杂性,通过将C#脚本转换为C++代码再编译成本地机器码,从而提高了反编译和逆向工程的难度。然而,借助工具和方法,如IL2CPPDumper和Frida,可以有效地分析和修改IL2CPP应用程序。
通过本文的分析和实践,我们了解了Unity3D项目的基本结构,以及IL2CPP的生成和加载过程。我们通过使用IL2CPPDumper成功地提取了关键的元数据和符号信息,并通过反编译工具和动态分析工具对其进行了深入分析和修改。具体的步骤包括解析global-metadata.dat文件,恢复libil2cpp.so的符号表,以及使用Frida进行运行时修改。
值得注意的是,随着技术的发展,各大厂商对global-metadata.dat文件的加密手段层出不穷,但无论如何加密,这些数据最终都会在运行时被解密并使用。因此,利用Hook等动态分析手段是获取解密后数据的有效方法。
在实际操作中,Frida提供了强大的动态修改和调试功能,可以避免重打包和签名绕过的问题,极大地简化了逆向工程的过程。通过本文的实例演示,我们成功地修改了游戏的购买逻辑,实现了零成本购买道具的效果。
总的来说,IL2CPP逆向工程虽然复杂,但通过合理利用现有工具和技术手段,可以有效地进行分析和修改,满足逆向工程的需求。希望本文的总结能够对读者在IL2CPP逆向工程中提供有价值的参考和帮助。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!