|
最近研究了一段时间的IL2Cpp编译出来的dll,整理下笔记记录下中间遇到的一些问题和解决方法。
加载UnityPlayer.dll对应PDB
这个其实是我无意间搜到了Unity官方提供了对应Symbol Server,Windows 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作者:
- Practical IL2CPP Reverse Engineering: Extracting Protobuf definitions from applications using protobuf-net (Case Study: Fall Guys)
- Reverse Engineering Adventures: League of Legends Wild Rift (IL2CPP)
- Reverse Engineering Adventures: Honkai Impact 3rd (Houkai 3) (IL2CPP) (Part 1)
- Reverse Engineering Adventures: VMProtect Control Flow Obfuscation (Case study: string algorithm cryptanalysis in Honkai Impact 3rd)
- IL2CPP Tutorial: Finding loaders for obfuscated global-metadata.dat files
- Reverse Engineering Adventures: Brute-force function search, or how to crack Genshin Impact with PowerShell
需要强调的是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>(&#34;Data&#34;), utils::StringView<char>(&#34;Metadata&#34;));
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>(&#34;Metadata&#34;));
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(&#34;ERROR: Could not open %s&#34;, 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来说,其实最首先的是找到codeRegistration和metadataRegistration这两个指针指向的内容,然后就可以依葫芦画瓢把里面的数据全部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有什么套路可以学习学习。 |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|