IL2CppDumper笔记

Author Avatar
Kanglai Qian 4月 23, 2022

最近研究了一段时间的IL2Cpp编译出来的dll,整理下笔记记录下中间遇到的一些问题和解决方法。

加载UnityPlayer.dll对应PDB

这个其实是我无意间搜到了Unity官方提供了对应Symbol ServerWindows Debugging里也给出了常见软件的使用方法:

.sympath+ SRVc:\symbols-cachehttp://symbolserver.unity3d.com/

参考为IDA加载调试符号我修改了.\cfg\pdb.cfg中的_NT_SYMBOL_PATH一栏,发现识别不出来orz

用浏览器访问了下对应地址,发现下载下来的是一个.pd_文件——直接用解压软件打开就能获得对应的.pdb文件对上。也许IDA也能支持压缩版本? 目前反正暂时手动档了…

ps. 后来还遇到一个情况是一开始IDA无法运行ida_with_struct_py3.py这个文件,运行下idapyswitch.exe就好。

绕过外部加壳

现在蛮多游戏都会做初步的加壳,可以防住一些Script Boy。这里推荐Katy大佬的很多文章,他本人也是Il2CppInspector作者:

需要强调的是Il2Cpp本身是能看到部分源代码的(对应Unity安装目录的Editor\Data\il2cpp\libil2cpp下),必须对这块有一定了解才能往下推进。核心我们其实需要获取两个文件:

  • GameAssembly.dll存储了逻辑本身
  • global-metadata.dat存储了类型、方法等信息

GameAssembly.dll

现在蛮多游戏都会对GameAssembly.dll加密,所以很多时候偷懒不高兴分析加壳手段,直接去内存里抓取。

安卓上之前一般是用Game Guardian来抓取,现在比较推荐直接使用Zygisk-Il2CppDumper

PC上的话最简单的情况是直接任务管理器里生成转储,就可以dump下来然后分析dll的magic header; 如果常规手段被禁用的话,可以请出KsDumper,直接从kernel角度入手解决问题。这里给自己挖了个坑,后文详述

global-metadata.dat

这里无法用dump内存的手段是因为这个文件是直接MemoryMap的,只有用到的地方才会加载,所以内存里内容很有可能是不完整的。

下图是我拿来练手的dll,一边参考一边各种rename下来,发现其实没有任何骚套路:

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 == 27);

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;
}

void* il2cpp::vm::MetadataLoader::LoadMetadataFile(const char* fileName)
{
#if IL2CPP_TARGET_ANDROID && IL2CPP_TINY_DEBUGGER && !IL2CPP_TINY_FROM_IL2CPP_BUILDER
std::string resourcesDirectory = utils::PathUtils::Combine(utils::StringView<char>("Data"), utils::StringView<char>("Metadata"));

std::string resourceFilePath = utils::PathUtils::Combine(resourcesDirectory, utils::StringView<char>(fileName, strlen(fileName)));

int size = 0;
return loadAsset(resourceFilePath.c_str(), &size, malloc);
#elif IL2CPP_TARGET_JAVASCRIPT && IL2CPP_TINY_DEBUGGER && !IL2CPP_TINY_FROM_IL2CPP_BUILDER
return g_MetadataForWebTinyDebugger;
#else
std::string resourcesDirectory = utils::PathUtils::Combine(utils::Runtime::GetDataDir(), utils::StringView<char>("Metadata"));

std::string resourceFilePath = utils::PathUtils::Combine(resourcesDirectory, utils::StringView<char>(fileName, strlen(fileName)));

int error = 0;
os::FileHandle* handle = os::File::Open(resourceFilePath, kFileModeOpen, kFileAccessRead, kFileShareRead, kFileOptionsNone, &error);
if (error != 0)
{
utils::Logging::Write("ERROR: Could not open %s", resourceFilePath.c_str());
return NULL;
}

void* fileBuffer = utils::MemoryMappedFile::Map(handle);

os::File::Close(handle, &error);
if (error != 0)
{
utils::MemoryMappedFile::Unmap(fileBuffer);
fileBuffer = NULL;
return NULL;
}

return fileBuffer;
#endif
}

Katy也在博客中整理了一些case,譬如混淆文件内容、修改文件名和路径等手段,这里就不再赘述等遇到到再说,一般来说先找到正常加载的口子(譬如搜索到global-metadata.dat这个常量然后xref看使用的地方),接着对比和正常逻辑有没有区别。

Il2CppDumper

获取脱壳结果后,就可以进行分析。对于GameAssembly.dll来说,其实最首先的是找到codeRegistrationmetadataRegistration这两个指针指向的内容,然后就可以依葫芦画瓢把里面的数据全部Dump出来。感谢Perfare大佬的工作特别是各种版本处理我看的都头大

void il2cpp_codegen_register(const Il2CppCodeRegistration* const codeRegistration, const Il2CppMetadataRegistration* const metadataRegistration, const Il2CppCodeGenOptions* const codeGenOptions)
{
il2cpp::vm::MetadataCache::Register(codeRegistration, metadataRegistration, codeGenOptions);
}

具体的流程其实对照il2cpp的源代码还是比较好理解的,这里就提一个我遇到的问题: 使用dump出来的GameAssembly.dll会遇到GetTypeDefinitionFromIl2CppType里数组越界。

我一开始以为是抓出来的dll有问题,分析了下雀实各种struct layout和指针都能完美对上。ps. 我看Katy在分析League of Legends Wild Rift时遇到类成员变量顺序改变的情况,这个方法有点意思的。

索性一路怼下去发现代码逻辑和公版都对的上,而且il2CppType的数据内容看上去似乎没问题(datapoint近乎连续,bits都是0x120000)——突然发现盲点,datapoint应该是一个内存地址阿! 换算了下这个image的baseAddr雀实对的上。

这样其实就解释的通了: GetTypeDefinitionFromIl2CppType其实应该走297行的那个分支,而不应该走303行的else里面。

正当我准备开始好好看下Il2CppDumper的实现的时候,发现作者刚好修复掉了马娘新版本DMMdump的DLL 无法使用

所以总结下,本质还是因为我是使用KsDumper抓取的内存中dll,而一开始工具只考虑了安卓的ELF格式可能会出现抓取的情况(PC上的PE默认是没有检查Dump的)。

小结

虽然很久没正经弄过Unity了,但是找点乐子来练练手还挺有意思的,而且作为开发者的思路和逆向似乎交叠验证的很好玩。后面如果有空准备研究研究新出的huatuo热更新方案,看看它对Il2Cpp有什么套路可以学习学习。