查看原文
其他

Il2Cpp恢复符号过程分析

R1mao 看雪学苑 2022-07-13


本文为看雪论坛优秀文章
看雪论坛作者ID:R1mao





Il2Cpp介绍


unity作为两大游戏引擎之一,其安全性也值得被关注。在早期unity的脚本都是采用C#编写的,直接编译成C#模块,所以直接使用C#反编译器即可非常完整的获得游戏的源码,此时的Unity脚本后处理引擎为Mono。

而由于C#的效率问题和安全性问题,Unity推出了新的脚本后处理引擎Il2Cpp,该引擎分为两个部分,一个是AOT静态编译引擎,一个是libil2cpp运行时库。

前者通过将C# IL编译成C++代码,从而交由不同平台编译器编译,后者则实现了诸如垃圾回收、线程/文件获取、内部调用直接修改托管数据结构的原生代码的服务与抽象。


Il2Cpp编译的游戏,往往有两个重要的文件,一个是GameAssembly.dll,该文件是由C++代码编译而成的程序,游戏重要的逻辑都在该文件内,其中包含了il2cpp的运行时库。另一个文件是global-metadata.dat,该文件保存了一些重要的字符串信息和一些元数据,用于il2cpp的动态特性,例如反射等。

这里着重分析如果通过解析GameAssembly.dll和global-metadata.dat来恢复其中包含的类名,方法名,field偏移。




解析global-metadata.dat


首先得弄明白该文件保存了哪些内容,可以找到libil2cpp的源码。直接搜索global-metadata.dat即可找到相关代码。该函数位于vm/GlobalMetadata.cpp下。


可以看到代码直接将文件内容读入后,转化成了一个结构体,Il2CppGlobalMetadataHeader。该结构体定义在了GlobalMetadataFileInternals.h里,其中包含了一些重要的信息。


1)获取所有Image


首先得知道其中包含的各个Image信息。直接定位到结构体中的这两个域,对应着Il2CppImageDefinition数组的偏移,该结构保存了Image的信息。imagesSize记录了Il2CppImageDefinition数组的大小。

可以通过如下方法获得Il2CppImageDefinition数组,并且进行遍历。
Il2CppGlobalMetadataHeader *header=(Il2CppGlobalMetadataHeader*)ptr; if(header->sanity!=0xFAB11BAF || header->stringLiteralOffset!=sizeof(Il2CppGlobalMetadataHeader)) { printf("invalid file..\n"); return 0; } int image_count=header->imagesSize/sizeof(Il2CppImageDefinition); for(int i=0;i<image_count;i++) { const Il2CppImageDefinition *image=&image_arr[i]; }

接着来查看Il2CppImageDefinition结构体,可以发现里面包含了Image的名字相关信息StringIndex nameIndex。


StringIndex是一个整型,是一个索引,global-metadata.dat中存在一个字符串表,所有metadata相关的字符串都放在了一起,通过这个索引进行引用,这个字符串表通过Il2CppGlobalMetadataHeader下的偏移stringOffset计算得到。

可以通过这样的函数获取StringIndex对应的字符串,ptr是Il2CppGlobalMetadataHeader的地址。
static const char* GetStringFromIndex(StringIndex index){ return (const char*)(((Il2CppGlobalMetadataHeader*)ptr)->stringOffset+ptr+index);}


2)获取Image下的类


如何获取该image所有的类呢,这就需要获取对应的Il2CppTypeDefinition,在Il2CppGlobalMetadataHeader结构体下存在typeDefinitionsOffset,而由于Il2CppImageDefinition存在一个TypeDefinitionIndex typeStart的域,所以可以类比StringIndex。

类比StringIndex写出如下代码。
static const Il2CppTypeDefinition* GetTypeDefinitionFromIndex(TypeDefinitionIndex index){ return (const Il2CppTypeDefinition*)(ptr+((Il2CppGlobalMetadataHeader*)ptr)->typeDefinitionsOffset)+index;}

然后便可以获得Image下的所有Il2CppTypeDefinition了。也就是获取所有类的元数据。
const Il2CppImageDefinition *image=&image_arr[i]; printf("image: %s\n",GetStringFromIndex(image->nameIndex)); for(int j=0;j<image->typeCount;j++) { const Il2CppTypeDefinition *type=GetTypeDefinitionFromIndex(image->typeStart+j); printf("class: %s:%s\n",GetStringFromIndex(type->namespaceIndex),GetStringFromIndex(type->nameIndex)); }

这样就能解析出了各种Image下的类。


3)获取类下的方法名和Field


此时需要我们进一步的分析类下面的方法,此时查看Il2CppTypeDefinition结构体可以发现其下有两个域值得注意。


同样的在Il2CppGlobalMetadataHeader由两个域正好对应的上。


类比上文的方法,可以写出如下代码,来获取类对应的方法和field的信息。
static const Il2CppMethodDefinition* GetMethodDefinitionFromIndex(MethodIndex index){ return (const Il2CppMethodDefinition*)(((Il2CppGlobalMetadataHeader*)ptr)->methodsOffset+ptr)+index;}static const Il2CppFieldDefinition* GetFieldDefinitionFromIndex(FieldIndex index){ return (const Il2CppFieldDefinition*)(ptr+((Il2CppGlobalMetadataHeader*)ptr)->fieldsOffset)+index;}

同样的,获得的结构体中都保存了名字。


效果如下:





恢复方法符号


由于此时仅仅获得了方法的名称,还无法和Gameassembly.dll中的函数对应起来,得继续查看il2cpp的源码。

从il2cpp的api入手,其中有个函数名称为il2cpp_runtime_invoke。

点进其中所调用的函数,可以发现有一个InvokeWithThrow,不难猜到这应该就是调用method的函数。


继续分析InvokeWithThrow,可以发现里面调用了method->invoker_method,并且其第一个参数method->methodPointer就是方法的指针。


继续搜索对method->methodPointer的修改,在Class.cpp文件中的Class::SetupMethodsLocked(Il2CppClass *klass, const il2cpp::os::FastAutoLock& lock)方法下成功找到了赋值语句。该函数的作用即通过metadata构造类的所有MethodInfo,而MethodInfo对象则包含了方法函数指针。

可以看到MetadataCache::GetMethodPointer通过image对象找到了codeGenModule,再定位到了其下的methodPointers数组,再根据token取出对应的函数指针。


刚好一个方法的token在Il2CppMethodDefinition中存在,就差怎么定位codeGenModule->methodPointers了。


在源代码中搜索codeGenModule的类型Il2CppCodeGenModule。

得到了一个结构体Il2CppCodeRegistration。


然后在libil2cpp中就没法找到相关的定义了,可能是由il2cpp aot编译器生成的,然后注入到il2cpp runtime里去,所以去看看GameAssembly.dll看看能否定位到Il2CppCodeRegistration这个结构体。

由于Il2CppCodeRegistration中的codeGenModules的Il2CppCodeGenModule最大的特征就是moduleName,试试字符串搜索。

合理猜测这个字符串就是Il2CppCodeGenModule的moduleName,交叉引用,猜测这个就是Il2CppCodeGenModule。

继续交叉引用,不出所料应该就得到了一个数组,这个数组应该就是Il2CppCodeRegistration->codeGenModules指向的数组。


再次交叉引用找到Il2CppCodeRegistration变量。


所以只需要定位到Il2CppCodeRegistration codeRegistration这个全局变量即可,然后遍历codeGenModules找到方法对应的Image,然后通过方法的token(Il2CppMethodDefinition下有)去codeGenModules->methodPointers取出函数指针,即可将方法和函数指针对应上。

具体代码如下:
uint32_t GetMethodPointer(const Il2CppImageDefinition *image,uint32_t token){ for(int i=0;i<CodeRegistration->codeGenModulesCount;i++) { const Il2CppCodeGenModule *module=CodeRegistration->codeGenModules[i]; if(!strcmp(module->moduleName,GetStringFromIndex(image->nameIndex))) { return module->methodPointers[GetTokenRowId(token)-1]; } } printf("invalid!\n"); return 0;}




获得field的偏移地址


同样的从il2cpp的api出发,可以发现该函数直接调用了Class::GetFieldFromName,直接去查看该函数。

找到Class::GetFieldFromName,它通过GetFields获得所有的FieldInfo,然后返回对应的FieldInfo。

看看GetFields函数,其中调用了SetupFieldsLocked。

继续找到SetupFieldsLocked,该函数递归初始化了类和父类的所有FieldInfo,使用SetupFieldsFromDefinitionLocked进行初始化。

找到SetupFieldsFromDefinitionLocked,在这个函数中就能找到具体的初始化过程了,可以看到其offset是通过MetadataCache::GetFieldOffsetFromIndexLocked计算而出的。


继续找到MetadataCache::GetFieldOffsetFromIndexLocked,该函数进一步调用了 GlobalMetadata::GetFieldOffset。

最后一步,GlobalMetadata::GetFieldOffset。该函数通过typeIndex和fieldIndexInType进行查表。typeIndex可以通过查当前type的Il2CppTypeDefinition在global-metadata.dat的序号获得,而fieldIndexInType在遍历type的field可以直接获得。

获取typeIndex代码如下:
static TypeDefinitionIndex GetIndexForTypeDefinitionInternal(const Il2CppTypeDefinition* typeDefinition){ const Il2CppTypeDefinition* typeDefinitions=(const Il2CppTypeDefinition*)(ptr+((Il2CppGlobalMetadataHeader*)ptr)->typeDefinitionsOffset); ptrdiff_t index=typeDefinition-typeDefinitions; return (TypeDefinitionIndex)index;}

索引信息都能获取了,现在需要找到的是s_Il2CppMetadataRegistration->fieldOffsets,也就是要定位到s_Il2CppMetadataRegistration。该变量的类型为Il2CppMetadataRegistration。


该变量存在于GameAssemly.dll,也是由il2cpp aot编译生成注册到il2cpp runtime的,而刚好之前发现codeRegistration注册时一并注册了metadataRegistration。

于是我们根据上文找到的codeRegistration进行交叉引用,成功定位到metadataRegistration。



所以只需要根据这个变量直接用typeIndex和fieldIndexInType查表即可。
uint32_t GetFieldOffset(TypeDefinitionIndex typeIndex,uint32_t index){ return MetadataRegistration->fieldOffsets[typeIndex][index]; }





完整代码


由于使用了大量结构体,不方便直接贴上来,请下载附件,同时还有对应的测试文件和输入。

代码不够完善,无法用于生产,仅用于学习。




看雪ID:R1mao

https://bbs.pediy.com/user-home-948449.htm

*本文由看雪论坛 R1mao 原创,转载请注明来自看雪社区



# 往期推荐

1.Android APP漏洞之战——调试与反调试详解

2.Fuzzm: 针对WebAssembly内存错误的模糊测试

3.0rays战队2021圣诞校内招新赛题解

4.2022腾讯游戏安全初赛一题解析

5.一文读懂PE文件签名并手工验证签名有效性

6.CNVD-2018-01084 漏洞复现报告(service.cgi 远程命令执行漏洞)






球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存