从注入到调用:一个完整的Unity il2cpp运行时Hook实战指南(附C++代码)
从注入到调用一个完整的Unity il2cpp运行时Hook实战指南附C代码在游戏开发与逆向工程领域Unity引擎的il2cpp后端因其性能优势被广泛采用但也带来了动态分析的独特挑战。本文将深入探讨如何通过运行时注入技术实现对il2cpp编译后应用的精准控制——从定位内存结构到重定向关键函数调用整个过程就像在黑暗森林中搭建一座可控制的桥梁。1. 理解il2cpp运行时架构il2cpp并非简单的C#到C的转译器而是一个完整的AOTAhead-Of-Time编译系统。当选择il2cpp后端时Unity会执行以下转换流程C#源码 → IL中间码 → C代码 → 原生机器码关键组件构成GameAssembly.dll/libil2cpp.so核心运行时库包含转换后的游戏逻辑global-metadata.dat保存类型系统映射关系的元数据仓库导出函数表提供运行时反射能力的API接口典型内存布局特征内存区域内容描述访问方式代码段编译后的机器指令函数指针调用元数据段类型系统结构体API查询指针解析动态内存区实例对象与运行时数据指针追踪偏移计算注意不同Unity版本中il2cpp内部结构体可能存在字段偏移差异实战中需结合具体版本验证。2. 注入环境的精密准备2.1 动态链接库注入方案Windows平台推荐采用经典的DLL注入技术// 基础注入器示例需管理员权限 HANDLE hProcess OpenProcess(PROCESS_ALL_ACCESS, FALSE, targetPID); LPVOID pMem VirtualAllocEx(hProcess, NULL, dllPath.size(), MEM_COMMIT, PAGE_READWRITE); WriteProcessMemory(hProcess, pMem, dllPath.c_str(), dllPath.size(), NULL); HANDLE hThread CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(kernel32), LoadLibraryA), pMem, 0, NULL);Android平台则需要结合ptrace或LD_PRELOAD技术这里展示一个dlopen的替代方案# 在注入器中执行 adb push injector /data/local/tmp adb push libhook.so /data/local/tmp adb shell chmod x /data/local/tmp/injector adb shell /data/local/tmp/injector com.game.package2.2 运行时API定位策略il2cpp的导出函数通常包含版本特征可通过模式匹配动态定位// 动态获取API函数指针示例 auto il2cpp_resolve_export(const char* pattern) { HMODULE base GetModuleHandle(GameAssembly); IMAGE_NT_HEADERS* nt (IMAGE_NT_HEADERS*)((BYTE*)base ((IMAGE_DOS_HEADER*)base)-e_lfanew); IMAGE_EXPORT_DIRECTORY* exports (IMAGE_EXPORT_DIRECTORY*)((BYTE*)base nt-OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress); DWORD* names (DWORD*)((BYTE*)base exports-AddressOfNames); for(DWORD i 0; i exports-NumberOfNames; i) { const char* name (const char*)base names[i]; if(strstr(name, pattern)) { return (void*)((BYTE*)base ((DWORD*)((BYTE*)base exports-AddressOfFunctions))[i]); } } return nullptr; }3. 元数据导航与类型定位3.1 程序集加载与类查询通过三级跳转完成类方法定位// 典型查询路径示例 auto domain il2cpp_domain_get(); auto assembly il2cpp_domain_assembly_open(domain, Assembly-CSharp); auto image il2cpp_assembly_get_image(assembly); auto targetClass il2cpp_class_from_name(image, , PlayerController);关键结构体关系图Il2CppDomain → Il2CppAssembly → Il2CppImage → Il2CppClass → MethodInfo3.2 方法签名处理技巧il2cpp方法调用存在隐式this指针参数需要特殊处理// 方法Hook前后对比 Original: int Player_GetHealth(Player* this) { return this-health; } il2cpp转换后: int Player_GetHealth(Player* this, void* unused) { return this-health; } // 因此调用时需 typedef int (*Player_GetHealth_t)(Player*, void*); auto original (Player_GetHealth_t)method-methodPointer; int health original(playerInstance, nullptr);4. 实战Hook实现方案4.1 虚函数表替换技术对于虚方法可采用直接修改vtable的方案void hook_virtual_method(Il2CppClass* klass, const char* name, void* newFunc) { for(uint16_t i 0; i klass-vtable_count; i) { if(strcmp(klass-vtable[i].method-name, name) 0) { DWORD oldProtect; VirtualProtect(klass-vtable[i].methodPtr, sizeof(void*), PAGE_READWRITE, oldProtect); klass-vtable[i].methodPtr newFunc; VirtualProtect(klass-vtable[i].methodPtr, sizeof(void*), oldProtect, oldProtect); break; } } }4.2 机器码级Hook方案更通用的指令跳转方案x64平台示例#pragma pack(push, 1) struct JmpCode { uint8_t opcode 0xE9; uint32_t offset; }; #pragma pack(pop) void install_detour(void* original, void* hook) { JmpCode jmp; jmp.offset (uint32_t)((BYTE*)hook - (BYTE*)original - sizeof(JmpCode)); DWORD oldProtect; VirtualProtect(original, sizeof(JmpCode), PAGE_EXECUTE_READWRITE, oldProtect); memcpy(original, jmp, sizeof(JmpCode)); VirtualProtect(original, sizeof(JmpCode), oldProtect, oldProtect); }4.3 上下文保存与恢复安全的Hook实现应保存原始上下文// 典型Hook代理函数 __declspec(naked) void HookProxy() { __asm { pushad pushfd // 自定义处理逻辑 call [g_customHandler] popfd popad // 跳回原函数或替代逻辑 jmp [g_originalFunc] } }5. 高级调试技巧与异常处理5.1 元数据校验绕过当遇到元数据加密时可采用动态重建策略Il2CppClass* safe_get_class(const char* name) { static std::unordered_mapstd::string, Il2CppClass* cache; if(auto it cache.find(name); it ! cache.end()) return it-second; auto klass il2cpp_class_from_name(/*...*/); if(!klass) { // 尝试通过特征码扫描定位类实例 uintptr_t classPtr scan_memory_for_class(name); if(classPtr) { klass reinterpret_castIl2CppClass*(classPtr); // 手动填充必要字段 klass-name strdup(name); } } cache[name] klass; return klass; }5.2 线程安全防护措施多线程环境下的Hook操作需要同步机制std::recursive_mutex g_hookMutex; void thread_safe_hook() { std::lock_guardstd::recursive_mutex lock(g_hookMutex); suspend_all_threads(); // 执行Hook操作 resume_all_threads(); }6. 实战案例属性修改器实现完整工作流程示例// 1. 注入初始化 void on_attach() { auto playerClass il2cpp_class_from_name(/*...*/, Player); auto healthProp il2cpp_class_get_property_from_name(playerClass, Health); // 2. 获取原始存取器 auto getter il2cpp_property_get_get_method(healthProp); auto setter il2cpp_property_get_set_method(healthProp); // 3. 安装Hook g_originalGet getter-methodPointer; install_detour(g_originalGet, hooked_getter); // 4. 持久化监控 create_mod_thread(); }典型问题排查表现象可能原因解决方案注入后立即崩溃版本不匹配验证Unity版本和偏移量调用时参数错乱this指针处理错误检查调用约定和参数数量部分功能失效元数据混淆采用动态特征码定位多线程环境下崩溃竞态条件添加线程同步机制在实际项目中我发现最有效的调试方式是结合Cheat Engine的内存扫描与IL2CPP Dumper的输出交叉验证。例如当Hook一个玩家移动方法时可以先用CE定位速度变量的内存地址再通过反推访问路径来确定需要拦截的类方法。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2557724.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!