本次主角是米X游的《原X》,起因还是在网上看到了一篇关于崩3的符号信息还原的文章,就想看看原神上有没有变化。
本次分析选取的是android平台的,游戏采用unity开发,使用il2cpp模式编译。
使用 IL2CPP 模式编译,游戏中使用的字符串都被保存在了一个global-metadata.dat的资源文件里,只有在动态运行时才会将这些字符串读入内存。一般使用Il2CppDumper就可以读取global-metadata.dat文件中的信息,帮助反编译。
有破解就有保护,可能会遇到无法dump或者global-metadata.dat文件结构被修改的情况,无法使用Il2CppDumper,这个时候就需要自己动手了。
github上随便找一个il2cpp的源码,搜索global-metadata.dat,发现只有函数MetadataCache::Initialize()处使用。
void MetadataCache::Initialize() { s_GlobalMetadata = vm::MetadataLoader::LoadMetadataFile("global-metadata.dat"); s_GlobalMetadataHeader = (const Il2CppGlobalMetadataHeader*)s_GlobalMetadata; ... }
查看LoadMetadataFile代码:
void* MetadataLoader::LoadMetadataFile(const char* fileName) { 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; FileHandle* handle = File::Open(resourceFilePath, kFileModeOpen, kFileAccessRead, kFileShareRead, kFileOptionsNone, &error); if (error != 0) return NULL; void* fileBuffer = utils::MemoryMappedFile::Map(handle); File::Close(handle, &error); if (error != 0) { utils::MemoryMappedFile::Unmap(fileBuffer); fileBuffer = NULL; return NULL; } return fileBuffer; }
很明显,就是Initialize时调用LoadMetadataFile将global-metadata.dat映射到内存中。
还需要稍微了解一下global-metadata.dat文件结构。
struct Il2CppGlobalMetadataHeader { int32_t sanity; int32_t version; int32_t stringLiteralOffset; // string data for managed code int32_t stringLiteralCount; int32_t stringLiteralDataOffset; int32_t stringLiteralDataCount; int32_t stringOffset; // string data for metadata int32_t stringCount; int32_t eventsOffset; // Il2CppEventDefinition int32_t eventsCount; int32_t propertiesOffset; // Il2CppPropertyDefinition int32_t propertiesCount; int32_t methodsOffset; // Il2CppMethodDefinition int32_t methodsCount; ... }
反编译原神的il2cpp.so,loadmetadataFile()函数,对比原始loadmetadataFile,可以清楚的看到将文件映射进内存后进行了一次解密操作。(在MetadataCache::Initialize中还有一次解密)
再查看MetadataCache::Initialize(),根据红框,对比原始代码,可以发现global-metadata.dat文件结构已经被修改,即使成功dump文件也不能使用Il2CppDumper。
为什么选择在SetupMethodsLocked hook,有其他大佬已经讲得很仔细了,可以参考还原使用IL2CPP编译的unity游戏的symbol
阅读源码,通过il2cpp_class_get_methods->Class::GetMethods->Class::SetupMethods跟踪到SetupMethodsLocked:
const MethodInfo* il2cpp_class_get_methods(Il2CppClass *klass, void* *iter)//导出函数很好找 { return Class::GetMethods(klass, iter); } const MethodInfo* Class::GetMethods(Il2CppClass *klass, void* *iter) { ... if (!*iter) { Class::SetupMethods(klass); if (klass->method_count == 0) return NULL; *iter = &klass->methods[0]; return klass->methods[0]; } ... } void Class::SetupMethods(Il2CppClass *klass) { if (klass->method_count || klass->rank) { FastAutoLock lock(&g_MetadataLock); SetupMethodsLocked(klass, lock); } }
void SetupMethodsLocked(Il2CppClass *klass, const FastAutoLock& lock) { if ((!klass->method_count && !klass->rank) || klass->methods) return; if (klass->generic_class) { InitLocked(GenericClass::GetTypeDefinition(klass->generic_class), lock); GenericClass::SetupMethods(klass); } else if (klass->rank) { InitLocked(klass->element_class, lock); SetupVTable(klass, lock); } else { if (klass->method_count == 0) { klass->methods = NULL; return; } klass->methods = (const MethodInfo**)IL2CPP_CALLOC(klass->method_count, sizeof(MethodInfo*)); MethodInfo* methods = (MethodInfo*)IL2CPP_CALLOC(klass->method_count, sizeof(MethodInfo)); MethodInfo* newMethod = methods; MethodIndex start = klass->typeDefinition->methodStart; IL2CPP_ASSERT(start != kFieldIndexInvalid); MethodIndex end = start + klass->method_count; for (MethodIndex index = start; index < end; ++index) { const Il2CppMethodDefinition* methodDefinition = MetadataCache::GetMethodDefinitionFromIndex(index); newMethod->name = MetadataCache::GetStringFromIndex(methodDefinition->nameIndex); newMethod->methodPointer = MetadataCache::GetMethodPointerFromIndex(methodDefinition->methodIndex); newMethod->invoker_method = MetadataCache::GetMethodInvokerFromIndex(methodDefinition->invokerIndex); newMethod->declaring_type = klass; newMethod->return_type = MetadataCache::GetIl2CppTypeFromIndex(methodDefinition->returnType); ParameterInfo* parameters = (ParameterInfo*)IL2CPP_CALLOC(methodDefinition->parameterCount, sizeof(ParameterInfo)); ParameterInfo* newParameter = parameters; for (uint16_t paramIndex = 0; paramIndex < methodDefinition->parameterCount; ++paramIndex) { const Il2CppParameterDefinition* parameterDefinition = MetadataCache::GetParameterDefinitionFromIndex(methodDefinition->parameterStart + paramIndex); newParameter->name = MetadataCache::GetStringFromIndex(parameterDefinition->nameIndex); newParameter->position = paramIndex; newParameter->token = parameterDefinition->token; newParameter->customAttributeIndex = parameterDefinition->customAttributeIndex; newParameter->parameter_type = MetadataCache::GetIl2CppTypeFromIndex(parameterDefinition->typeIndex); newParameter++; } newMethod->parameters = parameters; newMethod->customAttributeIndex = methodDefinition->customAttributeIndex; newMethod->flags = methodDefinition->flags; newMethod->iflags = methodDefinition->iflags; newMethod->slot = methodDefinition->slot; newMethod->parameters_count = static_cast<const uint8_t>(methodDefinition->parameterCount); newMethod->is_inflated = false; newMethod->token = methodDefinition->token; newMethod->methodDefinition = methodDefinition; newMethod->genericContainer = MetadataCache::GetGenericContainerFromIndex(methodDefinition->genericContainerIndex); if (newMethod->genericContainer) newMethod->is_generic = true; klass->methods[index - start] = newMethod; newMethod++; //在这里hook,就可以轻松拿到全部MethodInfo } } }
MethodInfo、Il2CppClass的结构也需要了解一下:
struct Il2CppClass { const Il2CppImage* image; void* gc_desc; const char* name; const char* namespaze; ... } struct MethodInfo { Il2CppMethodPointer methodPointer; InvokerMethod invoker_method; const char* name; Il2CppClass *declaring_type; const Il2CppType *return_type; const ParameterInfo* parameters; ... };
反编译获取hook地址:
编写frida js:
// hook SetupMethodsLocked var module = Process.findModuleByName("libil2cpp.so"); var p_size = 8; Interceptor.attach(ptr(module.base).add(0x72F09EC).add(0x204),{ onEnter:function(args){ var newMethod = this.context.x20 var pointer = newMethod.readPointer(); //MethodInfo var name = newMethod.add(p_size * 2).readPointer().readCString(); var klass = newMethod.add(p_size * 3).readPointer();//Il2CppClass var klass_name = klass.add(p_size * 2).readPointer().readCString(); var klass_paze = klass.add(p_size * 3).readPointer().readCString(); send(klass_paze+"."+klass_name+":"+name+" -> "+pointer.sub(module.base)); } });
成功hook,也成功获取了符号信息与地址。但大家也能看出,本次主角做了混淆。关于混淆又是另一个故事了。