24kcsplus
文章23
标签39
分类0
记一次手机Unity游戏逆向:处理被加密的libil2cpp.so

记一次手机Unity游戏逆向:处理被加密的libil2cpp.so

前言

距离上次逆向 Unity 游戏有些时间了,这次遇到个棘手的样本,所以文章更新的时间就完了很多。这篇文章主要讲怎么处理被加密的 libil2cpp.so 以使用 il2cppdumper 来恢复符号。

后面还会写一篇文章来讲讲怎么用 KPM 来绕过这个样本的 Frida 检测

本项目的初衷是研究 Unity 游戏加载流程与游戏安全,请勿将其用于非法盈利等任何商业用途,所有获取的附件请在的24小时内自行删除。

准备

从内存中 Dump

这次遇到的这个样本是对 libil2cpp.so 进行了加密的,使用的是杭州的一个反作弊公司的产品。如果直接用 il2cppdumper 来加载安装包里的 libil2cpp.so 和 global-metadata.dat,则会提示被加密。

除了无法使用 il2cppdumper 之外,直接使用 IDA 打开也会有严重的性能问题,你会看到左下角分析地址在一点一点的蠕动(

面对这种运行时解密的 SO 通常有两种解决办法,一种是逆向解密的算法,然后静态解密,一种是通过动态运行,然后直接从内存中获得解密后的部分。

对于本文的样本而言,使用静态解密要比动态提取难得多,一个是不好逆向出解密的算法,还有一个就是对于解密的程序也是上了强度的(负责解密的 SO 文件用 IDA 打开也是蠕动哈哈)

所以我们就使用动态的方式来获取解密后的 libil2cpp.so 和 global-metadata.dat

从内存中 Dump 的步骤非常简单,只需要使用 PADumper 这个工具就可以了,使用这个程序的效果跟直接使用 dd 命令从 /proc/[pid]/mem 里获取内存数据差不多,不过这个还继承了 SoFixer 的功能,dump 的同时也可以修复 SO 文件以便使用 IDA 分析

还有就是这个也可以顺带 dump global-metadata.dat

如图所示,只需要选择对应的进程,需要 dump 的 SO 名字,再勾选这两个选项,即可成功 dump 解密后的 libil2cpp.so

使用 Il2cppdumper 恢复符号表

照理来说解密后的这两个关键文件 dump 下来后一般 il2cppdumper 就能直接生成 dump.cs 等文件了,为什么还需要单独开一节讲呢?

哈哈😅,作者在这里踩了一个很大的坑,以至于在这步上卡了几乎一个月之久,期间一度以为是某 Guard 会在加载后重加密,疯狂鞭打 Claude Code(希望 AI 大人觉醒后不要因为这件事把我淦了(((),到最后与没加密过的 libil2cpp.so 对比后才发现 dump 下来的是可以用 il2cppdumper 的,只是 il2cppdumper 识别版本错误会报错而已,强制指定版本为 29 就能正常使用了。

使用 dump 下来的 libil2cpp.so 直接使用 il2cppdumper 会检测到是 dump 下来的,让你输入基址

本文样本中,输入基址后无法搜索到两个关键指针,需要手动找,这里作者是通过对照另一个样本来找的

我这里先用 il2cppdumper 来 dump 一个没有被加密的 libil2cpp.so(样本详见这里),来找到对应的两个指针的位置,然后一直查看交叉引用,最后找到 il2cpp_init 这个函数

刚好本文样本从内存 dump 下来之后导出表基本上都恢复了(导入表没有,大部分导入函数已经绑定了,IDA 里只能看到绝对地址),刚好就可以找到 il2cpp_init 这个函数

加载 dump 的 so 后建议在 Edit -> Segments -> Rebase program 里设置基址,方便 IDA 处理

调用的第二个函数就是调用加载 CodeRegistration 和 MetadataRegistration 这两个指针函数的函数,点进去伪代码可能不明显(IDA 貌似也没有反编译出来),对比一下汇编就能看出来了

4.0 mscorlib.dll __Generated 之类的特征非常明显,仔细对照一下就能找到使用这两个指针的函数就是 sub_70868D5748

CodeRegistration 和 MetadataRegistration 就直接出来了,分别是 sub_7086989528 函数的第一个参数和第二个参数,输入进 il2cppdumper 即可 dump

后面的就是分析代码咯,不过光是讲到这内容有点过于少了,上面的内容可以找找 il2cpp 源码的对应部分,顺便看看 il2cppdumper 是怎么处理的 (其实是写到这里感觉没什么东西可以写了,后面的人类含量可能会有点低,请做好准备)

il2cpp 源码分析

il2cpp 是 Unity 的一项技术,将 Unity C# 脚本先转换为 IL 中间语言,然后再将 IL 翻译为 CPP,最后编译。原生使得 Unity 游戏运行更快,不过我们逆向不需要知道它是如何转换和编译的,因为我们分析的是成品,分析这类最重要的应该是分析它二进制文件的结构和加载过程。

下面提到的例子版本都是 2021.3.45f2 的,因为本文逆向的例子版本就是这个。完整的 il2cpp 源码需要自行下载对应版本的 Unity 编辑器,然后在 <编辑器版本>/Editor/Data/il2cpp 下找到对应源码

上面的例子我们是从 il2cpp_init 这个函数开始入手的,从这个函数名字我们就可以看出,这个是初始化运行时的,它在 il2cpp-api.cpp 中,长这样:

int il2cpp_init(const char* domain_name)
{
    // Use environment's default locale
    setlocale(LC_ALL, "");

    return Runtime::Init(domain_name);
}

这和前面的例子可以对上,第一个函数是 setlocale,第二个则是正式初始化的函数 Runtime::Init,在 vm/Runtime.cpp

跳转到这个函数就可以看到调用了初始化各种东西的函数,包括线程、Codegen、GC 等等,也可以看到初始化类型系统等等,这个也和我们反编译时看到的相同

bool Runtime::Init(const char* domainName)
    {
        os::FastAutoLock lock(&s_InitLock);

        IL2CPP_ASSERT(s_RuntimeInitCount >= 0);
        if (s_RuntimeInitCount++ > 0)
            return true;

        SanityChecks();

        os::Initialize();
        os::Locale::Initialize();
        MetadataAllocInitialize();

        // NOTE(gab): the runtime_version needs to change once we
        // will support multiple runtimes.
        // For now we default to the one used by unity and don't
        // allow the callers to change it.
        s_FrameworkVersion = framework_version_for("v4.0.30319");

        os::Image::Initialize();
        os::Thread::Init();

#if !IL2CPP_TINY && !IL2CPP_MONO_DEBUGGER
        il2cpp::utils::DebugSymbolReader::LoadDebugSymbols();
#endif

#if IL2CPP_HAS_OS_SYNCHRONIZATION_CONTEXT
        // Has to happen after Thread::Init() due to it needing a COM apartment on Windows
        il2cpp::os::SynchronizationContext::Initialize();
#endif

        // This should be filled in by generated code.
        IL2CPP_ASSERT(g_CodegenRegistration != NULL);
        g_CodegenRegistration();

        if (!MetadataCache::Initialize())
        {
            s_RuntimeInitCount--;
            return false;
        }

        Assembly::Initialize();
        gc::GarbageCollector::Initialize();

        // Thread needs GC initialized
        Thread::Initialize();

        register_allocator(il2cpp::utils::Memory::Malloc);

        memset(&il2cpp_defaults, 0, sizeof(Il2CppDefaults));

        const Il2CppAssembly* assembly = Assembly::Load("mscorlib.dll");
        const Il2CppAssembly* assembly2 = Assembly::Load("__Generated");

        // It is not possible to use DEFAULTS_INIT_TYPE for managed types for which we have a native struct, if the
        // native struct does not map the complete managed type.
        // Which is the case for: Il2CppThread, Il2CppAppDomain, Il2CppCultureInfo, Il2CppReflectionProperty,
        // Il2CppDateTimeFormatInfo, Il2CppNumberFormatInfo

        il2cpp_defaults.corlib = Assembly::GetImage(assembly);
        il2cpp_defaults.corlib_gen = Assembly::GetImage(assembly2);
        DEFAULTS_INIT(object_class, "System", "Object");
        DEFAULTS_INIT(void_class, "System", "Void");
        DEFAULTS_INIT_TYPE(boolean_class, "System", "Boolean", bool);
        DEFAULTS_INIT_TYPE(byte_class, "System", "Byte", uint8_t);
        DEFAULTS_INIT_TYPE(sbyte_class, "System", "SByte", int8_t);
        DEFAULTS_INIT_TYPE(int16_class, "System", "Int16", int16_t);
        DEFAULTS_INIT_TYPE(uint16_class, "System", "UInt16", uint16_t);
        DEFAULTS_INIT_TYPE(int32_class, "System", "Int32", int32_t);
        DEFAULTS_INIT_TYPE(uint32_class, "System", "UInt32", uint32_t);
        DEFAULTS_INIT(uint_class, "System", "UIntPtr");
        DEFAULTS_INIT_TYPE(int_class, "System", "IntPtr", intptr_t);
        DEFAULTS_INIT_TYPE(int64_class, "System", "Int64", int64_t);
        DEFAULTS_INIT_TYPE(uint64_class, "System", "UInt64", uint64_t);
        DEFAULTS_INIT_TYPE(single_class, "System", "Single", float);
        DEFAULTS_INIT_TYPE(double_class, "System", "Double", double);
        DEFAULTS_INIT_TYPE(char_class, "System", "Char", Il2CppChar);
        DEFAULTS_INIT(string_class, "System", "String");
        DEFAULTS_INIT(enum_class, "System", "Enum");
        DEFAULTS_INIT(array_class, "System", "Array");
        DEFAULTS_INIT(value_type_class, "System", "ValueType");
......

g_CodegenRegistration 是外部提供的,而这个外部则是由 <编辑器版本>/Editor/Data/il2cpp/build/deploy/Unity.IL2CPP.dll 通过玩家的 Unity C# 来转换生成的。

(AI说的)生成内容如下:

  • Il2CppCodeRegistration.cpp — 定义 g_CodeRegistration 结构体
  • Il2CppMetadataRegistration.cpp — 定义 g_MetadataRegistration 结构体
  • Il2CppCodeGenRegistration.cpp — 包含 s_Il2CppCodeGenRegistration 函数,调用 il2cpp_codegen_register(&g_CodeRegistration, &g_MetadataRegistration)

使用 dnSpy 来反编译就可以看到生成逻辑了:

// Token: 0x0600001C RID: 28 RVA: 0x0000258E File Offset: 0x0000078E
public static string CodeRegistrationTableName(ReadOnlyContext context)
{
	return context.Global.Services.ContextScope.ForMetadataGlobalVar("g_CodeRegistration");
}

// Token: 0x0600001D RID: 29 RVA: 0x000025AC File Offset: 0x000007AC
private static void WriteCodeRegistration(SourceWritingContext context, TableInfo invokerTable, TableInfo reversePInvokeWrappersTable, TableInfo genericMethodPointerTable, TableInfo genericAdjustorThunkTable, UnresolvedVirtualsTablesInfo virtualCallTables, TableInfo interopDataTable, TableInfo windowsRuntimeFactoryTable, ReadOnlyCollection<string> codeGenModules, CodeRegistrationWriter.CodeRegistrationWriterMode mode)
{
	using (ICppCodeStream writer = context.CreateProfiledSourceWriterInOutputDirectory("Il2CppCodeRegistration.cpp"))
	{
		if (reversePInvokeWrappersTable.Count > 0)
		{
			writer.WriteLine(reversePInvokeWrappersTable.GetDeclaration());
		}
		if (genericMethodPointerTable.Count > 0)
		{
			writer.WriteLine(genericMethodPointerTable.GetDeclaration());
		}
		if (genericAdjustorThunkTable.Count > 0)
		{
			writer.WriteLine(genericAdjustorThunkTable.GetDeclaration());
		}
		if (invokerTable.Count > 0)
		{
			writer.WriteLine(invokerTable.GetDeclaration());
		}
		if (virtualCallTables.MethodPointersInfo.Count > 0)
		{
			writer.WriteLine(virtualCallTables.MethodPointersInfo.GetDeclaration());
		}
		if (interopDataTable.Count > 0)
		{
			writer.WriteLine(interopDataTable.GetDeclaration());
		}
		if (windowsRuntimeFactoryTable.Count > 0)
		{
			writer.WriteLine(windowsRuntimeFactoryTable.GetDeclaration());
		}
		if (mode == CodeRegistrationWriter.CodeRegistrationWriterMode.AllAssemblies || mode == CodeRegistrationWriter.CodeRegistrationWriterMode.PerAssemblyGlobal)
		{
			foreach (string codeGenModule in codeGenModules)
			{
				writer.WriteLine("IL2CPP_EXTERN_C_CONST Il2CppCodeGenModule " + codeGenModule + ";");
			}
			writer.WriteArrayInitializer("const Il2CppCodeGenModule*", "g_CodeGenModules", codeGenModules.Select(new Func<string, string>(Emit.AddressOf)), true, false);
		}
		writer.WriteStructInitializer("const Il2CppCodeRegistration", CodeRegistrationWriter.CodeRegistrationTableName(context), new string[]
		{
			reversePInvokeWrappersTable.Count.ToString(CultureInfo.InvariantCulture),
			(reversePInvokeWrappersTable.Count > 0) ? reversePInvokeWrappersTable.Name : "NULL",
			genericMethodPointerTable.Count.ToString(CultureInfo.InvariantCulture),
			(genericMethodPointerTable.Count > 0) ? genericMethodPointerTable.Name : "NULL",
			(genericAdjustorThunkTable.Count > 0) ? genericAdjustorThunkTable.Name : "NULL",
......
// Unity.IL2CPP.CodeRegistrationWriter
// Token: 0x0600001E RID: 30 RVA: 0x000028BC File Offset: 0x00000ABC
private static void WriteGlobalCodeRegistrationCalls(SourceWritingContext context, CodeRegistrationWriter.CodeRegistrationWriterMode mode, ICppCodeWriter writer)
{
	string metadataRegistrationVarPtr = "NULL";
	if (mode == CodeRegistrationWriter.CodeRegistrationWriterMode.AllAssemblies)
	{
		writer.WriteLine("IL2CPP_EXTERN_C_CONST Il2CppMetadataRegistration g_MetadataRegistration;");
		metadataRegistrationVarPtr = "&g_MetadataRegistration";
	}
	if (context.Global.Parameters.EnableReload)
	{
		writer.WriteLine("#if IL2CPP_ENABLE_RELOAD");
		writer.WriteLine("extern \"C\" void ClearMethodMetadataInitializedFlags();");
		writer.WriteLine("#endif");
	}
	string codeGenOptionsVariableStorageClass = "static";
	writer.WriteStructInitializer(codeGenOptionsVariableStorageClass + " const Il2CppCodeGenOptions", "s_Il2CppCodeGenOptions", new string[]
	{
		context.Global.Parameters.CanShareEnumTypes ? "true" : "false",
		context.Global.InputData.MaximumRecursiveGenericDepth.ToString(CultureInfo.InvariantCulture),
		context.Global.InputData.GenericVirtualMethodIterations.ToString(CultureInfo.InvariantCulture)
	}, false);
	writer.WriteLine("void s_Il2CppCodegenRegistration()");
	writer.BeginBlock();
	writer.WriteLine("il2cpp_codegen_register (&g_CodeRegistration, " + metadataRegistrationVarPtr + ", &s_Il2CppCodeGenOptions);");
	if (context.Global.Parameters.EnableDebugger)
	{
		writer.WriteLine("#if IL2CPP_MONO_DEBUGGER");
		writer.WriteLine("il2cpp_codegen_register_debugger_data(NULL);");
		writer.WriteLine("#endif");
	}
	if (context.Global.Parameters.EnableReload)
	{
		writer.WriteLine("#if IL2CPP_ENABLE_RELOAD");
		writer.WriteLine("il2cpp_codegen_register_metadata_initialized_cleanup(ClearMethodMetadataInitializedFlags);");
		writer.WriteLine("#endif");
	}
	writer.EndBlock(false);
	writer.WriteLine("#if RUNTIME_IL2CPP");
	writer.WriteLine("typedef void (*CodegenRegistrationFunction)();");
	writer.WriteLine("CodegenRegistrationFunction g_CodegenRegistration = s_Il2CppCodegenRegistration;"); // 这里给 Runtime::Init 里需要的那个 g_CodegenRegistration 指针赋值
	writer.WriteLine("#endif");
}

调用回来的那个函数则在 codegen/il2cpp-codegen-il2cpp.cpp 中,长这样:

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

这是又调用回 vm 命名空间下的方法了,不过我们追踪到这里就可以看到我们需要的两个指针了

我们需要的调用链大概长这样:

il2cpp_init->Runtime::Init->s_Il2CppCodegenRegistration(在Il2CppCodeRegistration.cpp中,AI好像说错了)->il2cpp_codegen_register

最后讲一下 il2Cpp 29 和 31 版本的差异,il2cppdumper 是采用 metadata 中的版本信息来解析 libil2cpp.so 的,不过本文的样本结构并不能用 metadata 中的版本信息,以至于作者卡在这半个月(我当初也是没注意到报错信息不一样)

报错主要是超出了 C# 的数组大小上限,如下图:

这个 count 过大,主要原因是偏移不对,29.1版本之后多了两个部分,如图

这导致 il2cppdumper 在解析时 codeGenModulesCount 和 codeGenModules 的位置不对,数据自然也不对了,在 Il2Cpp.cs 132 行中调用 MapVATR 又将错误的过大数据传入进去了,自然就报错了

尾声

这前前后后搞了接近一个月左右,终于完工了,学到了很多东西,虽然中间卡着很难受,而且文章还难产了几天,主要是这段时间太忙了,时间都拿去干其他事了

除了本文的 dump libil2cpp 以外,这样本还有 frida 检测,我后面还会写一篇文章来讲讲我是怎么绕过这个样本的检测的

最后还是得感慨 AI 还是太厉害了,讲这个 il2cpp 结构的文章不是特别多,这里基本上都是靠 AI 一点一点摸的的

我还打算写一个工具,看看能能恢复一下这个 dump 下来的这个文件的导入表,还有通过调用链来找到那两个关键指针,不过这都得等我写完下一个文章再说了

参考文章

  1. [Android 脱壳] 最新版某气骑士分析记录-绕过某讯的防御dump内存并解密存档

  2. 什么?IL2CPP APP分析这一篇就够啦!

本文作者:24kcsplus
本文链接:https://24kblog.top/posts/557806528/
版权声明:除非特别声明,否则本文采用 CC BY-NC-SA 3.0 CN 协议进行许可
×