找回密码
 立即注册
查看: 342|回复: 0

[笔记] IL2CppDumper笔记

[复制链接]
发表于 2022-5-19 09:23 | 显示全部楼层 |阅读模式
最近研究了一段时间的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>("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来说,其实最首先的是找到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有什么套路可以学习学习。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-5-3 09:21 , Processed in 0.159178 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表