#include #include #include #include #include #include #include "main.h" #include "util/util.h" #include #include #include #include #include #include #include #include #include "util/armhook.h" #include "util/armhooks.h" #include "game_events.h" #include "lan_event_injection.h" void* libapp_base = NULL; static int find_lib_callback(struct dl_phdr_info* info, size_t size, void* data) { if (strstr(info->dlpi_name, "libapp.so")) { libapp_base = (void*)info->dlpi_addr; LOGD("Found libapp.so at base: 0x%08X", (uintptr_t)libapp_base); return 1; // Останавливаем перебор } return 0; } bool get_libapp_base() { dl_iterate_phdr(find_lib_callback, NULL); if (!libapp_base) { Log("libapp.so not found in memory!"); return false; } return true; } int (*sub_4087CC)() = nullptr; int HOOK_sub_4087CC() { Log("Its hooked sub_4087CC()!"); return sub_4087CC(); } uintptr_t get_library_base(const char* lib_name) { FILE* maps = fopen("/proc/self/maps", "r"); if (!maps) return 0; char line[512]; uintptr_t base = 0; while (fgets(line, sizeof(line), maps)) { if (strstr(line, lib_name)) { base = (uintptr_t)strtoul(strtok(line, "-"), NULL, 16); break; } } fclose(maps); return base; } bool is_address_executable(void* addr) { uintptr_t page = (uintptr_t)addr & ~(getpagesize() - 1); return msync((void*)page, getpagesize(), MS_ASYNC) == 0; } bool is_memory_writable(void* addr, size_t size) { uintptr_t page_start = (uintptr_t)addr & ~(getpagesize() - 1); return mprotect((void*)page_start, size, PROT_READ | PROT_WRITE | PROT_EXEC) == 0; } #include #include using namespace std; // ---- RaceLoaderTask_BuildTrackScenePath hook (see ANALYSIS.md §6j) ---- // Target compiled in ARM mode (PUSH {R4-R11,LR}; ADD R11,SP,#0x1C - both // position-independent, safe to relocate into the trampoline as-is). #define BUILDTRACKSCENEPATH_OFFSET 0x2a8424 typedef int (*BuildTrackScenePathFn)(void* raceLoaderTask); static BuildTrackScenePathFn orig_BuildTrackScenePath = nullptr; // Deliberately different from any real event's track, so a successful // override is visually unmistakable. region3/colorado was tried first and // abandoned: it's cut/incomplete content in this build - models/environments/ // has no colorado/ folder at all (only chicago, desert, foothills, garage, // newyork), even though region3_colorado_track2.scene.sb itself contains // full embedded geometry. The scene's internal m3g loader still tries to // open "/published/models/environments/colorado/region3_colorado_track2.m3g" // as a loose file, which was never shipped -> geometry never loads -> every // downstream consumer (spatial index, checkpoints, ...) sees empty/zero data // and null-derefs, which is what the whole sub_53A5FC/sub_52A9B8/sub_52A620/ // sub_58E5E8 crash chain actually was (see ANALYSIS.md §6n/§6o). Switched to // region4_chicago_track4/chicago instead: confirmed shipped and playable // (event_01_race.prefabs.sb's real TrackName, verified start/finish actors // in the scene, and a full models/environments/chicago/ folder present). static const char* kOverrideTrackName = "region4_chicago_track4"; // Environment prefabs are per-track variants ("chicago1.prefabs.sb" .. // "chicago6.prefabs.sb", matching "region4_chicago_track1".."track6"), not a // single generic "chicago.prefabs.sb" - confirmed live: env="chicago" alone // hit "Could not open database at published/prefabs/environments/chicago. // prefabs.sb" followed by an immediate SIGSEGV. Only colorado shipped as one // un-numbered colorado.prefabs.sb instead of colorado1..6 - further evidence // region3/colorado is unfinished/cut content (see kOverrideTrackName above). static const char* kOverrideEnvName = "chicago4"; int Hook_BuildTrackScenePath(void* a1) { // a1[8] (word offset 8 = byte 32): pointer to the RaceDefinition-like // struct. Track name is a {begin,end} pair at byte offsets +72/+76, // environment name likewise at +100/+104. BuildTrackScenePath only // READS these fields (never frees them), so we just repoint begin/end // at our own static buffers instead of freeing/reallocating the // originals - avoids guessing an unconfirmed capacity-field offset. // The original buffers are deliberately leaked (two small allocations // per race load - negligible). void* raceDef = *(void**)((uint8_t*)a1 + 32); if (raceDef) { size_t trackLen = strlen(kOverrideTrackName); *(const char**)((uint8_t*)raceDef + 72) = kOverrideTrackName; *(const char**)((uint8_t*)raceDef + 76) = kOverrideTrackName + trackLen; size_t envLen = strlen(kOverrideEnvName); *(const char**)((uint8_t*)raceDef + 100) = kOverrideEnvName; *(const char**)((uint8_t*)raceDef + 104) = kOverrideEnvName + envLen; Log("BuildTrackScenePath hook fired: track -> %s, env -> %s", kOverrideTrackName, kOverrideEnvName); } else { Log("BuildTrackScenePath hook fired but raceDef (a1[8]) is NULL, skipping override"); } return orig_BuildTrackScenePath(a1); } static bool InstallBuildTrackScenePathHook() { uintptr_t target = (uintptr_t)libapp_base + BUILDTRACKSCENEPATH_OFFSET; uint32_t* target32 = (uint32_t*)target; void* tramp = mmap(nullptr, (size_t)getpagesize(), PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (tramp == MAP_FAILED) { Log("BuildTrackScenePath hook: mmap trampoline failed"); return false; } uint32_t* tramp32 = (uint32_t*)tramp; // Relocate the 2 displaced original ARM instructions verbatim (both // confirmed position-independent: plain PUSH and ADD, no PC-relative // addressing), then jump back into the function body past them. tramp32[0] = target32[0]; tramp32[1] = target32[1]; tramp32[2] = 0xE51FF004; // LDR PC, [PC, #-4] tramp32[3] = (uint32_t)(target + 8); orig_BuildTrackScenePath = (BuildTrackScenePathFn)tramp; uintptr_t page = target & ~((uintptr_t)getpagesize() - 1); if (mprotect((void*)page, (size_t)getpagesize(), PROT_READ | PROT_WRITE | PROT_EXEC) != 0) { Log("BuildTrackScenePath hook: mprotect target failed: %s", strerror(errno)); return false; } target32[0] = 0xE51FF004; // LDR PC, [PC, #-4] // &Hook_BuildTrackScenePath has bit0 set (Thumb-compiled mpcore code), // triggering the ARM->Thumb interworking switch on load into PC. target32[1] = (uint32_t)(uintptr_t)&Hook_BuildTrackScenePath; __builtin___clear_cache((char*)target, (char*)(target + 8)); __builtin___clear_cache((char*)tramp, (char*)tramp + 16); Log("Installed RaceLoaderTask_BuildTrackScenePath hook at %p, trampoline=%p", (void*)target, tramp); return true; } // ---- MapScreen constructor trace hook (temporary, RE discovery only) ---- // Purpose: capture the live `im::app::flow::nfs::MapScreen` instance pointer // so we can read its "scroll" layout-entity (found via sub_1332B8's // FindOrCreateLayoutEntity call with the literal name "scroll" - see // PROGRESS.md) - a Transform-shaped object whose position (offset +36/+40) // and scale (offset +44/+48) are hypothesized to be the map's current // pan/zoom state, needed to convert a MapTrack's world-space bounds rect // (found earlier, offsets +0x44.."+0x50") into real screen pixels. #define MAPSCREEN_CTOR_OFFSET 0x1781BC typedef void* (*MapScreenCtorFn)(void* a1); static MapScreenCtorFn orig_MapScreenCtor = nullptr; void* g_mapScreenInstance = nullptr; void* Hook_MapScreenCtor(void* a1) { void* result = orig_MapScreenCtor(a1); g_mapScreenInstance = a1; Log("MapScreen constructed: %p", a1); return result; } static bool InstallMapScreenCtorTraceHook() { uintptr_t target = (uintptr_t)libapp_base + MAPSCREEN_CTOR_OFFSET; uint32_t* target32 = (uint32_t*)target; // Confirmed ARM-mode, position-independent prologue this session // (PUSH {R4-R11,LR}; ADD R11,SP,#0x1C), same trampoline pattern as the // other hooks in this file. void* tramp = mmap(nullptr, (size_t)getpagesize(), PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (tramp == MAP_FAILED) { Log("MapScreen ctor hook: mmap trampoline failed"); return false; } uint32_t* tramp32 = (uint32_t*)tramp; tramp32[0] = target32[0]; tramp32[1] = target32[1]; tramp32[2] = 0xE51FF004; // LDR PC, [PC, #-4] tramp32[3] = (uint32_t)(target + 8); orig_MapScreenCtor = (MapScreenCtorFn)tramp; uintptr_t page = target & ~((uintptr_t)getpagesize() - 1); if (mprotect((void*)page, (size_t)getpagesize(), PROT_READ | PROT_WRITE | PROT_EXEC) != 0) { Log("MapScreen ctor hook: mprotect target failed: %s", strerror(errno)); return false; } target32[0] = 0xE51FF004; // LDR PC, [PC, #-4] target32[1] = (uint32_t)(uintptr_t)&Hook_MapScreenCtor; __builtin___clear_cache((char*)target, (char*)(target + 8)); __builtin___clear_cache((char*)tramp, (char*)tramp + 16); Log("Installed MapScreen ctor trace hook at %p, trampoline=%p", (void*)target, tramp); return true; } // Flip to false to run the game completely unmodified (e.g. to capture a // baseline/"before" comparison) - true installs the track-substitution hook. // Just edit this and rebuild, no need to touch anything else. static constexpr bool kEnableTrackSubstitutionHook = true; static constexpr bool kEnableLanEventInjectionHook = true; static constexpr bool kEnableMapScreenCtorTraceHook = false; // TEMP: isolating a reproducible SIGSEGV, see PROGRESS.md // See ANALYSIS.md §6ff/§6gg: prevents a QA-only "Soak Test" auto-race feature // from eventually crashing the process on entries our injection adds to the // prefab cache. Independent of kEnableLanEventInjectionHook so it can be kept // on even if injection itself is toggled off for testing. static constexpr bool kEnableSoakTestDisableHook = true; JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { JNIEnv* env = nullptr; if (vm->GetEnv((void**)&env, JNI_VERSION_1_6) == JNI_OK) { InitGameEvents(vm, env); } else { Log("JNI_OnLoad: GetEnv failed, GameEvents bridge not initialised"); } if (get_libapp_base()) { if (kEnableTrackSubstitutionHook) { InstallBuildTrackScenePathHook(); } if (kEnableLanEventInjectionHook) { InstallMapTrackHandleEventHook(); } if (kEnableSoakTestDisableHook) { InstallSoakTestDisableHook(); } if (kEnableMapScreenCtorTraceHook) { InstallMapScreenCtorTraceHook(); } InstallCopSoundsTickSkipHook(); InstallGetComponentNameSkipHook(); InstallStrlenNullGuardHook(); InstallInternStringDiagHook(); InstallFatalLogCallerTraceHook(); InstallResolveDisplayTextHook(); InstallResolveDisplayTextWrapperDiagHook(); InstallLayoutScreenCtorHook(); InstallModSlotSelectedHook(); InstallFireOutputDiagHook(); // NOT installed (cont.43): live-tested and found to break touch // responsiveness on car_select once installed, for reasons not yet // understood (sub_16C660 itself runs fine every frame through the // hook per its own diagnostics - "returned 0" every ~16ms, no hang // - yet taps stop registering; reproduced 5/5 tries with the hook // installed vs 1/1 without). sub_16C660 is called at a much higher, // more global frequency (~60/sec, from app boot onward) than any // other function hooked in this project - too risky to keep // chasing blind. See lan_event_injection.h for the full writeup; // the FireOutput-level interception was widened instead (does not // need this hook). // InstallConfirmCarSelectionHook(); InstallFlowNodeTickHook(); } return JNI_VERSION_1_6; } extern "C" JNIEXPORT void JNICALL Java_nfs_mod_mpcore_MultiplayerCore_bumpBackTraceToLogcat(JNIEnv *env, jobject thiz) { //backtraceToLogcat(); } // cont.44: lets Kotlin (eventually a real lobby-overlay button, for now a // debug broadcast receiver - see GameActivityMain.kt) open car_select on // demand instead of only automatically at boot. See // TriggerOpenCarSelectOnDemand in lan_event_injection.h for the details. extern "C" JNIEXPORT void JNICALL Java_nfs_mod_mpcore_MultiplayerCore_triggerCarSelectTest(JNIEnv *env, jobject thiz) { TriggerOpenCarSelectOnDemand(); } // cont.48: experimental TRUE direct jump to car_select, bypassing // EventDetails entirely - see TriggerTrueDirectCarSelectJump in // lan_event_injection.h for the details and the real risk involved. extern "C" JNIEXPORT void JNICALL Java_nfs_mod_mpcore_MultiplayerCore_triggerTrueDirectCarSelectJump(JNIEnv *env, jobject thiz) { TriggerTrueDirectCarSelectJump(); }