Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a73f0594e5 | ||
|
|
feb32247ed |
+14
-3
@@ -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\"")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user