Author SHA1 Message Date
megboyzz a73f0594e5 try to cut devmenu 2026-07-31 14:44:47 +03:00
megboyzz feb32247ed try to cut devmenu 2026-07-31 14:19:44 +03:00
3 changed files with 168 additions and 4 deletions
+14 -3
View File
@@ -8,11 +8,22 @@ android {
compileSdk = 34
defaultConfig {
applicationId = "com.ea.games.nfs13_na"
applicationId = "com.ea.games.nfs13_mod"
minSdk = 21
targetSdk = 34
versionCode = 1
versionName = "1.0"
// Matches the real game's own versionCode/versionName (and the
// main.1003128.com.ea.games.nfs13_mod.obb file already on the test
// device) so this build installs as an update rather than tripping
// Android's downgrade protection. NOTE: the claim that the game's own
// code looks up an OBB file via "main.<versionCode>.<package>.obb" is
// UNVERIFIED - grepped the entire launcher source tree (Java/Kotlin/
// XML) and libapp.so for "obb"/"addAssetPath"/"mountObb"/
// "StorageManager" and found zero references anywhere. This value
// just avoids the installer-level downgrade block; it is not known to
// affect asset/cache loading. See ANALYSIS.md for the still-open
// question of how this build actually locates game_cache/published/.
versionCode = 1003128
versionName = "1.3.128"
buildConfigField( "String", "DEV_MENU_VERSION", "\"0.1\"")
buildConfigField( "String", "DEV_MENU_ID", "\"DevMenu\"")
+6
View File
@@ -8,6 +8,12 @@ java {
targetCompatibility = JavaVersion.VERSION_17
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = "17"
}
}
dependencies {
implementation("javax.inject:javax.inject:1")
testImplementation("junit:junit:4.13.2")
+148 -1
View File
@@ -27,6 +27,136 @@ void pending_thread(){
}
// === EXPERIMENT: call TrackTestLayer::TrackTestLayer(path) directly ===
// See ANALYSIS.md §6d/6e for the reverse-engineering behind this. TrackTestLayer
// is a real, functional EA QA tool: its constructor loads an arbitrary track
// scene via the engine's generic scene loader, finds "start"/"finish" nodes,
// and spawns 8 reference cars bound to TrackNavigator - entirely bypassing
// RaceLoaderTask/Flow. We never found a legitimate, externally-triggerable path
// to it (the debug mode-string source is untraceable in shipped assets - see
// §6e), so this calls the constructor directly instead.
//
// libapp.so's internal "string" objects are a 3-word {begin,end,capacity}
// vector<char>-like struct (no small-string optimization), built via the
// sub_CF5F8 helper seen throughout the binary.
//
// CORRECTED after a live crash (see PROGRESS.md 2026-07-31): sub_CF5F8's real
// signature is `sub_CF5F8(dest, rangeBegin, rangeEnd)` - a [begin,end) range
// constructor (length = rangeEnd - rangeBegin, then memmove), NOT an implicit-
// strlen "assign from C-string" helper. Every caller seen during static
// analysis manually scanned for the string's end pointer first before calling
// it; first attempt here only passed 2 args, leaving the 3rd register (end)
// as garbage, which produced a bogus memmove length and SIGSEGV'd inside
// sub_CF5F8 itself (confirmed via the crash's PC landing at sub_CF5F8+0x68).
typedef void *(*StrAssignRange_t)(void *strObj, const char *rangeBegin, const char *rangeEnd);
typedef void *(*TrackTestLayerCtor_t)(void *self, void *pathStrObj);
void try_load_track_test_layer(){
// mpcore's JNI_OnLoad fires from GameActivity's Kotlin `init {}` block,
// i.e. before onCreate()/nativeOnCreate() - long before the engine's own
// resource manager and main loop are up. Give it time to boot first;
// this is a guess, not a traced signal - if this crashes or does nothing,
// that's the first thing to revisit.
Log("TrackTestLayer experiment: waiting 15s for engine boot...");
std::this_thread::sleep_for(std::chrono::seconds(15));
auto StrAssignRange = (StrAssignRange_t) APP_ADDR(0xcf5f8);
auto TrackTestLayerCtor = (TrackTestLayerCtor_t) APP_ADDR(0x24a1f4);
uint32_t pathStrObj[3] = {0, 0, 0};
// region4_chicago_track4 is referenced by TrackName in event_01_race.prefabs.sb
// (a real, shipped race event) and confirmed (via unpacking the .scene.sb with
// NFSMW12MobileTools) to contain top-level "actor" entities literally named
// "start" and "finish" - region1_foothills_track3 (first attempt) crashed on
// this exact lookup despite also containing "start"/"finish" actors somewhere,
// so nesting depth (not mere presence) may be what matters. See ANALYSIS.md §6f.
const char *trackPath = "published/prefabs/tracks/region4_chicago_track4.scene.sb";
StrAssignRange(pathStrObj, trackPath, trackPath + strlen(trackPath));
void *obj = malloc(0x110);
memset(obj, 0, 0x110);
Log("TrackTestLayer experiment: calling ctor @ 0x%08x obj=%p path=%s",
(unsigned) APP_ADDR(0x24a1f4), obj, trackPath);
TrackTestLayerCtor(obj, pathStrObj);
Log("TrackTestLayer experiment: ctor returned without crashing, obj=%p "
"(intentionally leaked for this test - not registered with any layer "
"manager, so it may not render even if construction succeeded)", obj);
}
// === ATTEMPT 2: hook RaceLoaderTask::BuildTrackScenePath instead ===
// See ANALYSIS.md §6f for why: TrackTestLayer's start/finish lookup crashes
// even on a confirmed race-linked track with the right data present, which
// points to a dependency on async load completion or ambient engine state a
// cold direct call can't replicate. Hooking the engine's own always-correctly-
// initialized code path (this function, called every time a real race loads)
// sidesteps that entirely - we let the player pick any real race from the
// menu and just substitute the track name at the last moment.
//
// IMPORTANT: unlike the Thumb-oriented code armhook.cpp's InstallHook/makeJMP
// assume, RaceLoaderTask_BuildTrackScenePath (0x2db384) is compiled in ARM
// mode (confirmed via disassembly: E92D41F0 = ARM "PUSH {r4-r8,lr}", not a
// Thumb encoding). Using the existing Thumb-oriented hook helpers here would
// misinterpret/corrupt the function. This installs a separate, ARM-mode-
// correct inline hook instead: overwrite the first 8 bytes (2 whole ARM
// instructions - safe, ARM instructions are always 4 bytes wide, so there's
// no mid-instruction split risk the way there is in mixed-width Thumb-2) with
// `LDR PC, [PC, #-4]` + hook-function-address. A trampoline re-executes the 2
// displaced instructions (both position-independent - PUSH/SUB, no PC-relative
// addressing - confirmed safe to relocate) then long-jumps back to target+8
// to resume the original function. The hook address's bit 0 is set
// automatically by the compiler (mpcore is Thumb-2 code) for correct
// ARM->Thumb interworking on the `LDR PC` jump into our hook; the trampoline's
// jump back to target+8 has bit 0 clear, staying in ARM mode as required.
typedef int (*BuildTrackScenePath_t)(uint32_t *raceLoaderTaskThis);
static BuildTrackScenePath_t g_origBuildTrackScenePath = nullptr;
static uint32_t *install_arm_inline_hook(uintptr_t targetAddr, void *hookFnPtr) {
auto *tramp = (uint32_t *) mmap(nullptr, 4096, PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
auto *target = (uint32_t *) targetAddr;
tramp[0] = target[0]; // relocated original instr 0 (PUSH, position-independent)
tramp[1] = target[1]; // relocated original instr 1 (SUB SP, position-independent)
tramp[2] = 0xE51FF004; // LDR PC, [PC, #-4]
tramp[3] = (uint32_t) (targetAddr + 8); // jump back into original, ARM mode (bit0=0)
unProtect(targetAddr, 8);
target[0] = 0xE51FF004; // LDR PC, [PC, #-4]
target[1] = (uint32_t) hookFnPtr; // our Thumb hook (bit0=1, set by the compiler)
__builtin___clear_cache((char *) target, (char *) (target + 2));
__builtin___clear_cache((char *) tramp, (char *) (tramp + 4));
return tramp;
}
int hook_BuildTrackScenePath(uint32_t *raceLoaderTaskThis){
Log("hook_BuildTrackScenePath: fired, this=%p", raceLoaderTaskThis);
uint32_t raceDefPtr = raceLoaderTaskThis[8];
if (raceDefPtr) {
auto StrAssignRange = (StrAssignRange_t) APP_ADDR(0xcf5f8);
// Deliberately different from any of this build's real tracks, so a
// successful override is visually unmistakable regardless of which
// race the player actually picked in the menu.
const char *overrideTrack = "region3_colorado_track2";
void *trackNameStrObj = (void *) (raceDefPtr + 72);
StrAssignRange(trackNameStrObj, overrideTrack, overrideTrack + strlen(overrideTrack));
Log("hook_BuildTrackScenePath: raceDefPtr=0x%08x, overrode track name to '%s'",
raceDefPtr, overrideTrack);
} else {
Log("hook_BuildTrackScenePath: raceDefPtr is null, skipping override");
}
return g_origBuildTrackScenePath(raceLoaderTaskThis);
}
jint JNI_OnLoad(JavaVM *vm, void *reserved){
@@ -39,7 +169,24 @@ jint JNI_OnLoad(JavaVM *vm, void *reserved){
//*(int *)APP_ADDR(0x00A522C4) = 0xE320F000;
//WriteMemory(APP_ADDR(0x00E4BA94), (uintptr_t)"my_tag", 6);
raise(SIGSTOP);
// TrackTestLayer experiment (2026-07-31): superseded by the
// RaceLoaderTask_BuildTrackScenePath hook below - see ANALYSIS.md §6f.
// std::thread(try_load_track_test_layer).detach();
// Installing the ARM inline hook is just a code patch - safe at any time,
// unlike calling into engine internals cold. No delay needed; the hook
// only fires later, whenever the player naturally starts a real race.
g_origBuildTrackScenePath = (BuildTrackScenePath_t)
install_arm_inline_hook(APP_ADDR(0x2db384), (void *) hook_BuildTrackScenePath);
Log("Installed RaceLoaderTask_BuildTrackScenePath hook, orig trampoline=%p",
(void *) g_origBuildTrackScenePath);
// raise(SIGSTOP) disabled for this experiment (2026-07-31): SIGSTOP
// suspends the entire process, including the thread spawned above, which
// would prevent the delayed call from ever firing. Re-enable if you need
// to attach a debugger before continuing normal boot - see PROGRESS.md.
//raise(SIGSTOP);
return JNI_VERSION_1_6;
}