Il2Cpp恢复符号过程分析

VSole2022-06-28 16:49:39

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..");      return 0;  }  int image_count=header->imagesSize/sizeof(Il2CppImageDefinition);   for(int i=0;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",GetStringFromIndex(image->nameIndex));  for(int j=0;jtypeCount;j++)  {      const Il2CppTypeDefinition *type=GetTypeDefinitionFromIndex(image->typeStart+j);      printf("class: %s:%s",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;icodeGenModulesCount;i++)  {      const Il2CppCodeGenModule *module=CodeRegistration->codeGenModules[i];      if(!strcmp(module->moduleName,GetStringFromIndex(image->nameIndex)))      {          return module->methodPointers[GetTokenRowId(token)-1];      }  }  printf("invalid!");  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];   }
符号函数符号计算
本作品采用《CC 协议》,转载必须注明作者和本文链接
iddm带你读论文——SymQEMU:Compilation-based symbolic execution for binaries 本篇文章收录于2021年网络安全顶会NDSS,介绍了最新的符号执行技术,并且清晰地比较了当前流行的各种符号执行的引擎的优劣势,可以比较系统的了解符号执行技术的相关知识
得益于Unicorn的强大的指令trace能力,可以很容易实现对cpu执行的每一条汇编指令的跟踪,进而对ollvm保护的函数进行剪枝,去掉虚假块,大大提高逆向分析效率。
Flutter APP逆向实践
2022-07-21 16:20:09
分析flutter的时候,没有交叉引用,那真是想它他拖进回收站。最近又遇到了几个flutter开发的app,对它进行了一些研究,总结了以下的分析流程。本文以iOS app为例子作为讲解,Android 的flutter app和iOS 的flutter app分析方法类似。
Kernel从0开始
2021-12-10 13:42:20
网上一大堆教编译内核的,但很多教程看得特别迷糊。第一次编译内核时,没设置好参数,直接把虚拟机编译炸开了。所以就想着能不能先做个一键获取内核源码和相关vmlinux以及bzImage的脚本,先试试题,后期再深入探究编译内核,加入debug符号,所以就有了这个一键脚本。
1、so地址页对其&第一个PT_LOAD和第二个PT_LOAD之间有多余占位数据2、实现libart.so中的inlinehook3、hook dex加载函数更改只读权限
今年初,Akamai 的研究人员发现了一个新型恶意僵尸网络,它以 Realtek SDK、华为路由器和 Hadoop YARN 服务器为目标,将设备引入到 DDoS 群中,有可能进行大规模攻击。这个新型僵尸网络是研究人员在自己的 HTTP 和 SSH 蜜罐上发现的,该僵尸网络利用了 CVE-2014-8361 和 CVE-2017-17215 等漏洞。
Android 应用gl,使用了加固i,老版本的应用gl是js源码,新版本更新后,刚开始以为是加密了源码,分析后才知道是使用了Hermes优化引擎。Hermes优化的优化效果如下:优化前:优化后:二、前期准备2.1 绕过root检测应用gl使用了加固i,在被root 的手机上运行,闪退。绕过方法也很简单,在magisk的设置中,开启遵守排除列表,然后在配置排除列表中选择应用gl。
前言最近一段时间在研究Android加壳和脱壳技术,其中涉及到了一些hook技术,于是将自己学习的一些hook技术进行了一下梳理,以便后面回顾和大家学习。主要是进行文本替换、宏展开、删除注释这类简单工作。所以动态链接是将链接过程推迟到了运行时才进行。
文中使用的示例代码可以从 这里 获取。的功能是在终端打印出hello这6个字符(包括结尾的?编译它们分别生成libtest.so和?存在严重的内存泄露问题,每调用一次say_hello函数,就会泄露1024字节的内存。
DLL劫持的防御策略
VSole
网络安全专家