记一次手机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 下来的这个文件的导入表,还有通过调用链来找到那两个关键指针,不过这都得等我写完下一个文章再说了
琼公网安备46010602001577号