#include "il2cpp_resolver.hpp" #include #include #include #ifdef _WIN32 #include #include #else #include #include #include #endif namespace devour { #ifdef _WIN32 static HMODULE g_gameAsm = nullptr; uintptr_t Il2CppResolver::gameAssemblyBase() { return reinterpret_cast(g_gameAsm); } size_t Il2CppResolver::gameAssemblySize() { if (!g_gameAsm) return 0; MODULEINFO mi{}; if (GetModuleInformation(GetCurrentProcess(), g_gameAsm, &mi, sizeof(mi))) return mi.SizeOfImage; return 0; } void* Il2CppResolver::getProc(const char* name) { if (!g_gameAsm) return nullptr; return reinterpret_cast(GetProcAddress(g_gameAsm, name)); } bool Il2CppResolver::init() { for (int i = 0; i < 600; ++i) { // 60s timeout g_gameAsm = GetModuleHandleA("GameAssembly.dll"); if (g_gameAsm) return true; std::this_thread::sleep_for(std::chrono::milliseconds(100)); } return false; } // Simple pattern scan std::vector Il2CppResolver::scanPattern(const char* pattern, const char* mask, uintptr_t start, size_t size) { std::vector out; size_t patLen = strlen(mask); for (size_t i = 0; i + patLen < size; ++i) { bool ok = true; for (size_t j = 0; j < patLen; ++j) { if (mask[j] == 'x' && reinterpret_cast(start + i)[j] != pattern[j]) { ok = false; break; } } if (ok) out.push_back(reinterpret_cast(start + i)); } return out; } ResolvedMethod Il2CppResolver::resolve(const char* klassName, const char* methodName) { // Use il2cpp exports to find method without offset using domain_get_t = void*(*)(); using thread_attach_t = void*(*)(void*); using assembly_foreach_t = void(*)(void(*)(void*, void*), void*); // We call il2cpp_resolve_icall-like path: try to locate via il2cpp_class_from_name auto il2cpp_domain_get = reinterpret_cast(getProc("il2cpp_domain_get")); auto il2cpp_thread_attach = reinterpret_cast(getProc("il2cpp_thread_attach")); auto klass_from_name = reinterpret_cast(getProc("il2cpp_class_from_name")); auto class_get_method = reinterpret_cast(getProc("il2cpp_class_get_method_from_name")); if (!il2cpp_domain_get || !klass_from_name || !class_get_method) { return resolveByStringXref(methodName); } void* domain = il2cpp_domain_get(); if (domain && il2cpp_thread_attach) il2cpp_thread_attach(domain); // NolanRankController lives in Assembly-CSharp — hosts IsRobeUnlocked etc. // Try common owner classes: "NolanRankController", "LobbyController", "InventoryController" const char* candidates[] = {"NolanRankController", "LobbyController", "InventoryController", "GameController", nullptr}; for (int ci = 0; candidates[ci]; ++ci) { void* klass = klass_from_name(nullptr, "Assembly-CSharp.dll", candidates[ci]); // Fallback: il2cpp_class_from_name expects (image, ns, name) — try image-less variant search if (!klass) { // Try global search via xref fallback continue; } void* method = class_get_method(klass, methodName); if (method) { // MethodInfo layout: methodPointer at offset 0x18 on x64 (stable across il2cpp) void* ptr = *reinterpret_cast(reinterpret_cast(method) + 0x18); if (ptr) return {methodName, ptr}; } } return resolveByStringXref(methodName); } ResolvedMethod Il2CppResolver::resolveByStringXref(const char* str, int occurrence) { // Scan for string pointer -> xref to find caller. Minimal heuristic: // Search for ansi string bytes inside GameAssembly rdata, then scan .text for lea rcx, [addr] uintptr_t base = gameAssemblyBase(); size_t sz = gameAssemblySize(); if (!base || !sz) return {str, nullptr}; // Find string bytes size_t slen = strlen(str); uintptr_t strAddr = 0; for (size_t i = 0; i + slen < sz; ++i) { if (memcmp(reinterpret_cast(base + i), str, slen) == 0) { // Check null terminator if (*reinterpret_cast(base + i + slen) == '\0') { if (occurrence-- == 0) { strAddr = base + i; break; } } } } if (!strAddr) return {str, nullptr}; // Scan for 48 8D 0D ? ? ? ? (lea rcx, [rip+disp]) referencing strAddr // Simplified: look for 4-byte disp that resolves to strAddr for (size_t i = 0; i + 7 < sz; ++i) { uint8_t* p = reinterpret_cast(base + i); if (p[0] == 0x48 && p[1] == 0x8D && p[2] == 0x0D) { int32_t disp = *reinterpret_cast(p + 3); uintptr_t target = base + i + 7 + disp; if (target == strAddr) { // Found xref — function start is previous 0xCC or 0x48 89 5C ? ... search back // Heuristic: walk back up to 0x200 for function prologue 40 53 / 48 83 EC for (int back = 0; back < 0x300; ++back) { uint8_t* q = p - back; if (q < reinterpret_cast(base)) break; if (q[0] == 0x40 && q[1] == 0x53) return {str, q}; // push rbx if (q[0] == 0x48 && q[1] == 0x83 && q[2] == 0xEC) return {str, q}; } return {str, p}; // fallback } } } return {str, nullptr}; } ResolvedMethod Il2CppResolver::isRobeUnlocked() { return resolve("NolanRankController", "IsRobeUnlocked"); } ResolvedMethod Il2CppResolver::isCharacterUnlocked() { return resolve("NolanRankController", "IsCharacterUnlocked"); } ResolvedMethod Il2CppResolver::hasOutfit() { return resolve("NolanRankController", "HasOutfit"); } ResolvedMethod Il2CppResolver::hasPerk() { return resolve("NolanRankController", "HasPerk"); } ResolvedMethod Il2CppResolver::hasPet() { return resolve("NolanRankController", "HasPet"); } ResolvedMethod Il2CppResolver::updateLockedPets() { return resolve("LobbyController", "UpdateLockedPets"); } ResolvedMethod Il2CppResolver::updateLockedPerks() { return resolve("LobbyController", "UpdateLockedPerks"); } ResolvedMethod Il2CppResolver::updateLockedFlashlights() { return resolve("LobbyController", "UpdateLockedFlashlights"); } ResolvedMethod Il2CppResolver::updateLockedEmotes() { return resolve("LobbyController", "UpdateLockedEmotes"); } ResolvedMethod Il2CppResolver::updateLockedOutfits() { return resolve("LobbyController", "UpdateLockedOutfits"); } ResolvedMethod Il2CppResolver::updateLockedRobes() { return resolve("LobbyController", "UpdateLockedRobes"); } #else // Linux stubs uintptr_t Il2CppResolver::gameAssemblyBase() { return 0; } size_t Il2CppResolver::gameAssemblySize() { return 0; } void* Il2CppResolver::getProc(const char*) { return nullptr; } bool Il2CppResolver::init() { return false; } std::vector Il2CppResolver::scanPattern(const char*, const char*, uintptr_t, size_t) { return {}; } ResolvedMethod Il2CppResolver::resolve(const char*, const char* m) { return {m, nullptr}; } ResolvedMethod Il2CppResolver::resolveByStringXref(const char* s, int) { return {s, nullptr}; } ResolvedMethod Il2CppResolver::isRobeUnlocked() { return {"IsRobeUnlocked", nullptr}; } ResolvedMethod Il2CppResolver::isCharacterUnlocked() { return {"IsCharacterUnlocked", nullptr}; } ResolvedMethod Il2CppResolver::hasOutfit() { return {"HasOutfit", nullptr}; } ResolvedMethod Il2CppResolver::hasPerk() { return {"HasPerk", nullptr}; } ResolvedMethod Il2CppResolver::hasPet() { return {"HasPet", nullptr}; } ResolvedMethod Il2CppResolver::updateLockedPets() { return {"UpdateLockedPets", nullptr}; } ResolvedMethod Il2CppResolver::updateLockedPerks() { return {"UpdateLockedPerks", nullptr}; } ResolvedMethod Il2CppResolver::updateLockedFlashlights() { return {"UpdateLockedFlashlights", nullptr}; } ResolvedMethod Il2CppResolver::updateLockedEmotes() { return {"UpdateLockedEmotes", nullptr}; } ResolvedMethod Il2CppResolver::updateLockedOutfits() { return {"UpdateLockedOutfits", nullptr}; } ResolvedMethod Il2CppResolver::updateLockedRobes() { return {"UpdateLockedRobes", nullptr}; } #endif } // namespace devour