Compare commits
2
Commits
947ba44245
...
a73f0594e5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a73f0594e5 | ||
|
|
feb32247ed |
+14
-3
@@ -8,11 +8,22 @@ android {
|
|||||||
compileSdk = 34
|
compileSdk = 34
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.ea.games.nfs13_na"
|
applicationId = "com.ea.games.nfs13_mod"
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 1
|
// Matches the real game's own versionCode/versionName (and the
|
||||||
versionName = "1.0"
|
// 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_VERSION", "\"0.1\"")
|
||||||
buildConfigField( "String", "DEV_MENU_ID", "\"DevMenu\"")
|
buildConfigField( "String", "DEV_MENU_ID", "\"DevMenu\"")
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ java {
|
|||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "17"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("javax.inject:javax.inject:1")
|
implementation("javax.inject:javax.inject:1")
|
||||||
testImplementation("junit:junit:4.13.2")
|
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){
|
jint JNI_OnLoad(JavaVM *vm, void *reserved){
|
||||||
|
|
||||||
@@ -39,7 +169,24 @@ jint JNI_OnLoad(JavaVM *vm, void *reserved){
|
|||||||
//*(int *)APP_ADDR(0x00A522C4) = 0xE320F000;
|
//*(int *)APP_ADDR(0x00A522C4) = 0xE320F000;
|
||||||
|
|
||||||
//WriteMemory(APP_ADDR(0x00E4BA94), (uintptr_t)"my_tag", 6);
|
//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;
|
return JNI_VERSION_1_6;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user