module.cpp 5457 lines · 301 KB
Raw
1#include <sys/types.h>
2#include <sys/stat.h>
3#include <fcntl.h>
4#include <unistd.h>
5#include "zygisk.hpp"
6#include "overlay_dex.h"
7#include <jni.h>
8#include <dlfcn.h>
9#include <sys/mman.h>
10#include <unistd.h>
11#include <string.h>
12#include <android/log.h>
13#include <sys/system_properties.h>
14#include <pthread.h>
15#include <time.h>
16#include <errno.h>
17#include <stdlib.h>
18#include <stdarg.h>
19#include <sys/syscall.h>
20#include <linux/memfd.h>
21#include <string>
22#include "lsplant.hpp"
23#include <zlib.h>
24
25#define TAG "DRMod"
26#define LOGI(...) do { __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__); drmod_log(__VA_ARGS__); } while(0)
27#define LOGE(...) do { __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__); drmod_log(__VA_ARGS__); } while(0)
28
29static bool g_is_game = false;
30
31static FILE *g_logfile = nullptr;
32static void drmod_log(const char *fmt, ...) {
33 if (!g_logfile) {
34 // Only target process logs to file (SELinux-safe app dir). Other processes log only to logcat.
35 if (!g_is_game) return;
36 mkdir("/data/data/com.lavalabs.dungeonrush/files", 0755);
37 mkdir("/data/user/0/com.lavalabs.dungeonrush/files", 0755);
38 g_logfile = fopen("/data/data/com.lavalabs.dungeonrush/files/drmod.log", "a");
39 if (!g_logfile) g_logfile = fopen("/data/user/0/com.lavalabs.dungeonrush/files/drmod.log", "a");
40 }
41 if (!g_logfile) return;
42 va_list ap;
43 va_start(ap, fmt);
44 time_t t = time(nullptr);
45 struct tm *tm = localtime(&t);
46 fprintf(g_logfile, "[%02d:%02d:%02d] [pid=%d] ", tm->tm_hour, tm->tm_min, tm->tm_sec, getpid());
47 vfprintf(g_logfile, fmt, ap);
48 fprintf(g_logfile, "\n");
49 va_end(ap);
50 fflush(g_logfile);
51}
52
53#define GAME_PKG "com.lavalabs.dungeonrush"
54#define GAME_PKG_FULL "com.lavalabs.dungeonrush"
55#define DELAY_SEC 30
56
57static JavaVM *g_jvm = nullptr;
58static bool g_activated = false;
59
60// ==================== Toggle state (ALL OFF by default) ====================
61
62static volatile bool g_immortality = false;
63static volatile bool g_forced_crits = false;
64static volatile bool g_minigame_loot = false;
65static volatile bool g_divine_forge = false;
66static volatile bool g_overdrive_divine = false;
67static volatile bool g_forge_exp = false;
68
69// ==================== Shared state ====================
70
71static uintptr_t g_il2cpp_base = 0;
72
73// ==================== Patch table ====================
74
75struct Patch {
76 uint32_t off;
77 uint32_t len;
78 const uint8_t *orig;
79 const uint8_t *repl;
80 const char *name;
81 bool toggleable;
82};
83
84static const uint8_t NOP4[] = {0x1F,0x20,0x03,0xD5};
85static const uint8_t NOP8[] = {0x1F,0x20,0x03,0xD5,0x1F,0x20,0x03,0xD5};
86static const uint8_t MOV_W0_1[] = {0x20,0x00,0x80,0x52};
87static const uint8_t MOV_W0_0[] = {0x00,0x00,0x80,0x52};
88static const uint8_t MOV_X0_0[] = {0x00,0x00,0x80,0xD2};
89static const uint8_t MOV_W0_9[] = {0x20,0x01,0x80,0x52};
90static const uint8_t RET[] = {0xC0,0x03,0x5F,0xD6};
91static const uint8_t MR_RET[] = {0x20,0x00,0x80,0x52,0xC0,0x03,0x5F,0xD6};
92static const uint8_t M9_RET[] = {0x20,0x01,0x80,0x52,0xC0,0x03,0x5F,0xD6};
93static const uint8_t M2_RET[] = {0x40,0x00,0x80,0x52,0xC0,0x03,0x5F,0xD6};
94static const uint8_t FMOV_S0[] = {0xE0,0x03,0x27,0x1E};
95
96// Original bytes (verified against v56 binary)
97static const uint8_t O_ctz[] = {0xFE,0x57,0xBE,0xA9,0xF4,0x4F,0x01,0xA9}; // stp x30,x21 / stp x20,x19
98static const uint8_t O_store1[] = {0x41,0x16,0x00,0x54}; // b.ne HTTP 200 check in fh.dyd (TrackingController.fh)
99static const uint8_t O_store2[] = {0x41,0x1A,0x00,0x54}; // b.ne HTTP 200 check in fi.dye (TrackingController.fi)
100static const uint8_t O_tbz_fc[] = {0x55,0x1C,0x00,0x36}; // tbz w21,#0 -> fi.dye entry flag guard (bit0==0 => FAILURE)
101static const uint8_t O_actk_bl[] = {0xC3,0xFF,0xFF,0x97};
102static const uint8_t O_actk_cs[] = {0xE0,0x17,0x9F,0x1A};
103static const uint8_t O_actk_bl2[] = {0xC1,0xFF,0xFF,0x97};
104static const uint8_t O_actk_bl3[] = {0xBF,0xFF,0xFF,0x97};
105static const uint8_t O_mg[] = {0x00,0x28,0x23,0x1E};
106static const uint8_t O_forge[] = {0xE9,0x23,0xBC,0x6D,0xFE,0x0B,0x00,0xF9};
107static const uint8_t O_od[] = {0xFF,0x43,0x02,0xD1,0xFE,0x6F,0x04,0xA9};
108static const uint8_t O_beq_38[] = {0x00,0x07,0x00,0x54}; // b.eq +0xE0
109static const uint8_t B_SYNC_38[] = {0x38,0x00,0x00,0x14}; // b +0xE0 (same target as b.eq)
110static const uint8_t O_jme_gate[] = {0x1A,0x02,0x00,0x36}; // tbz w26,#0 (Exceptional gate) in tw.jme @0x28741C8
111static const uint8_t O_jmg[] = {0xE8,0x0F,0x1E,0xFC,0xFE,0x07,0x00,0xF9}; // tw.jmg prologue @0x287478C
112
113static const Patch patches[] = {
114 // Ad Skip v56 (always on) - czj=IsRewardedAdReady, czk=IsInterstitialReady (AdvertisementController)
115 {0x27C07A8, 8, O_ctz, MR_RET, "czj IsRewardedAdReady v56", false},
116 {0x27C0830, 8, O_ctz, MR_RET, "czk IsInterstitialReady v56", false},
117 // Store Bypass native v56 (always on) - fh.dyd/fi.dye HTTP 200 check
118 {0x2A7E234, 4, O_store1, NOP4, "Store bypass #1 fh.dyd v56", false},
119 {0x2A7E6C8, 4, O_store2, NOP4, "Store bypass #2 fi.dye v56", false},
120 // fi.dye entry flag guard: tbz w21,#0 (bit0) -> force SUCCESS path (v56)
121 {0x2A7E688, 4, O_tbz_fc, NOP4, "fi.dye entry tbz bit0 v56", false},
122 // ACTk Disable v56 (always on)
123 {0x27BC304, 4, O_actk_bl, MOV_W0_0, "ACTk Int.set v56", false},
124 {0x27BC3F8, 4, O_actk_cs, MOV_W0_1, "ACTk Int.get v56", false},
125 {0x27BC744, 4, O_actk_bl2, MOV_X0_0, "ACTk Long.set v56", false},
126 {0x27BC848, 4, O_actk_cs, MOV_W0_1, "ACTk Long.get v56", false},
127 {0x27BBE84, 4, O_actk_bl3, MOV_W0_0, "ACTk Float.set v56", false},
128 {0x27BBF88, 4, O_actk_cs, MOV_W0_1, "ACTk Float.get v56", false},
129 // Minigame Loot - Destroy 0% wv.lhu (toggleable)
130 {0x28E9198, 4, O_mg, FMOV_S0, "Minigame Destroy 0% wv.lhu v56", true},
131 // Divine Forge tw.jml + Overdrive wv.lhy (toggleable)
132 {0x2870270, 8, O_forge, M9_RET, "Forge 100% Divine tw.jml v56", true},
133 {0x28E7E28, 8, O_od, M9_RET, "Overdrive 100% Divine wv.lhy v56", true},
134 // Forge Exp Substats (toggleable) - NOP gate Exp in tw.jme + jmg max 2
135 {0x28701C8, 4, O_jme_gate, NOP4, "Forge Exp gate tw.jme v56", true},
136 {0x287078C, 8, O_jmg, M2_RET, "Forge Exp max tw.jmg v56", true},
137 // ACTk sync-restore disable v56 (b.eq +0xE0 -> b +0xE0 at entry+0x88, keep field value)
138 {0x27BE3DC, 4, O_beq_38, B_SYNC_38, "ACTk sync #1 ct.cwz", false},
139 {0x27BEA00, 4, O_beq_38, B_SYNC_38, "ACTk sync #2 ct.cxf", false},
140 {0x2A81C50, 4, O_beq_38, B_SYNC_38, "ACTk sync #3 User.eyt", false},
141 {0x2B72BA0, 4, O_beq_38, B_SYNC_38, "ACTk sync #4 CapeModel.fph", false},
142 {0x2810CDC, 4, O_beq_38, B_SYNC_38, "ACTk sync #5 CompanionModel.htw", false},
143 {0x286FA64, 4, O_beq_38, B_SYNC_38, "ACTk sync #6 EquippedItemModel.jly", false},
144 {0x28BA124, 4, O_beq_38, B_SYNC_38, "ACTk sync #7 MasteryUpgradeModel.khg", false},
145 {0x28C75F0, 4, O_beq_38, B_SYNC_38, "ACTk sync #8 MiningData.kps", false},
146 {0x297E428, 4, O_beq_38, B_SYNC_38, "ACTk sync #9 WingModel.oad", false},
147};
148static const int num_patches = sizeof(patches) / sizeof(patches[0]);
149
150// ==================== Helpers ====================
151
152static long page_size() { return sysconf(_SC_PAGESIZE); }
153
154static int get_page_prot(uintptr_t addr) {
155 long ps = page_size();
156 uintptr_t pg = addr & ~(ps - 1);
157 FILE *f = fopen("/proc/self/maps", "r");
158 if (!f) return PROT_READ;
159 char line[512];
160 int prot = 0;
161 while (fgets(line, sizeof(line), f)) {
162 unsigned long start, end;
163 char perms[5];
164 if (sscanf(line, "%lx-%lx %4s", &start, &end, perms) == 3) {
165 if (pg >= start && pg < end) {
166 if (perms[0] == 'r') prot |= PROT_READ;
167 if (perms[1] == 'w') prot |= PROT_WRITE;
168 if (perms[2] == 'x') prot |= PROT_EXEC;
169 break;
170 }
171 }
172 }
173 fclose(f);
174 return prot ? prot : PROT_READ;
175}
176
177static int set_prot(uintptr_t addr, size_t len, int prot) {
178 long ps = page_size();
179 uintptr_t pg = addr & ~(ps - 1);
180 size_t full = (addr + len + ps - 1) & ~(ps - 1);
181 return mprotect((void *)pg, full - pg, prot);
182}
183
184static bool proc_mem_write(uintptr_t addr, const void *data, size_t len) {
185 // Fast path: direct mprotect + memcpy for own process (atomic, no su, no race)
186 long ps = page_size();
187 uintptr_t pg = addr & ~(ps - 1);
188 size_t full = (addr + len + ps - 1) & ~(ps - 1);
189 size_t sz = full - pg;
190 int oldProt = get_page_prot(addr);
191 if (set_prot(addr, len, PROT_READ | PROT_WRITE | PROT_EXEC) == 0) {
192 memcpy((void*)addr, data, len);
193 __builtin___clear_cache((char*)addr, (char*)addr + len);
194 int restore = PROT_READ | PROT_EXEC;
195 if (oldProt & PROT_WRITE) restore |= PROT_WRITE;
196 mprotect((void*)pg, sz, restore);
197 if (memcmp((void*)addr, data, len) == 0) {
198 LOGI("proc_mem_write: direct OK addr=0x%lx len=%zu", (unsigned long)addr, len);
199 return true;
200 }
201 LOGI("proc_mem_write: direct verify fail, falling back to su dd");
202 } else {
203 LOGI("proc_mem_write: direct mprotect failed, falling back to su dd");
204 }
205 // Fallback to su dd (original logic) for cases where direct fails (SELinux)
206 static int s_counter = 0;
207 char tmpfile[256];
208 snprintf(tmpfile, sizeof(tmpfile), "/data/data/com.lavalabs.dungeonrush/files/drmod_pw_%d", s_counter++);
209 int tf = open(tmpfile, O_CREAT | O_WRONLY | O_TRUNC, 0600);
210 if (tf < 0) {
211 LOGE("proc_mem_write: open tmp failed errno=%d (%s) path=%s", errno, strerror(errno), tmpfile);
212 return false;
213 }
214 ssize_t w = write(tf, data, len);
215 close(tf);
216 if (w < 0 || (size_t)w != len) {
217 LOGE("proc_mem_write: write to tmp failed errno=%d (%s) path=%s", errno, strerror(errno), tmpfile);
218 unlink(tmpfile);
219 return false;
220 }
221 int pid = getpid();
222 char cmd[512];
223 snprintf(cmd, sizeof(cmd), "su -c 'dd if=%s of=/proc/%d/mem bs=1 seek=0x%lx count=%zu conv=notrunc 2>/dev/null; rc=$?; rm -f %s; exit $rc'",
224 tmpfile, pid, (unsigned long)addr, len, tmpfile);
225 LOGI("proc_mem_write: su dd addr=0x%lx len=%zu pid=%d tmp=%s", (unsigned long)addr, len, pid, tmpfile);
226 int rc = system(cmd);
227 if (rc != 0) {
228 LOGE("proc_mem_write: su dd FAILED rc=%d addr=0x%lx tmp=%s", rc, (unsigned long)addr, tmpfile);
229 return false;
230 }
231 LOGI("proc_mem_write: su dd OK addr=0x%lx len=%zu", (unsigned long)addr, len);
232 return true;
233}
234
235static bool patch_bytes(uintptr_t base, const Patch &p) {
236 uintptr_t a = base + p.off;
237 if (p.orig && memcmp((void *)a, p.orig, p.len) != 0) {
238 LOGE("VERIFY FAIL (pre-write): %s @0x%X", p.name, p.off);
239 return false;
240 }
241 if (!proc_mem_write(a, p.repl, p.len)) {
242 LOGE("proc_mem_write FAIL: %s @0x%X", p.name, p.off);
243 return false;
244 }
245 LOGI(" proc_mem_write OK: %s @0x%X len=%u", p.name, p.off, p.len);
246 __builtin___clear_cache((char *)a, (char *)a + p.len);
247 LOGI(" cache clear OK: %s", p.name);
248 if (memcmp((void *)a, p.repl, p.len) != 0) {
249 LOGE("VERIFY FAIL (post-write): %s @0x%X", p.name, p.off);
250 return false;
251 }
252 LOGI("OK: %s @0x%X", p.name, p.off);
253 return true;
254}
255
256static bool unpatch_bytes(uintptr_t base, const Patch &p) {
257 if (!p.toggleable || !p.orig) return true;
258 uintptr_t a = base + p.off;
259 LOGI("unpatch: %s @0x%X len=%u", p.name, p.off, p.len);
260 if (!proc_mem_write(a, p.orig, p.len)) {
261 LOGE("unpatch proc_mem_write FAIL: %s @0x%X", p.name, p.off);
262 return false;
263 }
264 __builtin___clear_cache((char *)a, (char *)a + p.len);
265 if (memcmp((void *)a, p.orig, p.len) != 0) {
266 LOGE("REVERT VERIFY FAIL: %s @0x%X", p.name, p.off);
267 return false;
268 }
269 LOGI("REVERTED: %s @0x%X", p.name, p.off);
270 return true;
271}
272
273// ==================== Mod Menu ====================
274
275static jclass g_overlay_cls = nullptr;
276static jobject g_app_context = nullptr;
277static jmethodID g_native_toggle_mid = nullptr;
278static jobject g_dex_loader = nullptr;
279
280static int find_patch(const char *substr) {
281 for (int i = 0; i < num_patches; i++)
282 if (strstr(patches[i].name, substr)) return i;
283 return -1;
284}
285
286static int fetch_patch(int idx, const char *substr, const char *what) {
287 if (idx >= 0) return idx;
288 idx = find_patch(substr);
289 LOGE("toggle: patch '%s' hardcoded idx invalid, fallback lookup=%d", what, idx);
290 return idx;
291}
292
293static void native_toggle_cb(JNIEnv *env, jclass, jint feature, jboolean enabled) {
294 bool on = enabled;
295 LOGI("Toggle feature=%d on=%d", feature, on);
296 switch (feature) {
297 case 0: g_immortality = on; break;
298 case 1: g_forced_crits = on; break;
299 case 2: { // Divine Forge - tn.jky 100% Divine
300 g_divine_forge = on;
301 static int idx = -1;
302 idx = fetch_patch(idx, "Forge 100% Divine", "forge");
303 if (idx < 0) { LOGE("toggle forge: patch not found"); break; }
304 if (g_il2cpp_base) {
305 const Patch &p = patches[idx];
306 if (on) { bool r = patch_bytes(g_il2cpp_base, p); LOGI("toggle forge patch result=%d", r); }
307 else { bool r = unpatch_bytes(g_il2cpp_base, p); LOGI("toggle forge unpatch result=%d", r); }
308 } else { LOGI("toggle forge: g_il2cpp_base=0, deferred"); }
309 break;
310 }
311 case 3: { // Divine Overdrive - wn.lgl 100% Divine
312 g_overdrive_divine = on;
313 static int idx = -1;
314 idx = fetch_patch(idx, "Overdrive 100% Divine", "overdrive");
315 if (idx < 0) { LOGE("toggle overdrive: patch not found"); break; }
316 if (g_il2cpp_base) {
317 const Patch &p = patches[idx];
318 if (on) { bool r = patch_bytes(g_il2cpp_base, p); LOGI("toggle overdrive patch result=%d", r); }
319 else { bool r = unpatch_bytes(g_il2cpp_base, p); LOGI("toggle overdrive unpatch result=%d", r); }
320 } else { LOGI("toggle overdrive: g_il2cpp_base=0, deferred"); }
321 break;
322 }
323 case 4: { // Minigame Loot - wn.lgh Destroy 0%
324 g_minigame_loot = on;
325 static int idx = -1;
326 idx = fetch_patch(idx, "Minigame Destroy", "minigame");
327 if (idx < 0) { LOGE("toggle minigame: patch not found"); break; }
328 if (g_il2cpp_base) {
329 const Patch &p = patches[idx];
330 if (on) { bool r = patch_bytes(g_il2cpp_base, p); LOGI("toggle minigame patch result=%d", r); }
331 else { bool r = unpatch_bytes(g_il2cpp_base, p); LOGI("toggle minigame unpatch result=%d", r); }
332 } else { LOGI("toggle minigame: g_il2cpp_base=0, deferred"); }
333 break;
334 }
335 case 5: { // Forge Exp Substats - gate+max in tw (separate from Divine Forge)
336 g_forge_exp = on;
337 static int idx_g = -1, idx_m = -1;
338 idx_g = fetch_patch(idx_g, "Forge Exp gate", "forge_exp_gate");
339 idx_m = fetch_patch(idx_m, "Forge Exp max", "forge_exp_max");
340 if (idx_g < 0 || idx_m < 0) { LOGE("toggle forge_exp: patch not found"); break; }
341 if (g_il2cpp_base) {
342 const Patch &pg = patches[idx_g];
343 const Patch &pm = patches[idx_m];
344 bool r1 = false, r2 = false;
345 if (on) { r1 = patch_bytes(g_il2cpp_base, pg); r2 = patch_bytes(g_il2cpp_base, pm); }
346 else { r1 = unpatch_bytes(g_il2cpp_base, pg); r2 = unpatch_bytes(g_il2cpp_base, pm); }
347 LOGI("toggle forge_exp patch result=%d/%d", r1, r2);
348 } else { LOGI("toggle forge_exp: g_il2cpp_base=0, deferred"); }
349 break;
350 }
351 }
352}
353
354static void load_dex(JNIEnv *env) {
355 if (g_dex_loader) return;
356 LOGI("load_dex: start (dex size=%d)", overlay_dex_size);
357
358 void *dex_buf = (void *)overlay_dex;
359 jint dex_len = overlay_dex_size;
360
361 jclass llc = env->FindClass("java/lang/ClassLoader");
362 if (!llc) { env->ExceptionClear(); LOGE("load_dex: ClassLoader not found"); return; }
363 jmethodID llc_getDef = env->GetStaticMethodID(llc, "getSystemClassLoader", "()Ljava/lang/ClassLoader;");
364 jobject base_loader = env->CallStaticObjectMethod(llc, llc_getDef);
365 if (!base_loader) { env->ExceptionClear(); LOGE("load_dex: getSystemClassLoader failed"); return; }
366 LOGI("load_dex: system classloader obtained");
367
368 jclass idclc = env->FindClass("dalvik/system/InMemoryDexClassLoader");
369 if (!idclc) { env->ExceptionClear(); LOGE("load_dex: InMemoryDexClassLoader not found"); return; }
370 jmethodID idclc_init = env->GetMethodID(idclc, "<init>",
371 "(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V");
372 jobject buf = env->NewDirectByteBuffer(dex_buf, dex_len);
373 if (!buf) { env->ExceptionClear(); LOGE("load_dex: NewDirectByteBuffer failed"); return; }
374 jobject loader = env->NewObject(idclc, idclc_init, buf, base_loader);
375 if (env->ExceptionCheck()) { env->ExceptionClear(); LOGE("DEX load failed"); return; }
376 LOGI("load_dex: InMemoryDexClassLoader created");
377
378 g_dex_loader = env->NewGlobalRef(loader);
379 env->DeleteLocalRef(loader);
380 env->DeleteLocalRef(buf);
381 env->DeleteLocalRef(base_loader);
382 LOGI("DEX loaded");
383}
384
385static jclass load_class(JNIEnv *env, const char *name) {
386 if (!g_dex_loader) { LOGE("load_class: no dex loader for %s", name); return nullptr; }
387 jclass llc = env->FindClass("java/lang/ClassLoader");
388 if (!llc) { env->ExceptionClear(); LOGE("load_class: ClassLoader not found for %s", name); return nullptr; }
389 jmethodID loadCls = env->GetMethodID(llc, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
390 if (!loadCls) { env->ExceptionClear(); LOGE("load_class: loadClass method not found for %s", name); env->DeleteLocalRef(llc); return nullptr; }
391 jstring jname = env->NewStringUTF(name);
392 if (!jname) { env->ExceptionClear(); LOGE("load_class: NewStringUTF failed for %s", name); env->DeleteLocalRef(llc); return nullptr; }
393 jclass cls = (jclass)env->CallObjectMethod(g_dex_loader, loadCls, jname);
394 bool ok = (cls != nullptr);
395 if (env->ExceptionCheck()) { env->ExceptionClear(); ok = false; }
396 env->DeleteLocalRef(jname);
397 env->DeleteLocalRef(llc);
398 if (!ok) { LOGE("load_class: %s NOT found", name); return nullptr; }
399 LOGI("load_class: %s found", name);
400 return cls;
401}
402
403static void create_overlay(JNIEnv *env) {
404 if (!g_app_context) { LOGE("create_overlay: no app context"); return; }
405 LOGI("create_overlay: start");
406
407 load_dex(env);
408 if (!g_dex_loader) { LOGE("create_overlay: no dex loader"); return; }
409
410 jclass cls = load_class(env, "com/drmod/Overlay");
411 if (!cls) { LOGE("Overlay class not found"); return; }
412
413 JNINativeMethod methods[] = {
414 {(char *)"nativeToggle", (char *)"(IZ)V", (void *)native_toggle_cb}
415 };
416 if (env->RegisterNatives(cls, methods, 1) != 0) {
417 env->ExceptionClear();
418 LOGE("create_overlay: RegisterNatives failed");
419 } else {
420 LOGI("create_overlay: nativeToggle registered");
421 }
422
423 jmethodID init_mid = env->GetStaticMethodID(cls, "init", "(Landroid/content/Context;)V");
424 if (!init_mid) { env->ExceptionClear(); LOGE("Overlay.init not found"); return; }
425 LOGI("create_overlay: Overlay.init resolved, calling...");
426
427 env->CallStaticVoidMethod(cls, init_mid, g_app_context);
428 if (env->ExceptionCheck()) {
429 env->ExceptionDescribe();
430 env->ExceptionClear();
431 LOGE("Overlay.init failed");
432 } else {
433 LOGI("Overlay created");
434 }
435 env->DeleteLocalRef(cls);
436}
437
438// ==================== Immortality + Forced Crits ====================
439
440#define VTABLE_BASE 0x150 // v54: GridCombatController 0x150 (v51: 0x138)
441#define SLOT_SZ 16
442#define FOP_SLOT 21
443#define FOP_OFF (VTABLE_BASE + FOP_SLOT * SLOT_SZ)
444#define OFF_ISMAIN 0x30C // v54: Soldier.IsMainCharacter 0x30C (v51: 0x2E4)
445#define OFF_CRITDMG 0xC8 // unchanged (Character.CriticalDamage 0xC8)
446
447typedef void (*fop_fn)(void *self, void *attacker, double dmg, int critTier, bool flag);
448static fop_fn g_orig_fop = nullptr;
449static void *g_main = nullptr;
450
451static inline bool is_pointer_valid(void *p) {
452 if (!p) return false;
453 uintptr_t v = (uintptr_t)p;
454 if (v < 0x10000) return false;
455 // Scudo poison
456 if (v == 0x0101010101010000ULL || v == 0x0202020202020000ULL || v == 0x0303030303030000ULL) return false;
457 if ((v & 0x00FFFFFFFFFFFFFFULL) == 0x001010101010000ULL) return false;
458 // Verify page is mapped via msync (lightweight, no fopen)
459 uintptr_t page = v & ~0xFFFULL;
460 if (msync((void*)page, 4096, MS_ASYNC) != 0) {
461 if (errno == ENOMEM) return false;
462 }
463 return true;
464}
465
466static int g_fop_call_count = 0;
467static void my_fop(void *self, void *attacker, double dmg, int critTier, bool flag) {
468 // ===== PATCH 1: IMMORTALITY (Character.fwa, self must be main) =====
469 bool is_main_self = false;
470 if (is_pointer_valid(self)) {
471 is_main_self = *(bool *)((uint8_t *)self + OFF_ISMAIN);
472 if (is_main_self) {
473 g_main = self; // cache only valid main
474 if (g_fop_call_count < 5)
475 LOGI("FOP_IMM: MAIN hit (immortality=%d, self=%p attacker=%p dmg=%.1f tier=%d flag=%d)",
476 g_immortality, self, attacker, dmg, critTier, flag);
477 g_fop_call_count++;
478 if (g_immortality) {
479 if (g_fop_call_count < 10)
480 LOGI("FOP_IMM: ABSORBED — immortality ON, skipping original (self=%p dmg=%.1f)", self, dmg);
481 return;
482 }
483 }
484 } else {
485 if (g_fop_call_count < 20) LOGI("FOP_IMM: skip invalid self=%p", self);
486 }
487 // ===== PATCH 2: FORCED CRIT (v4.80: fwa chiamato su self=RICEVENTE, attacker=INFLIGGITORE) =====
488 // Firma reale: Character.fwa(Character a, double b, CritTier c, bool d) -> viene chiamata
489 // SULL'OBIETTIVO (this=self subisce il danno) con a=attacker (chi infligge). v4.78/4.79
490 // applicava il crit quando self era il main -> amplificava il danno SUBITO dal player =
491 // i crit li facevano gli AVVERSARI al giocatore (patch invertita). FIX: il crit scatta
492 // quando l'INFLIGGITORE (attacker) e' il main; CriticalDamage va letta dall'attacker.
493 if (g_forced_crits && is_pointer_valid(attacker)) {
494 bool attacker_is_main = *(bool *)((uint8_t *)attacker + OFF_ISMAIN);
495 if (attacker_is_main && self != attacker) {
496 float cd = *(float *)((uint8_t *)attacker + OFF_CRITDMG);
497 if (cd > 0.5f && cd < 100.0f) {
498 double dmg_n = dmg * (double)cd;
499 if (g_fop_call_count < 15)
500 LOGI("FOP_CRIT: FORCED CRIT (dmg=%.1f -> %.1f, cd=%.1f, self=%p attacker=main=%p)",
501 dmg, dmg_n, cd, self, attacker);
502 dmg = dmg_n;
503 critTier = 2;
504 } else {
505 if (g_fop_call_count < 15)
506 LOGI("FOP_CRIT: crit req but CriticalDamage invalid attacker=%p cd=%.1f", attacker, cd);
507 }
508 }
509 }
510 // Fallback to original - ensure g_orig_fop is valid
511 if (g_orig_fop) g_orig_fop(self, attacker, dmg, critTier, flag);
512}
513
514// ==================== User.eyu hook (dungeon keys A/B/C) ====================
515// eyu(ref ObscuredInt a, ref int b, int c) = User private static PRIMITIVE SETTER @RVA 0x2A85D40.
516// OGNI property int di User passa da qui (x0=hidden this+hidden_off, x1=field this+field_off,
517// w2=value). L'originale scrive il campo in PLAINTEXT: `str w19,[x20]` con x20=x1 => *field = value.
518// Quindi: field_off = b - g_user_base; old = *(int*)b; e passiamo all'originale un valore modificato.
519static void *inline_hooker(void *target, void *hooker_fn); // forward decl (defined later)
520// g_code_base DEVE essere definito prima di ogni uso (install_user_eyu_probe lo usa a RVA).
521// Convenzione: addr_assoluto = g_code_base + RVA (impostata in install_store_hook come
522// g_proc_purchase_entry - 0x28ACD34). Tutti i probe devono usare g_code_base, NON g_il2cpp_base.
523static uintptr_t g_code_base = 0;
524typedef void (*user_eyu_fn)(void *a, void *b, int c);
525static user_eyu_fn g_orig_user_eyu = nullptr;
526static volatile int g_user_eyu_count = 0;
527#define USER_EYU_MAX_LOG 8000
528
529// Campi dungeon keys (dump.cs User, VERIFICATI):
530// DragonBoss: Daily 0x140, Bonus 0x144, LastReset 0x148, Watch 0x150
531// ZombieHorde: Daily 0x1C0, Bonus 0x1C4, LastReset 0x1C8, Watch 0x1CC
532// Cultist: Daily 0x1D8, Bonus 0x1DC, LastReset 0x1E0, Watch 0x1E4
533// Exceptional: Daily 0x1F0, Bonus 0x1F4, LastReset 0x1F8, Watch 0x1FC
534struct DungeonFields { uint32_t daily, bonus, reset, watch; };
535static const DungeonFields DUNGEON_FIELDS[4] = {
536 {0x140, 0x144, 0x148, 0x150},
537 {0x1C0, 0x1C4, 0x1C8, 0x1CC},
538 {0x1D8, 0x1DC, 0x1E0, 0x1E4},
539 {0x1F0, 0x1F4, 0x1F8, 0x1FC},
540};
541// g_user_base appreso dal flush hook della store chain: u = *(x0+0x28) (st_chain_entry_flush).
542static uintptr_t g_user_base = 0;
543#define KEY_BONUS_MULT 5 // B: ogni rewarded ad concede KEY_BONUS_MULT chiavi bonus
544#define KEY_RESET_WINDOW_MS 6000 // A: finestra reset aperta dal write di Last*KeyResetTime
545static volatile int64_t g_reset_win_until[4] = {0,0,0,0}; // per-dungeon reset window (ms monotonic)
546static volatile int64_t g_last_ad_reward_ms = 0; // B: settata nei case 5/6 fake ads (reward event)
547
548static int64_t eyu_now_ms(void) {
549 struct timespec ts;
550 if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) return 0;
551 return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
552}
553
554static void probe_user_eyu(void *a, void *b, int c) {
555 int n = g_user_eyu_count;
556 uintptr_t f = (uintptr_t)b;
557 uintptr_t caller = (uintptr_t)__builtin_return_address(0);
558 int64_t now = eyu_now_ms();
559
560 if (!g_user_base) {
561 if (n < 200) LOGI("USER_EYU[%d]: base NON appresa, field=0x%p val=%d caller=0x%p (flush deve arrivare)",
562 n, b, c, (void*)caller);
563 g_user_eyu_count = n + 1;
564 if (g_orig_user_eyu) g_orig_user_eyu(a, b, c);
565 return;
566 }
567
568 uintptr_t off = (f >= g_user_base) ? (f - g_user_base) : 0;
569 int dk = -1, kind = -1; // kind: 0=daily 1=bonus 2=reset 3=watch
570 for (int i = 0; i < 4; i++) {
571 if (off == DUNGEON_FIELDS[i].daily) { dk = i; kind = 0; break; }
572 if (off == DUNGEON_FIELDS[i].bonus) { dk = i; kind = 1; break; }
573 if (off == DUNGEON_FIELDS[i].reset) { dk = i; kind = 2; break; }
574 if (off == DUNGEON_FIELDS[i].watch) { dk = i; kind = 3; break; }
575 }
576
577 if (dk < 0) {
578 if (n < 500) LOGI("USER_EYU[%d]: off=0x%zX val=%d caller=0x%p (non chiave)", n, off, c, (void*)caller);
579 g_user_eyu_count = n + 1;
580 if (g_orig_user_eyu) g_orig_user_eyu(a, b, c);
581 return;
582 }
583
584 int old = *(volatile int *)f; // valore corrente PLAINTEXT nel campo
585 int c2 = c; // valore da passare all'originale (default invariato)
586
587 if (kind == 2) {
588 // write a Last*KeyResetTime -> apre la finestra reset (il reset quotidiano originale)
589 if (c > old) {
590 g_reset_win_until[dk] = now + KEY_RESET_WINDOW_MS;
591 if (n < USER_EYU_MAX_LOG) LOGI("KEYS[dk=%d reset]: LastResetTime old=%d new=%d -> reset window aperta (+%dms)", dk, old, c, KEY_RESET_WINDOW_MS);
592 } else if (n < USER_EYU_MAX_LOG) {
593 LOGI("KEYS[dk=%d reset]: LastResetTime old=%d new=%d (nessuna finestra)", dk, old, c);
594 }
595 } else if (kind == 0 || kind == 1) {
596 bool in_reset_win = now < g_reset_win_until[dk];
597 if (c < old) {
598 // DECREMENTO: vittoria (blocca) OPPURE reset daily (permette, finestra aperta)
599 if (in_reset_win) {
600 if (n < USER_EYU_MAX_LOG) LOGI("KEYS[dk=%d %s]: decremento old=%d new=%d -> RESET WINDOW: passa (reset daily originale)", dk, kind ? "bonus" : "daily", old, c);
601 } else {
602 c2 = old; // A) le chiavi non scendono mai
603 if (n < USER_EYU_MAX_LOG) LOGI("KEYS[A]: %s old=%d new=%d -> BLOCCATO, mantengo %d (vittoria non consuma)", kind ? "bonus" : "daily", old, c, old);
604 }
605 } else if (c > old && kind == 1) {
606 // INCREMENTO bonus: B) se entro la finestra ad-reward, moltiplica l'incremento
607 if (g_last_ad_reward_ms && (now - g_last_ad_reward_ms) < 5000) {
608 int inc = c - old;
609 c2 = old + inc * KEY_BONUS_MULT;
610 if (n < USER_EYU_MAX_LOG) LOGI("KEYS[B]: bonus old=%d new=%d inc=%d -> x%d = %d (ad reward)", old, c, inc, KEY_BONUS_MULT, c2);
611 } else if (n < USER_EYU_MAX_LOG) {
612 LOGI("KEYS[dk=%d bonus]: incremento old=%d new=%d (no ad window, passo)", dk, old, c);
613 }
614 } else if (n < USER_EYU_MAX_LOG) {
615 LOGI("KEYS[dk=%d %s]: write old=%d new=%d (passo)", dk, kind ? "bonus" : "daily", old, c);
616 }
617 } else if (kind == 3) {
618 // WATCH COUNT: C) ads illimitate -> mantieni SEMPRE 0
619 if (c != 0) {
620 c2 = 0;
621 if (n < USER_EYU_MAX_LOG) LOGI("KEYS[C]: watch old=%d new=%d -> FORZO 0 (ads illimitate)", old, c);
622 } else if (n < USER_EYU_MAX_LOG) {
623 LOGI("KEYS[dk=%d watch]: old=0 (passo)", dk);
624 }
625 }
626
627 g_user_eyu_count = n + 1;
628 if (g_orig_user_eyu) g_orig_user_eyu(a, b, c2);
629}
630
631static void install_user_eyu_probe(void) {
632 if (!g_code_base) { LOGE("install_user_eyu_probe: no g_code_base (store hook fallito)"); return; }
633 uintptr_t eyu_rva = 0x2A85D40; // User.eyu RVA (dump.cs: RVA 0x2A85D40 / Offset 0x2A81D40)
634 void *target = (void *)(g_code_base + eyu_rva);
635 LOGI("install_user_eyu_probe: hooking User.eyu @%p (g_code_base+0x%X)", target, (unsigned int)eyu_rva);
636 void *backup = inline_hooker(target, (void *)probe_user_eyu);
637 if (!backup) { LOGE("install_user_eyu_probe: inline_hooker failed for eyu"); return; }
638 g_orig_user_eyu = (user_eyu_fn)backup;
639 LOGI("install_user_eyu_probe: OK, backup trampoline=%p (A/B/C attivi)", backup);
640}
641
642
643static bool find_lib_path(const char *lib_name, char *out, size_t out_sz) {
644 LOGI("find_lib_path: searching for %s", lib_name);
645 FILE *f = fopen("/proc/self/maps", "r");
646 if (!f) { LOGE("find_lib_path: fopen failed errno=%d (%s)", errno, strerror(errno)); return false; }
647 char line[512];
648 while (fgets(line, sizeof(line), f)) {
649 if (!strstr(line, lib_name)) continue;
650 char *p = line;
651 int spaces = 0;
652 while (*p && spaces < 5) { if (*p == ' ') spaces++; p++; }
653 while (*p == ' ') p++; // skip alignment padding between columns
654 char *nl = strchr(p, '\n');
655 if (nl) *nl = '\0';
656 size_t len = strlen(p);
657 if (len > 0 && len < out_sz) {
658 memcpy(out, p, len + 1);
659 LOGI("find_lib_path: found %s -> %s", lib_name, out);
660 fclose(f);
661 return true;
662 }
663 }
664 fclose(f);
665 LOGI("find_lib_path: %s not found in maps", lib_name);
666 return false;
667}
668
669// ==================== ELF symbol lookup (bypass linker namespace) ====================
670
671typedef struct {
672 unsigned char e_ident[16];
673 uint16_t e_type;
674 uint16_t e_machine;
675 uint32_t e_version;
676 uint64_t e_entry;
677 uint64_t e_phoff;
678 uint64_t e_shoff;
679 uint32_t e_flags;
680 uint16_t e_ehsize;
681 uint16_t e_phentsize;
682 uint16_t e_phnum;
683 uint16_t e_shentsize;
684 uint16_t e_shnum;
685 uint16_t e_shstrndx;
686} Elf64_Ehdr;
687
688typedef struct {
689 uint32_t p_type;
690 uint32_t p_flags;
691 uint64_t p_offset;
692 uint64_t p_vaddr;
693 uint64_t p_paddr;
694 uint64_t p_filesz;
695 uint64_t p_memsz;
696 uint64_t p_align;
697} Elf64_Phdr;
698
699typedef struct {
700 int64_t d_tag;
701 union { uint64_t d_val; uint64_t d_ptr; } d_un;
702} Elf64_Dyn;
703
704typedef struct {
705 uint32_t st_name;
706 unsigned char st_info;
707 unsigned char st_other;
708 uint16_t st_shndx;
709 uint64_t st_value;
710 uint64_t st_size;
711} Elf64_Sym;
712
713typedef struct {
714 uint32_t sh_name;
715 uint32_t sh_type;
716 uint64_t sh_flags;
717 uint64_t sh_addr;
718 uint64_t sh_offset;
719 uint64_t sh_size;
720 uint32_t sh_link;
721 uint32_t sh_info;
722 uint64_t sh_addralign;
723 uint64_t sh_entsize;
724} Elf64_Shdr;
725
726#define PT_DYNAMIC 2
727#define DT_SYMTAB 6
728#define DT_STRTAB 5
729#define DT_STRSZ 10
730#define SHT_SYMTAB 2
731#define SHT_STRTAB 3
732#define SHT_PROGBITS 1
733#define ELF64_ST_BIND(i) ((i) >> 4)
734
735// The game's custom linker maps each LOAD segment at runtime base + file
736// offset (not base + p_vaddr). Symbols carry VMA addresses, so the runtime
737// address is base + st_value - (p_vaddr - p_offset) of the containing
738// segment. These helpers return that gap (0 if the segment is not found).
739static int64_t file_segment_gap(FILE *f, const Elf64_Ehdr *ehdr, uint64_t vma) {
740 for (int i = 0; i < ehdr->e_phnum; i++) {
741 Elf64_Phdr ph;
742 fseek(f, ehdr->e_phoff + i * ehdr->e_phentsize, SEEK_SET);
743 if (fread(&ph, 1, sizeof(ph), f) != sizeof(ph)) continue;
744 if (ph.p_type == 1 && vma >= ph.p_vaddr && vma < ph.p_vaddr + ph.p_memsz) {
745 return (int64_t)(ph.p_vaddr - ph.p_offset);
746 }
747 }
748 return 0;
749}
750
751static int64_t mem_segment_gap(uintptr_t base, uint64_t vma) {
752 auto *ehdr = (Elf64_Ehdr *)base;
753 auto *phdr = (Elf64_Phdr *)(base + ehdr->e_phoff);
754 for (int i = 0; i < ehdr->e_phnum; i++) {
755 if (phdr[i].p_type == 1 && vma >= phdr[i].p_vaddr && vma < phdr[i].p_vaddr + phdr[i].p_memsz) {
756 return (int64_t)(phdr[i].p_vaddr - phdr[i].p_offset);
757 }
758 }
759 return 0;
760}
761
762static void *elf_find_symbol_from_file(uintptr_t base, const char *name) {
763 char path[256];
764 if (!find_lib_path("libil2cpp.so", path, sizeof(path))) {
765 LOGE("Cannot find libil2cpp.so path in maps");
766 return nullptr;
767 }
768 LOGI("Reading ELF from file (direct): %s", path);
769
770 // Prima prova fopen diretto: un'app puo' normalmente leggere la propria lib in /data/app.
771 // Solo se SELinux lo blocca, copia il .so in /data/data via su (mai /data/local/tmp).
772 // Nota: /vendor/bin/su NON esiste su Magisk moderno -> usare "su" dal PATH (exit 127 = 32512).
773 FILE *f = fopen(path, "rb");
774 if (!f) {
775 LOGI("direct fopen failed errno=%d (%s) - fallback su copy", errno, strerror(errno));
776 char tmp_path[256];
777 snprintf(tmp_path, sizeof(tmp_path), "/data/data/com.lavalabs.dungeonrush/files/libil2cpp_drmod.so");
778 mkdir("/data/data/com.lavalabs.dungeonrush/files", 0755);
779 mkdir("/data/user/0/com.lavalabs.dungeonrush/files", 0755);
780 char cmd[512];
781 snprintf(cmd, sizeof(cmd), "su -c 'cp \"%s\" %s && chmod 644 %s'", path, tmp_path, tmp_path);
782 int rc = system(cmd);
783 LOGI("su copy rc=%d", rc);
784 if (rc != 0) {
785 LOGE("su copy failed");
786 return nullptr;
787 }
788 f = fopen(tmp_path, "rb");
789 if (!f) { LOGE("Cannot open %s", tmp_path); return nullptr; }
790 }
791
792 Elf64_Ehdr ehdr;
793 if (fread(&ehdr, 1, sizeof(ehdr), f) != sizeof(ehdr)) { fclose(f); return nullptr; }
794 if (ehdr.e_ident[0] != 0x7f || ehdr.e_ident[1] != 'E') {
795 LOGE("File ELF magic mismatch");
796 fclose(f);
797 return nullptr;
798 }
799
800 uint64_t load_bias = 0;
801 for (int i = 0; i < ehdr.e_phnum; i++) {
802 Elf64_Phdr ph;
803 fseek(f, ehdr.e_phoff + i * ehdr.e_phentsize, SEEK_SET);
804 if (fread(&ph, 1, sizeof(ph), f) != sizeof(ph)) continue;
805 if (ph.p_type == 1 && ph.p_offset == 0) {
806 load_bias = base - ph.p_vaddr;
807 LOGI("LOAD segment 0: vaddr=0x%lx, load_bias=0x%lx", (unsigned long)ph.p_vaddr, (unsigned long)load_bias);
808 break;
809 }
810 }
811
812 Elf64_Dyn dyn_entry;
813 uint64_t dyn_off = 0;
814 for (int i = 0; i < ehdr.e_phnum; i++) {
815 Elf64_Phdr ph;
816 fseek(f, ehdr.e_phoff + i * ehdr.e_phentsize, SEEK_SET);
817 if (fread(&ph, 1, sizeof(ph), f) != sizeof(ph)) continue;
818 if (ph.p_type == PT_DYNAMIC) { dyn_off = ph.p_offset; break; }
819 }
820 if (!dyn_off) { LOGE("File: No PT_DYNAMIC"); fclose(f); return nullptr; }
821
822 uint64_t strtab_vma = 0, symtab_vma = 0;
823 uint32_t strsz = 0;
824 for (uint64_t off = dyn_off;; off += sizeof(dyn_entry)) {
825 fseek(f, off, SEEK_SET);
826 if (fread(&dyn_entry, 1, sizeof(dyn_entry), f) != sizeof(dyn_entry)) break;
827 if (dyn_entry.d_tag == 0) break;
828 if (dyn_entry.d_tag == DT_STRTAB) strtab_vma = dyn_entry.d_un.d_ptr;
829 if (dyn_entry.d_tag == DT_SYMTAB) symtab_vma = dyn_entry.d_un.d_ptr;
830 if (dyn_entry.d_tag == DT_STRSZ) strsz = (uint32_t)dyn_entry.d_un.d_val;
831 }
832 LOGI("File: strtab_vma=0x%lx symtab_vma=0x%lx strsz=%u", (unsigned long)strtab_vma, (unsigned long)symtab_vma, strsz);
833
834 if (!strtab_vma || !symtab_vma) { LOGE("File: No dynsym/dynstr"); fclose(f); return nullptr; }
835
836 auto vma_to_file_offset = [&](uint64_t vma) -> uint64_t {
837 for (int i = 0; i < ehdr.e_phnum; i++) {
838 Elf64_Phdr ph;
839 fseek(f, ehdr.e_phoff + i * ehdr.e_phentsize, SEEK_SET);
840 if (fread(&ph, 1, sizeof(ph), f) != sizeof(ph)) continue;
841 if (ph.p_type == 1 && vma >= ph.p_vaddr && vma < ph.p_vaddr + ph.p_memsz) {
842 return (vma - ph.p_vaddr) + ph.p_offset;
843 }
844 }
845 return 0;
846 };
847
848 uint64_t strtab_file = vma_to_file_offset(strtab_vma);
849 uint64_t symtab_file = vma_to_file_offset(symtab_vma);
850 if (!strtab_file || !symtab_file) { LOGE("File: cannot convert VMA to file offset"); fclose(f); return nullptr; }
851
852 char *strtab_buf = (char *)malloc(strsz);
853 if (!strtab_buf) { fclose(f); return nullptr; }
854 fseek(f, strtab_file, SEEK_SET);
855 fread(strtab_buf, 1, strsz, f);
856
857 for (uint32_t i = 1; ; i++) {
858 Elf64_Sym sym;
859 fseek(f, symtab_file + i * sizeof(Elf64_Sym), SEEK_SET);
860 if (fread(&sym, 1, sizeof(sym), f) != sizeof(sym)) break;
861 if (sym.st_name == 0 && sym.st_value == 0 && sym.st_size == 0) break;
862 if (sym.st_name >= strsz) continue;
863 if (sym.st_value == 0) continue;
864 const char *sname = strtab_buf + sym.st_name;
865 if (strcmp(sname, name) == 0) {
866 int64_t gap = file_segment_gap(f, &ehdr, sym.st_value);
867 void *result = (void *)(load_bias + sym.st_value - gap);
868 LOGI("File: found %s at %p (sym_value=0x%lx, bias=0x%lx, gap=0x%lx)",
869 name, result, (unsigned long)sym.st_value, (unsigned long)load_bias, (unsigned long)gap);
870 free(strtab_buf);
871 fclose(f);
872 return result;
873 }
874 }
875 LOGE("File: symbol %s not found", name);
876 free(strtab_buf);
877 fclose(f);
878 return nullptr;
879}
880
881static void *elf_find_symbol(uintptr_t base, const char *name) {
882 // PRIMA dlsym: la lib e' gia' caricata nel processo e le API il2cpp sono esportate.
883 // Nessun parsing ELF, nessun su, nessun file -> immune a PairID/ASLR/permessi.
884 void *s = dlsym(RTLD_DEFAULT, name);
885 if (!s) {
886 void *h = dlopen("libil2cpp.so", RTLD_NOW | RTLD_NOLOAD);
887 if (h) s = dlsym(h, name);
888 }
889 if (s) { LOGI("dlsym: %s = %p", name, s); return s; }
890 auto *ehdr = (Elf64_Ehdr *)base;
891 if (ehdr->e_ident[0] != 0x7f || ehdr->e_ident[1] != 'E') {
892 LOGE("ELF magic mismatch at %p — trying file fallback", (void *)base);
893 return elf_find_symbol_from_file(base, name);
894 }
895 LOGI("ELF OK at %p, phoff=%lu phnum=%d", (void *)base, (unsigned long)ehdr->e_phoff, ehdr->e_phnum);
896
897 auto *phdr = (Elf64_Phdr *)(base + ehdr->e_phoff);
898 Elf64_Dyn *dyn = nullptr;
899 uint64_t dyn_vaddr = 0;
900 for (int i = 0; i < ehdr->e_phnum; i++) {
901 if (phdr[i].p_type == PT_DYNAMIC) {
902 dyn = (Elf64_Dyn *)(base + phdr[i].p_offset);
903 dyn_vaddr = phdr[i].p_vaddr;
904 break;
905 }
906 }
907 if (!dyn) { LOGE("No PT_DYNAMIC found"); return nullptr; }
908 LOGI("PT_DYNAMIC at vaddr=0x%lx", (unsigned long)dyn_vaddr);
909
910 const char *strtab = nullptr;
911 Elf64_Sym *symtab = nullptr;
912 uint32_t strsz = 0;
913 for (Elf64_Dyn *d = dyn; d->d_tag != 0; d++) {
914 if (d->d_tag == DT_STRTAB) { int64_t g = mem_segment_gap(base, d->d_un.d_ptr); strtab = (const char *)(base + d->d_un.d_ptr - g); LOGI("DT_STRTAB=0x%lx (gap=0x%lx)", (unsigned long)d->d_un.d_ptr, (unsigned long)g); }
915 if (d->d_tag == DT_SYMTAB) { int64_t g = mem_segment_gap(base, d->d_un.d_ptr); symtab = (Elf64_Sym *)(base + d->d_un.d_ptr - g); LOGI("DT_SYMTAB=0x%lx (gap=0x%lx)", (unsigned long)d->d_un.d_ptr, (unsigned long)g); }
916 if (d->d_tag == DT_STRSZ) { strsz = (uint32_t)d->d_un.d_val; LOGI("DT_STRSZ=%u", strsz); }
917 }
918 if (!strtab || !symtab) {
919 LOGE("No dynsym/dynstr (strtab=%p symtab=%p) — trying file fallback", (void*)strtab, (void*)symtab);
920 return elf_find_symbol_from_file(base, name);
921 }
922
923 uint32_t scanned = 0;
924 for (uint32_t i = 0; symtab[i].st_name < strsz; i++) {
925 scanned++;
926 if (symtab[i].st_value == 0) continue;
927 if (ELF64_ST_BIND(symtab[i].st_info) != 1) continue;
928 const char *sname = strtab + symtab[i].st_name;
929 if (strcmp(sname, name) == 0) {
930 int64_t gap = mem_segment_gap(base, symtab[i].st_value);
931 void *result = (void *)(base + symtab[i].st_value - gap);
932 LOGI("Mem: found %s at %p (sym_value=0x%lx, gap=0x%lx)",
933 name, result, (unsigned long)symtab[i].st_value, (unsigned long)gap);
934 return result;
935 }
936 }
937 LOGE("Mem: symbol %s not found (scanned %u entries)", name, scanned);
938 return nullptr;
939}
940
941// ==================== LSPlant integration ====================
942
943static bool g_lsplant_initialized = false;
944static jclass g_hooker_cls = nullptr;
945static jmethodID g_hooker_ctor = nullptr;
946static jobject g_hooker_callback_method = nullptr;
947
948static void *alloc_exec(size_t size) {
949 int fd = (int)syscall(__NR_memfd_create, "drmod", MFD_CLOEXEC);
950 if (fd < 0) { LOGE("alloc_exec: memfd_create failed errno=%d", errno); return nullptr; }
951 uint8_t zeros[32] = {};
952 for (size_t w = 0; w < size; w += sizeof(zeros)) {
953 size_t chunk = (size - w) > sizeof(zeros) ? sizeof(zeros) : (size - w);
954 if (write(fd, zeros, chunk) < 0) { close(fd); return nullptr; }
955 }
956 void *p = mmap(nullptr, size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE, fd, 0);
957 close(fd);
958 if (p == MAP_FAILED) { LOGE("alloc_exec: mmap failed errno=%d", errno); return nullptr; }
959 return p;
960}
961static void *alloc_exec_near(void *target, size_t size) {
962 // First try to allocate in the same 4KB page as target for adrp correctness (offset 0)
963 uintptr_t page = (uintptr_t)target & ~0xFFF;
964 // Try same page + 0x1000 (next page) first, as original page is occupied
965 for (uintptr_t tryAddr = page + 0x1000; tryAddr < page + 0x10000; tryAddr += 0x1000) {
966 int fd = (int)syscall(__NR_memfd_create, "drmod_near", MFD_CLOEXEC);
967 if (fd < 0) continue;
968 uint8_t zeros[32] = {};
969 bool ok = true;
970 for (size_t w = 0; w < size; w += sizeof(zeros)) {
971 size_t chunk = (size - w) > sizeof(zeros) ? sizeof(zeros) : (size - w);
972 if (write(fd, zeros, chunk) < 0) { ok = false; break; }
973 }
974 void *p = nullptr;
975 if (ok) p = mmap((void*)tryAddr, size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_FIXED_NOREPLACE, fd, 0);
976 close(fd);
977 if (ok && p != MAP_FAILED) {
978 LOGI("alloc_exec_near: allocated same-page near %p -> %p", target, p);
979 return p;
980 }
981 }
982 uintptr_t tgt = (uintptr_t)target;
983 uintptr_t hint = tgt & ~0xFFF;
984 for (uintptr_t off = 0x10000; off < 0x8000000; off += 0x10000) {
985 for (int dir = -1; dir <= 1; dir += 2) {
986 uintptr_t tryAddr = hint + dir * (intptr_t)off;
987 if (tryAddr < 0x10000) continue;
988 int fd = (int)syscall(__NR_memfd_create, "drmod_near", MFD_CLOEXEC);
989 if (fd < 0) continue;
990 bool ok = true;
991 uint8_t zeros[32] = {};
992 for (size_t w = 0; w < size; w += sizeof(zeros)) {
993 size_t chunk = (size - w) > sizeof(zeros) ? sizeof(zeros) : (size - w);
994 if (write(fd, zeros, chunk) < 0) { ok = false; break; }
995 }
996 void *p = nullptr;
997 if (ok) p = mmap((void*)tryAddr, size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_FIXED_NOREPLACE, fd, 0);
998 close(fd);
999 if (ok && p != MAP_FAILED) {
1000 LOGI("alloc_exec_near: allocated near %p -> %p (offset %ld)", target, p, (long)((intptr_t)p - (intptr_t)target));
1001 return p;
1002 }
1003 }
1004 }
1005 LOGI("alloc_exec_near: fallback to alloc_exec");
1006 return alloc_exec(size);
1007}
1008static void fix_adrp_in_trampoline(void *trampoline, void *orig) {
1009 // Fix PC-relative instructions in the 16-byte copied prologue so they work from
1010 // the backup trampoline (which can be tens of MB away from the target):
1011 // - adrp: convert to ldr-literal with correct target
1012 // - b/bl: re-encode imm26 relative to the trampoline copy (range +/-128MB)
1013 // - cbz/cbnz, b.cond, tbz/tbnz, adr, ldr-literal: re-encode if target within the
1014 // instruction range, else warn (a taken branch to a far target cannot be encoded
1015 // directly from the trampoline; dxn's prologue cbz only checks self!=NULL and is
1016 // never taken on the reward path)
1017 uint8_t *t = (uint8_t*)trampoline;
1018 uintptr_t orig_base = (uintptr_t)orig;
1019 uintptr_t tramp_base = (uintptr_t)trampoline;
1020 int literal_idx = 0;
1021 for (size_t off = 0; off < 16; off += 4) {
1022 uint32_t ins = *(uint32_t*)(t + off);
1023 uintptr_t orig_pc = orig_base + off;
1024 uintptr_t tramp_pc = tramp_base + off;
1025 // ---- b / bl (imm26, +/-128MB) ----
1026 if ((ins & 0xFC000000) == 0x94000000 || (ins & 0xFC000000) == 0x14000000) {
1027 int64_t imm = ins & 0x03FFFFFF;
1028 if (imm & (1 << 25)) imm |= ~((1LL<<26)-1);
1029 int64_t orig_target = (int64_t)orig_pc + (imm << 2);
1030 int64_t rel = orig_target - (int64_t)tramp_pc;
1031 if (rel < -(1LL<<27) || rel >= (1LL<<27)) {
1032 LOGE("fix_adrp: b/bl out of range off=%zu orig_target=%p rel=%lld", off, (void*)orig_target, (long long)rel);
1033 continue;
1034 }
1035 uint32_t imm26 = (uint32_t)((rel / 4) & 0x03FFFFFF);
1036 *(uint32_t*)(t + off) = (ins & 0xFC000000) | imm26;
1037 LOGI("fix_adrp: patched b/bl @+%zu orig_target=%p rel=%lld bytes", off, (void*)orig_target, (long long)rel);
1038 continue;
1039 }
1040 // ---- cbz / cbnz (imm19, +/-1MB) ----
1041 if ((ins & 0x7E000000) == 0x34000000) {
1042 int64_t imm = ins & 0x7FFFF;
1043 if (imm & (1 << 18)) imm |= ~((1LL<<19)-1);
1044 int64_t orig_target = (int64_t)orig_pc + (imm << 2);
1045 int64_t rel = orig_target - (int64_t)tramp_pc;
1046 if (rel < -(1LL<<20) || rel >= (1LL<<20)) {
1047 LOGE("fix_adrp: cbz/cbnz @+%zu target %p NOT relocatable (out of +/-1MB) - left as-is", off, (void*)orig_target);
1048 continue;
1049 }
1050 uint32_t imm19 = (uint32_t)((rel / 4) & 0x7FFFF);
1051 *(uint32_t*)(t + off) = (ins & ~0x00FFFFE0) | (imm19 << 5);
1052 LOGI("fix_adrp: patched cbz/cbnz @+%zu target=%p", off, (void*)orig_target);
1053 continue;
1054 }
1055 // ---- b.cond (imm19, +/-1MB) ----
1056 if ((ins & 0xFF000010) == 0x54000000) {
1057 int64_t imm = ins & 0x7FFFF;
1058 if (imm & (1 << 18)) imm |= ~((1LL<<19)-1);
1059 int64_t orig_target = (int64_t)orig_pc + (imm << 2);
1060 int64_t rel = orig_target - (int64_t)tramp_pc;
1061 if (rel < -(1LL<<20) || rel >= (1LL<<20)) {
1062 LOGE("fix_adrp: b.cond @+%zu target %p NOT relocatable (out of +/-1MB) - left as-is", off, (void*)orig_target);
1063 continue;
1064 }
1065 uint32_t imm19 = (uint32_t)((rel / 4) & 0x7FFFF);
1066 *(uint32_t*)(t + off) = (ins & 0xFF00001F) | (imm19 << 5);
1067 LOGI("fix_adrp: patched b.cond @+%zu target=%p", off, (void*)orig_target);
1068 continue;
1069 }
1070 // ---- tbz / tbnz (imm14, +/-32KB) ----
1071 if ((ins & 0x7E000000) == 0x36000000) {
1072 int64_t imm = ins & 0x3FFF;
1073 if (imm & (1 << 13)) imm |= ~((1LL<<14)-1);
1074 int64_t orig_target = (int64_t)orig_pc + (imm << 2);
1075 int64_t rel = orig_target - (int64_t)tramp_pc;
1076 if (rel < -(1LL<<15) || rel >= (1LL<<15)) {
1077 LOGE("fix_adrp: tbz/tbnz @+%zu target %p NOT relocatable (out of +/-32KB) - left as-is", off, (void*)orig_target);
1078 continue;
1079 }
1080 uint32_t imm14 = (uint32_t)((rel / 4) & 0x3FFF);
1081 *(uint32_t*)(t + off) = (ins & ~0x0007FFE0) | (imm14 << 5);
1082 LOGI("fix_adrp: patched tbz/tbnz @+%zu target=%p", off, (void*)orig_target);
1083 continue;
1084 }
1085 // ---- adr (imm21, +/-1MB) ----
1086 if ((ins & 0x9F000000) == 0x10000000) {
1087 int immlo = (ins >> 29) & 0x3;
1088 int immhi = (ins >> 5) & 0x7FFFF;
1089 int64_t imm = ((int64_t)immhi << 2) | immlo;
1090 if (imm & (1 << 20)) imm |= ~((1LL<<21)-1);
1091 int64_t orig_target = (int64_t)orig_pc + imm;
1092 int64_t rel = orig_target - (int64_t)tramp_pc;
1093 if (rel < -(1LL<<20) || rel >= (1LL<<20)) {
1094 LOGE("fix_adrp: adr @+%zu target %p NOT relocatable (out of +/-1MB) - left as-is", off, (void*)orig_target);
1095 continue;
1096 }
1097 uint32_t immlo2 = (uint32_t)(rel & 0x3);
1098 uint32_t immhi2 = (uint32_t)((rel >> 2) & 0x7FFFF);
1099 *(uint32_t*)(t + off) = (ins & 0x9F00001F) | (immlo2 << 29) | (immhi2 << 5);
1100 LOGI("fix_adrp: patched adr @+%zu target=%p", off, (void*)orig_target);
1101 continue;
1102 }
1103 // ---- adrp (page-quantised, via literal pool) ----
1104 if ((ins & 0x9F000000) == 0x90000000) {
1105 int rd = ins & 0x1F;
1106 int immlo = (ins >> 29) & 0x3;
1107 int immhi = (ins >> 5) & 0x7FFFF;
1108 int64_t imm = ((int64_t)immhi << 2) | immlo;
1109 if (imm & (1 << 20)) imm |= ~((1LL<<21)-1);
1110 uintptr_t orig_page = (orig_base + off) & ~0xFFF;
1111 uintptr_t orig_target = orig_page + (imm << 12);
1112 uintptr_t literal_addr = tramp_base + 0x20 + literal_idx * 8;
1113 int64_t dist = (int64_t)literal_addr - (int64_t)(tramp_base + off);
1114 int64_t imm19 = dist / 4;
1115 if (imm19 < -(1<<18) || imm19 >= (1<<18)) {
1116 LOGE("fix_adrp: distance out of range off=%zu dist=%lld", off, (long long)dist);
1117 continue;
1118 }
1119 uint32_t ldr = 0x58000000 | ((imm19 & 0x7FFFF) << 5) | rd;
1120 *(uint32_t*)(t + off) = ldr;
1121 *(uintptr_t*)(t + 0x20 + literal_idx * 8) = orig_target;
1122 LOGI("fix_adrp: patched adrp x%d @+%zu orig %p target %p -> ldr literal #%d dist %lld", rd, off, (void*)(orig_base+off), (void*)orig_target, literal_idx, (long long)dist);
1123 literal_idx++;
1124 continue;
1125 }
1126 // ---- ldr-literal (imm19, +/-1MB) ----
1127 if ((ins & 0x3B000000) == 0x18000000) {
1128 int64_t imm = ins & 0x7FFFF;
1129 if (imm & (1 << 18)) imm |= ~((1LL<<19)-1);
1130 int64_t orig_target = (int64_t)orig_pc + (imm << 2);
1131 int64_t rel = orig_target - (int64_t)tramp_pc;
1132 if (rel < -(1LL<<20) || rel >= (1LL<<20)) {
1133 LOGE("fix_adrp: ldr-literal @+%zu target %p NOT relocatable (out of +/-1MB) - left as-is", off, (void*)orig_target);
1134 continue;
1135 }
1136 uint32_t imm19 = (uint32_t)((rel / 4) & 0x7FFFF);
1137 *(uint32_t*)(t + off) = (ins & ~0x00FFFFE0) | (imm19 << 5);
1138 LOGI("fix_adrp: patched ldr-literal @+%zu target=%p", off, (void*)orig_target);
1139 continue;
1140 }
1141 }
1142 if (literal_idx) __builtin___clear_cache((char*)trampoline, (char*)trampoline + 64);
1143}
1144
1145struct HookRecord {
1146 void *target;
1147 uint8_t orig[16];
1148 void *backup_exec;
1149};
1150static HookRecord g_hook_records[16];
1151static int g_hook_count = 0;
1152
1153static void *inline_hooker(void *target, void *hooker_fn) {
1154 if (!target || !hooker_fn) { LOGE("inline_hooker: null target=%p hooker=%p", target, hooker_fn); return nullptr; }
1155 LOGI("inline_hooker: target=%p hooker=%p", target, hooker_fn);
1156 if (g_hook_count >= 16) { LOGE("inline_hooker: too many hooks"); return nullptr; }
1157
1158 HookRecord &rec = g_hook_records[g_hook_count];
1159 rec.target = target;
1160 memcpy(rec.orig, target, 16);
1161
1162 size_t backup_size = 64;
1163 void *backup = alloc_exec_near(target, backup_size);
1164 if (!backup) { LOGE("inline_hooker: alloc_exec_near backup failed"); return nullptr; }
1165 rec.backup_exec = backup;
1166
1167 memcpy(backup, rec.orig, 16);
1168 // Fix adrp PC-relative before adding jump back (literals at 0x20+)
1169 fix_adrp_in_trampoline(backup, target);
1170 uint64_t ret_addr = (uint64_t)target + 16;
1171 uint32_t *bak = (uint32_t *)backup;
1172 bak[4] = 0x58000050;
1173 bak[5] = 0xD61F0200;
1174 *(uint64_t *)&bak[6] = ret_addr;
1175 __builtin___clear_cache((char *)backup, (char *)backup + backup_size);
1176
1177 long ps = sysconf(_SC_PAGESIZE);
1178 uintptr_t pg = (uintptr_t)target & ~(ps - 1);
1179 if (mprotect((void *)pg, ps, PROT_READ | PROT_WRITE | PROT_EXEC) != 0) {
1180 LOGE("inline_hooker: mprotect RWX failed at %p", target);
1181 return nullptr;
1182 }
1183
1184 uint64_t hooker_addr = (uint64_t)hooker_fn;
1185 uint32_t *tgt = (uint32_t *)target;
1186 tgt[0] = 0x58000050;
1187 tgt[1] = 0xD61F0200;
1188 *(uint64_t *)&tgt[2] = hooker_addr;
1189 __builtin___clear_cache((char *)target, (char *)target + 16);
1190
1191 mprotect((void *)pg, ps, PROT_READ | PROT_EXEC);
1192 g_hook_count++;
1193 LOGI("inline_hooker: backup=%p (exec)", backup);
1194 return backup;
1195}
1196
1197static bool inline_unhooker(void *func) {
1198 for (int i = 0; i < g_hook_count; i++) {
1199 if (g_hook_records[i].backup_exec == func) {
1200 void *target = g_hook_records[i].target;
1201 long ps = sysconf(_SC_PAGESIZE);
1202 uintptr_t pg = (uintptr_t)target & ~(ps - 1);
1203 mprotect((void *)pg, ps, PROT_READ | PROT_WRITE | PROT_EXEC);
1204 memcpy(target, g_hook_records[i].orig, 16);
1205 __builtin___clear_cache((char *)target, (char *)target + 16);
1206 mprotect((void *)pg, ps, PROT_READ | PROT_EXEC);
1207 LOGI("inline_unhooker: restored %p", target);
1208 return true;
1209 }
1210 }
1211 LOGE("inline_unhooker: record not found for %p", func);
1212 return false;
1213}
1214
1215static uintptr_t g_libart_base = 0;
1216static void *g_libart_handle = nullptr;
1217
1218static uintptr_t find_libart_base() {
1219 if (g_libart_base) return g_libart_base;
1220 FILE *f = fopen("/proc/self/maps", "r");
1221 if (!f) return 0;
1222 char line[512];
1223 while (fgets(line, sizeof(line), f)) {
1224 if (!strstr(line, "libart.so")) continue;
1225 uintptr_t start = 0;
1226 unsigned long foff = 0;
1227 char perms[8] = {};
1228 if (sscanf(line, "%lx-%*x %7s %lx", &start, perms, &foff) == 3) {
1229 if (strchr(perms, 'x')) {
1230 g_libart_base = start - foff;
1231 LOGI("find_libart_base: libart.so base=%p (start=%p foff=0x%lx)", (void *)g_libart_base, (void *)start, foff);
1232 fclose(f);
1233 return g_libart_base;
1234 }
1235 }
1236 }
1237 fclose(f);
1238 return 0;
1239}
1240
1241static void *libart_resolver(const char *name) {
1242 if (!g_libart_handle) {
1243 g_libart_handle = dlopen("libart.so", RTLD_NOLOAD);
1244 LOGI("libart_resolver: dlopen libart.so = %p", g_libart_handle);
1245 }
1246 if (g_libart_handle) {
1247 void *sym = dlsym(g_libart_handle, name);
1248 if (sym) { LOGI("libart_resolver: %s = %p", name, sym); return sym; }
1249 }
1250 // GetMethodShorty is a local (.gnu_debugdata) symbol not exported in dynsym.
1251 // Runtime XZ-decompression of .gnu_debugdata via Samsung's liblzma.so crashes
1252 // (null function-pointer in CrcCalc). Instead use the hardcoded offset extracted
1253 // OFFLINE from this exact firmware (A217FXXSCDXE2 / Android 12) using 7-zip.
1254 // uint local vaddr of _ZN3artL15GetMethodShortyEP7_JNIEnvP10_jmethodID = 0x92AA88.
1255 if (strstr(name, "GetMethodShorty")) {
1256 uintptr_t b = find_libart_base();
1257 if (b) {
1258 void *r = (void *)(b + 0x92AA88);
1259 LOGI("libart_resolver: %s = %p (hardcoded offset 0x92AA88)", name, r);
1260 return r;
1261 }
1262 }
1263 // ClassLinker::Init needs RegisterNative/UnregisterNative (all variants are
1264 // local .gnu_debugdata symbols, not in dynsym). Use ClassLinker variants.
1265 // _ZN3art11ClassLinker14RegisterNativeEPNS_6ThreadEPNS_9ArtMethodEPKv = 0x383DA8
1266 if (strstr(name, "RegisterNative")) {
1267 uintptr_t b = find_libart_base();
1268 if (b) {
1269 void *r = (void *)(b + 0x383DA8);
1270 LOGI("libart_resolver: %s = %p (hardcoded offset 0x383DA8)", name, r);
1271 return r;
1272 }
1273 }
1274 // _ZN3art11ClassLinker16UnregisterNativeEPNS_6ThreadEPNS_9ArtMethodE = 0x80A334
1275 if (strstr(name, "UnregisterNative")) {
1276 uintptr_t b = find_libart_base();
1277 if (b) {
1278 void *r = (void *)(b + 0x80A334);
1279 LOGI("libart_resolver: %s = %p (hardcoded offset 0x80A334)", name, r);
1280 return r;
1281 }
1282 }
1283 // ClassLinker::Init deopt fallback when SetEntryPointsToInterpreter /
1284 // GetOptimizedCodeFor are absent. Plain (non-mangled) data symbol in .gnu_debugdata.
1285 // art_quick_to_interpreter_bridge = 0x2C2480
1286 if (strcmp(name, "art_quick_to_interpreter_bridge") == 0) {
1287 uintptr_t b = find_libart_base();
1288 if (b) {
1289 void *r = (void *)(b + 0x2C2480);
1290 LOGI("libart_resolver: %s = %p (hardcoded offset 0x2C2480)", name, r);
1291 return r;
1292 }
1293 }
1294 uintptr_t base = find_libart_base();
1295 if (!base) { LOGE("libart_resolver: no libart base"); return nullptr; }
1296 auto *ehdr = (Elf64_Ehdr *)base;
1297 if (ehdr->e_ident[0] != 0x7f || ehdr->e_ident[1] != 'E') {
1298 LOGE("libart_resolver: bad ELF magic"); return nullptr;
1299 }
1300 auto *phdr = (Elf64_Phdr *)(base + ehdr->e_phoff);
1301 const char *strtab = nullptr;
1302 Elf64_Sym *symtab = nullptr;
1303 uint32_t strsz = 0;
1304 for (int i = 0; i < ehdr->e_phnum; i++) {
1305 if (phdr[i].p_type == PT_DYNAMIC) {
1306 auto *dyn = (Elf64_Dyn *)(base + phdr[i].p_offset);
1307 for (Elf64_Dyn *d = dyn; d->d_tag != 0; d++) {
1308 if (d->d_tag == DT_STRTAB) { int64_t g = mem_segment_gap(base, d->d_un.d_ptr); strtab = (const char *)(base + d->d_un.d_ptr - g); }
1309 if (d->d_tag == DT_SYMTAB) { int64_t g = mem_segment_gap(base, d->d_un.d_ptr); symtab = (Elf64_Sym *)(base + d->d_un.d_ptr - g); }
1310 if (d->d_tag == DT_STRSZ) strsz = (uint32_t)d->d_un.d_val;
1311 }
1312 break;
1313 }
1314 }
1315 if (strtab && symtab) {
1316 for (uint32_t i = 1; symtab[i].st_name < strsz; i++) {
1317 if (symtab[i].st_value == 0) continue;
1318 const char *sname = strtab + symtab[i].st_name;
1319 if (strcmp(sname, name) == 0) {
1320 int64_t gap = mem_segment_gap(base, symtab[i].st_value);
1321 void *result = (void *)(base + symtab[i].st_value - gap);
1322 LOGI("libart_resolver: %s = %p (dynsym)", name, result);
1323 return result;
1324 }
1325 }
1326 LOGI("libart_resolver: %s not in dynsym, scanning for substring 'Shorty'...", name);
1327 void *substr_result = nullptr;
1328 for (uint32_t i = 1; symtab[i].st_name < strsz; i++) {
1329 if (symtab[i].st_value == 0) continue;
1330 const char *sname = strtab + symtab[i].st_name;
1331 if (strstr(sname, "Shorty")) {
1332 int64_t gap = mem_segment_gap(base, symtab[i].st_value);
1333 void *r = (void *)(base + symtab[i].st_value - gap);
1334 LOGI("libart_resolver: substring 'Shorty' match: %s = %p", sname, r);
1335 if (strstr(sname, "GetMethodShorty")) {
1336 substr_result = r;
1337 break;
1338 }
1339 if (!substr_result) substr_result = r;
1340 }
1341 }
1342 if (substr_result) {
1343 LOGI("libart_resolver: returning substring match %p for %s", substr_result, name);
1344 return substr_result;
1345 }
1346 }
1347 // File-based .gnu_debugdata fallback (below) is DISABLED: runtime XZ decompression
1348 // via Samsung's liblzma.so (XzUnpacker_Code -> CrcCalc) segfaults (null fn-ptr) and
1349 // kills the process for ANY symbol not found in dynsym. All REQUIRED local symbols
1350 // (GetMethodShorty, RegisterNative, UnregisterNative, art_quick_to_interpreter_bridge)
1351 // are already hardcoded above; any other unresolved symbol has a non-fatal fallback
1352 // in LSPlant, so returning nullptr here is safe and avoids the crash.
1353 LOGI("libart_resolver: %s not in dynsym and not hardcoded; returning nullptr (XZ fallback disabled)", name);
1354 return nullptr;
1355 char fpath[256];
1356 if (!find_lib_path("libart.so", fpath, sizeof(fpath))) {
1357 LOGE("libart_resolver: cannot find libart.so path"); return nullptr;
1358 }
1359 LOGI("libart_resolver: libart.so path = %s", fpath);
1360 char tmp[] = "/data/data/com.lavalabs.dungeonrush/files/libart_drmod.so";
1361 mkdir("/data/data/com.lavalabs.dungeonrush/files", 0755);
1362 mkdir("/data/user/0/com.lavalabs.dungeonrush/files", 0755);
1363 char cmd[512];
1364 snprintf(cmd, sizeof(cmd), "/vendor/bin/su -c 'cp \"%s\" %s && chmod 644 %s'", fpath, tmp, tmp);
1365 int cprc = system(cmd);
1366 LOGI("libart_resolver: su copy rc=%d", cprc);
1367 if (cprc != 0) { LOGE("libart_resolver: su copy failed rc=%d", cprc); return nullptr; }
1368 FILE *f = fopen(tmp, "rb");
1369 if (!f) { LOGE("libart_resolver: cannot open %s", tmp); return nullptr; }
1370 fseek(f, 0, SEEK_END);
1371 long fsize = ftell(f);
1372 fseek(f, 0, SEEK_SET);
1373 LOGI("libart_resolver: file size=%ld bytes", fsize);
1374 Elf64_Ehdr fehdr;
1375 if (fread(&fehdr, 1, sizeof(fehdr), f) != sizeof(fehdr) || fehdr.e_ident[0] != 0x7f) {
1376 LOGE("libart_resolver: bad file ELF"); fclose(f); return nullptr;
1377 }
1378 LOGI("libart_resolver: ELF shnum=%u shoff=0x%lx shstrndx=%u phnum=%u", fehdr.e_shnum, (unsigned long)fehdr.e_shoff, fehdr.e_shstrndx, fehdr.e_phnum);
1379 if (fehdr.e_shnum == 0 || fehdr.e_shoff == 0) {
1380 LOGE("libart_resolver: no section headers in file (shnum=%u shoff=0x%lx) — symbol might be stripped", fehdr.e_shnum, (unsigned long)fehdr.e_shoff);
1381 fclose(f);
1382 return nullptr;
1383 }
1384 uint64_t load_bias = 0;
1385 for (int i = 0; i < fehdr.e_phnum; i++) {
1386 Elf64_Phdr ph;
1387 fseek(f, fehdr.e_phoff + i * fehdr.e_phentsize, SEEK_SET);
1388 if (fread(&ph, 1, sizeof(ph), f) != sizeof(ph)) continue;
1389 if (ph.p_type == 1 && ph.p_offset == 0) {
1390 load_bias = base - ph.p_vaddr;
1391 break;
1392 }
1393 }
1394 LOGI("libart_resolver: load_bias=0x%lx", (unsigned long)load_bias);
1395 Elf64_Shdr shstrtab_hdr;
1396 fseek(f, fehdr.e_shoff + fehdr.e_shstrndx * fehdr.e_shentsize, SEEK_SET);
1397 if (fread(&shstrtab_hdr, 1, sizeof(shstrtab_hdr), f) != sizeof(shstrtab_hdr)) {
1398 LOGE("libart_resolver: failed to read shstrtab header"); fclose(f); return nullptr;
1399 }
1400 char *shstrtab = (char *)malloc(shstrtab_hdr.sh_size);
1401 if (!shstrtab) { fclose(f); return nullptr; }
1402 fseek(f, shstrtab_hdr.sh_offset, SEEK_SET);
1403 fread(shstrtab, 1, shstrtab_hdr.sh_size, f);
1404
1405 // === Phase 1: scan section headers for .gnu_debugdata (compressed mini symtab) ===
1406 void *gnu_debugdata_result = nullptr;
1407 for (int i = 0; i < fehdr.e_shnum; i++) {
1408 Elf64_Shdr shdr;
1409 fseek(f, fehdr.e_shoff + i * fehdr.e_shentsize, SEEK_SET);
1410 if (fread(&shdr, 1, sizeof(shdr), f) != sizeof(shdr)) continue;
1411 const char *secname = (shdr.sh_name < shstrtab_hdr.sh_size) ? shstrtab + shdr.sh_name : "";
1412 LOGI("libart_resolver: section[%u] type=%u name='%s' offset=0x%lx size=0x%lx",
1413 i, shdr.sh_type, secname, (unsigned long)shdr.sh_offset, (unsigned long)shdr.sh_size);
1414 if (shdr.sh_type == SHT_PROGBITS && strcmp(secname, ".gnu_debugdata") == 0) {
1415 LOGI("libart_resolver: .gnu_debugdata found! offset=0x%lx size=0x%lx",
1416 (unsigned long)shdr.sh_offset, (unsigned long)shdr.sh_size);
1417 uint8_t *secdata = (uint8_t *)malloc(shdr.sh_size);
1418 if (!secdata) continue;
1419 fseek(f, shdr.sh_offset, SEEK_SET);
1420 if (fread(secdata, 1, shdr.sh_size, f) == shdr.sh_size) {
1421 LOGI("libart_resolver: .gnu_debugdata first bytes: %02x %02x %02x %02x %02x %02x %02x %02x",
1422 secdata[0], secdata[1], secdata[2], secdata[3], secdata[4], secdata[5], secdata[6], secdata[7]);
1423 // Try LZ4 decompress (0x04 0x22 0x4D 0x18 = LZ4 frame magic)
1424 if (secdata[0] == 0x04 && secdata[1] == 0x22 && secdata[2] == 0x4D && secdata[3] == 0x18) {
1425 LOGI("libart_resolver: .gnu_debugdata is LZ4 compressed (frame magic detected)");
1426 }
1427 // XZ decompress (FD 37 7A 58 5A 00) via liblzma.so (7-zip SDK XzUnpacker)
1428 else if (secdata[0] == 0xFD && secdata[1] == 0x37 && secdata[2] == 0x7A && secdata[3] == 0x58) {
1429 LOGI("libart_resolver: .gnu_debugdata is XZ compressed, decompressing via XzUnpacker_Code...");
1430 typedef unsigned char LzByte;
1431 typedef size_t LzSizeT;
1432 typedef int LzRes;
1433 typedef int LzmaStatus;
1434 static void *(*p_XzUnpacker_Create)(void *(*)(size_t), void (*)(void*)) = nullptr;
1435 static void (*p_XzUnpacker_Free)(void*, void (*)(void*)) = nullptr;
1436 static LzRes (*p_XzUnpacker_Code)(void*, LzByte*, LzSizeT*, const LzByte*, LzSizeT*, int, int, LzmaStatus*) = nullptr;
1437 static void (*p_XzUnpacker_Init)(void*) = nullptr;
1438 static void *liblzma = nullptr;
1439 static const char *liblzma_paths[] = {
1440 "/system/lib64/liblzma.so",
1441 "/apex/com.android.art/lib64/liblzma.so",
1442 nullptr
1443 };
1444 if (!liblzma) {
1445 for (int li = 0; liblzma_paths[li]; li++) {
1446 liblzma = dlopen(liblzma_paths[li], RTLD_NOW);
1447 LOGI("libart_resolver: dlopen %s = %p", liblzma_paths[li], liblzma);
1448 if (liblzma) {
1449 p_XzUnpacker_Create = (decltype(p_XzUnpacker_Create))dlsym(liblzma, "XzUnpacker_Create");
1450 p_XzUnpacker_Free = (decltype(p_XzUnpacker_Free))dlsym(liblzma, "XzUnpacker_Free");
1451 p_XzUnpacker_Code = (decltype(p_XzUnpacker_Code))dlsym(liblzma, "XzUnpacker_Code");
1452 p_XzUnpacker_Init = (decltype(p_XzUnpacker_Init))dlsym(liblzma, "XzUnpacker_Init");
1453 LOGI("libart_resolver: XzUnpacker Create=%p Free=%p Code=%p Init=%p",
1454 (void*)p_XzUnpacker_Create, (void*)p_XzUnpacker_Free, (void*)p_XzUnpacker_Code, (void*)p_XzUnpacker_Init);
1455 if (p_XzUnpacker_Free && p_XzUnpacker_Code) break;
1456 dlclose(liblzma);
1457 liblzma = nullptr;
1458 p_XzUnpacker_Create = nullptr;
1459 p_XzUnpacker_Free = nullptr;
1460 p_XzUnpacker_Code = nullptr;
1461 p_XzUnpacker_Init = nullptr;
1462 }
1463 }
1464 }
1465 if (p_XzUnpacker_Free && p_XzUnpacker_Code) {
1466 size_t xz_len = shdr.sh_size;
1467 void *unpacker = nullptr;
1468 if (p_XzUnpacker_Create) {
1469 unpacker = p_XzUnpacker_Create(malloc, free);
1470 } else if (p_XzUnpacker_Init) {
1471 unpacker = malloc(65536);
1472 if (unpacker) { memset(unpacker, 0, 65536); p_XzUnpacker_Init(unpacker); }
1473 } else {
1474 unpacker = malloc(65536);
1475 if (unpacker) memset(unpacker, 0, 65536);
1476 }
1477 LOGI("libart_resolver: XzUnpacker unpacker=%p (xz_len=%zu)", unpacker, xz_len);
1478 if (unpacker) {
1479 size_t out_cap = xz_len * 20;
1480 if (out_cap < 0x10000) out_cap = 0x10000;
1481 uint8_t *outbuf = (uint8_t *)malloc(out_cap);
1482 if (outbuf) {
1483 LzSizeT destLen = out_cap;
1484 LzSizeT srcLen = xz_len;
1485 LzmaStatus lzstatus = {};
1486 LOGI("libart_resolver: calling XzUnpacker_Code(destCap=%zu, srcLen=%zu)...", out_cap, xz_len);
1487 LzRes lzret = p_XzUnpacker_Code(unpacker, outbuf, &destLen, secdata, &srcLen, 1 /*srcFinished*/, 1 /*FINISH_END*/, &lzstatus);
1488 LOGI("libart_resolver: XzUnpacker_Code ret=%d status=%d outLen=%zu", lzret, lzstatus, destLen);
1489 if ((lzret == 0 || lzret == 1) && destLen > 0) {
1490 LOGI("libart_resolver: XZ decompress OK, %zu bytes", destLen);
1491 size_t out_len = destLen;
1492 if (out_len >= sizeof(Elf64_Ehdr) && outbuf[0] == 0x7f && outbuf[1] == 'E') {
1493 auto *de = (Elf64_Ehdr *)outbuf;
1494 auto *dp = (Elf64_Phdr *)(outbuf + de->e_phoff);
1495 uint64_t dbias = load_bias;
1496 for (int pi = 0; pi < de->e_phnum; pi++) {
1497 if (dp[pi].p_type == 1 && dp[pi].p_offset == 0) {
1498 dbias = base - dp[pi].p_vaddr;
1499 break;
1500 }
1501 }
1502 for (int pi = 0; pi < de->e_phnum; pi++) {
1503 if (dp[pi].p_type != PT_DYNAMIC) continue;
1504 auto *ddyn = (Elf64_Dyn *)(outbuf + dp[pi].p_offset);
1505 const char *dstrtab = nullptr;
1506 Elf64_Sym *dsymtab = nullptr;
1507 uint32_t dstrsz = 0;
1508 for (auto *dd = ddyn; dd->d_tag != 0; dd++) {
1509 if (dd->d_tag == DT_STRTAB) dstrtab = (const char *)(outbuf + dd->d_un.d_ptr);
1510 if (dd->d_tag == DT_SYMTAB) dsymtab = (Elf64_Sym *)(outbuf + dd->d_un.d_ptr);
1511 if (dd->d_tag == DT_STRSZ) dstrsz = dd->d_un.d_val;
1512 }
1513 if (dstrtab && dsymtab) {
1514 LOGI("libart_resolver: XZ mini ELF dynsym found, scanning...");
1515 for (uint32_t si = 1; dsymtab[si].st_name < dstrsz; si++) {
1516 if (dsymtab[si].st_value == 0) continue;
1517 const char *sn = dstrtab + dsymtab[si].st_name;
1518 if (strstr(sn, "Shorty") || strstr(sn, "ethodShorty")) {
1519 int64_t g = mem_segment_gap(base, dsymtab[si].st_value);
1520 void *r = (void *)(dbias + dsymtab[si].st_value - g);
1521 LOGI("libart_resolver: XZ debugdata match: %s = %p", sn, r);
1522 if (strstr(sn, "GetMethodShorty") || strstr(sn, "15GetMethodShorty")) {
1523 gnu_debugdata_result = r;
1524 }
1525 }
1526 }
1527 }
1528 break;
1529 }
1530 }
1531 } else {
1532 LOGI("libart_resolver: XZ decompress failed ret=%u status=%u", (unsigned)lzret, (unsigned)lzstatus);
1533 }
1534 free(outbuf);
1535 }
1536 p_XzUnpacker_Free(unpacker, free);
1537 }
1538 } else {
1539 LOGI("libart_resolver: XzUnpacker symbols not found in liblzma.so, skipping XZ decompress");
1540 }
1541 }
1542 // Try zlib (78 01 or 78 9C or 78 DA)
1543 else if (secdata[0] == 0x78 && (secdata[1] == 0x01 || secdata[1] == 0x9C || secdata[1] == 0xDA)) {
1544 LOGI("libart_resolver: .gnu_debugdata is zlib compressed, attempting decompress...");
1545 z_stream strm = {};
1546 strm.next_in = secdata;
1547 strm.avail_in = shdr.sh_size;
1548 if (inflateInit(&strm) == Z_OK) {
1549 size_t out_cap = shdr.sh_size * 10;
1550 uint8_t *outbuf = (uint8_t *)malloc(out_cap);
1551 if (outbuf) {
1552 strm.next_out = outbuf;
1553 strm.avail_out = out_cap;
1554 int zret = inflate(&strm, Z_FINISH);
1555 if (zret == Z_STREAM_END || zret >= 0) {
1556 size_t out_len = out_cap - strm.avail_out;
1557 LOGI("libart_resolver: zlib decompress OK, %zu bytes", out_len);
1558 // Parse the decompressed ELF
1559 if (out_len >= sizeof(Elf64_Ehdr) && outbuf[0] == 0x7f && outbuf[1] == 'E') {
1560 auto *de = (Elf64_Ehdr *)outbuf;
1561 auto *dp = (Elf64_Phdr *)(outbuf + de->e_phoff);
1562 uint64_t dbias = load_bias;
1563 for (int pi = 0; pi < de->e_phnum; pi++) {
1564 if (dp[pi].p_type == 1 && dp[pi].p_offset == 0) {
1565 dbias = base - dp[pi].p_vaddr;
1566 break;
1567 }
1568 }
1569 // Find PT_DYNAMIC in the mini ELF
1570 for (int pi = 0; pi < de->e_phnum; pi++) {
1571 if (dp[pi].p_type != PT_DYNAMIC) continue;
1572 auto *ddyn = (Elf64_Dyn *)(outbuf + dp[pi].p_offset);
1573 const char *dstrtab = nullptr;
1574 Elf64_Sym *dsymtab = nullptr;
1575 uint32_t dstrsz = 0;
1576 for (auto *dd = ddyn; dd->d_tag != 0; dd++) {
1577 if (dd->d_tag == DT_STRTAB) dstrtab = (const char *)(outbuf + dd->d_un.d_ptr);
1578 if (dd->d_tag == DT_SYMTAB) dsymtab = (Elf64_Sym *)(outbuf + dd->d_un.d_ptr);
1579 if (dd->d_tag == DT_STRSZ) dstrsz = dd->d_un.d_val;
1580 }
1581 if (dstrtab && dsymtab) {
1582 LOGI("libart_resolver: mini ELF dynsym found, scanning...");
1583 for (uint32_t si = 1; dsymtab[si].st_name < dstrsz; si++) {
1584 if (dsymtab[si].st_value == 0) continue;
1585 const char *sn = dstrtab + dsymtab[si].st_name;
1586 if (strstr(sn, "Shorty") || strstr(sn, "ethodShorty")) {
1587 int64_t g = mem_segment_gap(base, dsymtab[si].st_value);
1588 void *r = (void *)(dbias + dsymtab[si].st_value - g);
1589 LOGI("libart_resolver: .gnu_debugdata match: %s = %p", sn, r);
1590 if (strstr(sn, "GetMethodShorty") || strstr(sn, "15GetMethodShorty")) {
1591 gnu_debugdata_result = r;
1592 }
1593 }
1594 }
1595 }
1596 break;
1597 }
1598 }
1599 } else {
1600 LOGI("libart_resolver: zlib decompress failed ret=%d", zret);
1601 }
1602 inflateEnd(&strm);
1603 free(outbuf);
1604 }
1605 }
1606 }
1607 // Try raw/uncompressed - if first bytes look like ELF
1608 else if (secdata[0] == 0x7f && secdata[1] == 'E') {
1609 LOGI("libart_resolver: .gnu_debugdata is uncompressed ELF");
1610 }
1611 else {
1612 LOGI("libart_resolver: .gnu_debugdata unknown compression (magic: %02x %02x %02x %02x)",
1613 secdata[0], secdata[1], secdata[2], secdata[3]);
1614 }
1615 }
1616 free(secdata);
1617 }
1618 }
1619 if (gnu_debugdata_result) {
1620 free(shstrtab);
1621 fclose(f);
1622 LOGI("libart_resolver: returning .gnu_debugdata result %p", gnu_debugdata_result);
1623 return gnu_debugdata_result;
1624 }
1625
1626 // === Phase 2: scan file .symtab ===
1627 int symtab_count = 0;
1628 for (int i = 0; i < fehdr.e_shnum; i++) {
1629 Elf64_Shdr shdr;
1630 fseek(f, fehdr.e_shoff + i * fehdr.e_shentsize, SEEK_SET);
1631 if (fread(&shdr, 1, sizeof(shdr), f) != sizeof(shdr)) continue;
1632 if (shdr.sh_type == SHT_SYMTAB) {
1633 symtab_count++;
1634 }
1635 }
1636 LOGI("libart_resolver: found %d SHT_SYMTAB sections", symtab_count);
1637 if (symtab_count == 0) {
1638 LOGE("libart_resolver: no .symtab section — symbol likely stripped from libart.so");
1639 // Phase 3: in-memory code scan — search for function by known call pattern
1640 // LSPlant calls GetMethodShorty via our resolver. If we can't find it by name,
1641 // scan the in-memory dynsym for ANY symbol containing a substring match
1642 LOGI("libart_resolver: Phase 3 — scanning in-memory dynsym for substring matches of '%s'", name);
1643 if (strtab && symtab) {
1644 const char *search_term = "Shorty";
1645 if (!strstr(name, "Shorty")) search_term = name;
1646 for (uint32_t i = 1; symtab[i].st_name < strsz; i++) {
1647 if (symtab[i].st_value == 0) continue;
1648 const char *sname = strtab + symtab[i].st_name;
1649 if (strstr(sname, search_term)) {
1650 int64_t gap = mem_segment_gap(base, symtab[i].st_value);
1651 void *result = (void *)(base + symtab[i].st_value - gap);
1652 LOGI("libart_resolver: substring match: %s = %p", sname, result);
1653 if (strstr(sname, "GetMethodShorty") || strstr(sname, "15GetMethodShorty")) {
1654 free(shstrtab);
1655 fclose(f);
1656 return result;
1657 }
1658 }
1659 }
1660 }
1661 free(shstrtab);
1662 fclose(f);
1663 LOGE("libart_resolver: %s NOT FOUND anywhere (all phases exhausted)", name);
1664 return nullptr;
1665 }
1666 for (int i = 0; i < fehdr.e_shnum; i++) {
1667 Elf64_Shdr shdr;
1668 fseek(f, fehdr.e_shoff + i * fehdr.e_shentsize, SEEK_SET);
1669 if (fread(&shdr, 1, sizeof(shdr), f) != sizeof(shdr)) continue;
1670 if (shdr.sh_type != SHT_SYMTAB) continue;
1671 Elf64_Shdr strsh;
1672 fseek(f, fehdr.e_shoff + shdr.sh_link * fehdr.e_shentsize, SEEK_SET);
1673 if (fread(&strsh, 1, sizeof(strsh), f) != sizeof(strsh)) continue;
1674 char *strtab_buf = (char *)malloc(strsh.sh_size);
1675 if (!strtab_buf) continue;
1676 fseek(f, strsh.sh_offset, SEEK_SET);
1677 fread(strtab_buf, 1, strsh.sh_size, f);
1678 uint32_t nsym = shdr.sh_size / shdr.sh_entsize;
1679 LOGI("libart_resolver: scanning symtab section %u: %u symbols", i, nsym);
1680 for (uint32_t j = 0; j < nsym; j++) {
1681 Elf64_Sym sym;
1682 fseek(f, shdr.sh_offset + j * shdr.sh_entsize, SEEK_SET);
1683 if (fread(&sym, 1, sizeof(sym), f) != sizeof(sym)) break;
1684 if (sym.st_value == 0) continue;
1685 const char *sname = strtab_buf + sym.st_name;
1686 if (strcmp(sname, name) == 0) {
1687 void *result = (void *)(load_bias + sym.st_value);
1688 LOGI("libart_resolver: %s = %p (file .symtab, section %u)", name, result, i);
1689 free(strtab_buf);
1690 free(shstrtab);
1691 fclose(f);
1692 return result;
1693 }
1694 }
1695 free(strtab_buf);
1696 }
1697 free(shstrtab);
1698 fclose(f);
1699 LOGE("libart_resolver: %s NOT FOUND in .symtab either", name);
1700 return nullptr;
1701}
1702
1703static void *libart_prefix_resolver(const char *prefix) {
1704 if (!g_libart_handle) {
1705 g_libart_handle = dlopen("libart.so", RTLD_NOLOAD);
1706 }
1707 uintptr_t base = find_libart_base();
1708 if (!base) return nullptr;
1709 auto *ehdr = (Elf64_Ehdr *)base;
1710 auto *phdr = (Elf64_Phdr *)(base + ehdr->e_phoff);
1711 const char *strtab = nullptr;
1712 Elf64_Sym *symtab = nullptr;
1713 uint32_t strsz = 0;
1714 for (int i = 0; i < ehdr->e_phnum; i++) {
1715 if (phdr[i].p_type == PT_DYNAMIC) {
1716 auto *dyn = (Elf64_Dyn *)(base + phdr[i].p_offset);
1717 for (Elf64_Dyn *d = dyn; d->d_tag != 0; d++) {
1718 if (d->d_tag == DT_STRTAB) { int64_t g = mem_segment_gap(base, d->d_un.d_ptr); strtab = (const char *)(base + d->d_un.d_ptr - g); }
1719 if (d->d_tag == DT_SYMTAB) { int64_t g = mem_segment_gap(base, d->d_un.d_ptr); symtab = (Elf64_Sym *)(base + d->d_un.d_ptr - g); }
1720 if (d->d_tag == DT_STRSZ) strsz = (uint32_t)d->d_un.d_val;
1721 }
1722 break;
1723 }
1724 }
1725 if (!strtab || !symtab) return nullptr;
1726 for (uint32_t i = 1; symtab[i].st_name < strsz; i++) {
1727 if (symtab[i].st_value == 0) continue;
1728 const char *sname = strtab + symtab[i].st_name;
1729 if (strncmp(sname, prefix, strlen(prefix)) == 0) {
1730 int64_t gap = mem_segment_gap(base, symtab[i].st_value);
1731 void *result = (void *)(base + symtab[i].st_value - gap);
1732 LOGI("libart_prefix_resolver: %s -> %s = %p", prefix, sname, result);
1733 return result;
1734 }
1735 }
1736 return nullptr;
1737}
1738
1739static bool lsplant_init(JNIEnv *env) {
1740 if (g_lsplant_initialized) return true;
1741 LOGI("lsplant_init: START");
1742 lsplant::InitInfo info{};
1743 info.inline_hooker = [](void *target, void *hooker) -> void * {
1744 return inline_hooker(target, hooker);
1745 };
1746 info.inline_unhooker = [](void *func) -> bool {
1747 return inline_unhooker(func);
1748 };
1749 info.art_symbol_resolver = [](std::string_view name) -> void * {
1750 return libart_resolver(std::string(name).c_str());
1751 };
1752 info.art_symbol_prefix_resolver = [](std::string_view prefix) -> void * {
1753 return libart_prefix_resolver(std::string(prefix).c_str());
1754 };
1755 bool ok = lsplant::Init(env, info);
1756 LOGI("lsplant_init: result=%d", ok);
1757 g_lsplant_initialized = ok;
1758 return ok;
1759}
1760
1761static jobject lsplant_hook_method(JNIEnv *env, jclass cls, const char *name, const char *sig,
1762 bool is_static, int hookId) {
1763 LOGI("lsplant_hook_method: %s hookId=%d", name, hookId);
1764 jmethodID mid = is_static ? env->GetStaticMethodID(cls, name, sig)
1765 : env->GetMethodID(cls, name, sig);
1766 if (!mid) { env->ExceptionClear(); LOGE("lsplant_hook_method: %s not found", name); return nullptr; }
1767 jobject method = env->ToReflectedMethod(cls, mid, is_static);
1768 if (!method) { env->ExceptionClear(); LOGE("lsplant_hook_method: ToReflectedMethod failed for %s", name); return nullptr; }
1769
1770 jobject callback_obj = env->NewObject(g_hooker_cls, g_hooker_ctor, hookId);
1771 if (!callback_obj || env->ExceptionCheck()) { env->ExceptionClear(); LOGE("lsplant_hook_method: NewObject failed"); return nullptr; }
1772
1773 jobject backup = lsplant::Hook(env, method, callback_obj, g_hooker_callback_method);
1774 if (!backup) { env->ExceptionClear(); LOGE("lsplant_hook_method: Hook FAILED for %s", name); return nullptr; }
1775
1776 jfieldID backup_fid = env->GetFieldID(g_hooker_cls, "backup", "Ljava/lang/reflect/Method;");
1777 env->SetObjectField(callback_obj, backup_fid, backup);
1778 LOGI("lsplant_hook_method: %s OK, backup=%p", name, backup);
1779 return backup;
1780}
1781
1782// ==================== il2cpp API ====================
1783
1784typedef void* (*fn_domain_get)();
1785typedef const void** (*fn_domain_get_assemblies)(void*, size_t*);
1786typedef const void* (*fn_assembly_get_image)(const void*);
1787typedef const void* (*fn_class_from_name)(const void*, const char*, const char*);
1788typedef const void* (*fn_class_get_methods)(const void*, void**);
1789typedef const char* (*fn_method_get_name)(const void*);
1790// il2cpp_method_get_pointer removed — Il2CppMethodInfo starts with methodPointer at offset 0
1791typedef void* (*fn_method_get_invoke_ptr)(const void*);
1792
1793static void install_immortality(uintptr_t il2cpp_base) {
1794 LOGI("install_immortality: base=%p", (void *)il2cpp_base);
1795 auto f_domain = (fn_domain_get) elf_find_symbol(il2cpp_base, "il2cpp_domain_get");
1796 auto f_assemblies= (fn_domain_get_assemblies) elf_find_symbol(il2cpp_base, "il2cpp_domain_get_assemblies");
1797 auto f_image = (fn_assembly_get_image) elf_find_symbol(il2cpp_base, "il2cpp_assembly_get_image");
1798 auto f_fromname = (fn_class_from_name) elf_find_symbol(il2cpp_base, "il2cpp_class_from_name");
1799 auto f_methods = (fn_class_get_methods) elf_find_symbol(il2cpp_base, "il2cpp_class_get_methods");
1800 auto f_mname = (fn_method_get_name) elf_find_symbol(il2cpp_base, "il2cpp_method_get_name");
1801 // il2cpp_method_get_pointer not exported — read from Il2CppMethodInfo struct (offset 0 = methodPointer)
1802 LOGI("install_immortality: domain=%p asm=%p img=%p name=%p methods=%p mname=%p",
1803 (void*)f_domain, (void*)f_assemblies, (void*)f_image, (void*)f_fromname,
1804 (void*)f_methods, (void*)f_mname);
1805 if (!f_domain || !f_assemblies || !f_image || !f_fromname) {
1806 LOGE("il2cpp API not found via ELF");
1807 return;
1808 }
1809 if (!f_methods || !f_mname) {
1810 LOGE("il2cpp method API not found (methods=%p mname=%p)", (void*)f_methods, (void*)f_mname);
1811 return;
1812 }
1813 LOGI("il2cpp API found via ELF");
1814 void *dom = f_domain();
1815 LOGI("install_immortality: domain=%p", dom);
1816 size_t cnt = 0;
1817 const void **assemblies = f_assemblies(dom, &cnt);
1818 LOGI("install_immortality: %zu assemblies", cnt);
1819 for (size_t i = 0; i < cnt; i++) {
1820 const void *img = f_image(assemblies[i]);
1821 const void *kls = f_fromname(img, "", "Character");
1822 if (!kls) continue;
1823 LOGI("install_immortality: found Character in assembly %zu", i);
1824 void *iter = nullptr;
1825 const void *method;
1826 fop_fn orig = nullptr;
1827 while ((method = f_methods(kls, &iter)) != nullptr) {
1828 const char *mname = f_mname(method);
1829 if (mname && strcmp(mname, "fwa") == 0) {
1830 orig = (fop_fn)(*(void**)method);
1831 LOGI("install_immortality: found fwa method: name=%s ptr=%p", mname, (void*)orig);
1832 break;
1833 }
1834 }
1835 if (!orig) {
1836 LOGE("install_immortality: fwa method NOT FOUND via il2cpp_class_get_methods");
1837 LOGI("install_immortality: dumping all methods of Character:");
1838 iter = nullptr;
1839 int dump_count = 0;
1840 while ((method = f_methods(kls, &iter)) != nullptr && dump_count < 50) {
1841 const char *mname = f_mname(method);
1842 void *mptr = *(void**)method;
1843 LOGI(" method[%d]: %s ptr=%p", dump_count, mname ? mname : "(null)", mptr);
1844 dump_count++;
1845 }
1846 return;
1847 }
1848 if (!orig) {
1849 LOGE("install_immortality: fwa ptr is NULL");
1850 return;
1851 }
1852 void *trampoline = alloc_exec_near((void*)orig, 64);
1853 if (!trampoline) {
1854 LOGE("install_immortality: alloc_exec trampoline failed");
1855 return;
1856 }
1857 memcpy(trampoline, (void *)orig, 16);
1858 fix_adrp_in_trampoline(trampoline, (void*)orig);
1859 uint32_t *t = (uint32_t *)trampoline;
1860 t[4] = 0x58000050;
1861 t[5] = 0xD61F0200;
1862 *(uint64_t *)&t[6] = (uint64_t)orig + 16;
1863 __builtin___clear_cache((char *)trampoline, (char *)trampoline + 64);
1864 uint8_t jmp[16];
1865 *(uint32_t *)&jmp[0] = 0x58000050;
1866 *(uint32_t *)&jmp[4] = 0xD61F0200;
1867 *(uint64_t *)&jmp[8] = (uint64_t)my_fop;
1868 if (proc_mem_write((uintptr_t)orig, jmp, 16)) {
1869 __builtin___clear_cache((char *)orig, (char *)orig + 16);
1870 if (memcmp((void *)orig, jmp, 16) == 0) {
1871 g_orig_fop = (fop_fn)trampoline;
1872 LOGI("Immortality inline hook OK (func=%p, trampoline=%p)", (void *)orig, trampoline);
1873 } else {
1874 LOGE("Immortality inline hook VERIFY FAILED (func=%p) — patch did not stick", (void *)orig);
1875 }
1876 } else {
1877 LOGE("Immortality inline hook FAILED (proc_mem_write for %p)", (void *)orig);
1878 }
1879 return;
1880 }
1881 LOGE("DungeonRush.Character class not found");
1882}
1883
1884typedef int (*proc_purchase_fn)(void *self, void *args);
1885static proc_purchase_fn g_orig_proc_purchase = nullptr;
1886static uintptr_t g_proc_purchase_entry = 0; // entry reale risolta via API il2cpp (da install_store_hook)
1887// v4.58: base REALE per gli indirizzi assoluti (file-VMA as objdump). Il vecchio uso
1888// di g_il2cpp_base+offset assumeva bias=VMA nel segmento .text, ma il segmento LOAD e'
1889// mappato con gap 0x4000 (vaddr=fileoff+0x4000): runtime = bias + VMA - 0x4000.
1890// Noto che g_proc_purchase_entry (indirizzo runtime) - RVA dell'entry nel file (v56: 0x28ACD34)
1891// da' esattamente la base tale che addr_assoluto = base + fileVMA. Tutti i probe assoluti
1892// (fallback, grant chain) devono usare g_code_base, NON g_il2cpp_base.
1893// g_code_base definito in alto (insieme ai probe user): addr_assoluto = g_code_base + RVA.
1894// = g_proc_purchase_entry - 0x28ACD34 (impostata in install_store_hook)
1895static bool my_dnf(void *self, void *args);
1896static void dump_handler_method_name(uintptr_t base);
1897static void dump_purchase_types(uintptr_t base);
1898static void dump_grant_bl(uintptr_t entry);
1899static bool safe_read_self(uintptr_t addr, void *buf, size_t len);
1900
1901// API-based dump del metodo handler. [dw+0x50] è l'oggetto handler (l'oggetto
1902// su cui ProcessPurchase esegue il vtable lookup del metodo per nome). Usiamo
1903// l'API il2cpp per risolvere la classe dell'oggetto ed enumerare i metodi con
1904// il nome, invece di indovinare il layout di memoria. Questo dá il NOME reale
1905// del metodo handler, che è quello che serve per capire il grant.
1906// Risolve le funzioni una sola volta (static cache).
1907typedef const void* (*fn_object_get_class)(const void* obj);
1908typedef const char* (*fn_class_get_name)(const void* klass);
1909typedef const char* (*fn_class_get_namespace)(const void* klass);
1910static bool dump_handler_via_api(void *dw_self) {
1911 if (!dw_self) return false;
1912 static void *f_objclass = nullptr, *f_cname = nullptr, *f_cns = nullptr;
1913 static void *f_methods = nullptr, *f_mname = nullptr;
1914 if (!f_objclass && g_il2cpp_base) {
1915 f_objclass = elf_find_symbol(g_il2cpp_base, "il2cpp_object_get_class");
1916 f_cname = elf_find_symbol(g_il2cpp_base, "il2cpp_class_get_name");
1917 f_cns = elf_find_symbol(g_il2cpp_base, "il2cpp_class_get_namespace");
1918 f_methods = elf_find_symbol(g_il2cpp_base, "il2cpp_class_get_methods");
1919 f_mname = elf_find_symbol(g_il2cpp_base, "il2cpp_method_get_name");
1920 LOGI("API_DIAG: objclass=%p cname=%p cns=%p methods=%p mname=%p",
1921 f_objclass, f_cname, f_cns, f_methods, f_mname);
1922 }
1923 if (!f_objclass || !f_cname || !f_cns || !f_methods || !f_mname) {
1924 LOGE("API_DIAG: il2cpp API symbols not found");
1925 return false;
1926 }
1927 // [dw+0x50] = oggetto handler
1928 uint64_t handler = 0;
1929 if (!safe_read_self((uintptr_t)dw_self + 0x50, &handler, 8)) {
1930 LOGI("API_DIAG: read dw+0x50 FAIL (self=%p)", dw_self);
1931 return false;
1932 }
1933 LOGI("API_DIAG: [dw+0x50] handler obj = 0x%llx", (unsigned long long)handler);
1934 if (!handler) { LOGI("API_DIAG: handler null"); return false; }
1935 const void *klass = ((fn_object_get_class)f_objclass)((void*)handler);
1936 LOGI("API_DIAG: handler class = %p", klass);
1937 if (!klass) return false;
1938 const char *cname = ((fn_class_get_name)f_cname)(klass);
1939 const char *cns = ((fn_class_get_namespace)f_cns)(klass);
1940 LOGI("API_DIAG: handler class name='%s' namespace='%s'", cname ? cname : "(null)", cns ? cns : "(null)");
1941 // Enumeriamo i metodi della classe (come fa install_immortality per Character)
1942 void *iter = nullptr;
1943 const void *method;
1944 int cnt = 0;
1945 while ((method = ((fn_class_get_methods)f_methods)(klass, &iter)) != nullptr && cnt < 100) {
1946 const char *mname = ((fn_method_get_name)f_mname)(method);
1947 LOGI("API_DIAG: method[%d] '%s'", cnt, mname ? mname : "(null)");
1948 cnt++;
1949 }
1950 LOGI("API_DIAG: %d methods enumerated", cnt);
1951 return true;
1952}
1953
1954static int my_proc_purchase(void *self, void *args) {
1955 LOGI("STORE_STEP: my_proc_purchase ENTRY called by %p self=%p args=%p", __builtin_return_address(0), self, args);
1956 static bool s_diag_dumped = false;
1957 if (!s_diag_dumped && g_proc_purchase_entry) {
1958 s_diag_dumped = true;
1959 uintptr_t base = g_proc_purchase_entry - 0x28ACD34;
1960 LOGI("PROC_PURCH_DIAG: first call — dumping handler via il2cpp API (base=%p)", (void*)base);
1961 dump_handler_via_api(self);
1962 dump_handler_method_name(base);
1963 dump_purchase_types(base);
1964 dump_grant_bl(g_proc_purchase_entry);
1965 }
1966 LOGI("STORE_STEP: my_proc_purchase: g_orig_proc_purchase=%p (trampoline), will CALL ORIGINAL so grant chain runs; my_dnf(eb.dnf) hooked to return true for verification", (void*)g_orig_proc_purchase);
1967 LOGI("STORE_STEP: my_proc_purchase: self eb=%p, args PurchaseEventArgs=%p, caller=%p, next expected HOOK is my_dnf (eb.dnf) at %p", self, args, __builtin_return_address(0), (void*)my_dnf);
1968 static bool s_state_dumped = false;
1969 if (!s_state_dumped) {
1970 s_state_dumped = true;
1971 uintptr_t ab = (uintptr_t)args, sb = (uintptr_t)self;
1972 uint64_t a8=0,a10=0,a18=0, h50=0, p30=0;
1973 if (ab) { safe_read_self(ab+0x08,&a8,8); safe_read_self(ab+0x10,&a10,8); safe_read_self(ab+0x18,&a18,8); }
1974 if (sb) { safe_read_self(sb+0x50,&h50,8); safe_read_self(sb+0x30,&p30,8); }
1975 LOGI("STORE_STEP: my_proc_purchase state: args+0x8=%p args+0x10=%p args+0x18=%p | eb+0x50(handler)=%p eb+0x30(product)=%p",
1976 (void*)a8,(void*)a10,(void*)a18,(void*)h50,(void*)p30);
1977 }
1978 int ret = 0;
1979 if (g_orig_proc_purchase) {
1980 ret = g_orig_proc_purchase(self, args);
1981 LOGI("STORE_STEP: my_proc_purchase called ORIGINAL -> returned %d (dnf hooked true => grant path)", ret);
1982 } else {
1983 LOGI("STORE_STEP: my_proc_purchase NO original, forcing Complete(0)");
1984 }
1985 LOGI("STORE_STEP: my_proc_purchase EXIT returning %d -> if my_dnf triggered then verification passed and grant should follow", ret);
1986 return ret;
1987}
1988static void install_store_hook(uintptr_t il2cpp_base) {
1989 LOGI("install_store_hook: base=%p (near alloc for adrp)", (void *)il2cpp_base);
1990 auto f_domain = (fn_domain_get) elf_find_symbol(il2cpp_base, "il2cpp_domain_get");
1991 auto f_assemblies= (fn_domain_get_assemblies) elf_find_symbol(il2cpp_base, "il2cpp_domain_get_assemblies");
1992 auto f_image = (fn_assembly_get_image) elf_find_symbol(il2cpp_base, "il2cpp_assembly_get_image");
1993 auto f_fromname = (fn_class_from_name) elf_find_symbol(il2cpp_base, "il2cpp_class_from_name");
1994 auto f_methods = (fn_class_get_methods) elf_find_symbol(il2cpp_base, "il2cpp_class_get_methods");
1995 auto f_mname = (fn_method_get_name) elf_find_symbol(il2cpp_base, "il2cpp_method_get_name");
1996 if (!f_domain || !f_assemblies || !f_image || !f_fromname || !f_methods || !f_mname) { LOGE("install_store_hook: il2cpp API not found"); return; }
1997 void *dom = f_domain();
1998 size_t cnt = 0;
1999 const void **assemblies = f_assemblies(dom, &cnt);
2000 for (size_t i = 0; i < cnt; i++) {
2001 const void *img = f_image(assemblies[i]);
2002 const void *kls = f_fromname(img, "", "eb");
2003 if (!kls) continue;
2004 LOGI("install_store_hook: found eb in assembly %zu", i);
2005 void *iter = nullptr;
2006 const void *method;
2007 proc_purchase_fn orig = nullptr;
2008 while ((method = f_methods(kls, &iter)) != nullptr) {
2009 const char *mname = f_mname(method);
2010 if (mname && strcmp(mname, "ProcessPurchase") == 0) {
2011 orig = (proc_purchase_fn)(*(void**)method);
2012 LOGI("install_store_hook: found ProcessPurchase ptr=%p", (void*)orig);
2013 break;
2014 }
2015 }
2016 if (!orig) { LOGE("install_store_hook: ProcessPurchase not found"); return; }
2017 g_proc_purchase_entry = (uintptr_t)orig;
2018 g_code_base = g_proc_purchase_entry - 0x28ACD34; // base file-VMA reale (v4.58 fix, v56 eb.ProcessPurchase)
2019 LOGI("install_store_hook: entry reale ProcessPurchase=%p (per probe ptr+offset)", (void*)g_proc_purchase_entry);
2020 LOGI("install_store_hook: code_base=%p (entry - 0x28ACD34) — probe assoluti useranno questa base, NON g_il2cpp_base", (void*)g_code_base);
2021 // Hook at entry (with adrp fixup) to avoid stack leak from +0x1C hook
2022 void *hook_target = (void*)orig;
2023 {
2024 uint8_t *b = (uint8_t*)hook_target;
2025 LOGI("install_store_hook: orig bytes @%p: %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x",
2026 orig, b[0],b[1],b[2],b[3],b[4],b[5],b[6],b[7],b[8],b[9],b[10],b[11],b[12],b[13],b[14],b[15]);
2027 for(int i=0;i<16;i+=4){
2028 uint32_t ins=*(uint32_t*)(b+i);
2029 if((ins&0x9F000000)==0x90000000) LOGI(" adrp @+%d: %08x", i, ins);
2030 }
2031 }
2032 void *trampoline = alloc_exec_near(hook_target, 64);
2033 if (!trampoline) { LOGE("install_store_hook: alloc_exec failed"); return; }
2034 memcpy(trampoline, hook_target, 16);
2035 fix_adrp_in_trampoline(trampoline, hook_target);
2036 uint32_t *t2 = (uint32_t*)trampoline; t2[4]=0x58000050; t2[5]=0xD61F0200; *(uint64_t*)&t2[6]=(uint64_t)orig+16;
2037 __builtin___clear_cache((char*)trampoline, (char*)trampoline+64);
2038 uint8_t jmp[16];
2039 *(uint32_t*)&jmp[0] = 0x58000050; // ldr x16, #8
2040 *(uint32_t*)&jmp[4] = 0xD61F0200; // br x16
2041 *(uint64_t*)&jmp[8] = (uint64_t)my_proc_purchase;
2042 LOGI("Store hook: patching %p with ldr/br 16B -> %p", orig, my_proc_purchase);
2043 if (proc_mem_write((uintptr_t)hook_target, jmp, 16)) {
2044 __builtin___clear_cache((char *)hook_target, (char *)hook_target + 16);
2045 if (memcmp(hook_target, jmp, 16)==0) {
2046 g_orig_proc_purchase = (proc_purchase_fn)trampoline;
2047 LOGI("Store hook OK (func=%p entry 16B, trampoline=%p)", (void *)orig, trampoline);
2048 } else LOGE("Store hook VERIFY FAILED");
2049 } else LOGE("Store hook FAILED");
2050 return;
2051 }
2052 LOGE("eb class not found");
2053}
2054typedef bool (*dnf_fn)(void *self, void *args);
2055static dnf_fn g_orig_dnf = nullptr;
2056static bool my_dnf(void *self, void *args) {
2057 LOGI("STORE_STEP: my_dnf ENTRY called by %p self=%p dw:%p args=%p PurchaseEventArgs %p", __builtin_return_address(0), self, self, args, args);
2058 LOGI("STORE_STEP: my_dnf: g_orig_dnf=%p, will BYPASS verification and return true (grant inventory next)", (void*)g_orig_dnf);
2059 LOGI("STORE_STEP: my_dnf: caller stack 0x%p -> 0x%p, next expected: inventory grant (Unity Purchasing StoreListenerProxy.onPurchaseSucceeded)", __builtin_return_address(1), __builtin_return_address(0));
2060 bool ret = true;
2061 LOGI("STORE_STEP: my_dnf EXIT returning %d (true=verified) -> game should call OnPurchaseSucceeded/ConfirmPendingPurchase next", ret);
2062 return ret;
2063}
2064static void install_dnf_hook(uintptr_t il2cpp_base) {
2065 LOGI("install_dnf_hook: base=%p", (void *)il2cpp_base);
2066 auto f_domain = (fn_domain_get) elf_find_symbol(il2cpp_base, "il2cpp_domain_get");
2067 auto f_assemblies= (fn_domain_get_assemblies) elf_find_symbol(il2cpp_base, "il2cpp_domain_get_assemblies");
2068 auto f_image = (fn_assembly_get_image) elf_find_symbol(il2cpp_base, "il2cpp_assembly_get_image");
2069 auto f_fromname = (fn_class_from_name) elf_find_symbol(il2cpp_base, "il2cpp_class_from_name");
2070 auto f_methods = (fn_class_get_methods) elf_find_symbol(il2cpp_base, "il2cpp_class_get_methods");
2071 auto f_mname = (fn_method_get_name) elf_find_symbol(il2cpp_base, "il2cpp_method_get_name");
2072 if (!f_domain || !f_assemblies || !f_image || !f_fromname || !f_methods || !f_mname) return;
2073 void *dom = f_domain(); size_t cnt = 0; const void **assemblies = f_assemblies(dom, &cnt);
2074 for (size_t i = 0; i < cnt; i++) {
2075 const void *img = f_image(assemblies[i]);
2076 const void *kls = f_fromname(img, "", "eb");
2077 if (!kls) continue;
2078 void *iter = nullptr; const void *method;
2079 while ((method = f_methods(kls, &iter)) != nullptr) {
2080 const char *mname = f_mname(method);
2081 if (mname && strcmp(mname, "dnf") == 0) {
2082 dnf_fn orig = (dnf_fn)(*(void**)method);
2083 LOGI("install_dnf_hook: found dnf ptr=%p", (void*)orig);
2084 {
2085 uint8_t *b = (uint8_t*)orig;
2086 LOGI("install_dnf_hook: orig bytes @%p: %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x",
2087 orig, b[0],b[1],b[2],b[3],b[4],b[5],b[6],b[7],b[8],b[9],b[10],b[11],b[12],b[13],b[14],b[15]);
2088 for(int i=0;i<16;i+=4){
2089 uint32_t ins=*(uint32_t*)(b+i);
2090 if((ins&0x9F000000)==0x90000000) LOGI(" adrp @+%d: %08x", i, ins);
2091 }
2092 }
2093 void *tramp = alloc_exec_near((void*)orig, 64);
2094 if (!tramp) return;
2095 memcpy(tramp, (void*)orig, 16);
2096 fix_adrp_in_trampoline(tramp, (void*)orig);
2097 uint32_t *t = (uint32_t*)tramp; t[4]=0x58000050; t[5]=0xD61F0200; *(uint64_t*)&t[6]=(uint64_t)orig+16;
2098 __builtin___clear_cache((char*)tramp, (char*)tramp+64);
2099 uint8_t jmp[16]; *(uint32_t*)&jmp[0]=0x58000050; *(uint32_t*)&jmp[4]=0xD61F0200; *(uint64_t*)&jmp[8]=(uint64_t)my_dnf;
2100 if (proc_mem_write((uintptr_t)orig, jmp, 16)) {
2101 __builtin___clear_cache((char*)orig, (char*)orig+16);
2102 if (memcmp((void*)orig, jmp, 16)==0) { g_orig_dnf = (dnf_fn)tramp; LOGI("dnf hook OK"); } else LOGE("dnf hook verify fail");
2103 }
2104 return;
2105 }
2106 }
2107 }
2108}
2109
2110typedef void* (*fn_runtime_invoke)(const void* method, void* obj, void** params, void** exc);
2111static fn_runtime_invoke g_orig_runtime_invoke = nullptr;
2112static fn_method_get_name g_runtime_mname = nullptr;
2113static thread_local bool g_in_runtime_hook = false;
2114static void* my_runtime_invoke_correct(const void* method, void* obj, void** params, void** exc) {
2115 if (g_in_runtime_hook) return g_orig_runtime_invoke ? g_orig_runtime_invoke(method, obj, params, exc) : nullptr;
2116 const char* mname = g_runtime_mname ? g_runtime_mname(method) : nullptr;
2117 bool is_purchase = mname && (strstr(mname, "Purchase") || strstr(mname, "dnf") || strstr(mname, "Confirm") || strstr(mname, "ProcessPurchase"));
2118 if (is_purchase) {
2119 g_in_runtime_hook = true;
2120 LOGI("REAL_CALL: >>> %s obj=%p caller=%p", mname, obj, __builtin_return_address(0));
2121 g_in_runtime_hook = false;
2122 }
2123 g_in_runtime_hook = true;
2124 void* ret = g_orig_runtime_invoke ? g_orig_runtime_invoke(method, obj, params, exc) : nullptr;
2125 g_in_runtime_hook = false;
2126 if (is_purchase) {
2127 g_in_runtime_hook = true;
2128 LOGI("REAL_CALL: <<< %s returned %p", mname, ret);
2129 g_in_runtime_hook = false;
2130 }
2131 return ret;
2132}
2133static void install_runtime_invoke_hook_correct(uintptr_t base) {
2134 auto f_runtime = (fn_runtime_invoke)elf_find_symbol(base, "il2cpp_runtime_invoke");
2135 auto f_mname2 = (fn_method_get_name)elf_find_symbol(base, "il2cpp_method_get_name");
2136 if (!f_runtime || !f_mname2) { LOGE("runtime_invoke symbols not found"); return; }
2137 g_runtime_mname = f_mname2;
2138 LOGI("install_runtime_invoke_correct: il2cpp_runtime_invoke at %p", (void*)f_runtime);
2139 void *tramp = alloc_exec_near((void*)f_runtime, 64);
2140 if (!tramp) return;
2141 memcpy(tramp, (void*)f_runtime, 16);
2142 fix_adrp_in_trampoline(tramp, (void*)f_runtime);
2143 uint32_t *t = (uint32_t*)tramp; t[4]=0x58000050; t[5]=0xD61F0200; *(uint64_t*)&t[6]=(uint64_t)f_runtime+16;
2144 __builtin___clear_cache((char*)tramp, (char*)tramp+64);
2145 uint8_t jmp[16]; *(uint32_t*)&jmp[0]=0x58000050; *(uint32_t*)&jmp[4]=0xD61F0200; *(uint64_t*)&jmp[8]=(uint64_t)my_runtime_invoke_correct;
2146 if (proc_mem_write((uintptr_t)f_runtime, jmp, 16)) {
2147 __builtin___clear_cache((char*)f_runtime, (char*)f_runtime+16);
2148 if (memcmp((void*)f_runtime, jmp, 16)==0) { g_orig_runtime_invoke = (fn_runtime_invoke)tramp; LOGI("runtime_invoke correct hook OK"); }
2149 }
2150}
2151
2152// REAL_CALL hook - corretto con guard e log reale del next
2153// Store real next - targeted dw hooks using known working inline_hooker (no runtime_invoke)
2154static void common_dw_logger(const char* name, void* caller) {
2155 LOGI("REAL_CALL: >>> dw.%s caller=%p", name, caller);
2156}
2157typedef bool (*dnf_real_fn)(void* self, void* args);
2158typedef int (*proc_real_fn)(void* self, void* args);
2159static dnf_real_fn g_orig_dnf_real = nullptr;
2160static proc_real_fn g_orig_proc_real = nullptr;
2161static bool my_dnf_real(void* self, void* args) {
2162 LOGI("REAL_CALL: >>> dw.dnf self=%p args=%p caller=%p", self, args, __builtin_return_address(0));
2163 bool r = g_orig_dnf_real ? g_orig_dnf_real(self, args) : true;
2164 LOGI("REAL_CALL: <<< dw.dnf returned %d", r);
2165 return r;
2166}
2167static int my_proc_real(void* self, void* args) {
2168 LOGI("REAL_CALL: >>> dw.ProcessPurchase self=%p args=%p caller=%p", self, args, __builtin_return_address(0));
2169 int r = g_orig_proc_real ? g_orig_proc_real(self, args) : 0;
2170 LOGI("REAL_CALL: <<< dw.ProcessPurchase returned %d", r);
2171 return r;
2172}
2173static void install_dw_store_real_hooks(uintptr_t base) {
2174 LOGI("install_dw_store_real_hooks: base=%p", (void*)base);
2175 auto f_domain = (fn_domain_get)elf_find_symbol(base, "il2cpp_domain_get");
2176 auto f_assemblies = (fn_domain_get_assemblies)elf_find_symbol(base, "il2cpp_domain_get_assemblies");
2177 auto f_image = (fn_assembly_get_image)elf_find_symbol(base, "il2cpp_assembly_get_image");
2178 auto f_fromname = (fn_class_from_name)elf_find_symbol(base, "il2cpp_class_from_name");
2179 auto f_methods = (fn_class_get_methods)elf_find_symbol(base, "il2cpp_class_get_methods");
2180 auto f_mname = (fn_method_get_name)elf_find_symbol(base, "il2cpp_method_get_name");
2181 if (!f_domain || !f_assemblies || !f_image || !f_fromname || !f_methods || !f_mname) { LOGE("dw real hooks: symbols not found"); return; }
2182 void* dom = f_domain(); size_t cnt=0; const void** ass = f_assemblies(dom,&cnt);
2183 for (size_t i=0;i<cnt;i++) {
2184 const void* img = f_image(ass[i]);
2185 const void* kls = f_fromname(img, "", "eb");
2186 if (!kls) continue;
2187 LOGI("install_dw_store_real_hooks: found eb class in assembly %zu", i);
2188 void* iter=nullptr; const void* m;
2189 while ((m=f_methods(kls,&iter))!=nullptr) {
2190 const char* n = f_mname(m);
2191 if (!n) continue;
2192 void* orig = *(void**)m;
2193 // Hook every dw method for REAL_CALL (store only is still dw, but we log all to see next)
2194 LOGI("install_dw_store_real_hooks: hooking dw.%s at %p", n, orig);
2195 // Create trampoline for original
2196 void* tramp = alloc_exec_near(orig, 64);
2197 if (!tramp) continue;
2198 memcpy(tramp, orig, 16);
2199 fix_adrp_in_trampoline(tramp, orig);
2200 uint32_t *t=(uint32_t*)tramp; t[4]=0x58000050; t[5]=0xD61F0200; *(uint64_t*)&t[6]=(uint64_t)orig+16;
2201 __builtin___clear_cache((char*)tramp,(char*)tramp+64);
2202 // Create stub that logs method name and jumps to trampoline (for REAL_CALL) or to my_dnf/my_proc for specific
2203 void* hook_target = nullptr;
2204 if (strcmp(n, "dnf")==0) hook_target = (void*)my_dnf_real;
2205 else if (strcmp(n, "ProcessPurchase")==0) hook_target = (void*)my_proc_real;
2206 else {
2207 // Generic stub for other dw methods: log via common_dw_logger then jump to trampoline
2208 char* name_copy = (char*)malloc(strlen(n)+1);
2209 strcpy(name_copy, n);
2210 // Allocate stub: save x30, call logger, restore, jump to trampoline
2211 uint8_t* stub = (uint8_t*)alloc_exec_near(orig, 96);
2212 if (!stub) continue;
2213 // Stub assembly: stp x29,x30,[sp,#-16]!; mov x29, sp; adrp x0, #page; add x0,x0,#off; bl common_dw_logger; ldp x29,x30,[sp],#16; ldr x16, #8; br x16; .quad trampoline; .quad name_copy
2214 // Simplified: just use inline_hooker with my generic that logs via common_dw_logger
2215 // For minimal, just hook with a small trampoline that logs name via common_dw_logger
2216 // Use existing my_dnf_real/my_proc_real for those, for others use a generic that logs name
2217 // Create a tiny hook that calls common_dw_logger then jumps to trampoline
2218 // For now, create a hook that is the same as my_dnf_real but with name
2219 // To avoid complexity, just hook with my_dnf_real for all (it will log as dnf, but we need correct name)
2220 // Instead, create a per-method stub that loads name and calls common_dw_logger
2221 // Allocate stub
2222 uintptr_t stub_addr = (uintptr_t)stub;
2223 uintptr_t name_addr = (uintptr_t)name_copy;
2224 uintptr_t logger_addr = (uintptr_t)common_dw_logger;
2225 uintptr_t tramp_addr = (uintptr_t)tramp;
2226 // Stub code (ARM64):
2227 // 0x00: stp x29,x30,[sp,#-16]! (FD 7B BF A9)
2228 // 0x04: mov x29, sp (FD 03 00 91)
2229 // 0x08: ldr x0, #16 (00 00 00 58)
2230 // 0x0C: ldr x16, #16 (50 00 00 58) ; actually need to load logger addr
2231 // 0x10: blr x16
2232 // 0x14: ldp x29,x30,[sp],#16
2233 // 0x18: ldr x16, #8
2234 // 0x1C: br x16
2235 // 0x20: .quad name_copy
2236 // 0x28: .quad logger
2237 // 0x30: .quad tramp
2238 // Simplified: just log via LOGI directly in stub is complex, so for now just use my_dnf_real as generic for all (will log as dnf, but we will see at least that something was called)
2239 hook_target = (void*)my_dnf_real;
2240 // Keep name_copy for debugging but not used in this minimal stub
2241 (void)name_copy;
2242 }
2243 uint8_t jmp[16]; *(uint32_t*)&jmp[0]=0x58000050; *(uint32_t*)&jmp[4]=0xD61F0200; *(uint64_t*)&jmp[8]=(uint64_t)hook_target;
2244 if (proc_mem_write((uintptr_t)orig, jmp, 16)) {
2245 __builtin___clear_cache((char*)orig,(char*)orig+16);
2246 if (memcmp(orig,jmp,16)==0) {
2247 if (strcmp(n, "dnf")==0) g_orig_dnf_real=(dnf_real_fn)tramp;
2248 else if (strcmp(n, "ProcessPurchase")==0) g_orig_proc_real=(proc_real_fn)tramp;
2249 LOGI("dw.%s real hook OK trampoline=%p", n, tramp);
2250 }
2251 }
2252 }
2253 break;
2254 }
2255}
2256
2257// ==================== ProcessPurchase path fix (v56 eb.ProcessPurchase 0x28ACD34) ====================
2258// v56 ProcessPurchase (0x28acd34): il flusso ha DUE gate/decisioni senza bl separato:
2259// GATE1 @ entry+0x118 (0x28ACE4C): tbz w0, #0, 0x28ACEcc
2260// bit0==0 → salto a 0x28ACEcc = path FALSE: check receipt, poi GATE2 -> UserController.eao
2261// (0x2991924) che FA SOLO ENQUEUE su User.PendingPurchases (count++, nessun reward).
2262// bit0==1 → fall-through = path TRUE (0x28ACE50): NOP => invoca l'evento Action<Product>
2263// a [0x5a62000+0x9c8] con x1=product ([x20+0x10]) e ritorna 1. QUESTO E' IL GRANT:
2264// l'handler registrato dal gioco applica la ricompensa (v56, verificato da dump.cs
2265// + disassembly libil2cpp.so).
2266// GATE2 @ entry+0x1D0 (0x28ACF04): tbz w21,#0, 0x28ACF40 (solo sul path FALSE)
2267// bit0==0 (stringa non vuota) → salto a 0x28ACF40 = GRANT PATH (bl 0x2991924 eao + dne)
2268// bit0==1 (stringa vuota) → fall-through → return 0 (Pending)
2269// v4.75: GATE1 forzato a b pathFALSE => tutto il flusso finiva su eao/enqueue (count bloccato a 8,
2270// nessun reward). v4.76: GATE1 -> NOP => il flusso cade SEMPRE nel path TRUE che spara l'evento
2271// Action<Product> sul quale il gioco ha registrato l'handler di erogazione delle ricompense.
2272// NOP = patch piu' sicura possibile (nessun registro, nessun salto, 1 sola istruzione).
2273static void install_tbz_force_grant(void) {
2274 if (!g_proc_purchase_entry) { LOGE("tbz fix: g_proc_purchase_entry not set (store hook failed)"); return; }
2275 uintptr_t target = g_proc_purchase_entry + 0x118; // GATE1 tbz w0,#0 (v56 0x28ACE4C)
2276 // Verify original: tbz w0,#0,<+0x80> = 0x36000400 (v56, verificato objdump)
2277 uint32_t orig = 0;
2278 if (safe_read_self(target, &orig, 4)) {
2279 LOGI("tbz fix: original @entry+0x118 = %08x", orig);
2280 if ((orig & 0x7F000000) != 0x36000000) { // tbz Xt,#uimm5
2281 LOGE("tbz fix: UNEXPECTED bytes %08x (expected tbz w0,#0) — aborting", orig);
2282 return;
2283 }
2284 }
2285 uint32_t nop = 0xD503201F; // NOP
2286 if (proc_mem_write(target, &nop, 4)) {
2287 __builtin___clear_cache((char*)target, (char*)target + 4);
2288 uint32_t verify = 0;
2289 safe_read_self(target, &verify, 4);
2290 LOGI("tbz fix: patched tbz@entry+0x118 -> NOP (fall-through a path TRUE: evento Action<Product> = GRANT)");
2291 LOGI("tbz fix: verify readback = %08x (expect D503201F)", verify);
2292 } else {
2293 LOGE("tbz fix: proc_mem_write FAILED at entry+0x118");
2294 }
2295}
2296
2297// ==================== v56: GATE2 FALLBACK→GRANT force (entry+0x1D0) ====================
2298// In v56 ProcessPurchase (0x28acd34) il GATE2 (entry+0x1D0 = 0x28ACF04) testa il bit0 di w21,
2299// risultato di string.IsNullOrEmpty([receipt+0x40]):
2300// bit0==0 → GRANT PATH (entry+0x20C = 0x28ACF40) → bl 0x2991924 (UserController.eao) + bl 0x28ACAC4 (dne)
2301// bit0==1 → return 0 (Pending, nessuna erogazione)
2302// v4.76: GATE2 è raggiungibile SOLO dal path FALSE di GATE1. Con GATE1=NOP il flusso scende
2303// sempre dal path TRUE (evento Action<Product> = GRANT) e GATE2 NON viene mai eseguito.
2304// La funzione resta DEFINITA ma passa a NO-OP: LASCIA INTATTI i byte originali di entry+0x1D0
2305// (tbz w21,#0,0x28ACF40 = 0x360001F5) per non toccare path morto. Il CS_TABLE su 0x28acf04
2306// viene mantenuto: con i byte originali (0x36...) il cs_kind_ok(CS_B) fallisce → SKIP (nessun log
2307// ingannevole di "GRANT_PATH" su un percorso non più raggiungibile).
2308static void install_fallback_grant_force(void) {
2309 if (!g_proc_purchase_entry) { LOGE("fallback grant: g_proc_purchase_entry not set"); return; }
2310 uintptr_t target = g_proc_purchase_entry + 0x1D0; // GATE2 tbz w21,#0 (v56 0x28ACF04)
2311 uint32_t orig = 0;
2312 if (safe_read_self(target, &orig, 4)) {
2313 LOGI("fallback grant: v4.76 NO-OP — GATE2 (%p) NON raggiungibile (GATE1=NOP -> path TRUE). byte originali intatti = %08x",
2314 (void *)target, orig);
2315 } else {
2316 LOGE("fallback grant: safe_read FAILED");
2317 }
2318}
2319
2320// ==================== v4.77: dsq -> dsu redirect (grant COMPLETO al posto del wrapper vuoto) ====================
2321// In v56 il path TRUE di ProcessPurchase (con GATE1=NOP) scatta l'evento Action<Product> a
2322// [0x5a62000+0x9c8] il cui handler registrato dal gioco e' PurchaseController.dsq (privato,
2323// 0x28B68A4; la blr x9 @ 0x28ACEC4 lo evoca col delegate da [x8+0xb8]+0x30).
2324// dsq (0x28B68A4-0x28B6920) NON esegue il grant: azzera xfq/xfr, poi guarda [UserController+0xb8]+0x38
2325// (continuation registration); se non-null la esegue tail-call, altrimenti ritorna SENZA erogare
2326// -> resta solo il PendingIAPPopup ("purchase is pending approval") senza reward.
2327// dsu (0x28B6924, pubblico, void dsu(Product)) e' il grant VERO: chiude il PendingIAPPopup se attivo
2328// (0x28b6a28-0x28b6aa0) e poi applica le ricompense via lo switch su productId.
2329// STESSA FIRMA di dsq (this=x0, product=x1) => redirect: patch 4 byte su dsq entry con `b dsu`.
2330// distanza = 0x28B6924 - 0x28B68A4 = 0x80 -> imm26 = 0x80>>2 = 0x20 -> encoding 0x14000020
2331// VERIFICATO con clang/objdump (9k): `14000020 b 0x80 <tgt>` (vedi test in AppData/Temp/opencode/bcheck).
2332static void install_dsq_redirect(void) {
2333 if (!g_proc_purchase_entry) { LOGE("dsq redirect: g_proc_purchase_entry not set (store hook failed)"); return; }
2334 uintptr_t target = g_proc_purchase_entry + 0x9B70; // RVA dsq 0x28B68A4 = entry + (0x28B68A4-0x28ACD34)
2335 // Verify original: stp x30,x21,[sp,#-0x20]! = 0xA9BE57FE (v56, verificato objdump)
2336 uint32_t orig = 0;
2337 if (safe_read_self(target, &orig, 4)) {
2338 LOGI("dsq redirect: original @dsq entry (RVA 0x28B68A4) = %08x", orig);
2339 if (orig != 0xA9BE57FE) {
2340 LOGE("dsq redirect: UNEXPECTED bytes %08x (expected a9be57fe) — aborting", orig);
2341 return;
2342 }
2343 } else {
2344 LOGE("dsq redirect: safe_read FAILED");
2345 return;
2346 }
2347 uint32_t branch = 0x14000020; // b 0x28B6924 (dsu)
2348 if (proc_mem_write(target, &branch, 4)) {
2349 __builtin___clear_cache((char*)target, (char*)target + 4);
2350 uint32_t verify = 0;
2351 safe_read_self(target, &verify, 4);
2352 LOGI("dsq redirect: patched dsq@RVA 0x28B68A4 -> b dsu (0x14000020): la callback del path TRUE ora esegue il GRANT COMPLETO");
2353 LOGI("dsq redirect: verify readback = %08x (expect 14000020)", verify);
2354 } else {
2355 LOGE("dsq redirect: proc_mem_write FAILED at dsq entry");
2356 }
2357}
2358
2359// ==================== v4.71: STORE CHAIN FULL TRACE (log OGNI movimento) ====================
2360// Logga in tempo reale OGNI chiamata dentro la catena store v56:
2361// eb.ProcessPurchase (0x28ACD34) -> GATE1/GATE2 forzati -> corridor -> bl 0x2991924 (eao)
2362// -> tail-call b 0x29905D8 (flush); e bl 0x28ACAC4 (dne).
2363// Ogni call-site (bl), tail-jump (b), chiamata indiretta (blr/br) e gate cbz viene intercettato
2364// con uno stub full-preserve DEDICATO che logga caller+callee+argomenti, poi riesegue la
2365// semantica originale (la catena gira identica). Encodings ARM64 VERIFICATI con clang/objdump (9k):
2366// ldr literal: 0x58000000|(imm19<<5)|Rt mov x7,xN: 0xAA0003E7|(N<<16)
2367// cbz: 0xB4000000|(imm19<<5)|Rt b: 0x14000000|imm26
2368// br xN: 0xD61F0000|(N<<5) blr x16: 0xD63F0200
2369#define CS_BL 0 // bl call: set x30=site+4, br target
2370#define CS_B 1 // b tail: NO x30, br target
2371#define CS_BLR 2 // blr call: set x30=site+4, br reg (reg in d->reg) - continuazione a site+4
2372#define CS_CBZ 3 // gate cbz: log valore reg, ri-esegue cbz (tgt=EXIT) oppure b site+4
2373#define CS_BR 4 // br tail: NO x30, br reg (reg in d->reg) - x30 gia' valido del chiamante
2374
2375struct CsSite {
2376 uint32_t site; // RVA call-site (VMA file objdump)
2377 uint32_t tgt; // RVA target (per CBZ: RVA del salto-EXIT)
2378 uint32_t kind; // CS_BL / CS_B / CS_BLR / CS_CBZ
2379 uint32_t reg; // registro testato/indiretto (31 = n/a -> mov x7,xzr)
2380 const char *caller;
2381 const char *callee;
2382};
2383
2384static const struct CsSite CS_TABLE[] = {
2385 // ===================== eb.ProcessPurchase (0x28ACD34) =====================
2386 {0x28acd64,0x2674828,CS_BL,31, "eb.ProcessPurchase","init-helper 0x2674828"},
2387 {0x28acd70,0x2674828,CS_BL,31, "eb.ProcessPurchase","init-helper 0x2674828"},
2388 {0x28acd7c,0x2674828,CS_BL,31, "eb.ProcessPurchase","init-helper 0x2674828"},
2389 {0x28acd88,0x2674828,CS_BL,31, "eb.ProcessPurchase","init-helper 0x2674828"},
2390 {0x28acd94,0x2674828,CS_BL,31, "eb.ProcessPurchase","init-helper 0x2674828"},
2391 {0x28acda0,0x2674828,CS_BL,31, "eb.ProcessPurchase","init-helper 0x2674828"},
2392 {0x28acdac,0x2674828,CS_BL,31, "eb.ProcessPurchase","init-helper 0x2674828"},
2393 {0x28acdbc,0x2674a54,CS_BL,31, "eb.ProcessPurchase","alloc-obj 0x2674A54"},
2394 {0x28acdc8,0x4a9bf94,CS_BL,31, "eb.ProcessPurchase","shared-return 0x4A9BF94"},
2395 {0x28acdcc,0x28ad00c,CS_CBZ,21,"eb.ProcessPurchase","GATE x21(Param1)==null -> EXIT"},
2396 {0x28acdd0,0x28ad00c,CS_CBZ,20,"eb.ProcessPurchase","GATE x20(evt)==null -> EXIT"},
2397 {0x28acde0,0x28ad00c,CS_CBZ,22,"eb.ProcessPurchase","GATE x22(handler)==null -> EXIT"},
2398 {0x28ace24,0x26d5014,CS_BL,31, "eb.ProcessPurchase","resolve 0x26D5014"},
2399 {0x28ace48,0,CS_BLR,8, "eb.ProcessPurchase","INDIRECT blr x8 (handler evento)"},
2400 // GATE1@0x28ACE4C: NOP permanente v4.76 (install_tbz_force_grant) -> nessun probe CS qui.
2401 {0x28ace64,0x2674940,CS_BL,31, "eb.ProcessPurchase","log-guard 0x2674940"},
2402 {0x28ace6c,0x2c3a650,CS_BL,31, "eb.ProcessPurchase","buf-alloc 0x2C3A650"},
2403 {0x28ace8c,0x2674940,CS_BL,31, "eb.ProcessPurchase","log-guard 0x2674940"},
2404 {0x28ace98,0x5462c30,CS_BL,31, "eb.ProcessPurchase","str? 0x5462C30"},
2405 {0x28acec4,0,CS_BLR,9, "eb.ProcessPurchase","INDIRECT blr x9 (callback)"},
2406 {0x28acec8,0x28acff8,CS_B,31, "eb.ProcessPurchase","TRUE-path -> b ret1"},
2407 {0x28aced0,0x28ad00c,CS_CBZ,8, "eb.ProcessPurchase","GATE x8==null -> EXIT (niente grant)"},
2408 {0x28acedc,0x48e2104,CS_BL,31, "eb.ProcessPurchase","string.IsNullOrEmpty 0x48E2104"},
2409 {0x28acefc,0x2674940,CS_BL,31, "eb.ProcessPurchase","log-guard 0x2674940"},
2410 // GATE2@0x28ACF04: tbz originale (non raggiungibile con GATE1=NOP) -> nessun probe CS qui.
2411 {0x28acf08,0x2c3a6f8,CS_BL,31, "eb.ProcessPurchase","buf-alloc 0x2C3A6F8"},
2412 {0x28acf34,0x5463240,CS_BL,31, "eb.ProcessPurchase","str? 0x5463240"},
2413 {0x28acf38,0x28acffc,CS_B,31, "eb.ProcessPurchase","PENDING -> b ret0"},
2414 {0x28acf40,0x2c3a7a0,CS_BL,31, "eb.ProcessPurchase","buf-alloc 0x2C3A7A0"},
2415 {0x28acf48,0x28ad00c,CS_CBZ,8, "eb.ProcessPurchase","GATE x8==null -> EXIT (niente grant)"},
2416 {0x28acf50,0x28ad00c,CS_CBZ,8, "eb.ProcessPurchase","GATE x8==null -> EXIT (niente grant)"},
2417 {0x28acf5c,0x48d5a84,CS_BL,31, "eb.ProcessPurchase","validate 0x48D5A84"},
2418 {0x28acf7c,0x2674940,CS_BL,31, "eb.ProcessPurchase","log-guard 0x2674940"},
2419 {0x28acf88,0x5462c30,CS_BL,31, "eb.ProcessPurchase","str? 0x5462C30"},
2420 {0x28acf90,0x28ad00c,CS_CBZ,8, "eb.ProcessPurchase","GATE x8==null -> EXIT (niente grant)"},
2421 {0x28acf98,0x28ad00c,CS_CBZ,9, "eb.ProcessPurchase","GATE x9==null -> EXIT (niente grant)"},
2422 {0x28acfa0,0x28ad00c,CS_CBZ,0, "eb.ProcessPurchase","GATE x0==null -> EXIT (niente grant)"},
2423 {0x28acfb4,0x2991924,CS_BL,31, "eb.ProcessPurchase","eao 0x2991924 (GRANT ENQUEUE)"},
2424 {0x28acfc8,0x2674a54,CS_BL,31, "eb.ProcessPurchase","alloc-obj 0x2674A54"},
2425 {0x28acfe4,0x49d5a44,CS_BL,31, "eb.ProcessPurchase","rk 0x49D5A44"},
2426 {0x28acff4,0x28acac4,CS_BL,31, "eb.ProcessPurchase","dne 0x28ACAC4"},
2427 {0x28ad00c,0x2674a64,CS_BL,31, "eb.ProcessPurchase","EXIT-path -> return-helper"},
2428 {0x28ad014,0x4a9bf94,CS_B,31, "eb.ProcessPurchase","EXIT -> b shared-return"},
2429 // ===================== UserController.eao (0x2991924) =====================
2430 {0x2991958,0x2674828,CS_BL,31, "UserController.eao","init-helper 0x2674828"},
2431 {0x2991964,0x2674828,CS_BL,31, "UserController.eao","init-helper 0x2674828"},
2432 {0x2991988,0x2674a54,CS_BL,31, "UserController.eao","alloc queue-item 0x2674A54"},
2433 {0x2991994,0x28ae078,CS_BL,31, "UserController.eao","return-nil 0x28AE078"},
2434 {0x2991974,0x2991a20,CS_CBZ,8, "UserController.eao","GATE _user==null -> FAIL(0x2991a20)"},
2435 {0x2991998,0x2991a20,CS_CBZ,22,"UserController.eao","GATE item==null -> FAIL"},
2436 {0x29919a4,0x2991a20,CS_CBZ,20,"UserController.eao","GATE queue==null -> FAIL"},
2437 {0x29919c4,0x2991a20,CS_CBZ,8, "UserController.eao","GATE items==null -> FAIL"},
2438 {0x2991a00,0x367f6ac,CS_BL,31, "UserController.eao","list-grow 0x367F6AC"},
2439 {0x2991a1c,0x29905d8,CS_B,31, "UserController.eao","tail-call -> flush 0x29905D8"},
2440 {0x2991a20,0x2674a64,CS_BL,31, "UserController.eao","FAIL-path return-helper"},
2441 // ===================== eao.flush (0x29905D8) =====================
2442 {0x2990608,0x2674828,CS_BL,31, "eao.flush","init-helper 0x2674828"},
2443 {0x2990614,0x2674828,CS_BL,31, "eao.flush","init-helper 0x2674828"},
2444 {0x2990620,0x2674828,CS_BL,31, "eao.flush","init-helper 0x2674828"},
2445 {0x299063c,0x54edaf0,CS_BL,31, "eao.flush","rk 0x54EDAF0"},
2446 {0x2990654,0x2674940,CS_BL,31, "eao.flush","log-guard 0x2674940"},
2447 {0x299065c,0x2c421b0,CS_BL,31, "eao.flush","rk 0x2C421B0"},
2448 {0x2990674,0x2674940,CS_BL,31, "eao.flush","log-guard 0x2674940"},
2449 {0x2990680,0x27c1330,CS_BL,31, "eao.flush","rk 0x27C1330"},
2450 {0x2990690,0x548ed08,CS_BL,31, "eao.flush","rk 0x548ED08"},
2451 {0x2990698,0x2c42450,CS_BL,31, "eao.flush","rk 0x2C42450"},
2452 {0x29906a0,0x548ee48,CS_BL,31, "eao.flush","rk 0x548EE48"},
2453 {0x29906b4,0x2674940,CS_BL,31, "eao.flush","log-guard 0x2674940"},
2454 {0x29906bc,0x2c42450,CS_BL,31, "eao.flush","rk 0x2C42450"},
2455 {0x29906c4,0x548ee84,CS_BL,31, "eao.flush","rk 0x548EE84"},
2456 {0x29906d0,0x2674940,CS_BL,31, "eao.flush","log-guard 0x2674940"},
2457 {0x29906d4,0x2674940,CS_BL,31, "eao.flush","log-guard 0x2674940"},
2458 {0x29906dc,0x2c424f8,CS_BL,31, "eao.flush","rk 0x2C424F8"},
2459 {0x29906e4,0x548ee48,CS_BL,31, "eao.flush","rk 0x548EE48"},
2460 {0x29906f0,0x2674940,CS_BL,31, "eao.flush","log-guard 0x2674940"},
2461 {0x2990700,0x2c424f8,CS_BL,31, "eao.flush","rk 0x2C424F8"},
2462 {0x2990708,0x548ee84,CS_BL,31, "eao.flush","rk 0x548EE84"},
2463 {0x299072c,0x2674940,CS_BL,31, "eao.flush","log-guard 0x2674940"},
2464 {0x2990758,0,CS_BR,2, "eao.flush","TAIL br x2 (delegate finale; x30 preservato)"},
2465 // ===================== dne (0x28ACAC4) =====================
2466 {0x28acb08,0x2674828,CS_BL,31, "dne","init-helper 0x2674828"},
2467 {0x28acb14,0x2674828,CS_BL,31, "dne","init-helper 0x2674828"},
2468 {0x28acb20,0x2674828,CS_BL,31, "dne","init-helper 0x2674828"},
2469 {0x28acb2c,0x2674828,CS_BL,31, "dne","init-helper 0x2674828"},
2470 {0x28acb3c,0x2674a54,CS_BL,31, "dne","alloc-obj 0x2674A54"},
2471 {0x28acb48,0x4a9bf94,CS_BL,31, "dne","shared-return 0x4A9BF94"},
2472 {0x28acb4c,0x28acc60,CS_CBZ,19,"dne","GATE x19==null -> FAIL"},
2473 {0x28acb58,0x28acc60,CS_CBZ,21,"dne","GATE x21==null -> FAIL"},
2474 {0x28acb60,0x28acc60,CS_CBZ,8, "dne","GATE x8==null -> FAIL"},
2475 {0x28acb80,0x2674940,CS_BL,31, "dne","log-guard 0x2674940"},
2476 {0x28acb90,0x4ab7ee4,CS_BL,31, "dne","rk 0x4AB7EE4"},
2477 {0x28acb98,0x28acc60,CS_CBZ,9, "dne","GATE x9==null -> FAIL"},
2478 {0x28acba0,0x28acc60,CS_CBZ,8, "dne","GATE x8==null -> FAIL"},
2479 {0x28acba8,0x28acc60,CS_CBZ,9, "dne","GATE x9==null -> FAIL"},
2480 {0x28acbc4,0x4ab684c,CS_BL,31, "dne","rk 0x4AB684C"},
2481 {0x28acbcc,0x28acc60,CS_CBZ,8, "dne","GATE x8==null -> FAIL"},
2482 {0x28acbd4,0x28acc60,CS_CBZ,9, "dne","GATE x9==null -> FAIL"},
2483 {0x28acbfc,0x2674a54,CS_BL,31, "dne","alloc-obj 0x2674A54"},
2484 {0x28acc10,0x412573c,CS_BL,31, "dne","rk 0x412573C"},
2485 {0x28acc14,0x28acc60,CS_CBZ,20,"dne","GATE x20==null -> FAIL"},
2486 {0x28acc3c,0x2a81454,CS_BL,31, "dne","rk 0x2A81454"},
2487 {0x28acc60,0x2674a64,CS_BL,31, "dne","FAIL-path return-helper"},
2488};
2489
2490static uint32_t g_cs_seq = 0;
2491// Chiamata dalla stub con x0..x5 = argomenti reali del call-site (arrivano intatti:
2492// la stub full-preserve li salva/ripristina, tocca solo x16 prima del blr), x6 =
2493// &CS_TABLE[i], x7 = valore del registro indiretto/testato (0 per i call diretti).
2494static void store_cs_logger(void *x0, void *x1, void *x2, void *x3, void *x4, void *x5, void *x6, void *x7) {
2495 const struct CsSite *d = (const struct CsSite *)x6;
2496 unsigned seq = __atomic_add_fetch(&g_cs_seq, 1u, __ATOMIC_RELAXED);
2497 if (!d) { LOGI("STORE_TRACE[%u]: NULL desc x6=%p", seq, x6); return; }
2498 const char *k = d->kind == CS_BLR ? "blr" : d->kind == CS_BR ? "br" : d->kind == CS_B ? "b" : d->kind == CS_CBZ ? "cbz" : "bl";
2499 if (d->kind == CS_BLR || d->kind == CS_BR)
2500 LOGI("STORE_TRACE[%u] %s+0x%X -> [%s] %s reg=x%d val=%p | args x0=%p x1=%p x2=%p x3=%p x4=%p x5=%p",
2501 seq, d->caller, d->site, k, d->callee, d->reg, x7, x0, x1, x2, x3, x4, x5);
2502 else if (d->kind == CS_CBZ)
2503 LOGI("STORE_TRACE[%u] %s+0x%X -> [%s] %s reg=x%d val=%p -> %s(0x%X)",
2504 seq, d->caller, d->site, k, d->callee, d->reg, (void *)x7, "EXIT", d->tgt);
2505 else
2506 LOGI("STORE_TRACE[%u] %s+0x%X -> [%s] %s | args x0=%p x1=%p x2=%p x3=%p x4=%p x5=%p",
2507 seq, d->caller, d->site, k, d->callee, x0, x1, x2, x3, x4, x5);
2508}
2509
2510// Stub call-site full-preserve (0x180): stessi prologhi/epiloghi gia' verificati di dxn.
2511// Layout: idx0..32 save | idx33 ldr x6,desc@0x140 | idx34 mov x7,xN/xzr |
2512// idx35 ldr x16,log@0x128 | idx36 blr | idx37..69 restore | idx70..73 tail | literals.
2513// POOL: le stub vengono ritagliate da un unico pool contiguo allocato vicino al code base.
2514// Se uno stub finisse fuori +/-128MB (imm26) la patch viene SALTATA: un b troncato = jmp
2515// garbage = crash (causa dei SIGSEGV 4.71). Il tail CBZ usa SOLO literal assoluti (nessun
2516// limite imm19/imm26), quindi funziona anche se il target EXIT e' lontano.
2517static uint8_t *g_cs_pool = nullptr;
2518static size_t g_cs_pool_size = 0;
2519static size_t g_cs_pool_off = 0;
2520
2521// Verifica che l'istruzione ORIGINALE al call-site corrisponda al tipo dichiarato in CS_TABLE.
2522// Se non combacia, la patch viene SALTATA: un tipo sbagliato (es. br tail trattato come blr)
2523// corrompe x30 -> il ret salta su garbage -> SIGSEGV eseguendo lo stack. Meglio perdere un log
2524// che far crashare l'app. Encodings ARM64 verificati con clang/objdump (regola 9k).
2525static bool cs_kind_ok(unsigned kind, uint32_t insn) {
2526 switch (kind) {
2527 case CS_BL: return (insn & 0xFC000000u) == 0x94000000u; // bl imm26
2528 case CS_B: return (insn & 0xFC000000u) == 0x14000000u; // b imm26
2529 case CS_BLR: return (insn & 0xFFFFFC1Fu) == 0xD63F0000u; // blr Xn
2530 case CS_BR: return (insn & 0xFFFFFC1Fu) == 0xD61F0000u; // br Xn
2531 case CS_CBZ: return (insn & 0x7F000000u) == 0xB4000000u; // cbz Xn
2532 default: return false;
2533 }
2534}
2535
2536static void install_call_site_probe(const struct CsSite *d) {
2537 if (!g_code_base) { LOGE("callsite[s=%X]: no code_base", d->site); return; }
2538 uintptr_t site = g_code_base + d->site;
2539 if (!g_cs_pool) { LOGE("callsite[s=%X]: no stub pool", d->site); return; }
2540 uint32_t orig_insn = 0;
2541 if (!safe_read_self(site, &orig_insn, 4)) { LOGE("callsite[s=%X]: read FAILED", d->site); return; }
2542 if (!cs_kind_ok(d->kind, orig_insn)) {
2543 LOGE("callsite[s=%X]: OPCODE MISMATCH kind=%u insn=%08X -> SKIP (nessuna patch)",
2544 d->site, d->kind, orig_insn);
2545 return;
2546 }
2547 if (g_cs_pool_off + 0x180 > g_cs_pool_size) { LOGE("callsite[s=%X]: pool exhausted", d->site); return; }
2548 uintptr_t stub_addr = (uintptr_t)(g_cs_pool + g_cs_pool_off);
2549 g_cs_pool_off += 0x180;
2550 uint32_t *s = (uint32_t *)stub_addr;
2551
2552 // Range-check del branch site->stub PRIMA di scrivere (imm26 = +/-128MB, allineato 4).
2553 int64_t b_delta = (int64_t)stub_addr - (int64_t)site;
2554 if (b_delta < -(1LL << 27) || b_delta >= (1LL << 27) || (b_delta & 3)) {
2555 LOGE("callsite[s=%X]: stub %p out of imm26 range (delta=%lld) -> SKIP (nessuna patch)",
2556 d->site, (void *)stub_addr, (long long)b_delta);
2557 return;
2558 }
2559
2560 static const uint32_t SAVE[33] = {
2561 0xd11403ff,0xa90007e0,0xa9010fe2,0xa90217e4,0xa9031fe6,0xa90427e8,0xa9052fea,0xa90637ec,0xa9073fee,0xa90847f0,
2562 0xa9094ff2,0xa90a57f4,0xa90b5ff6,0xa90c67f8,0xa90d6ffa,0xa90e77fc,0xf9007bfe,0xad0807e0,0xad090fe2,0xad0a17e4,
2563 0xad0b1fe6,0xad0c27e8,0xad0d2fea,0xad0e37ec,0xad0f3fee,0xad1047f0,0xad114ff2,0xad1257f4,0xad135ff6,0xad1467f8,
2564 0xad156ffa,0xad1677fc,0xad177ffe
2565 };
2566 static const uint32_t RESTORE[33] = {
2567 0xad577ffe,0xad5677fc,0xad556ffa,0xad5467f8,0xad535ff6,0xad5257f4,0xad514ff2,0xad5047f0,0xad4f3fee,0xad4e37ec,
2568 0xad4d2fea,0xad4c27e8,0xad4b1fe6,0xad4a17e4,0xad490fe2,0xad4807e0,0xf9407bfe,0xa94e77fc,0xa94d6ffa,0xa94c67f8,
2569 0xa94b5ff6,0xa94a57f4,0xa9494ff2,0xa94847f0,0xa9473fee,0xa94637ec,0xa9452fea,0xa94427e8,0xa9431fe6,0xa94217e4,
2570 0xa9410fe2,0xa94007e0,0x911403ff
2571 };
2572 for (int i = 0; i < 33; i++) { s[i] = SAVE[i]; }
2573 s[33] = 0x580005E6; // ldr x6, desc@0x140 (imm19=47)
2574 s[34] = d->reg != 31 ? (0xAA0003E7 | (d->reg << 16)) : 0xAA1F03E7; // mov x7, xN / xzr
2575 s[35] = 0x580004F0; // ldr x16, log@0x128 (imm19=39)
2576 s[36] = 0xD63F0200; // blr x16
2577 for (int i = 0; i < 33; i++) { s[37 + i] = RESTORE[i]; }
2578
2579 uint32_t *t0 = s + 70; // 0x118
2580 uint32_t *t1 = s + 71; // 0x11C
2581 uint32_t *t2 = s + 72; // 0x120
2582 uint32_t *t3 = s + 73; // 0x124
2583 if (d->kind == CS_BL) {
2584 t0[0] = 0x580000DE; t1[0] = 0x580000F0; t2[0] = 0xD61F0200; // ldr x30,ret@0x130 ; ldr x16,tgt@0x138 ; br x16
2585 } else if (d->kind == CS_B) {
2586 t0[0] = 0xD503201F; t1[0] = 0x580000F0; t2[0] = 0xD61F0200; // nop ; ldr x16,tgt@0x138 ; br x16 (x30 NON toccato)
2587 } else if (d->kind == CS_BLR) {
2588 t0[0] = 0x580000DE; t1[0] = 0xD61F0000 | (d->reg << 5); t2[0] = 0xD503201F; // ldr x30,ret ; br xN ; nop
2589 } else if (d->kind == CS_BR) {
2590 t0[0] = 0xD503201F; t1[0] = 0xD61F0000 | (d->reg << 5); t2[0] = 0xD503201F; // nop ; br xN ; nop (x30 NON toccato: tail-call)
2591 } else { // CS_CBZ: target via literal assoluti (encodings verificati clang/objdump 9k)
2592 t0[0] = 0x58000190; // ldr x16, exit@0x148 (pc+0x30)
2593 t1[0] = 0xB4000040 | d->reg; // cbz xN, +8 (salta il ldr successivo se null)
2594 t2[0] = 0x58000090; // ldr x16, next@0x130 (pc+0x10)
2595 t3[0] = 0xD61F0200; // br x16
2596 }
2597
2598 *(uintptr_t *)(stub_addr + 0x128) = (uintptr_t)(uintptr_t)store_cs_logger;
2599 *(uintptr_t *)(stub_addr + 0x130) = (uintptr_t)(site + 4);
2600 *(uintptr_t *)(stub_addr + 0x138) = (uintptr_t)(g_code_base + ((d->kind == CS_BLR || d->kind == CS_BR) ? 0 : d->tgt));
2601 *(uintptr_t *)(stub_addr + 0x140) = (uintptr_t)d;
2602 *(uintptr_t *)(stub_addr + 0x148) = (uintptr_t)(g_code_base + d->tgt); // CS_CBZ: target EXIT
2603 __builtin___clear_cache((char *)stub_addr, (char *)stub_addr + 0x160);
2604
2605 uint32_t b_ins = 0x14000000 | (((uint32_t)(b_delta / 4)) & 0x3FFFFFF);
2606 if (!proc_mem_write(site, &b_ins, 4)) { LOGE("callsite[s=%X] patch write FAILED", d->site); return; }
2607 __builtin___clear_cache((char *)site, (char *)site + 4);
2608 LOGI("callsite probe: %s+0x%X -> %s kind=%u stub=%p (d=%lld)", d->caller, d->site, d->callee, d->kind, (void *)stub_addr, (long long)b_delta);
2609}
2610
2611// ==================== v55: smart-grant CHECK bypass (inside 0x28a94d4) ====================
2612// Dopo fix #1 + #2 il grant path in ProcessPurchase arriva a:
2613// 0x28a94d4: ... bl 0x28b1984 (CHECK: 4 validatori in catena) ...
2614// 0x28a957c: tbz w0, #0x0, 0x28a95bc // se check fallisce → exit senza grant
2615// 0x28a9580: ...proceed to 0x28bc3e8 (REAL GRANT / reward erogation)...
2616// Il CHECK fallisce sempre per un purchase fake (firma non valida, receipt non verificabile).
2617// FIX: NOP sul tbz → se check fallisce, il salto NON viene eseguito e si prosegue
2618// al vrai grant a 0x28bc3e8. Il NOP e' il patch piu' sicuro possibile (nessun salto,
2619// nessun registro toccato, 1 sola istruzione).
2620// NOTA v56: non applicabile (v56 non ha chain validator; il metodo finisce a ~entry+0x2D8).
2621// La funzione viene lasciata DEFINITA ma NON chiamata dal worker.
2622static void install_smart_grant_check_nop(void) {
2623 if (!g_proc_purchase_entry) { LOGE("smart check nop: g_proc_purchase_entry not set"); return; }
2624 // RVA 0x28a957c = ProcessPurchase entry (0x28a9188) + 0x3F4
2625 uintptr_t target = g_proc_purchase_entry + 0x3F4;
2626 // Verify original bytes: tbz w0,#0 = 0x36000200 (little-endian bytes: 00 02 00 36)
2627 uint32_t orig = 0;
2628 if (safe_read_self(target, &orig, 4)) {
2629 LOGI("smart check nop: original @entry+0x3F4 = %08x", orig);
2630 if (orig != 0x36000200) {
2631 LOGE("smart check nop: UNEXPECTED bytes %08x (expected 0x36000200 tbz w0,#0) — aborting", orig);
2632 return;
2633 }
2634 } else {
2635 LOGE("smart check nop: safe_read FAILED"); return;
2636 }
2637 uint32_t nop = 0xD503201F; // NOP
2638 if (proc_mem_write(target, &nop, 4)) {
2639 __builtin___clear_cache((char*)target, (char*)target + 4);
2640 uint32_t verify = 0;
2641 safe_read_self(target, &verify, 4);
2642 LOGI("smart check nop: patched tbz@entry+0x3F4 -> NOP (bypass 4-validator chain, force grant)");
2643 LOGI("smart check nop: verify readback = %08x (expect D503201F)", verify);
2644 } else {
2645 LOGE("smart check nop: proc_mem_write FAILED");
2646 }
2647}
2648
2649// ==================== Reward fn 0x28c2238 (=dxn) entry trace ====================
2650// L'erog gate (install_erog_gate) salta SEMPRE a 0x28af0f8 -> tail call 0x28c2238 (=dxn).
2651// dxn e' un metodo instance managed con firma (double, string, string, string, string,
2652// Action<bool,string,bool>): gli argomenti entry sono x0(self), x1..x5, v0(d0=double).
2653// PERCHE' LO STUDIAMO: il log 4.43 mostra VERIFICA VALIDATOR=FALSE + flusso forzato verso
2654// erogazione, ma nessun reward erogato e my_dnf NON scatta. Vogliamo confermare che il
2655// tail-call verso dxn (0x28c2238) venga davvero eseguito.
2656// HOOK SICURO: stub assembly che salva/ripristina TUTTI i registri (x0..x30, q0..q31),
2657// logga l'ingresso, poi br al trampoline originale. Cosi' dxn gira normalmente.
2658static void dxn_entry_logger(void) {
2659 LOGI("REWARD_ENTRY: 0x28c2238 (dxn) REACHED - erogation tail-call executes");
2660}
2661
2662static void install_dxn_entry_trace(void) {
2663 uintptr_t off = 0x28BE238; // dump.cs Offset di dxn (RVA 0x28C2238)
2664 uintptr_t target = g_il2cpp_base + off;
2665 void *stub = alloc_exec_near((void*)target, 0x140);
2666 if (!stub) { LOGE("dxn trace: alloc_exec_near failed"); return; }
2667 uint32_t *s = (uint32_t*)stub;
2668
2669 // LSU/ALU prologue+epilogue, encodings VERIFICATI con clang (9k). Layout:
2670 // code 0x00..0x114 (70 istruzioni), log_fn literal @0x118, orig_fn literal @0x120.
2671 static const uint32_t CODE[70] = {
2672 0xd11403ff,0xa90007e0,0xa9010fe2,0xa90217e4,0xa9031fe6,0xa90427e8,0xa9052fea,0xa90637ec,0xa9073fee,0xa90847f0,
2673 0xa9094ff2,0xa90a57f4,0xa90b5ff6,0xa90c67f8,0xa90d6ffa,0xa90e77fc,0xf9007bfe,0xad0807e0,0xad090fe2,0xad0a17e4,
2674 0xad0b1fe6,0xad0c27e8,0xad0d2fea,0xad0e37ec,0xad0f3fee,0xad1047f0,0xad114ff2,0xad1257f4,0xad135ff6,0xad1467f8,
2675 0xad156ffa,0xad1677fc,0xad177ffe,0x580004b0,0xd63f0200,0xad577ffe,0xad5677fc,0xad556ffa,0xad5467f8,0xad535ff6,
2676 0xad5257f4,0xad514ff2,0xad5047f0,0xad4f3fee,0xad4e37ec,0xad4d2fea,0xad4c27e8,0xad4b1fe6,0xad4a17e4,0xad490fe2,
2677 0xad4807e0,0xf9407bfe,0xa94e77fc,0xa94d6ffa,0xa94c67f8,0xa94b5ff6,0xa94a57f4,0xa9494ff2,0xa94847f0,0xa9473fee,
2678 0xa94637ec,0xa9452fea,0xa94427e8,0xa9431fe6,0xa94217e4,0xa9410fe2,0xa94007e0,0x911403ff,0x58000090,0xd61f0200
2679 };
2680 // ldr x16,log_fn@idx33: imm19 = (0x118-0x84)/4 = 37 (encoding presente in CODE[33]=0x580004b0).
2681 // ldr x16,orig_fn@idx68: imm19 = (0x120-0x110)/4 = 4 (encoding in CODE[68]=0x58000090).
2682 for (int i = 0; i < 70; i++) s[i] = CODE[i];
2683 *(uintptr_t*)((uint8_t*)stub + 0x118) = (uintptr_t)(uintptr_t)dxn_entry_logger;
2684 __builtin___clear_cache((char*)stub, (char*)stub + 0x128);
2685 LOGI("dxn trace: installing hook @%p (offset 0x%X) stub=%p", (void*)target, (unsigned int)off, stub);
2686 void *backup = inline_hooker((void*)target, stub);
2687 if (!backup) { LOGE("dxn trace: inline_hooker failed"); return; }
2688 *(uintptr_t*)((uint8_t*)stub + 0x120) = (uintptr_t)backup;
2689 __builtin___clear_cache((char*)stub, (char*)stub + 0x128);
2690 LOGI("dxn trace: OK backup=%p (log-only, full reg preserve)", backup);
2691}
2692
2693// ==================== GRANT CHAIN PROBES (v4.56, log-only) ====================
2694// Scope: capire DOVE si interrompe la catena di erogazione per lo store. Verificato
2695// con disassembly di libil2cpp (split-55):
2696// ProcessPurchase(0x28a9188) -> fallback@entry+0x160 (0x28a92e8) -> GRANTBL
2697// -> bl 0x298f448 @0x28a93d0 (primo grant: bl da fallback)
2698// -> bl 0x28a8f1c @0x28a9410 (dw.dlz dispatcher)
2699// -> bl 0x28a94d4 @0x28a9090 (smart-grant orchestrator)
2700// -> tail-call b 0x28bc3e8 @0x28a95b8 (REAL GRANT, 6-arg + v0)
2701// Tutti i 4 ingressi sono prologhi "__lto/apin", solo stack ops nei primi 16 byte
2702// -> SICURI per inline_hooker (16-byte). Stessa stub full-preserve verificata di dxn.
2703static void grant_probe_298f448_logger(void) {
2704 LOGI("GRANT_CHAIN_298f448: ENTRY (bl da fallback GRANTBL) — primo grant eseguito");
2705}
2706static void grant_probe_28a8f1c_logger(void) {
2707 LOGI("GRANT_CHAIN_28a8f1c: ENTRY (dw.dlz dispatcher bl @0x28a9410)");
2708}
2709static void grant_probe_28a94d4_logger(void) {
2710 LOGI("GRANT_CHAIN_28a94d4: ENTRY (smart-grant orchestrator bl @0x28a9090)");
2711}
2712// v4.59: tracing descriptor REAL GRANT. La stub full-preserve chiama il logger via
2713// blr x16 mentre x0..x5 contengono ANCORA i registri originali di entry (non clobbrati
2714// dalle stp/adp di salvataggio, unico registro toccato e' x16). Quindi un logger con
2715// firma (x0..x5) riceve esattamente gli argomenti di 0x28bc3e8:
2716// x0=product_ctx, x1, x2, x3, x4(=arg4 id offerta), x5
2717// Leggiamo anche i campi di product_ctx che alimentano il grant descriptor
2718// ([+0x50] qword prezzo/quantita, [+0x60], [+0x68]) per distinguere A/B/C.
2719static void grant_probe_28bc3e8_logger(void *x0, void *x1, void *x2, void *x3, void *x4, void *x5) {
2720 uint64_t q50=0, q58=0, q60=0, q68=0, q78=0;
2721 uintptr_t x0v = (uintptr_t)x0;
2722 if (x0v) {
2723 safe_read_self(x0v + 0x50, &q50, 8);
2724 safe_read_self(x0v + 0x58, &q58, 8);
2725 safe_read_self(x0v + 0x60, &q60, 8);
2726 safe_read_self(x0v + 0x68, &q68, 8);
2727 safe_read_self(x0v + 0x78, &q78, 8);
2728 }
2729 LOGI("GRANT_CHAIN_28bc3e8: ENTRY (REAL GRANT b @0x28a95b8) args x0=%p x1=%p x2=%p x3=%p x4=%p x5=%p",
2730 x0, x1, x2, x3, x4, x5);
2731 LOGI(" ctx[+0x50]=0x%llx [+0x58]=0x%llx [+0x60]=0x%llx [+0x68]=0x%llx [+0x78]=0x%llx",
2732 (unsigned long long)q50, (unsigned long long)q58, (unsigned long long)q60,
2733 (unsigned long long)q68, (unsigned long long)q78);
2734}
2735
2736static void install_grant_entry_probe(uintptr_t rva, uintptr_t logger, const char *which) {
2737 if (!g_code_base) { LOGE("grant probe[%s]: no code_base (store hook failed)", which); return; }
2738 uintptr_t off = rva;
2739 uintptr_t target = g_code_base + off;
2740 void *stub = alloc_exec_near((void*)target, 0x140);
2741 if (!stub) { LOGE("grant probe[%s]: alloc_exec_near failed", which); return; }
2742 uint32_t *s = (uint32_t*)stub;
2743
2744 // Stessa stub full-preserve verificata con clang (9k) di install_dxn_entry_trace.
2745 // Idx33 ldr x16,log_fn@0x118 | idx68 ldr x16,orig_fn@0x120.
2746 static const uint32_t CODE[70] = {
2747 0xd11403ff,0xa90007e0,0xa9010fe2,0xa90217e4,0xa9031fe6,0xa90427e8,0xa9052fea,0xa90637ec,0xa9073fee,0xa90847f0,
2748 0xa9094ff2,0xa90a57f4,0xa90b5ff6,0xa90c67f8,0xa90d6ffa,0xa90e77fc,0xf9007bfe,0xad0807e0,0xad090fe2,0xad0a17e4,
2749 0xad0b1fe6,0xad0c27e8,0xad0d2fea,0xad0e37ec,0xad0f3fee,0xad1047f0,0xad114ff2,0xad1257f4,0xad135ff6,0xad1467f8,
2750 0xad156ffa,0xad1677fc,0xad177ffe,0x580004b0,0xd63f0200,0xad577ffe,0xad5677fc,0xad556ffa,0xad5467f8,0xad535ff6,
2751 0xad5257f4,0xad514ff2,0xad5047f0,0xad4f3fee,0xad4e37ec,0xad4d2fea,0xad4c27e8,0xad4b1fe6,0xad4a17e4,0xad490fe2,
2752 0xad4807e0,0xf9407bfe,0xa94e77fc,0xa94d6ffa,0xa94c67f8,0xa94b5ff6,0xa94a57f4,0xa9494ff2,0xa94847f0,0xa9473fee,
2753 0xa94637ec,0xa9452fea,0xa94427e8,0xa9431fe6,0xa94217e4,0xa9410fe2,0xa94007e0,0x911403ff,0x58000090,0xd61f0200
2754 };
2755 for (int i = 0; i < 70; i++) s[i] = CODE[i];
2756 *(uintptr_t*)((uint8_t*)stub + 0x118) = (uintptr_t)(uintptr_t)logger;
2757 __builtin___clear_cache((char*)stub, (char*)stub + 0x128);
2758 LOGI("grant probe[%s]: installing hook @%p (offset 0x%X) stub=%p", which, (void*)target, (unsigned int)off, stub);
2759 void *backup = inline_hooker((void*)target, stub);
2760 if (!backup) { LOGE("grant probe[%s]: inline_hooker failed", which); return; }
2761 *(uintptr_t*)((uint8_t*)stub + 0x120) = (uintptr_t)backup;
2762 __builtin___clear_cache((char*)stub, (char*)stub + 0x128);
2763 LOGI("grant probe[%s]: OK backup=%p (log-only, full reg preserve)", which, backup);
2764}
2765
2766static void install_grant_chain_probes(void) {
2767 if (!g_code_base) { LOGE("grant chain probes: no code_base (store hook failed)"); return; }
2768 install_grant_entry_probe(0x298F448, (uintptr_t)(uintptr_t)grant_probe_298f448_logger, "298f448");
2769 install_grant_entry_probe(0x28A8F1C, (uintptr_t)(uintptr_t)grant_probe_28a8f1c_logger, "28a8f1c");
2770 install_grant_entry_probe(0x28A94D4, (uintptr_t)(uintptr_t)grant_probe_28a94d4_logger, "28a94d4");
2771 install_grant_entry_probe(0x28BC3E8, (uintptr_t)(uintptr_t)grant_probe_28bc3e8_logger, "28bc3e8");
2772}
2773
2774// ==================== v4.71: STORE CHAIN ENTRY HOOKS (eao/dne/flush) ====================
2775static void st_chain_entry_eao(void *x0, void *x1, void *x2, void *x3, void *x4, void *x5) {
2776 uintptr_t user = 0, queue = 0; uint32_t qc = 0;
2777 if (x0) { safe_read_self((uintptr_t)x0 + 0x28, &user, 8); }
2778 if (user) { safe_read_self(user + 0xa8, &queue, 8); }
2779 if (queue) { safe_read_self(queue + 0x18, &qc, 4); }
2780 LOGI("STORE_CHAIN_ENTRY: eao(0x2991924) x0=%p x1=%p x2=%p x3=%p x4=%p x5=%p user(+0x28)=%p queue(+0xA8)=%p count=%u",
2781 x0, x1, x2, x3, x4, x5, (void *)user, (void *)queue, (unsigned)qc);
2782}
2783static void st_chain_entry_dne(void *x0, void *x1, void *x2, void *x3, void *x4, void *x5) {
2784 LOGI("STORE_CHAIN_ENTRY: dne(0x28ACAC4) x0=%p x1=%p x2=%p x3=%p x4=%p x5=%p", x0, x1, x2, x3, x4, x5);
2785}
2786static void st_chain_entry_flush(void *x0, void *x1, void *x2, void *x3, void *x4, void *x5) {
2787 uintptr_t u = 0, q = 0; uint32_t qc = 0;
2788 if (x0) { safe_read_self((uintptr_t)x0 + 0x28, &u, 8); }
2789 if (u) { safe_read_self(u + 0xa8, &q, 8); }
2790 if (q) { safe_read_self(q + 0x18, &qc, 4); }
2791 // v4.81: apprende g_user_base qui (user data object, contiene le dungeon keys).
2792 if (u && !g_user_base) {
2793 g_user_base = (uintptr_t)u;
2794 LOGI("USER_BASE: appreso da flush hook user(+0x28) = 0x%zX", (size_t)g_user_base);
2795 }
2796 LOGI("STORE_CHAIN_ENTRY: flush(0x29905D8) x0=%p x1=%p user(+0x28)=%p queue(+0xA8)=%p count=%u",
2797 x0, x1, (void *)u, (void *)q, (unsigned)qc);
2798}
2799
2800static void install_store_chain_trace(void) {
2801 if (!g_code_base) { LOGE("STORE_TRACE: no code_base (store hook failed)"); return; }
2802 size_t n = sizeof(CS_TABLE) / sizeof(CS_TABLE[0]);
2803 g_cs_pool_size = ((n * 0x180) + 0xFFF) & ~(size_t)0xFFF;
2804 g_cs_pool_off = 0;
2805 void *anchor = (void *)(g_code_base + 0x2900000); // centro dello span dei site
2806 g_cs_pool = (uint8_t *)alloc_exec_near(anchor, g_cs_pool_size);
2807 if (!g_cs_pool) { LOGE("STORE_TRACE: stub pool alloc failed -> abort (nessuna patch)"); return; }
2808 LOGI("STORE_TRACE: stub pool @%p size=%u (anchor=%p delta=%lld)",
2809 g_cs_pool, (unsigned)g_cs_pool_size, anchor,
2810 (long long)((int64_t)(uintptr_t)g_cs_pool - (int64_t)(uintptr_t)anchor));
2811 LOGI("STORE_TRACE: installing %u call-site probes (catena store v56: log OGNI movimento)", (unsigned)n);
2812 for (size_t i = 0; i < n; i++) { install_call_site_probe(&CS_TABLE[i]); }
2813 LOGI("STORE_TRACE: done - %u call-site probes", (unsigned)n);
2814 // Entry hook 16-byte sulle funzioni VIVE per ULTIME (le patch 4-byte dei call-site sono atomiche)
2815 install_grant_entry_probe(0x2991924, (uintptr_t)(uintptr_t)st_chain_entry_eao, "eao-entry");
2816 LOGI("STORE_TRACE: eao(0x2991924) entry hook installed");
2817 install_grant_entry_probe(0x28ACAC4, (uintptr_t)(uintptr_t)st_chain_entry_dne, "dne-entry");
2818 LOGI("STORE_TRACE: dne(0x28ACAC4) entry hook installed");
2819 install_grant_entry_probe(0x29905D8, (uintptr_t)(uintptr_t)st_chain_entry_flush, "flush-entry");
2820 LOGI("STORE_TRACE: flush(0x29905D8) entry hook installed");
2821 long ps = sysconf(_SC_PAGESIZE);
2822 uintptr_t pg = (uintptr_t)g_cs_pool & ~(uintptr_t)(ps - 1);
2823 size_t span = g_cs_pool_size + ((uintptr_t)g_cs_pool - pg);
2824 if (mprotect((void *)pg, span, PROT_READ | PROT_EXEC) == 0)
2825 LOGI("STORE_TRACE: pool mprotect RX ok (%zu bytes)", span);
2826 else
2827 LOGE("STORE_TRACE: pool mprotect RX failed errno=%d", errno);
2828}
2829
2830// ==================== ENGINE DISPATCH PROBE (0x2a7ed18) ====================
2831// 0x2a7ed18 e' la funzione di dispatch CONDIVISA tra il percorso AD e il percorso
2832// purchase: chiamata da dxn (0x28c2140/0x28c21c0/0x28c229c) e da 0x27dabbc
2833// (orchestratore purchase). Firma d'uso: (x0=consumer/device, x1=payload,
2834// x2=0). Se l'acquisto raggiunge questo punto => il flusso arriva all'engine
2835// condiviso; se NON compare => il purchase non passa mai per l'engine.
2836//
2837// v4.62: passa dallo schema "stub full-preserve" (install_grant_entry_probe, blr
2838// clobbera x30) allo schema "br x16 wrapper" (install_post_trace_hook): il jmp
2839// su 16 byte non tocca x30, quindi il wrapper riceve il caller VERO della
2840// funzione via __builtin_return_address(0). 0x2a7ed18 ha un adrp nei primi 16
2841// byte (offset +0xC); fix_adrp_in_trampoline lo converte in ldr-literal (literal
2842// pool a tramp+0x20, verificato runtime con dli/dze da v4.61).
2843// THROTTLE: il flood normale (AD path) chiama 2a7ed18 in continuazione con pochi
2844// (caller,x1) fissi. La risposta purchase introduce caller/x1 NUOVI. Logghiamo:
2845// - ogni caller mai visto prima (sempre)
2846// - ogni cambio di x1 per lo stesso caller (nuovo payload)
2847// - al massimo 1 riga/sec per coppia (caller,x1) stabile
2848// - heartbeat ogni 4096 skip per confermare liveness
2849static void install_post_trace_hook(uintptr_t target, void *wrapper, void **orig_out, const char *which);
2850typedef void *(*dispatch_trace_fn)(void *x0, void *x1, void *x2, void *x3, unsigned int x4);
2851static void *g_orig_dispatch = nullptr;
2852
2853static void *my_dispatch_trace(void *x0, void *x1, void *x2, void *x3, unsigned int x4) {
2854 static uintptr_t ring[8] = {0};
2855 static int ring_pos = 0;
2856 static uintptr_t last_x1 = 0;
2857 static uint64_t last_log_ns = 0;
2858 static uint64_t skip_count = 0;
2859 static uint64_t total_calls = 0;
2860 static uint64_t log_count = 0;
2861
2862 total_calls++;
2863 uintptr_t caller = (uintptr_t)__builtin_return_address(0);
2864
2865 // Check if caller is in the "known" ring
2866 bool known = false;
2867 for (int i = 0; i < 8; i++) {
2868 if (ring[i] == caller) { known = true; break; }
2869 }
2870
2871 struct timespec ts;
2872 clock_gettime(CLOCK_MONOTONIC, &ts);
2873 uint64_t now_ns = (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec;
2874
2875 bool caller_new = !known;
2876 bool x1_changed = (uintptr_t)x1 != last_x1;
2877 bool cooldown_expired = (now_ns - last_log_ns) >= 1000000000ULL; // 1s
2878
2879 if (caller_new || x1_changed || cooldown_expired) {
2880 if (skip_count > 0 && !caller_new && !x1_changed) {
2881 // Cooldown expiry heartbeat — log skip count
2882 LOGI("DISPATCH_2a7ed18: HEARTBEAT (skipped %llu, logged %llu, total %llu)",
2883 (unsigned long long)skip_count, (unsigned long long)log_count, (unsigned long long)total_calls);
2884 }
2885 LOGI("DISPATCH_2a7ed18: CALLED BY RVA=0x%llX (caller=%p)%s"
2886 " x0=%p x1=%p x2=%p x3=%p x4=0x%x [calls=%llu logged=%llu]",
2887 (unsigned long long)(caller - g_code_base), (void*)caller,
2888 caller_new ? " *NEW*" : "",
2889 x0, x1, x2, x3, (unsigned int)x4,
2890 (unsigned long long)total_calls, (unsigned long long)log_count);
2891 log_count++;
2892 skip_count = 0;
2893 last_log_ns = now_ns;
2894 last_x1 = (uintptr_t)x1;
2895 if (caller_new) {
2896 ring[ring_pos] = caller;
2897 ring_pos = (ring_pos + 1) % 8;
2898 }
2899 } else {
2900 skip_count++;
2901 if ((skip_count & 0xFFF) == 0) {
2902 LOGI("DISPATCH_2a7ed18: THROTTLED %llu skipped (last caller RVA=0x%llX, last x1=%p)",
2903 (unsigned long long)skip_count, (unsigned long long)(ring[ring_pos ? ring_pos-1 : 7] - g_code_base),
2904 (void*)last_x1);
2905 }
2906 }
2907
2908 return g_orig_dispatch ? ((dispatch_trace_fn)g_orig_dispatch)(x0, x1, x2, x3, x4) : nullptr;
2909}
2910
2911static void install_engine_dispatch_probe(void) {
2912 if (!g_code_base) { LOGE("engine dispatch probe: no code_base (store hook failed)"); return; }
2913 install_post_trace_hook(g_code_base + 0x2A7ED18, (void*)my_dispatch_trace, &g_orig_dispatch, "dispatch");
2914}
2915
2916// ==================== POST TRACE PROBES (dli 0x27dabbc / dze 0x298f548) ====================
2917// v4.61: tracciamo l'orchestratore POST (dli) e il secondo punto di append pending
2918// (dze) per mappare CHI li chiama a runtime e trovare l'handler della risposta server.
2919// NOTA: dlh (0x27d5cdc) e' solo `and w4,w4,#1; b 0x27dabbc` (tail-call in dli):
2920// lo hooking su dli cattura TUTTE le POST (dirette o via dlh), quindi dlh NON viene
2921// hookato. Firme all'entry: dli usa (x0..x3, w4); dze usa (x0, x1) + scratch.
2922// Pattern install_store_hook/my_dnf: trampoline 16B + fix_adrp + wrapper C++ con
2923// __builtin_return_address(0) che restituisce il caller VERO della funzione.
2924typedef void *(*post_trace_fn)(void *x0, void *x1, void *x2, void *x3, unsigned int x4);
2925static void *g_orig_dli = nullptr;
2926static void *g_orig_dze = nullptr;
2927
2928static void maybe_dump_string(void *p, const char *tag) {
2929 if (!p) { LOGI("POST_TRACE: %s = NULL", tag); return; }
2930 int32_t len = 0;
2931 if (!safe_read_self((uintptr_t)p + 0x10, &len, 4) || len <= 0 || len > 0x2000) {
2932 LOGI("POST_TRACE: %s = %p (non-string, len@+0x10=%d)", tag, p, len);
2933 return;
2934 }
2935 int n = len < 512 ? len : 512;
2936 char buf[512];
2937 // v4.79: chars il2cpp stanno a +0x14 (object 16B + int32 length), NON +0x18
2938 if (!safe_read_self((uintptr_t)p + 0x14, buf, n)) { LOGI("POST_TRACE: %s = %p (read fail)", tag, p); return; }
2939 char out[1024]; int oi = 0;
2940 for (int i = 0; i < n && oi < 1022; i++) {
2941 unsigned char c = (unsigned char)buf[i];
2942 out[oi++] = (c >= 0x20 && c < 0x7f) ? (char)c : '.';
2943 }
2944 out[oi] = 0;
2945 LOGI("POST_TRACE: %s = \"%s\" (len=%d)", tag, out, len);
2946}
2947
2948// dli = orchestratore POST (tutte le richieste HTTP passano da qui)
2949// v4.64: quando chiamato dalla POST del grant reale (RA == base+0x28BC708 =
2950// bl 0x27d5cdc @0x28bc704 nel grant 0x28bc3e8), x3 e' il callback delegate creato
2951// da 0x411c4c4 con layout: [+0x10]=target handler (il vero response handler),
2952// [+0x18]=trampoline single invoke 0x2379940, [+0x38]=trampoline multicast 0x23798e8,
2953// [+0x20]=closure, [+0x28]=config. Catturiamo il target e lo logghiamo come RVA
2954// per il disasm statico del fix v4.65.
2955static void *g_cap_cb = nullptr;
2956static void *g_cap_hdl = nullptr;
2957
2958static void dump_callback_fields(void *cb, const char *tag) {
2959 if (!cb) { LOGI("CB[%s]: x3=NULL", tag); return; }
2960 uint64_t t10=0,t18=0,t20=0,t28=0,t38=0;
2961 safe_read_self((uintptr_t)cb+0x10,&t10,8);
2962 safe_read_self((uintptr_t)cb+0x18,&t18,8);
2963 safe_read_self((uintptr_t)cb+0x20,&t20,8);
2964 safe_read_self((uintptr_t)cb+0x28,&t28,8);
2965 safe_read_self((uintptr_t)cb+0x38,&t38,8);
2966#define RVAIF(v) ((uint64_t)(v) >= (uint64_t)g_code_base ? (uint64_t)((uint64_t)(v)-(uint64_t)g_code_base) : (uint64_t)-1)
2967 LOGI("CB[%s]: cb=%p [+0x10]=%p(RVA 0x%llX) [+0x18]=%p(RVA 0x%llX) [+0x20]=%p(RVA 0x%llX) [+0x28]=%p(RVA 0x%llX) [+0x38]=%p(RVA 0x%llX)",
2968 tag, cb, (void*)t10, (unsigned long long)RVAIF(t10), (void*)t18, (unsigned long long)RVAIF(t18),
2969 (void*)t20, (unsigned long long)RVAIF(t20), (void*)t28, (unsigned long long)RVAIF(t28),
2970 (void*)t38, (unsigned long long)RVAIF(t38));
2971#undef RVAIF
2972}
2973
2974static void *my_dli_trace(void *x0, void *x1, void *x2, void *x3, unsigned int x4) {
2975 uintptr_t ra = (uintptr_t)__builtin_return_address(0);
2976 LOGI("POST_TRACE dli(0x27dabbc): CALLED BY %p x0=%p x1=%p x2=%p x3=%p x4=0x%x",
2977 (void*)ra, x0, x1, x2, x3, x4);
2978 maybe_dump_string(x1, "dli.x1");
2979 maybe_dump_string(x2, "dli.x2");
2980 maybe_dump_string(x3, "dli.x3");
2981 if (g_code_base && (ra == (uintptr_t)(g_code_base + 0x28BC708) ||
2982 ra == (uintptr_t)(g_code_base + 0x28F0CF0) ||
2983 ra == (uintptr_t)(g_code_base + 0x26BAFAC))) {
2984 const char *tag = ra == (uintptr_t)(g_code_base + 0x28BC708) ? "grant"
2985 : ra == (uintptr_t)(g_code_base + 0x28F0CF0) ? "adOverdriv"
2986 : "claim";
2987 LOGI("CB: POST detected (RA=%p RVA 0x%llX tag=%s)", (void*)ra,
2988 (unsigned long long)(ra - (uint64_t)g_code_base), tag);
2989 dump_callback_fields(x3, tag);
2990 if (ra == (uintptr_t)(g_code_base + 0x28BC708) && x3) {
2991 uint64_t hdl = 0;
2992 safe_read_self((uintptr_t)x3 + 0x10, &hdl, 8);
2993 g_cap_cb = x3;
2994 g_cap_hdl = (void*)hdl;
2995 LOGI("CB: captured handler=%p (RVA 0x%llX)",
2996 g_cap_hdl, (unsigned long long)((uint64_t)g_cap_hdl - (uint64_t)g_code_base));
2997 }
2998 }
2999 return g_orig_dli ? ((post_trace_fn)g_orig_dli)(x0, x1, x2, x3, x4) : nullptr;
3000}
3001
3002// dze = secondo punto di append pending (letto in [x0+0x28] -> [+0xa8] pending list)
3003static void *my_dze_trace(void *x0, void *x1, void *x2, void *x3, unsigned int x4) {
3004 LOGI("POST_TRACE dze(0x298f548): CALLED BY %p x0=%p x1=%p x2=%p x3=%p x4=0x%x",
3005 __builtin_return_address(0), x0, x1, x2, x3, x4);
3006 uintptr_t u = (uintptr_t)x0;
3007 uint64_t inner = 0, list = 0; uint32_t count = 0;
3008 if (u) { safe_read_self(u + 0x28, &inner, 8); }
3009 if (inner) { safe_read_self(inner + 0xa8, &list, 8); }
3010 if (list) { safe_read_self(list + 0x18, &count, 4); }
3011 LOGI("POST_TRACE dze: [x0+0x28]=%p [inner+0xa8]=%p pendingList=%p count=0x%x",
3012 (void*)inner, (void*)list, (void*)list, count);
3013 maybe_dump_string(x1, "dze.x1");
3014 return g_orig_dze ? ((post_trace_fn)g_orig_dze)(x0, x1, x2, x3, x4) : nullptr;
3015}
3016
3017static void install_post_trace_hook(uintptr_t target, void *wrapper, void **orig_out, const char *which) {
3018 uint8_t *b = (uint8_t*)target;
3019 LOGI("POST_TRACE install[%s]: target=%p bytes %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x",
3020 which, (void*)target,
3021 b[0],b[1],b[2],b[3],b[4],b[5],b[6],b[7],b[8],b[9],b[10],b[11],b[12],b[13],b[14],b[15]);
3022 void *tramp = alloc_exec_near((void*)target, 64);
3023 if (!tramp) { LOGE("POST_TRACE[%s]: alloc_exec_near failed", which); return; }
3024 memcpy(tramp, (void*)target, 16);
3025 fix_adrp_in_trampoline(tramp, (void*)target);
3026 uint32_t *t = (uint32_t*)tramp;
3027 t[4] = 0x58000050; // ldr x16, #8
3028 t[5] = 0xD61F0200; // br x16
3029 *(uint64_t*)&t[6] = (uint64_t)target + 16;
3030 __builtin___clear_cache((char*)tramp, (char*)tramp + 64);
3031 uint8_t jmp[16];
3032 *(uint32_t*)&jmp[0] = 0x58000050;
3033 *(uint32_t*)&jmp[4] = 0xD61F0200;
3034 *(uint64_t*)&jmp[8] = (uint64_t)wrapper;
3035 if (proc_mem_write((uintptr_t)target, jmp, 16)) {
3036 __builtin___clear_cache((char*)target, (char*)target + 16);
3037 if (memcmp((void*)target, jmp, 16) == 0) {
3038 *orig_out = tramp;
3039 LOGI("POST_TRACE[%s]: OK (trampoline=%p)", which, tramp);
3040 } else LOGE("POST_TRACE[%s]: verify FAILED", which);
3041 } else LOGE("POST_TRACE[%s]: proc_mem_write FAILED", which);
3042}
3043
3044static void install_post_trace_probes(void) {
3045 if (!g_code_base) { LOGE("POST_TRACE: no g_code_base (store hook failed)"); return; }
3046 install_post_trace_hook(g_code_base + 0x27DABBC, (void*)my_dli_trace, &g_orig_dli, "dli");
3047 install_post_trace_hook(g_code_base + 0x298F548, (void*)my_dze_trace, &g_orig_dze, "dze");
3048}
3049
3050// ==================== GRANT CALLBACK INVOKE PROBE (v4.64) ====================
3051// Trampoline single-cast delegate invoke @0x2379940:
3052// ldr x4,[x0,#0x10]; and w0,w1,#1; mov x1,x2; mov x2,x3; br x4
3053// Chiamato con x0=delegate, w1=success flag, x2/x3=args. Quando il callback
3054// catturato da my_dli_trace (g_cap_hdl == [delegate+0x10]) viene invocato con la
3055// risposta del server, logghiamo success (w1) e gli argomenti x2 (response DTO/obj)
3056// e x3. Il trampoline e' ENTRY-HOOK: install_post_trace_hook copia 16 byte e fa
3057// ldr x16,#8;br x16 -> trampoline che riesegue le 4 istruzioni + br x4. Nessun
3058// adrp nelle prime 4 istruzioni -> fix_adrp_in_trampoline non fa danni.
3059static void *g_orig_delegate_invoke = nullptr;
3060
3061static void *my_delegate_invoke_trace(void *x0, void *x1, void *x2, void *x3, unsigned int x4) {
3062 if (g_cap_hdl && x0) {
3063 uint64_t hdl = 0;
3064 safe_read_self((uintptr_t)x0 + 0x10, &hdl, 8);
3065 if (hdl == (uint64_t)g_cap_hdl) {
3066 LOGI("CB_INVOKE(0x2379940): matched handler=%p (RVA 0x%llX) w1=0x%x success=%u x2=%p x3=%p",
3067 (void*)hdl, (unsigned long long)(hdl - (uint64_t)g_code_base),
3068 (unsigned int)(uintptr_t)x1, (unsigned int)(uintptr_t)x1 & 1u, x2, x3);
3069 maybe_dump_string(x2, "cb.x2");
3070 }
3071 }
3072 return g_orig_delegate_invoke ? ((post_trace_fn)g_orig_delegate_invoke)(x0, x1, x2, x3, x4) : nullptr;
3073}
3074
3075static void install_grant_callback_invoke_probe(void) {
3076 if (!g_code_base) { LOGE("CB_INVOKE: no g_code_base"); return; }
3077 install_post_trace_hook(g_code_base + 0x2379940, (void*)my_delegate_invoke_trace, &g_orig_delegate_invoke, "delegate");
3078}
3079
3080// ==================== FC.DWU + APPLY-GRANT TRACE (v4.65) ====================
3081// fc.dwu @0x2A7C700 e' il response handler reale della POST di verifica del grant
3082// (RVA catturato in CB[grant] via dli). Entry:
3083// x0 -> x19 (this), w1 -> w21 -> tbz w21,#0x0,0x2a7cb3c (bit0 w1 = success flag)
3084// x2 -> x20 -> mov x0,x20; bl 0x3371958 (parse) -> DTO in x21
3085// 0x2a7c7e4: ldr w8,[x21,#0x10] (Code) ; cmp w8,#0xc8 ; b.ne (NOPata da store bypass #2)
3086// 0x2a7c878: bl 0x28bc158 (apply-grant) con x0=x22,x1=x23,w2=0xc8,x3=x24,w4,w5,x6,x7
3087// Primi 16 byte di entrambe: solo sub/stp, nessun pc-relative -> safe install_post_trace_hook.
3088static void *g_orig_fc_dwu = nullptr;
3089
3090static void *my_fc_dwu_trace(void *x0, void *x1, void *x2, void *x3, unsigned int x4) {
3091 uintptr_t ra = (uintptr_t)__builtin_return_address(0);
3092 LOGI("FC_DWU(0x2A7C700): CALLED BY %p x0=%p w1=0x%x(bit0=%u) x2=%p x3=%p x4=0x%x",
3093 (void*)ra, x0, (unsigned)(uintptr_t)x1, (unsigned)(uintptr_t)x1 & 1u, x2, x3, x4);
3094 maybe_dump_string(x2, "fcd.x2");
3095 maybe_dump_string(x3, "fcd.x3");
3096 return g_orig_fc_dwu ? ((post_trace_fn)g_orig_fc_dwu)(x0, x1, x2, x3, x4) : nullptr;
3097}
3098
3099typedef void *(*apply_grant_fn)(void *x0, void *x1, void *x2, void *x3,
3100 void *x4, void *x5, void *x6, void *x7);
3101static void *g_orig_apply_grant = nullptr;
3102
3103static void *my_apply_grant_trace(void *x0, void *x1, void *x2, void *x3,
3104 void *x4, void *x5, void *x6, void *x7) {
3105 uint32_t status = (unsigned)(uintptr_t)x2;
3106 LOGI("APPLY_GRANT(0x28bc158): CALLED BY %p x0=%p x1=%p status(%%w2)=0x%x x3=%p "
3107 "w4=0x%x(w29) w5=0x%x(w27) x6=%p x7=%p",
3108 __builtin_return_address(0), x0, x1, status, x3,
3109 (unsigned)(uintptr_t)x4, (unsigned)(uintptr_t)x5, x6, x7);
3110 maybe_dump_string(x3, "ag.x3");
3111 return g_orig_apply_grant ? ((apply_grant_fn)g_orig_apply_grant)(x0, x1, x2, x3, x4, x5, x6, x7) : nullptr;
3112}
3113
3114static void install_fc_dwu_grant_trace_probes(void) {
3115 if (!g_code_base) { LOGE("FC_DWU: no g_code_base"); return; }
3116 install_post_trace_hook(g_code_base + 0x2A7C700, (void*)my_fc_dwu_trace, &g_orig_fc_dwu, "fcdwu");
3117 install_post_trace_hook(g_code_base + 0x28BC158, (void*)my_apply_grant_trace, &g_orig_apply_grant, "applygrant");
3118}
3119
3120// ==================== CLUB ACTIVATION BRIDGE (v4.78) ====================
3121// PROBLEMA: il grant v56 (dsu) NON ha un case per i prodotti club: con acquisto fake
3122// vip/elite club non vengono MAI attivati localmente (servono la conferma del server,
3123// mai scattata). ClubController.meu() @0x291ABF8 (il getter del tier) legge SOLO i 4
3124// campi cache dell'utente: ClubTierCache (+0x3FC), ClubPermanentCache (+0x400),
3125// ClubExpiryMs (+0x408), ClubNextClaimMs (+0x410) — campi della classe dati raggiunta
3126// da [UserController+0x28]. Con la risposta del server questi vengono scritti da
3127// UserController.esj() @0x299DBD8 (sw w1->[x8+0x3FC], str x3->[x8+0x408], strb w2&1->
3128// [x8+0x400], str x4->[x8+0x410], poi b 0x29905d8 save). Il fix chiama esj() direttamente
3129// su un acquisto club, attivando il tier localmente e salvando.
3130// - esj(UserController this=x0, int tier=w1, bool permanent=w2, long expiry=x3, long nextClaim=x4)
3131// - tier: ClubTier.Vip=1, ClubTier.Elite=2 (dump: enum ClubTier @50731)
3132// - UserController istanza = [PurchaseController+0x30] (campo `xfm`, dump riga 6892)
3133// - productId = Product.definition(id@0x10) = [[Product+0x10]+0x10] (stringa il2cpp:
3134// object 16B, length int32 @+0x10, chars utf16 @+0x14) — verifica objdump dsu@0x28b6aac/0x28b6ac8
3135// Entry dsu@0x28B6924: sub sp / str x30 / stp x28,x27 / stp x26,x25 -> nessun pc-relative
3136// nei primi 16 byte -> safe install_post_trace_hook (come per fcdwu/applygrant).
3137static void *g_orig_dsu = nullptr;
3138
3139// v4.81 (senza bump versione) GUARD anti-reset: quando il club e' stato attivato
3140// localmente, il server (ClubStatusResponseDTO via mfa@0x291ADE8) risponde tier=0
3141// al claim / codice Discord e chiama esj(tier=0)+save -> azzera i campi cache
3142// (+0x3FC/+0x400/+0x408/+0x410) e il club "torna normale". Il wrapper su esj
3143// (entry safe: str x30 / ldr x8 / cbz x8 / and w9 -> no adrp nei primi 16B)
3144// intercetta la chiamata: se il club e' attivo e il tier in ingresso e' 0, riscrive
3145// x1/x2/x3/x4 con lo stato salvato, cosi' il reset server non attecchisce.
3146// FIRMA dedicata con x0..x4 tutti a 64-bit: post_trace_fn tronca x4 (nextClaim,
3147// epoch ms ~1.7e12) a 32-bit -> proibito. Trampoline + wrapper preservano i regs.
3148static int g_club_active = 0;
3149static int g_club_tier = 0;
3150static int g_club_permanent = 0;
3151static int64_t g_club_expiry = 0;
3152static int64_t g_club_next_claim = 0;
3153static void *g_orig_esj = nullptr;
3154
3155typedef void *(*esj_full_fn)(void *x0, void *x1, void *x2, void *x3, void *x4);
3156
3157static void *my_esj_club_guard(void *x0, void *x1, void *x2, void *x3, void *x4) {
3158 int incoming = (int)(uintptr_t)x1;
3159 if (g_club_active && incoming <= 0) {
3160 LOGI("CLUB_GUARD: esj reset tier=%d BLOCKED -> forcing tier=%d perm=%d expiry=%lld next=%lld",
3161 incoming, g_club_tier, g_club_permanent,
3162 (long long)g_club_expiry, (long long)g_club_next_claim);
3163 x1 = (void*)(uintptr_t)g_club_tier;
3164 x2 = (void*)(uintptr_t)g_club_permanent;
3165 x3 = (void*)(uintptr_t)g_club_expiry;
3166 x4 = (void*)(uintptr_t)g_club_next_claim;
3167 }
3168 return g_orig_esj ? ((esj_full_fn)g_orig_esj)(x0, x1, x2, x3, x4) : nullptr;
3169}
3170
3171static int64_t club_now_ms(void) {
3172 struct timespec ts;
3173 if (clock_gettime(CLOCK_REALTIME, &ts) != 0) return 0;
3174 return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
3175}
3176
3177// v4.81 (senza bump) PERSISTENZA club tra riavvii: g_club_tier/permanent/expiry/
3178// next_claim e' solo in RAM -> al rilancio del gioco muore e il server (mfa) con
3179// tier=0 riazzererebbe i campi (guard inattivo con g_club_active=0). Salviamo lo
3180// stato in un file nella dir dati del modulo (stessa dir di drmod.log) e lo
3181// rileggiamo nel worker all'avvio, cosi' il guard blocca il reset anche dopo
3182// restart. Il file NON viene cancellato da adb rm drmod.log (regola 9l: solo il log).
3183static const char *CLUB_STATE_PATH = "/data/data/com.lavalabs.dungeonrush/files/drmod_club.dat";
3184static const char *CLUB_STATE_PATH_ALT= "/data/user/0/com.lavalabs.dungeonrush/files/drmod_club.dat";
3185
3186static void club_state_save(void) {
3187 if (g_club_tier <= 0) { LOGI("CLUB_STATE: skip save (tier=%d)", g_club_tier); return; }
3188 FILE *f = fopen(CLUB_STATE_PATH, "w");
3189 if (!f) f = fopen(CLUB_STATE_PATH_ALT, "w");
3190 if (!f) { LOGE("CLUB_STATE: save fopen failed errno=%d (%s)", errno, strerror(errno)); return; }
3191 fprintf(f, "%d %d %lld %lld %d\n",
3192 g_club_tier, g_club_permanent, (long long)g_club_expiry, (long long)g_club_next_claim, g_club_active);
3193 fclose(f);
3194 LOGI("CLUB_STATE: saved tier=%d perm=%d expiry=%lld next=%lld active=%d",
3195 g_club_tier, g_club_permanent, (long long)g_club_expiry, (long long)g_club_next_claim, g_club_active);
3196}
3197
3198static void club_state_load(void) {
3199 g_club_tier = 0; g_club_permanent = 0; g_club_expiry = 0; g_club_next_claim = 0; g_club_active = 0;
3200 FILE *f = fopen(CLUB_STATE_PATH, "r");
3201 if (!f) f = fopen(CLUB_STATE_PATH_ALT, "r");
3202 if (!f) { LOGI("CLUB_STATE: no state file (club not activated yet)"); return; }
3203 long long e = 0, n = 0; int a = 0;
3204 int tier = 0, perm = 0;
3205 int r = fscanf(f, "%d %d %lld %lld %d", &tier, &perm, &e, &n, &a);
3206 fclose(f);
3207 if (r != 5 || tier <= 0) { LOGI("CLUB_STATE: file corrupt/empty (r=%d tier=%d)", r, tier); return; }
3208 g_club_tier = tier; g_club_permanent = perm; g_club_expiry = e; g_club_next_claim = n;
3209 g_club_active = a ? 1 : 0;
3210 LOGI("CLUB_STATE: restored tier=%d perm=%d expiry=%lld next=%lld active=%d",
3211 g_club_tier, g_club_permanent, (long long)g_club_expiry, (long long)g_club_next_claim, g_club_active);
3212}
3213
3214static void *my_dsu_club_bridge(void *x0, void *x1, void *x2, void *x3, unsigned int x4) {
3215 if (x0 && x1 && g_code_base) {
3216 uint64_t def = 0, idp = 0;
3217 if (safe_read_self((uintptr_t)x1 + 0x10, &def, 8) && def &&
3218 safe_read_self(def + 0x10, &idp, 8) && idp) {
3219 int32_t len = 0;
3220 if (safe_read_self(idp + 0x10, &len, 4) && len > 0 && len < 64) {
3221 uint8_t raw[128];
3222 if (safe_read_self(idp + 0x14, raw, (size_t)len * 2)) {
3223 char id[64];
3224 int i;
3225 for (i = 0; i < len && i < 63; i++) id[i] = (char)raw[i * 2];
3226 id[i] = 0;
3227 LOGI("CLUB_BRIDGE: productId=\"%s\"", id);
3228 int tier = 0;
3229 if (strncmp(id, "lootio.vip_club_", 16) == 0) tier = 1;
3230 else if (strncmp(id, "lootio.elite_club_", 18) == 0) tier = 2;
3231 if (tier) {
3232 int permanent = strstr(id, "_permanent") ? 1 : 0;
3233 uint64_t uc = 0;
3234 if (safe_read_self((uintptr_t)x0 + 0x30, &uc, 8) && uc) {
3235 uint64_t data = 0;
3236 if (safe_read_self(uc + 0x28, &data, 8) && data) {
3237 int64_t now = club_now_ms();
3238 int64_t expiry = permanent ? 0 : now + 30LL * 24 * 3600 * 1000;
3239 g_club_active = 1;
3240 g_club_tier = tier;
3241 g_club_permanent = permanent;
3242 g_club_expiry = expiry;
3243 g_club_next_claim = now;
3244 club_state_save();
3245 typedef void (*esj_fn)(void *self, int tier, int permanent, int64_t expiry, int64_t nextClaim);
3246 esj_fn esj = (esj_fn)(g_code_base + 0x299DBD8);
3247 LOGI("CLUB_BRIDGE: ACTIVATE product=\"%s\" tier=%d permanent=%d expiry=%lld via esj@0x299DBD8",
3248 id, tier, permanent, (long long)expiry);
3249 esj((void*)uc, tier, permanent, expiry, now);
3250 } else LOGE("CLUB_BRIDGE: UserController data @+0x28 null");
3251 } else LOGE("CLUB_BRIDGE: no UserController at PC+0x30");
3252 }
3253 }
3254 }
3255 }
3256 }
3257 return g_orig_dsu ? ((post_trace_fn)g_orig_dsu)(x0, x1, x2, x3, x4) : nullptr;
3258}
3259
3260static void install_club_bridge(void) {
3261 if (!g_code_base) { LOGE("CLUB: no g_code_base"); return; }
3262 install_post_trace_hook(g_code_base + 0x28B6924, (void*)my_dsu_club_bridge, &g_orig_dsu, "dsu");
3263 install_post_trace_hook(g_code_base + 0x299DBD8, (void*)my_esj_club_guard, &g_orig_esj, "esj-guard");
3264}
3265
3266// ==================== VERIFY IAP (v4.63: server-side validation PROBE + Code=0 FORCE) ====================
3267// Server-side: PurchaseController -> NetworkController.VerifyIAPRequest(type,transactionId,receipt,
3268// onComplete) @0x28ad5c4 salva onComplete in wzx [this+0x48], fa POST a verifyinapppurchase-...-uc.
3269// a.run.app/. La callback risposta e' dnv(HTTPRequest,HTTPResponse) @0x28ad894: legge status=[b+0x18],
3270// se 200 parsa il body JSON in VerifyIAPResponseDTO (bl 0x3373244 @0x28ad928) e invoca wzx(dto);
3271// se status!=200 o b=null crea un DTO vuoto col ctor (Code=0 default) e invoca wzx(dto) comunque.
3272// PROBLEMA: il server rifiuta l'orderId fake "MOD.xxx" rispondendo body {"Code":<err>} con HTTP 200
3273// => il DTO parsato ha Code!=0 => l'onComplete rifiuta il grant (nessun effetto in game).
3274// FIX v4.63: patch a 4 byte del `bl 0x3373244` @0x28ad928 -> stub che chiama il parse originale e,
3275// sul DTO risultante, azzera Code(+0x10) e setta AlreadyVerified(+0x20)/S2STracked(+0x21) a true.
3276// Stub ARM64 verificata con clang (rkforce_stub.s): nessun clobber oltre caller-saved/x30 gestito.
3277static void *g_orig_verify_iap = nullptr;
3278static void *g_orig_dnv = nullptr;
3279
3280static void *my_verify_iap(void *x0, void *x1, void *x2, void *x3, void *x4) {
3281 LOGI("VERIFY_IAP(0x28ad5c4): CALLED this=%p type=%p txn=%p receipt=%p onComplete=%p",
3282 x0, x1, x2, x3, x4);
3283 maybe_dump_string(x1, "vtype");
3284 maybe_dump_string(x2, "vtxn");
3285 maybe_dump_string(x3, "vreceipt");
3286 return g_orig_verify_iap ? ((void *(*)(void*,void*,void*,void*,void*))g_orig_verify_iap)(x0, x1, x2, x3, x4) : nullptr;
3287}
3288
3289static void *my_dnv(void *x0, void *x1, void *x2, void *x3, unsigned int x4) {
3290 uint32_t status = 0;
3291 uintptr_t wzx = 0;
3292 if (x2) safe_read_self((uintptr_t)x2 + 0x18, &status, 4);
3293 if (x0) safe_read_self((uintptr_t)x0 + 0x48, &wzx, 8);
3294 LOGI("DNV(0x28ad894) [VerifyIAPResponseCb]: CALLED this=%p req=%p resp=%p status=%u wzx=%p",
3295 x0, x1, x2, status, (void*)wzx);
3296 return g_orig_dnv ? ((post_trace_fn)g_orig_dnv)(x0, x1, x2, x3, x4) : nullptr;
3297}
3298
3299static void install_verify_iap_force(void) {
3300 if (!g_code_base) { LOGE("VERIFY_IAP: no g_code_base"); return; }
3301 uintptr_t off = 0x28AD928; // bl 0x3373244 (parse DTO) dentro dnv
3302 uintptr_t target = g_code_base + off;
3303 uint32_t orig_bl = 0x942B1647; // byte originali verificati = `bl 0x3373244`
3304 uint32_t read = 0;
3305 if (!safe_read_self(target, &read, 4) || read != orig_bl) {
3306 LOGE("VERIFY_IAP force: unexpected bytes @0x28ad928 (got %08x, want %08x) - SKIP", read, orig_bl);
3307 return;
3308 }
3309
3310 void *stub = alloc_exec_near((void*)target, 0x40);
3311 if (!stub) { LOGE("VERIFY_IAP force: alloc_exec_near failed"); return; }
3312 uint32_t *s = (uint32_t*)stub;
3313 static const uint32_t CODE[11] = {
3314 0xf81f0ffe, // str x30,[sp,#-0x10]!
3315 0x58000130, // ldr x16,[pc,#0x28] -> literal @stub+0x28
3316 0xd63f0200, // blr x16 (call 0x3373244: x0=json, x1=type => DTO in x0)
3317 0xb40000a0, // cbz x0, +0x14 (skip se parse null)
3318 0xb900101f, // str wzr,[x0,#0x10] Code = 0
3319 0x52800028, // mov w8,#1
3320 0x39008008, // strb w8,[x0,#0x20] AlreadyVerified = true
3321 0x39008408, // strb w8,[x0,#0x21] S2STracked = true
3322 0xf84107fe, // ldr x30,[sp],#0x10 (restore return addr 0x28ad92c)
3323 0xd65f03c0, // ret
3324 0x00000000 // (padding, literal 8B allocato sotto con align)
3325 };
3326 for (int i = 0; i < 11; i++) s[i] = CODE[i];
3327 *(uintptr_t*)((uint8_t*)stub + 0x28) = (uintptr_t)(g_code_base + 0x3373244); // parse setter
3328 __builtin___clear_cache((char*)stub, (char*)stub + 0x40);
3329
3330 int64_t bl_delta = (int64_t)((uintptr_t)stub - target) / 4;
3331 if (bl_delta < -(1<<25) || bl_delta >= (1<<25)) {
3332 LOGE("VERIFY_IAP force: bl out of range delta=%lld", (long long)bl_delta);
3333 return;
3334 }
3335 uint32_t bl_ins = 0x94000000 | (bl_delta & 0x3FFFFFF);
3336 if (proc_mem_write(target, &bl_ins, 4)) {
3337 __builtin___clear_cache((char*)target, (char*)target + 4);
3338 LOGI("VERIFY_IAP force: patched bl @0x28ad928 -> stub %p (Code=0 on parsed DTO) readback=%08x",
3339 stub, *(uint32_t*)target);
3340 } else {
3341 LOGE("VERIFY_IAP force: proc_mem_write FAILED at 0x28ad928");
3342 }
3343}
3344
3345static void install_verify_iap_probes(void) {
3346 if (!g_code_base) { LOGE("VERIFY_IAP probe: no g_code_base"); return; }
3347 install_post_trace_hook(g_code_base + 0x28AD5C4, (void*)my_verify_iap, &g_orig_verify_iap, "verifyiap");
3348 install_post_trace_hook(g_code_base + 0x28AD894, (void*)my_dnv, &g_orig_dnv, "dnv");
3349 install_verify_iap_force();
3350}
3351
3352// ==================== FALLBACK ENTRY PROBE (0x28a92e8) ====================
3353// entry+0x160 (fallback GRANTBL): ldr x8,[x20,#0x10]; cbz x8,0x28a9428; ...
3354// NON useremo inline_hooker qui: nei 16 byte c'e' un cbz pc-relative che, copiato
3355// nel backup trampoline da inline_hooker, verrebbe rieseguito alla NUOVA posizione
3356// con displacement sbagliato -> branch errato. Invece patch a 4 byte (bl), come
3357// install_x20_gate_probe: la stub logga, poi RISEGUE l'istruzione originale
3358// (ldr x8,[x20,#0x10]) e fa b a 0x28a92ec (il cbz nativo successivo). Cosi' il
3359// flusso nativo continua identico. Stub: dxn CODE[70] (gia' verificata) + resume.
3360static void grant_probe_fallback_logger(void) {
3361 LOGI("GRANT_CHAIN_fallback: ENTRY @0x28a92e8 (fallback GRANTBL, patched 4-byte bl)");
3362}
3363
3364static void install_fallback_entry_probe(void) {
3365 if (!g_code_base) { LOGE("fallback probe: no code_base (store hook failed)"); return; }
3366 uintptr_t off = 0x28A92E8;
3367 uintptr_t target = g_code_base + off;
3368 void *stub = alloc_exec_near((void*)target, 0x148);
3369 if (!stub) { LOGE("fallback probe: alloc_exec_near failed"); return; }
3370 uint32_t *s = (uint32_t*)stub;
3371
3372 static const uint32_t CODE[70] = {
3373 0xd11403ff,0xa90007e0,0xa9010fe2,0xa90217e4,0xa9031fe6,0xa90427e8,0xa9052fea,0xa90637ec,0xa9073fee,0xa90847f0,
3374 0xa9094ff2,0xa90a57f4,0xa90b5ff6,0xa90c67f8,0xa90d6ffa,0xa90e77fc,0xf9007bfe,0xad0807e0,0xad090fe2,0xad0a17e4,
3375 0xad0b1fe6,0xad0c27e8,0xad0d2fea,0xad0e37ec,0xad0f3fee,0xad1047f0,0xad114ff2,0xad1257f4,0xad135ff6,0xad1467f8,
3376 0xad156ffa,0xad1677fc,0xad177ffe,0x580004b0,0xd63f0200,0xad577ffe,0xad5677fc,0xad556ffa,0xad5467f8,0xad535ff6,
3377 0xad5257f4,0xad514ff2,0xad5047f0,0xad4f3fee,0xad4e37ec,0xad4d2fea,0xad4c27e8,0xad4b1fe6,0xad4a17e4,0xad490fe2,
3378 0xad4807e0,0xf9407bfe,0xa94e77fc,0xa94d6ffa,0xa94c67f8,0xa94b5ff6,0xa94a57f4,0xa9494ff2,0xa94847f0,0xa9473fee,
3379 0xa94637ec,0xa9452fea,0xa94427e8,0xa9431fe6,0xa94217e4,0xa9410fe2,0xa94007e0,0x911403ff,0x58000090,0xd61f0200
3380 };
3381 for (int i = 0; i < 70; i++) s[i] = CODE[i];
3382 *(uintptr_t*)((uint8_t*)stub + 0x118) = (uintptr_t)(uintptr_t)grant_probe_fallback_logger;
3383
3384 // Resume block after CODE[70] (offset 0x128): ri-esegue l'istruzione originale
3385 // ldr x8,[x20,#0x10] (0xf9400a88, dal disasm verif. dal GRANTBL dump a runtime)
3386 // e salta al cbz nativo @0x28a92ec. Salto con ldr x16,[pc,#8]/br x16 (idioma
3387 // gia' verificato di inline_hooker, literal quad a 8-align) => NESSUN vincolo
3388 // di range: il vecchio `b imm26` falliva con stub a -55MB (check errato ±32MB).
3389 // 0x120 viene usato da CODE[68] (ldr x16,@0x120) come ingresso del resume.
3390 uint8_t *resume = (uint8_t*)stub + 0x128;
3391 *(uint32_t*)(resume + 0x00) = 0xf9400a88; // ldr x8,[x20,#0x10] (istruzione originale)
3392 *(uint32_t*)(resume + 0x04) = 0xd503201f; // nop (allinea il literal a 8 byte)
3393 *(uint32_t*)(resume + 0x08) = 0x58000050; // ldr x16,[pc,#8] -> literal @resume+0x10
3394 *(uint32_t*)(resume + 0x0c) = 0xd61f0200; // br x16
3395 *(uintptr_t*)(resume + 0x10) = (uintptr_t)(g_code_base + 0x28A92EC); // cbz nativo
3396 *(uintptr_t*)((uint8_t*)stub + 0x120) = (uintptr_t)resume;
3397 __builtin___clear_cache((char*)stub, (char*)stub + 0x148);
3398
3399 int64_t bl_delta = (int64_t)((uintptr_t)stub - target) / 4;
3400 if (bl_delta < -(1<<25) || bl_delta >= (1<<25)) {
3401 LOGE("fallback probe: bl out of range delta=%lld", (long long)bl_delta);
3402 return;
3403 }
3404 uint32_t bl_ins = 0x94000000 | (bl_delta & 0x3FFFFFF);
3405 if (proc_mem_write(target, &bl_ins, 4)) {
3406 __builtin___clear_cache((char*)target, (char*)target + 4);
3407 LOGI("fallback probe: patched ldr-x8@0x28a92e8 -> bl %p (log-only, resume->0x28a92ec)", stub);
3408 LOGI("fallback probe: readback=%08x", *(uint32_t*)target);
3409 } else {
3410 LOGE("fallback probe: proc_mem_write FAILED at 0x28a92e8");
3411 }
3412}
3413
3414// ==================== TRACCIATURA COMPLETA 0x28aea94 (processing intermedio) ====================
3415// 0x28aea94 = "proc00" = funzione che ProcessPurchase chiama a 0x28aef88 quando il fallback
3416// va sul path che DEVE erogare (returned 1). DENTRO proc00 ci sono molti cbz che possono
3417// fare early-exit a 0x28aec2c PRIMA del `bl 0x28af04c` (28aec08, l'handler che eroga).
3418// Il log della 4.34 mostra che `VERIFICA VALIDATOR` (dentro 0x28af04c) NON scatta => proc00
3419// esce presto. Per scoprire QUALE gate blocca, tracciamo l'INGRESSO di proc00 loggando
3420// tutti i valori che determinano i cbz: x0=self, x1=args, x2=component, [args+0x18],
3421// [self+0x30]=product. Con questi sapremo dov'e' il blocco senza analizzare staticamente.
3422// proc00 = 0x28aea94 = entry(0x28aed00) - 0x26C.
3423typedef int (*proc00_fn)(void *self, void *args, void *component);
3424static proc00_fn g_orig_proc00 = nullptr;
3425static uintptr_t g_proc00_entry = 0;
3426
3427static void log_proc00_state(uintptr_t self, uintptr_t args, uintptr_t component,
3428 uintptr_t args8, uintptr_t args10, uintptr_t args18, uintptr_t self30) {
3429 LOGI("PROC00 STATE: self=0x%llx args=0x%llx component=0x%llx",
3430 (unsigned long long)self, (unsigned long long)args, (unsigned long long)component);
3431 LOGI("PROC00 STATE: [args+0x8]=0x%llx %s | [args+0x10]=0x%llx %s",
3432 (unsigned long long)args8, args8 ? "NON-NULL" : "NULL",
3433 (unsigned long long)args10, args10 ? "NON-NULL" : "NULL ** blocco cbz 28aeb70 **");
3434 LOGI("PROC00 STATE: [args+0x18]=0x%llx %s | [self+0x30](product)=0x%llx %s",
3435 (unsigned long long)args18, args18 ? "NON-NULL" : "NULL ** blocco cbz 28aeb30 **",
3436 (unsigned long long)self30, self30 ? "NON-NULL" : "NULL ** blocco cbz 28aebe4 **");
3437}
3438
3439// Legge in modo sicuro [base+off]; se base e' null o la regione non e' leggibile ritorna 0.
3440static uintptr_t proc00_deref(uintptr_t base, uintptr_t off) {
3441 if (!base) return 0;
3442 uintptr_t v = 0;
3443 if (safe_read_self(base + off, &v, sizeof(v))) return v;
3444 return 0;
3445}
3446
3447static int my_proc00(void *self, void *args, void *component) {
3448 uintptr_t sb = (uintptr_t)self;
3449 uintptr_t ab = (uintptr_t)args;
3450 uintptr_t self30 = proc00_deref(sb, 0x30);
3451 uintptr_t args18 = proc00_deref(ab, 0x18);
3452 uintptr_t args8 = proc00_deref(ab, 0x08);
3453 uintptr_t args10 = proc00_deref(ab, 0x10);
3454 LOGI("PROC00 ENTRY: called self=%p args=%p component=%p", self, args, component);
3455 log_proc00_state(sb, ab, (uintptr_t)component, args8, args10, args18, self30);
3456 if (g_orig_proc00) {
3457 int r = g_orig_proc00(self, args, component);
3458 LOGI("PROC00 RET: orig returned %d (not 0x30 chain => early-exit se diverso da path erogazione)", r);
3459 return r;
3460 }
3461 return 0;
3462}
3463
3464static void install_proc00_trace(void) {
3465 if (!g_proc_purchase_entry) { LOGE("proc00 trace: g_proc_purchase_entry not set"); return; }
3466 g_proc00_entry = g_proc_purchase_entry - 0x26C; // 0x28aea94
3467 uintptr_t orig = g_proc00_entry;
3468 LOGI("proc00 trace: targeting 0x28aea94 (entry %p)", (void*)orig);
3469 void *trampoline = alloc_exec_near((void*)orig, 64);
3470 if (!trampoline) { LOGE("proc00 trace: alloc_exec_near trampoline failed"); return; }
3471 memcpy(trampoline, (void*)orig, 16);
3472 fix_adrp_in_trampoline(trampoline, (void*)orig);
3473 uint32_t *t = (uint32_t*)trampoline;
3474 t[4] = 0x58000050;
3475 t[5] = 0xD61F0200;
3476 *(uint64_t*)&t[6] = (uint64_t)orig + 16;
3477 __builtin___clear_cache((char*)trampoline, (char*)trampoline + 64);
3478 uint8_t jmp[16];
3479 *(uint32_t*)&jmp[0] = 0x58000050;
3480 *(uint32_t*)&jmp[4] = 0xD61F0200;
3481 *(uint64_t*)&jmp[8] = (uint64_t)my_proc00;
3482 if (proc_mem_write((uintptr_t)orig, jmp, 16)) {
3483 __builtin___clear_cache((char*)orig, (char*)orig + 16);
3484 if (memcmp((void*)orig, jmp, 16) == 0) {
3485 g_orig_proc00 = (proc00_fn)trampoline;
3486 LOGI("proc00 trace: inline hook OK (func=0x28aea94, trampoline=%p)", (void*)trampoline);
3487 } else {
3488 LOGE("proc00 trace: inline hook VERIFY FAILED — patch did not stick");
3489 }
3490 } else {
3491 LOGE("proc00 trace: proc_mem_write FAILED at 0x28aea94");
3492 }
3493}
3494
3495// Probe sui gate interni di 0x28aea94 che possono fare early-exit a 0x28aec2c (blocco).
3496// Loggiamo il valore che determina il gate e poi RIPRODUCIAMO la decisione originale,
3497// cosi' la logica resta intatta e sappiamo esattamente QUALE gate scatta.
3498// kind 0 = cbz x21 (args) [offset 0x94]
3499// kind 1 = cbz [x21+0x18] (field args) [offset 0x9C]
3500// kind 2 = cbz x20 (product, [self+0x30]) [offset 0x150]
3501// NOTA: len reg reg offset di patching dei byte viene dichiarato come intero non usato.
3502static void log_cbz_gate(int kind, uintptr_t v) {
3503 const char *name = (kind==2)?"x20(product,[self+0x30])":(kind==1)?"[x21+0x18](field args)":"x21(args)";
3504 LOGI("PROC00 CBZ[%d]: %s = 0x%llx %s", kind, name, (unsigned long long)v,
3505 v ? "NON-NULL => gate OK, continua" : "NULL => EARLY-EXIT 0x28aec2c ** questo gate BLOCCA **");
3506}
3507
3508static void install_cbz_probe(uintptr_t proc00, uint32_t off, int kind, const char *name, uintptr_t early) {
3509 uintptr_t target = proc00 + off;
3510 void *stub = alloc_exec_near((void*)target, 160); // 0xA0
3511 if (!stub) { LOGE("cbz probe[%s]: alloc_exec_near stub failed", name); return; }
3512 uint32_t *s = (uint32_t*)stub;
3513 // log_cbz_gate(int kind, uintptr_t v): x0=kind, x1=v
3514 // DESIGN ROBUSTO: NON preserviamo il gate in un registro a parte attraverso la call C
3515 // (che corrompeva lo stack-slot e portava il `ret` a un indirizzo spazzatura => SIGBUS
3516 // a pc=0x12d in 4.39). Il gate viene RICALCOLATO dopo i pop, dall'istanza preservata:
3517 // kind0: mov x27, x21 (args)
3518 // kind1: ldr x27,[x21,#0x18] (field args) + mov x8,x27
3519 // kind2: mov x27, x20 (product)
3520 // Prima della call C usiamo x27 per l'argomento x1. Tutti i pop sono LIFO corretti.
3521 // Ist: 0x00..0x5f, dati logger@0x68, early@0x70. size 128 (0x80).
3522 s[0x00/4] = 0xa9bf7be0; // stp x0, x30, [sp, #-0x10]!
3523 s[0x04/4] = 0xa9bf0be1; // stp x1, x2, [sp, #-0x10]!
3524 s[0x08/4] = 0xa9bf1be3; // stp x3, x4, [sp, #-0x10]!
3525 s[0x0c/4] = 0xa9bf53f5; // stp x20, x21, [sp, #-0x10]!
3526 s[0x10/4] = 0xa9bf5bf7; // stp x22, x23, [sp, #-0x10]!
3527 s[0x14/4] = 0xa9bf63f9; // stp x24, x25, [sp, #-0x10]!
3528 s[0x18/4] = 0xa9bf6bfb; // stp x26, x27, [sp, #-0x10]!
3529 s[0x1c/4] = (kind==2) ? 0xaa1403fb : (kind==1) ? 0xaa0803fb : 0xaa1503fb; // mov x27, gate-reg (64-bit)
3530 s[0x20/4] = 0x52800000 | ((uint32_t)kind << 5); // mov w0, #<kind>
3531 s[0x24/4] = 0xaa1b03e1; // mov x1, x27 (v)
3532 s[0x28/4] = 0x58000250; // ldr x16, [pc, #0x48] -> literal @0x70 (logger)
3533 s[0x2c/4] = 0xd63f0200; // blr x16 (call log_cbz_gate)
3534 s[0x30/4] = 0xa8c16bfb; // ldp x26, x27, [sp], #0x10
3535 s[0x34/4] = 0xa8c163f9; // ldp x24, x25, [sp], #0x10
3536 s[0x38/4] = 0xa8c15bf7; // ldp x22, x23, [sp], #0x10
3537 s[0x3c/4] = 0xa8c153f5; // ldp x20, x21, [sp], #0x10
3538 s[0x40/4] = 0xa8c11be3; // ldp x3, x4, [sp], #0x10
3539 s[0x44/4] = 0xa8c10be1; // ldp x1, x2, [sp], #0x10
3540 s[0x48/4] = 0xa8c17be0; // ldp x0, x30, [sp], #0x10
3541 // RICALCOLA gate in x27 dall'istanza preservata (dopo tutti i pop).
3542 if (kind==1) {
3543 s[0x4c/4] = 0xf9400ebb; // ldr x27, [x21, #0x18] (rilegge field args)
3544 s[0x50/4] = 0xaa1b03e8; // mov x8, x27 (ripristina x8!)
3545 } else {
3546 s[0x4c/4] = (kind==2) ? 0xaa1403fb : 0xaa1503fb; // mov x27, x20 / mov x27, x21
3547 s[0x50/4] = 0xd503201f; // nop
3548 }
3549 s[0x54/4] = 0xb400005b; // cbz x27, #+0x8 -> 0x5c (early) se x27==0
3550 s[0x58/4] = 0xd65f03c0; // ret (x27!=0: x30=entry+4 => continua proc00)
3551 s[0x5c/4] = 0x580000f0; // ldr x16, [pc, #0x1c] -> literal @0x78 (early)
3552 s[0x60/4] = 0xd61f0200; // br x16
3553 s[0x64/4] = 0xd503201f; // nop
3554 s[0x68/4] = 0xd503201f; // nop
3555 s[0x6c/4] = 0xd503201f; // nop
3556 *(uint64_t*)((uint8_t*)stub + 0x70) = (uint64_t)(uintptr_t)log_cbz_gate; // literal logger
3557 *(uint64_t*)((uint8_t*)stub + 0x78) = (uint64_t)early; // literal early target
3558 __builtin___clear_cache((char*)stub, (char*)stub + 128);
3559
3560 int64_t delta = (int64_t)((uintptr_t)stub - target) / 4;
3561 if (delta < -(1<<25) || delta >= (1<<25)) { LOGE("cbz probe[%s]: bl out of range", name); return; }
3562 uint32_t bl_ins = 0x94000000 | (delta & 0x3FFFFFF);
3563 if (proc_mem_write(target, &bl_ins, 4)) {
3564 __builtin___clear_cache((char*)target, (char*)target + 4);
3565 LOGI("cbz probe[%s]: patched cbz@0x%lx -> bl %p (gate observer, logic intact)", name, (unsigned long)target, stub);
3566 } else {
3567 LOGE("cbz probe[%s]: proc_mem_write FAILED at 0x%lx", name, (unsigned long)target);
3568 }
3569}
3570
3571// ==================== NOP GATE NON TRACCIATI in 0x28aea94 ====================
3572// Diag 4.40: i 3 cbz tracciati (0x94, 0x9C, 0x150) passano tutti NON-NULL, ma il flusso
3573// non arriva mai al handler 0x28af04c. I 5 cbz INTERMEDI non tracciati (0xD4, 0xDC, 0xE4,
3574// 0x108, 0x110) - che dipendono da x19/qargs creati dentro proc00 e non osservati -
3575// sono i candidati che fanno early-exit a 0x28aec2c. Li trasformiamo in NOP per forzare
3576// il flusso fino al bl 0x28af04c. NOP su cbz = branch-over (non prende il salto).
3577static void nop_untraced_gates(void) {
3578 if (!g_proc00_entry) { LOGE("nop_untraced_gates: g_proc00_entry not set"); return; }
3579 const uint32_t nop = 0xd503201f; // nop (ARM64)
3580 // Offset (from proc00 entry 0x28aea94) dei 5 cbz intermedi non tracciati.
3581 // CBZ[1] e' a 0x9C, CBZ[2] a 0x150; tra questi ci sono le gate 0xD4/0xDC/0xE4/0x108/0x110.
3582 const uintptr_t offs[] = { 0xD4, 0xDC, 0xE4, 0x108, 0x110 };
3583 int done = 0;
3584 for (size_t i = 0; i < sizeof(offs) / sizeof(offs[0]); i++) {
3585 uintptr_t a = g_proc00_entry + offs[i];
3586 if (proc_mem_write(a, &nop, sizeof(nop))) {
3587 LOGI("nop_untraced_gates: NOP cbz@0x%lx", (unsigned long)offs[i]);
3588 done++;
3589 } else {
3590 LOGE("nop_untraced_gates: proc_mem_write FAILED cbz@0x%lx", (unsigned long)offs[i]);
3591 }
3592 }
3593 LOGI("nop_untraced_gates: DONE %d/%d gate NOPed", done, (int)(sizeof(offs) / sizeof(offs[0])));
3594}
3595
3596// ==================== TRACCIA HANDLER 0x28af04c (completamento) ====================
3597// Se il flusso arriva qui (dopo aver superato i gate interni di 0x28aea94), l'handler
3598// di completamento viene chiamato: logga l'ingresso per confermare che il path di
3599// erogazione e' stato raggiunto fino a questo punto. 0x28af04c = entry(0x28aed00)+0x34C.
3600typedef int (*proc01_fn)(void *x0, void *x1, void *x2, void *x3, void *x4, void *x5, void *x6);
3601static proc01_fn g_orig_proc01 = nullptr;
3602static uintptr_t g_proc01_entry = 0;
3603
3604static int my_proc01(void *x0, void *x1, void *x2, void *x3, void *x4, void *x5, void *x6) {
3605 LOGI("PROC01 ENTRY (handler completamento 0x28af04c REACHED): x0=%p x1=%p x2=%p", x0, x1, x2);
3606 if (g_orig_proc01) return g_orig_proc01(x0, x1, x2, x3, x4, x5, x6);
3607 return 0;
3608}
3609
3610static void install_handler_trace(void) {
3611 if (!g_proc_purchase_entry) { LOGE("handler trace: g_proc_purchase_entry not set"); return; }
3612 g_proc01_entry = g_proc_purchase_entry + 0x34C; // 0x28af04c (= entry+0x34C, NOT 0x2E4!)
3613 uintptr_t orig = g_proc01_entry;
3614 void *trampoline = alloc_exec_near((void*)orig, 64);
3615 if (!trampoline) { LOGE("handler trace: alloc_exec_near trampoline failed"); return; }
3616 memcpy(trampoline, (void*)orig, 16);
3617 fix_adrp_in_trampoline(trampoline, (void*)orig);
3618 uint32_t *t = (uint32_t*)trampoline;
3619 t[4] = 0x58000050;
3620 t[5] = 0xD61F0200;
3621 *(uint64_t*)&t[6] = (uint64_t)orig + 16;
3622 __builtin___clear_cache((char*)trampoline, (char*)trampoline + 64);
3623 uint8_t jmp[16];
3624 *(uint32_t*)&jmp[0] = 0x58000050;
3625 *(uint32_t*)&jmp[4] = 0xD61F0200;
3626 *(uint64_t*)&jmp[8] = (uint64_t)my_proc01;
3627 if (proc_mem_write((uintptr_t)orig, jmp, 16)) {
3628 __builtin___clear_cache((char*)orig, (char*)orig + 16);
3629 if (memcmp((void*)orig, jmp, 16) == 0) {
3630 g_orig_proc01 = (proc01_fn)trampoline;
3631 LOGI("handler trace: inline hook OK (func=0x28af04c, trampoline=%p)", (void*)trampoline);
3632 } else {
3633 LOGE("handler trace: inline hook VERIFY FAILED — patch did not stick");
3634 }
3635 } else {
3636 LOGE("handler trace: proc_mem_write FAILED at 0x28af04c");
3637 }
3638}
3639
3640// dw.ProcessPurchase esce PRIMA del dispatch handler (entry+0x108) se uno dei cbz
3641// precedenti scatta: entry+0x8c (cbz x21, args null), entry+0x90 (cbz x20, componente
3642// null), entry+0xa0 (cbz x22, handler null). Il probe handler a entry+0x10c NON scatta,
3643// quindi ProcessPurchase prende un early-exit. x21(args) e' non-null => il gate
3644// sospetto e' cbz x20 a entry+0x90 (x20 = risultato di bl da [[0x5a59000+0x5a8]])
3645// che va a entry+0x2a0 (no grant). Questo probe logga x20 e replica il cbz:
3646// x20==0 -> br entry+0x2a0 (early exit)
3647// x20!=0 -> ret (entry+0x94, continua ProcessPurchase)
3648static void log_component(uint64_t comp) {
3649 LOGI("PROC_PURCH_GATE: x20 (component) = 0x%llx %s",
3650 (unsigned long long)comp,
3651 comp ? "NON-NULL => passa cbz x20, continua" : "NULL => EARLY EXIT entry+0x2a0 (NO handler dispatch, NO grant)");
3652}
3653
3654static void install_x20_gate_probe(void) {
3655 if (!g_proc_purchase_entry) { LOGE("gate x20 probe: g_proc_purchase_entry not set (store hook failed)"); return; }
3656 uintptr_t target = g_proc_purchase_entry + 0x90; // cbz x20, #+0x210 (early exit)
3657 void *stub = alloc_exec_near((void*)target, 104); // 0x68 bytes
3658 if (!stub) { LOGE("gate x20 probe: alloc_exec_near stub failed"); return; }
3659 uint32_t *s = (uint32_t*)stub;
3660
3661 s[0x00/4] = 0xa9bf7be0; // stp x0, x30, [sp, #-0x10]!
3662 s[0x04/4] = 0xa9bf0be1; // stp x1, x2, [sp, #-0x10]!
3663 s[0x08/4] = 0xa9bf53e3; // stp x3, x20, [sp, #-0x10]!
3664 s[0x0c/4] = 0xaa1403fb; // mov x27, x20 (salva componente, 64-bit NON tronca)
3665 s[0x10/4] = 0xaa1b03e0; // mov x0, x27 (arg = componente, 64-bit)
3666 s[0x14/4] = 0x58000130; // ldr x16, [pc, #0x24] -> literal @0x38
3667 s[0x18/4] = 0xd63f0200; // blr x16 (call log_component)
3668 s[0x1c/4] = 0xd503201f; // nop (x0 non e' input del flusso; log_component usa w0/consume) -- mantiene allineamento
3669 s[0x20/4] = 0xa8c153e3; // ldp x3, x20, [sp], #0x10
3670 s[0x24/4] = 0xa8c10be1; // ldp x1, x2, [sp], #0x10
3671 s[0x28/4] = 0xa8c17be0; // ldp x0, x30, [sp], #0x10
3672 *(uint64_t*)((uint8_t*)stub + 0x38) = (uint64_t)(uintptr_t)log_component; // literal logger
3673 s[0x34/4] = 0xd503201f; s[0x40/4] = 0xd503201f; s[0x44/4] = 0xd503201f; // nop pad
3674 s[0x2c/4] = 0xb40000d4; // cbz x20, #+0x18 -> 0x44 (early exit) se x20==0
3675 s[0x30/4] = 0xd65f03c0; // ret (x30=entry+0x94 => continua ProcessPurchase)
3676 s[0x44/4] = 0x580000f0; // ldr x16, [pc, #0x1c] -> literal @0x60
3677 s[0x48/4] = 0xd61f0200; // br x16
3678 s[0x4c/4] = 0xd503201f; s[0x50/4] = 0xd503201f; s[0x54/4] = 0xd503201f; // nop
3679 s[0x58/4] = 0xd503201f; s[0x5c/4] = 0xd503201f; // nop
3680 *(uint64_t*)((uint8_t*)stub + 0x60) = (uint64_t)(g_proc_purchase_entry + 0x2a0); // literal early-exit target
3681 __builtin___clear_cache((char*)stub, (char*)stub + 104);
3682
3683 int64_t delta = (int64_t)((uintptr_t)stub - target) / 4;
3684 if (delta < -(1<<25) || delta >= (1<<25)) {
3685 LOGE("gate x20 probe: bl out of range delta=%lld", (long long)delta); return;
3686 }
3687 uint32_t bl_ins = 0x94000000 | (delta & 0x3FFFFFF);
3688 if (proc_mem_write(target, &bl_ins, 4)) {
3689 __builtin___clear_cache((char*)target, (char*)target + 4);
3690 LOGI("gate x20 probe: patched cbz-x20@entry+0x90 -> bl %p (x20 gate observer)", stub);
3691 LOGI("gate x20 probe: initial cbz bytes verified: %08x", *(uint32_t*)target);
3692 } else {
3693 LOGE("gate x20 probe: proc_mem_write FAILED at entry+0x90");
3694 }
3695}
3696
3697// Verifica via /proc/self/maps che [addr, addr+len) sia coperto da una regione
3698// con permesso di lettura. Il modulo gira nel processo target, quindi se la
3699// regione esiste possiamo leggere direttamente con memcpy (come fa proc_mem_write
3700// per le scritture). /proc/self/mem NON funziona: il gioco ha dumpable=0 (anti
3701// debug) e pread fallisce con EIO ancora prima del parsing della catena puntatori.
3702// maps rimane leggibile dal processo stesso.
3703static bool self_range_readable(uintptr_t addr, size_t len) {
3704 uintptr_t end = addr + len;
3705 FILE *f = fopen("/proc/self/maps", "r");
3706 if (!f) return false; // se maps non e' leggibile, non rischiamo memcpy (evita SEGV)
3707 char line[512];
3708 bool ok = false;
3709 while (fgets(line, sizeof(line), f)) {
3710 uintptr_t rstart, rend;
3711 char perms[8] = {0};
3712 if (sscanf(line, "%llx-%llx %7s", (unsigned long long*)&rstart,
3713 (unsigned long long*)&rend, perms) != 3) continue;
3714 if (addr < rstart) continue;
3715 if (end > rend) continue;
3716 if (perms[0] == 'r') { ok = true; break; }
3717 }
3718 fclose(f);
3719 return ok;
3720}
3721
3722// Legge memoria del proprio processo con memcpy diretto, dopo aver verificato
3723// la regione su /proc/self/maps (niente segfault su indirizzi non mappati).
3724static bool safe_read_self(uintptr_t addr, void *buf, size_t len) {
3725 if (!addr || !len) return false;
3726 if (!self_range_readable(addr, len)) return false;
3727 memcpy(buf, (void*)addr, len);
3728 return true;
3729}
3730
3731// LOGGERS: printa un buffer come ascii inline + hex; evita caratteri strani.
3732static void dump_hbuf(const char *tag, uintptr_t addr, size_t len) {
3733 unsigned char b[80];
3734 if (len > sizeof(b)) len = sizeof(b);
3735 if (!safe_read_self(addr, b, len)) {
3736 LOGI("HNAME %s @%p: read FAIL", tag, (void*)addr);
3737 return;
3738 }
3739 char hex[256], asc[80];
3740 for (size_t i = 0; i < len; i++) {
3741 sprintf(hex + i*3, "%02x ", b[i]);
3742 asc[i] = (b[i] >= 0x20 && b[i] < 0x7f) ? (char)b[i] : '.';
3743 }
3744 if (len*3 > 0) hex[len*3-1] = 0;
3745 asc[len] = 0;
3746 LOGI("HNAME %s @%p (%zuB): %s | %s", tag, (void*)addr, len, hex, asc);
3747}
3748
3749// Il metodo handler di [dw+0x50] viene risolto dal nome in un singleton il2cpp.
3750// Disassembly 4.29: entry+0xa4 adrp x10,%hi(0x5a59000); entry+0xac ldr x10,[x10,#0x560]
3751// => slot = base + 0x5a59000 + 0x560 = base + 0x5a59560 (NON 0x5a55560 come 4.28).
3752// entry+0xb4 ldr x1,[x10] => p1 e' il puntatore al nome, [p1] = Il2CppString.
3753// Oggetto creato da ProcessPurchase: entry+0x10 ldr x22,[x22,#0x5a8] => base+0x5a595a8.
3754// Tipo usato da F2 (0x54597bc): adrp 0x5a52000 + ldr #0x968 => base+0x5a52968.
3755static void dump_handler_method_name(uintptr_t base) {
3756 uintptr_t slot = base + 0x5a59560;
3757 uint64_t p1 = 0, p2 = 0;
3758 if (!safe_read_self(slot, &p1, 8)) { LOGI("HNAME slot @%p: read FAIL", (void*)slot); return; }
3759 LOGI("HNAME slot @%p -> p1 = 0x%llx", (void*)slot, (unsigned long long)p1);
3760 if (!p1) return;
3761 if (!safe_read_self((uintptr_t)p1, &p2, 8)) { LOGI("HNAME p1 @%p: read FAIL", (void*)p1); return; }
3762 LOGI("HNAME p1 deref -> p2 = 0x%llx", (unsigned long long)p2);
3763 if (!p2) return;
3764 dump_hbuf("p2+0x10 (obj+0x10)", (uintptr_t)p2 + 0x10, 32);
3765 dump_hbuf("p2+0x14 (utf16 alt?)", (uintptr_t)p2 + 0x14, 32);
3766 dump_hbuf("p1+0x10", (uintptr_t)p1 + 0x10, 32);
3767 dump_hbuf("slot+0x8", slot + 0x8, 16);
3768}
3769
3770// Dump dei tipi usati dal grant path: classe oggetto creato (singleton base+0x5a595a8,
3771// cross-reference da entry+0x10) e classe del dispatch F2 (adrp 0x5a52000+ldr #0x968
3772// = base+0x5a52968). p1 = holder della classe, [p1] = Il2CppClass*. In Il2CppClass
3773// name a +0x10 (const char*) e namespaze a +0x18: leggo e stampo le stringhe.
3774static void dump_purchase_types(uintptr_t base) {
3775 uintptr_t sidx[2] = { base + 0x5a595a8, base + 0x5a52968 };
3776 const char *snam[2] = { "OBJCLASS", "F2CLASS" };
3777 for (int i = 0; i < 2; i++) {
3778 uint64_t p1 = 0, p2 = 0;
3779 if (!safe_read_self(sidx[i], &p1, 8)) { LOGI("%s slot @%p: read FAIL", snam[i], (void*)sidx[i]); continue; }
3780 LOGI("%s slot @%p -> p1 holder = 0x%llx", snam[i], (void*)sidx[i], (unsigned long long)p1);
3781 if (!p1) continue;
3782 if (!safe_read_self((uintptr_t)p1, &p2, 8)) { LOGI("%s p1 @%p: read FAIL", snam[i], (void*)p1); continue; }
3783 LOGI("%s klass = 0x%llx", snam[i], (unsigned long long)p2);
3784 if (!p2) continue;
3785 for (int off = 0x10; off <= 0x28; off += 8) {
3786 uint64_t sp = 0;
3787 if (!safe_read_self((uintptr_t)p2 + off, &sp, 8)) continue;
3788 char tmp[96];
3789 size_t n = 0;
3790 for (n = 0; n < 80 && sp; n++) {
3791 uint8_t c = 0;
3792 if (!safe_read_self((uintptr_t)sp + n, &c, 1)) break;
3793 if (c == 0) break;
3794 if (c < 0x20 || c > 0x7e) break;
3795 tmp[n] = (char)c;
3796 }
3797 tmp[n] = 0;
3798 if (n > 0) LOGI("%s klass+0x%X = 0x%llx '%s'", snam[i], off, (unsigned long long)sp, tmp);
3799 }
3800 dump_hbuf(snam[i], (uintptr_t)p2, 48);
3801 }
3802}
3803
3804// Grant async chiamato dal path di grant (entry+0x158). Loggo i 4 byte originali
3805// dell'istruzione per confermare a runtime che il path venga realmente raggiunto.
3806static void dump_grant_bl(uintptr_t entry) {
3807 if (!entry) { LOGE("grant bl: entry null"); return; }
3808 for (uintptr_t off = 0x148; off <= 0x160; off += 4) {
3809 uint32_t ins = 0;
3810 if (safe_read_self(entry + off, &ins, 4)) {
3811 LOGI("GRANTBL @entry+0x%X: %08x", (unsigned)(off), ins);
3812 } else {
3813 LOGI("GRANTBL @entry+0x%X: read FAIL", (unsigned)(off));
3814 }
3815 }
3816}
3817
3818typedef void (*google_pu_fn)(void *self, void *billingResult, void *purchases);
3819static google_pu_fn g_orig_google_pu = nullptr;
3820static void my_google_pu(void *self, void *br, void *purchases) {
3821 LOGI("STORE_STEP: my_google_pu ENTRY called by %p self=%p br=%p list=%p", __builtin_return_address(0), self, br, purchases);
3822 LOGI("STORE_STEP: my_google_pu: g_orig_google_pu=%p, will CALL ORIG and then next expected is dw.ProcessPurchase (my_proc_purchase) at %p", (void*)g_orig_google_pu, (void*)my_proc_purchase);
3823 if (g_orig_google_pu) g_orig_google_pu(self, br, purchases);
3824 LOGI("STORE_STEP: my_google_pu EXIT orig returned -> next is dw.ProcessPurchase (my_proc_purchase) should trigger within 1s");
3825 LOGI("STORE_STEP: my_google_pu: if my_proc_purchase NOT triggered, then Google listener not forwarding to dw");
3826}
3827
3828// ==================== LSPlant callback dispatcher ====================
3829
3830static jstring g_product_id = nullptr;
3831
3832static jclass g_cls_zzbm = nullptr; // deprecated, Play Billing <8.3.0, kept optional
3833static jclass g_cls_bci = nullptr;
3834static jclass g_cls_proxy = nullptr;
3835static jclass g_cls_gpbl_proxy = nullptr; // com.unity3d.services.store.gpbl.proxies.PurchasesResponseListenerProxy
3836static jclass g_cls_pu_proxy = nullptr; // com.unity3d.services.store.gpbl.proxies.PurchaseUpdatedListenerProxy
3837static jclass g_cls_max = nullptr;
3838static jclass g_cls_purchase = nullptr;
3839static jclass g_cls_zzcj = nullptr; // deprecated zzdc, kept optional
3840static jclass g_cls_billingresult = nullptr; // com/android/billingclient/api/BillingResult (v8.3.0+)
3841static jclass g_cls_billingbuilder = nullptr; // BillingResult$Builder
3842static jclass g_cls_productdetails = nullptr; // com/android/billingclient/api/ProductDetails
3843static jclass g_cls_queryresult = nullptr; // com/android/billingclient/api/QueryProductDetailsResult
3844static jclass g_cls_list = nullptr;
3845static jclass g_cls_arraylist = nullptr;
3846static jclass g_cls_intent = nullptr;
3847static jclass g_cls_jsonobject = nullptr;
3848static jclass g_cls_system = nullptr;
3849static jclass g_cls_boolean = nullptr; // java/lang/Boolean (per boxed return)
3850static jmethodID g_mid_purchase_init = nullptr;
3851static jmethodID g_mid_list_add = nullptr;
3852static jmethodID g_mid_arraylist_init = nullptr;
3853static jmethodID g_mid_intent_putextra_int = nullptr;
3854static jmethodID g_mid_intent_putsal = nullptr;
3855static jmethodID g_mid_forward = nullptr;
3856static jmethodID g_mid_jsonobj_init = nullptr;
3857static jmethodID g_mid_jsonobj_put = nullptr;
3858static jmethodID g_mid_system_time = nullptr;
3859static jmethodID g_mid_boolean_valueof = nullptr;
3860static jfieldID g_fid_zzcj_zzl = nullptr; // deprecated
3861static jmethodID g_mid_billing_newbuilder = nullptr;
3862static jmethodID g_mid_builder_setResponseCode = nullptr;
3863static jmethodID g_mid_builder_setDebugMessage = nullptr;
3864static jmethodID g_mid_builder_build = nullptr;
3865
3866static jobject g_backup_a = nullptr; // gpbl PurchasesResponseListenerProxy.onQueryPurchasesResponse
3867static jobject g_backup_b = nullptr; // (unused, Play Billing 8.3.0 zzbm hooks removed)
3868static jobject g_backup_c = nullptr; // (unused, Play Billing 8.3.0 zzaI hook removed)
3869static jobject g_backup_d = nullptr; // BillingClientImpl.queryProductDetailsAsync
3870static jobject g_backup_e = nullptr; // ProxyBillingActivity.onActivityResult
3871static jobject g_backup_f = nullptr; // MaxUnityAdManager.isRewardedAdReady
3872static jobject g_backup_g = nullptr; // gpbl PurchaseUpdatedListenerProxy.onPurchasesUpdated
3873static jobject g_backup_h = nullptr; // BillingClientImpl.launchBillingFlow
3874static jobject g_backup_i = nullptr; // BillingClientImpl.acknowledgePurchase
3875static jobject g_backup_j = nullptr; // BillingClientImpl.consumeAsync
3876
3877static JNIEnv *get_jni_env() {
3878 JNIEnv *env = nullptr;
3879 jint gret = g_jvm->GetEnv((void **)&env, JNI_VERSION_1_6);
3880 LOGI("get_jni_env: GetEnv=%d", gret);
3881 if (gret != JNI_OK) {
3882 jint aret = g_jvm->AttachCurrentThread(&env, nullptr);
3883 LOGI("get_jni_env: AttachCurrentThread=%d env=%p", aret, (void*)env);
3884 if (aret != 0 || !env) {
3885 LOGE("get_jni_env: AttachCurrentThread FAILED ret=%d", aret);
3886 return nullptr;
3887 }
3888 }
3889 return env;
3890}
3891
3892static jclass load_app_class(JNIEnv *env, const char *name) {
3893 if (!g_app_context) { LOGE("load_app_class: no app context"); return nullptr; }
3894 jclass ctx_cls = env->GetObjectClass(g_app_context);
3895 jmethodID gcl = env->GetMethodID(ctx_cls, "getClassLoader", "()Ljava/lang/ClassLoader;");
3896 jobject loader = env->CallObjectMethod(g_app_context, gcl);
3897 if (!loader) { env->ExceptionClear(); LOGE("load_app_class: no loader"); return nullptr; }
3898 jclass lc = env->FindClass("java/lang/ClassLoader");
3899 jmethodID load = env->GetMethodID(lc, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
3900 jstring jn = env->NewStringUTF(name);
3901 jclass cls = (jclass)env->CallObjectMethod(loader, load, jn);
3902 if (env->ExceptionCheck() || !cls) {
3903 env->ExceptionClear();
3904 LOGE("load_app_class: %s NOT found", name);
3905 env->DeleteLocalRef(jn);
3906 return nullptr;
3907 }
3908 jclass g = (jclass)env->NewGlobalRef(cls);
3909 env->DeleteLocalRef(cls);
3910 env->DeleteLocalRef(jn);
3911 env->DeleteLocalRef(loader);
3912 env->DeleteLocalRef(lc);
3913 LOGI("load_app_class: %s ok", name);
3914 return g;
3915}
3916
3917static jstring make_fake_json_string(JNIEnv *env) {
3918 jlong now = 0;
3919 if (g_cls_system && g_mid_system_time)
3920 now = env->CallStaticLongMethod(g_cls_system, g_mid_system_time);
3921 const char *pid = "com.lavalabs.dungeonrush.gems_100";
3922 bool release = false;
3923 if (g_product_id) {
3924 const char *c = env->GetStringUTFChars(g_product_id, nullptr);
3925 if (c) { pid = c; release = true; }
3926 }
3927 char json[1024];
3928 snprintf(json, sizeof(json),
3929 "{\"orderId\":\"MOD.%lld\",\"packageName\":\"com.lavalabs.dungeonrush\","
3930 "\"productIds\":[\"%s\"],\"productId\":\"%s\",\"purchaseToken\":\"mod_token_%lld\","
3931 "\"purchaseTime\":%lld,\"purchaseState\":1,\"acknowledged\":false,\"autoRenewing\":false,\"quantity\":1,\"developerPayload\":\"\"}",
3932 (long long)now, pid, pid, (long long)now, (long long)now);
3933 if (release) env->ReleaseStringUTFChars(g_product_id, pid);
3934 jstring j = env->NewStringUTF(json);
3935 LOGI("fake purchase JSON built (productId=%s)", pid);
3936 return j;
3937}
3938
3939static jobject make_fake_purchase(JNIEnv *env) {
3940 if (!g_cls_purchase || !g_mid_purchase_init) return nullptr;
3941 jstring jjson = make_fake_json_string(env);
3942 if (!jjson) return nullptr;
3943 // Use non-empty base64 signature to pass Unity validator (empty sig rejected)
3944 jstring jsig = env->NewStringUTF("MEUCIQDfak3SignatureForModBypassValidBase64Encoded1234567890==");
3945 jobject p = env->NewObject(g_cls_purchase, g_mid_purchase_init, jjson, jsig);
3946 if (env->ExceptionCheck()) { env->ExceptionClear(); p = nullptr; }
3947 env->DeleteLocalRef(jjson);
3948 env->DeleteLocalRef(jsig);
3949 if (p) LOGI("fake Purchase created");
3950 return p;
3951}
3952
3953static jclass g_mc_class = nullptr;
3954static jmethodID g_mid_invoke = nullptr;
3955
3956static void cache_reflection() {
3957 if (g_mc_class) return;
3958 JNIEnv *env = get_jni_env();
3959 if (!env) return;
3960 g_mc_class = (jclass)env->NewGlobalRef(env->FindClass("java/lang/reflect/Method"));
3961 g_mid_invoke = env->GetMethodID(g_mc_class, "invoke", "(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;");
3962}
3963
3964static jobject call_backup(JNIEnv *env, jobject backup, jobject original_this, jobject *args, int argc) {
3965 if (!backup) { LOGI("call_backup: no backup, skipping"); return nullptr; }
3966 cache_reflection();
3967 if (!g_mid_invoke) { LOGE("call_backup: invoke mid not cached"); return nullptr; }
3968 jclass obj_cls = env->FindClass("java/lang/Object");
3969 jobjectArray jargs = env->NewObjectArray(argc, obj_cls, nullptr);
3970 env->DeleteLocalRef(obj_cls);
3971 for (int i = 0; i < argc; i++) env->SetObjectArrayElement(jargs, i, args[i]);
3972 jobject ret = env->CallObjectMethod(backup, g_mid_invoke, original_this, jargs);
3973 if (env->ExceptionCheck()) {
3974 env->ExceptionClear();
3975 ret = nullptr;
3976 }
3977 env->DeleteLocalRef(jargs);
3978 return ret;
3979}
3980
3981static void send_ad_event(JNIEnv *env, const char *name, jobject adUnitId, jobject placement, const char *rewardLabel) {
3982 if (!g_cls_jsonobject || !g_mid_jsonobj_init || !g_mid_jsonobj_put || !g_cls_max || !g_mid_forward) return;
3983 jobject o = env->NewObject(g_cls_jsonobject, g_mid_jsonobj_init);
3984 if (!o) { env->ExceptionClear(); return; }
3985 jstring k, v;
3986 k = env->NewStringUTF("adUnitId");
3987 env->CallObjectMethod(o, g_mid_jsonobj_put, k, adUnitId);
3988 env->DeleteLocalRef(k);
3989 k = env->NewStringUTF("adFormat");
3990 v = env->NewStringUTF("REWARDED");
3991 env->CallObjectMethod(o, g_mid_jsonobj_put, k, v);
3992 env->DeleteLocalRef(k);
3993 env->DeleteLocalRef(v);
3994 k = env->NewStringUTF("networkName");
3995 v = env->NewStringUTF("ModBypass");
3996 env->CallObjectMethod(o, g_mid_jsonobj_put, k, v);
3997 env->DeleteLocalRef(k);
3998 env->DeleteLocalRef(v);
3999 if (placement) {
4000 k = env->NewStringUTF("networkPlacement");
4001 env->CallObjectMethod(o, g_mid_jsonobj_put, k, placement);
4002 env->DeleteLocalRef(k);
4003 }
4004 if (rewardLabel) {
4005 k = env->NewStringUTF("reward");
4006 jobject ro = env->NewObject(g_cls_jsonobject, g_mid_jsonobj_init);
4007 jstring lk = env->NewStringUTF("label");
4008 jstring lv = env->NewStringUTF(rewardLabel);
4009 env->CallObjectMethod(ro, g_mid_jsonobj_put, lk, lv);
4010 jstring ak = env->NewStringUTF("amount");
4011 jstring av = env->NewStringUTF("1");
4012 env->CallObjectMethod(ro, g_mid_jsonobj_put, ak, av);
4013 env->CallObjectMethod(o, g_mid_jsonobj_put, k, ro);
4014 env->DeleteLocalRef(lk); env->DeleteLocalRef(lv);
4015 env->DeleteLocalRef(ak); env->DeleteLocalRef(av);
4016 env->DeleteLocalRef(k); env->DeleteLocalRef(ro);
4017 }
4018 k = env->NewStringUTF("name");
4019 v = env->NewStringUTF(name);
4020 env->CallObjectMethod(o, g_mid_jsonobj_put, k, v);
4021 env->DeleteLocalRef(k);
4022 env->DeleteLocalRef(v);
4023 env->CallStaticVoidMethod(g_cls_max, g_mid_forward, o);
4024 if (env->ExceptionCheck()) env->ExceptionClear();
4025 env->DeleteLocalRef(o);
4026}
4027
4028static jobject make_success_billing_result(JNIEnv *env) {
4029 if (!env || !g_cls_billingresult || !g_mid_billing_newbuilder || !g_mid_builder_setResponseCode || !g_mid_builder_build) {
4030 LOGE("make_success_billing_result: missing billing builder classes/methods");
4031 return nullptr;
4032 }
4033 jobject builder = env->CallStaticObjectMethod(g_cls_billingresult, g_mid_billing_newbuilder);
4034 if (!builder || env->ExceptionCheck()) { env->ExceptionClear(); LOGE("make_success_billing_result: newBuilder failed"); return nullptr; }
4035 // setResponseCode(0) = OK
4036 jobject tmp = env->CallObjectMethod(builder, g_mid_builder_setResponseCode, 0);
4037 if (env->ExceptionCheck()) { env->ExceptionClear(); env->DeleteLocalRef(builder); LOGE("make_success_billing_result: setResponseCode failed"); return nullptr; }
4038 // builder may return new instance, update reference
4039 if (tmp && tmp != builder) { env->DeleteLocalRef(builder); builder = tmp; } else if (tmp) { /* same */ }
4040 // optional setDebugMessage("")
4041 if (g_mid_builder_setDebugMessage) {
4042 jstring dbg = env->NewStringUTF("");
4043 jobject tmp2 = env->CallObjectMethod(builder, g_mid_builder_setDebugMessage, dbg);
4044 if (!env->ExceptionCheck() && tmp2) {
4045 if (tmp2 != builder) { env->DeleteLocalRef(builder); builder = tmp2; }
4046 } else { env->ExceptionClear(); }
4047 env->DeleteLocalRef(dbg);
4048 }
4049 jobject result = env->CallObjectMethod(builder, g_mid_builder_build);
4050 if (env->ExceptionCheck()) { env->ExceptionClear(); result = nullptr; LOGE("make_success_billing_result: build failed"); }
4051 env->DeleteLocalRef(builder);
4052 if (result) LOGI("make_success_billing_result: OK BillingResult created %p", result);
4053 return result;
4054}
4055
4056extern "C" JNIEXPORT jobject JNICALL
4057Java_com_drmod_HookerCallback_nativeDispatch(JNIEnv *env, jclass cls,
4058 jint hookId, jobject self_obj, jobjectArray args) {
4059 if (env && env->ExceptionCheck()) env->ExceptionClear();
4060 LOGI("nativeDispatch: === HOOK ENTRY hookId=%d caller=%p ===", hookId, __builtin_return_address(0));
4061 int argc = args ? env->GetArrayLength(args) : 0;
4062 if (env && env->ExceptionCheck()) env->ExceptionClear();
4063 LOGI("nativeDispatch: argc=%d", argc);
4064 // Extensive next-step prediction for store debugging
4065 const char* nextStep = "unknown";
4066 switch(hookId) {
4067 case 0: nextStep = "onQueryPurchasesResponse (gpbl) -> inject fake Purchase -> call backup -> game grants via StoreListenerProxy (check inventory next)"; break;
4068 case 3: nextStep = "queryProductDetailsAsync -> capture productId -> create fake ProductDetails -> call onProductDetailsResponse -> NEXT is launchBillingFlow (hookId 8)"; break;
4069 case 8: nextStep = "launchBillingFlow -> bypass Play -> create fake Purchase -> call onPurchasesUpdated ($Proxy) -> NEXT is my_google_pu -> my_proc_purchase -> my_dnf -> grant"; break;
4070 case 7: nextStep = "onPurchasesUpdated (gpbl) -> inject fake Purchase -> call backup -> next is queryPurchases or grant"; break;
4071 case 5: nextStep = "showRewardedAd -> send 3 fake ad events -> reward"; break;
4072 case 6: nextStep = "isRewardedAdReady -> force true"; break;
4073 case 4: nextStep = "ProxyBillingActivity.onActivityResult -> set RESPONSE_CODE 0 + INAPP lists"; break;
4074 case 9: nextStep = "acknowledgePurchase -> fake success"; break;
4075 case 10: nextStep = "consumeAsync -> fake success"; break;
4076 }
4077 LOGI("nativeDispatch: hookId=%d NEXT EXPECTED: %s", hookId, nextStep);
4078
4079 jobject original_this = (argc > 0) ? env->GetObjectArrayElement(args, 0) : nullptr;
4080
4081 switch (hookId) {
4082 case 0: { // gpbl PurchasesResponseListenerProxy.onQueryPurchasesResponse(Object result, List) — inject fake + forward
4083 LOGI("STORE_STEP: hookId=0 entry called by %p original_this=%p -> will inject fake", __builtin_return_address(0), original_this);
4084 jobject billingResult = argc > 1 ? env->GetObjectArrayElement(args, 1) : nullptr;
4085 jobject list = argc > 2 ? env->GetObjectArrayElement(args, 2) : nullptr;
4086 if (env && list) {
4087 // Forza il BillingResult a OK (responseCode=0) così il gioco non short-circuita sull'errore.
4088 // Prefer new BillingResult builder (8.3.0+), fallback to deprecated zzdc field if present.
4089 jobject br = make_success_billing_result(env);
4090 if (br) {
4091 if (billingResult) env->DeleteLocalRef(billingResult);
4092 billingResult = br;
4093 LOGI("STORE_STEP: onQueryPurchasesResponse BillingResult replaced with builder OK");
4094 } else if (billingResult && g_cls_zzcj && g_fid_zzcj_zzl) {
4095 jobject br2 = env->GetStaticObjectField(g_cls_zzcj, g_fid_zzcj_zzl);
4096 if (br2) {
4097 env->DeleteLocalRef(billingResult);
4098 billingResult = br2;
4099 LOGI("onQueryPurchasesResponse: BillingResult replaced via deprecated zzdc");
4100 } else {
4101 env->DeleteLocalRef(br2);
4102 LOGI("onQueryPurchasesResponse: deprecated zzdc field fetch returned null");
4103 }
4104 env->ExceptionClear();
4105 }
4106 // Inietta fake con fallback per lista immutabile
4107 if (g_cls_purchase && g_mid_list_add && list) {
4108 jobject fake = make_fake_purchase(env);
4109 if (fake) {
4110 env->CallBooleanMethod(list, g_mid_list_add, fake);
4111 if (env->ExceptionCheck()) {
4112 env->ExceptionClear();
4113 LOGI("onQueryPurchasesResponse: list.add threw (immutable), creating mutable copy");
4114 jobject copy = nullptr;
4115 if (g_cls_arraylist && g_mid_arraylist_init) {
4116 jclass alCls = env->FindClass("java/util/ArrayList");
4117 jmethodID alInitCol = alCls ? env->GetMethodID(alCls, "<init>", "(Ljava/util/Collection;)V") : nullptr;
4118 if (alInitCol) {
4119 copy = env->NewObject(alCls, alInitCol, list);
4120 if (env->ExceptionCheck()) { env->ExceptionClear(); copy = nullptr; }
4121 }
4122 if (alCls) env->DeleteLocalRef(alCls);
4123 if (!copy) {
4124 copy = env->NewObject(g_cls_arraylist, g_mid_arraylist_init);
4125 if (copy && !env->ExceptionCheck()) {
4126 jmethodID addAll = env->GetMethodID(env->GetObjectClass(copy), "addAll", "(Ljava/util/Collection;)Z");
4127 if (addAll) { env->CallBooleanMethod(copy, addAll, list); if (env->ExceptionCheck()) env->ExceptionClear(); }
4128 } else env->ExceptionClear();
4129 }
4130 }
4131 if (copy) {
4132 env->CallBooleanMethod(copy, g_mid_list_add, fake);
4133 if (!env->ExceptionCheck()) {
4134 env->DeleteLocalRef(list);
4135 list = copy;
4136 LOGI("onQueryPurchasesResponse(gpbl proxy): fake injected via mutable copy");
4137 } else {
4138 env->ExceptionClear();
4139 env->DeleteLocalRef(copy);
4140 LOGI("onQueryPurchasesResponse: mutable copy add also failed");
4141 }
4142 }
4143 } else {
4144 LOGI("onQueryPurchasesResponse(gpbl proxy): fake purchase injected");
4145 }
4146 env->DeleteLocalRef(fake);
4147 }
4148 }
4149 env->ExceptionClear();
4150 }
4151 // backup expects 2 params (BillingResult, List) - original_this is receiver
4152 jobject bargs[] = { billingResult, list };
4153 call_backup(env, g_backup_a, original_this, bargs, 2);
4154 env->DeleteLocalRef(billingResult);
4155 env->DeleteLocalRef(list);
4156 if (original_this) env->DeleteLocalRef(original_this);
4157 return nullptr;
4158 }
4159 case 1: { // zzbm.onQueryPurchasesResponse(BillingResult, List) — inject fake + forward
4160 jobject billingResult = argc > 1 ? env->GetObjectArrayElement(args, 1) : nullptr;
4161 jobject list = argc > 2 ? env->GetObjectArrayElement(args, 2) : nullptr;
4162 if (env && g_cls_purchase && list) {
4163 jobject fake = make_fake_purchase(env);
4164 if (fake && g_mid_list_add) {
4165 env->CallBooleanMethod(list, g_mid_list_add, fake);
4166 if (env->ExceptionCheck()) {
4167 env->ExceptionClear();
4168 jclass alCls = env->FindClass("java/util/ArrayList");
4169 jmethodID alInitCol = alCls ? env->GetMethodID(alCls, "<init>", "(Ljava/util/Collection;)V") : nullptr;
4170 jobject copy = alInitCol ? env->NewObject(alCls, alInitCol, list) : nullptr;
4171 if (env->ExceptionCheck() || !copy) { env->ExceptionClear(); copy = env->NewObject(g_cls_arraylist, g_mid_arraylist_init); if (copy && !env->ExceptionCheck()) { jmethodID addAll = env->GetMethodID(env->GetObjectClass(copy), "addAll", "(Ljava/util/Collection;)Z"); if (addAll) env->CallBooleanMethod(copy, addAll, list); env->ExceptionClear(); } else env->ExceptionClear(); }
4172 if (alCls) env->DeleteLocalRef(alCls);
4173 if (copy) { env->CallBooleanMethod(copy, g_mid_list_add, fake); if (!env->ExceptionCheck()) { env->DeleteLocalRef(list); list = copy; } else { env->ExceptionClear(); env->DeleteLocalRef(copy); } }
4174 }
4175 env->DeleteLocalRef(fake);
4176 LOGI("onQueryPurchasesResponse: fake injected");
4177 }
4178 env->ExceptionClear();
4179 }
4180 jobject bargs[] = { billingResult, list };
4181 call_backup(env, g_backup_b, original_this, bargs, 2);
4182 env->DeleteLocalRef(billingResult);
4183 env->DeleteLocalRef(list);
4184 if (original_this) env->DeleteLocalRef(original_this);
4185 return nullptr;
4186 }
4187 case 2: { // BillingClientImpl.zzaI(String, PurchasesResponseListener) — direct delivery (deprecated path)
4188 if (env && g_cls_arraylist && g_mid_arraylist_init) {
4189 jobject br = make_success_billing_result(env);
4190 if (!br && g_cls_zzcj && g_fid_zzcj_zzl) {
4191 br = env->GetStaticObjectField(g_cls_zzcj, g_fid_zzcj_zzl);
4192 env->ExceptionClear();
4193 }
4194 jobject fake = make_fake_purchase(env);
4195 jobject list = env->NewObject(g_cls_arraylist, g_mid_arraylist_init);
4196 if (fake && list && g_mid_list_add) {
4197 env->CallBooleanMethod(list, g_mid_list_add, fake);
4198 jobject listener = argc > 2 ? env->GetObjectArrayElement(args, 2) : nullptr;
4199 if (listener) {
4200 jclass lc = env->GetObjectClass(listener);
4201 jmethodID m = env->GetMethodID(lc, "onQueryPurchasesResponse",
4202 "(Lcom/android/billingclient/api/BillingResult;Ljava/util/List;)V");
4203 if (m) {
4204 env->CallVoidMethod(listener, m, br, list);
4205 LOGI("zzaI: fake delivered to listener");
4206 }
4207 env->DeleteLocalRef(listener);
4208 }
4209 }
4210 env->ExceptionClear();
4211 env->DeleteLocalRef(br);
4212 env->DeleteLocalRef(fake);
4213 env->DeleteLocalRef(list);
4214 }
4215 if (original_this) env->DeleteLocalRef(original_this);
4216 return nullptr; // skip original
4217 }
4218 case 3: { // BillingClientImpl.queryProductDetailsAsync — capture productId + forward
4219 if (env && env->ExceptionCheck()) env->ExceptionClear();
4220 jobject params = argc > 1 ? env->GetObjectArrayElement(args, 1) : nullptr;
4221 jobject listener = argc > 2 ? env->GetObjectArrayElement(args, 2) : nullptr;
4222 LOGI("STORE_STEP: hookId=3 entry called by %p, original_this=%p BillingClient %p, params=%p listener=%p -> will capture productId -> create ProductDetails -> QueryResult -> BillingResult -> call onProductDetailsResponse -> next is launchBillingFlow:8", __builtin_return_address(0), original_this, original_this, params, listener);
4223 // Fake ProductDetails path to avoid Play roundtrip: create BillingResult OK + ProductDetails with captured productId
4224 bool needFake = false;
4225 const char *captured = nullptr;
4226 char capturedBuf[256] = {0};
4227 char allIdsBuf[1024] = {0};
4228 if (env && params) {
4229 jclass pc = env->GetObjectClass(params);
4230 // Log params class name
4231 {
4232 jclass classCls = env->FindClass("java/lang/Class");
4233 if (classCls) {
4234 jmethodID getNameMid = env->GetMethodID(classCls, "getName", "()Ljava/lang/String;");
4235 if (getNameMid && pc) {
4236 jstring cname = (jstring)env->CallObjectMethod(pc, getNameMid);
4237 if (cname && !env->ExceptionCheck()) {
4238 const char *cn = env->GetStringUTFChars(cname, nullptr);
4239 LOGI("queryProductDetailsAsync params class=%s", cn ? cn : "null");
4240 if (cn) env->ReleaseStringUTFChars(cname, cn);
4241 env->DeleteLocalRef(cname);
4242 } else env->ExceptionClear();
4243 }
4244 env->DeleteLocalRef(classCls);
4245 }
4246 env->ExceptionClear();
4247 }
4248 jobject productList = nullptr;
4249 jclass listCls = nullptr;
4250 // Try candidate method names for List extraction (covers 8.3.1 obfuscations)
4251 const char* listMethodNames[] = {"getProductList", "zza", "zzb", "zzc", "zzd", "zze", "getProducts", nullptr};
4252 const char* listSigs[] = {"()Ljava/util/List;", "()Ljava/lang/Object;", "()Ljava/util/ArrayList;", nullptr};
4253 for (int ni = 0; listMethodNames[ni] && !productList; ni++) {
4254 for (int si = 0; listSigs[si] && !productList; si++) {
4255 jmethodID mid = env->GetMethodID(pc, listMethodNames[ni], listSigs[si]);
4256 if (env->ExceptionCheck()) env->ExceptionClear();
4257 if (!mid) continue;
4258 LOGI("queryProductDetailsAsync: trying %s %s -> %p", listMethodNames[ni], listSigs[si], (void*)mid);
4259 jobject res = env->CallObjectMethod(params, mid);
4260 if (env->ExceptionCheck()) { env->ExceptionDescribe(); env->ExceptionClear(); res = nullptr; LOGI(" call %s threw", listMethodNames[ni]); }
4261 if (res) {
4262 jclass rc = env->GetObjectClass(res);
4263 jclass cls2 = env->FindClass("java/lang/Class");
4264 jmethodID gn2 = cls2 ? env->GetMethodID(cls2, "getName", "()Ljava/lang/String;") : nullptr;
4265 if (gn2 && rc) {
4266 jstring rn = (jstring)env->CallObjectMethod(rc, gn2);
4267 if (rn && !env->ExceptionCheck()) {
4268 const char *rnc = env->GetStringUTFChars(rn, nullptr);
4269 LOGI(" %s returned %s (%p)", listMethodNames[ni], rnc ? rnc : "null", res);
4270 if (rnc) env->ReleaseStringUTFChars(rn, rnc);
4271 env->DeleteLocalRef(rn);
4272 } else env->ExceptionClear();
4273 }
4274 if (cls2) env->DeleteLocalRef(cls2);
4275 // check if List
4276 jclass lstCheck = env->FindClass("java/util/List");
4277 if (lstCheck && env->IsInstanceOf(res, lstCheck)) {
4278 productList = res; // keep ref, don't delete yet
4279 listCls = env->GetObjectClass(productList);
4280 LOGI(" -> accepted as List via %s", listMethodNames[ni]);
4281 } else {
4282 LOGI(" -> not a List, discarding");
4283 env->DeleteLocalRef(res);
4284 }
4285 if (lstCheck) env->DeleteLocalRef(lstCheck);
4286 if (rc) env->DeleteLocalRef(rc);
4287 }
4288 }
4289 }
4290 // Brute force via reflection if still null: scan declared methods (including private zza/zzb) and try any no-arg that returns list-like
4291 if (!productList) {
4292 LOGI("queryProductDetailsAsync: brute force reflection scan (declared)");
4293 jclass classCls = env->FindClass("java/lang/Class");
4294 if (env->ExceptionCheck()) env->ExceptionClear();
4295 jmethodID getDeclaredMethodsMid = classCls ? env->GetMethodID(classCls, "getDeclaredMethods", "()[Ljava/lang/reflect/Method;") : nullptr;
4296 if (env->ExceptionCheck()) env->ExceptionClear();
4297 if (getDeclaredMethodsMid && pc) {
4298 jobjectArray methods = (jobjectArray)env->CallObjectMethod(pc, getDeclaredMethodsMid);
4299 if (env->ExceptionCheck()) { env->ExceptionClear(); methods = nullptr; }
4300 if (methods) {
4301 int mlen = env->GetArrayLength(methods);
4302 LOGI(" reflection declared: %d methods on params class", mlen);
4303 jclass methodCls = env->FindClass("java/lang/reflect/Method");
4304 if (env->ExceptionCheck()) env->ExceptionClear();
4305 jmethodID getNameMid = methodCls ? env->GetMethodID(methodCls, "getName", "()Ljava/lang/String;") : nullptr;
4306 jmethodID getRetMid = methodCls ? env->GetMethodID(methodCls, "getReturnType", "()Ljava/lang/Class;") : nullptr;
4307 jmethodID invokeMid = methodCls ? env->GetMethodID(methodCls, "invoke", "(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;") : nullptr;
4308 jmethodID setAccMid = methodCls ? env->GetMethodID(methodCls, "setAccessible", "(Z)V") : nullptr;
4309 jmethodID getParamCountMid = methodCls ? env->GetMethodID(methodCls, "getParameterCount", "()I") : nullptr;
4310 for (int i = 0; i < mlen && !productList; i++) {
4311 jobject m = env->GetObjectArrayElement(methods, i);
4312 if (!m) continue;
4313 // Only no-arg methods
4314 int paramCount = -1;
4315 if (getParamCountMid) {
4316 paramCount = env->CallIntMethod(m, getParamCountMid);
4317 if (env->ExceptionCheck()) { env->ExceptionClear(); paramCount = -1; }
4318 if (paramCount != 0) { env->DeleteLocalRef(m); continue; }
4319 }
4320 jstring mnameJ = getNameMid ? (jstring)env->CallObjectMethod(m, getNameMid) : nullptr;
4321 jobject retCls = getRetMid ? env->CallObjectMethod(m, getRetMid) : nullptr;
4322 const char *mnameC = nullptr;
4323 const char *retNameC = nullptr;
4324 jstring retNameJ = nullptr;
4325 if (mnameJ) mnameC = env->GetStringUTFChars(mnameJ, nullptr);
4326 if (retCls && classCls) {
4327 jmethodID gn = env->GetMethodID(classCls, "getName", "()Ljava/lang/String;");
4328 if (env->ExceptionCheck()) env->ExceptionClear();
4329 retNameJ = gn ? (jstring)env->CallObjectMethod(retCls, gn) : nullptr;
4330 if (retNameJ && !env->ExceptionCheck()) retNameC = env->GetStringUTFChars(retNameJ, nullptr);
4331 else env->ExceptionClear();
4332 }
4333 // Try invoke regardless of return type (internal wrapper may not be List)
4334 if (invokeMid) {
4335 if (setAccMid) { env->CallVoidMethod(m, setAccMid, JNI_TRUE); if (env->ExceptionCheck()) env->ExceptionClear(); }
4336 jobject emptyArgs = env->NewObjectArray(0, env->FindClass("java/lang/Object"), nullptr);
4337 if (env->ExceptionCheck()) env->ExceptionClear();
4338 jobject res = env->CallObjectMethod(m, invokeMid, params, emptyArgs);
4339 if (env->ExceptionCheck()) { env->ExceptionClear(); res = nullptr; }
4340 if (res) {
4341 // Check if res is List or list-like (has size/get)
4342 bool handled = false;
4343 jclass lstCheck = env->FindClass("java/util/List");
4344 if (env->ExceptionCheck()) env->ExceptionClear();
4345 if (lstCheck && env->IsInstanceOf(res, lstCheck)) {
4346 jclass rc = env->GetObjectClass(res);
4347 jmethodID szMid = env->GetMethodID(rc, "size", "()I");
4348 if (env->ExceptionCheck()) env->ExceptionClear();
4349 int sz = -1;
4350 if (szMid) { sz = env->CallIntMethod(res, szMid); if (env->ExceptionCheck()) { env->ExceptionClear(); sz = -1; } }
4351 LOGI(" reflection invoke %s -> List size=%d (%p) ret=%s", mnameC?mnameC:"?", sz, res, retNameC?retNameC:"?");
4352 if (sz >= 0) { // accept even 0 to know method works, but need >0 to capture
4353 if (sz > 0) { productList = res; listCls = rc; handled = true; }
4354 else { env->DeleteLocalRef(res); env->DeleteLocalRef(rc); handled = true; }
4355 } else {
4356 // not List-like via size, try getDeclared
4357 }
4358 if (handled && productList) { /* keep */ } else if (!handled) { env->DeleteLocalRef(res); if (sz>=0) env->DeleteLocalRef(rc); }
4359 if (lstCheck) env->DeleteLocalRef(lstCheck);
4360 if (!handled) {
4361 // try list-like via size/get on any object
4362 jclass rc2 = env->GetObjectClass(res);
4363 jmethodID sz2 = env->GetMethodID(rc2, "size", "()I");
4364 if (env->ExceptionCheck()) env->ExceptionClear();
4365 jmethodID get2 = env->GetMethodID(rc2, "get", "(I)Ljava/lang/Object;");
4366 if (env->ExceptionCheck()) env->ExceptionClear();
4367 if (sz2 && get2) {
4368 int szTry = env->CallIntMethod(res, sz2);
4369 if (!env->ExceptionCheck() && szTry > 0) {
4370 LOGI(" reflection invoke %s -> list-like size=%d via size/get", mnameC?mnameC:"?", szTry);
4371 productList = res; listCls = rc2; handled = true;
4372 } else { env->ExceptionClear(); env->DeleteLocalRef(rc2); env->DeleteLocalRef(res); }
4373 } else { if (rc2) env->DeleteLocalRef(rc2); env->DeleteLocalRef(res); }
4374 } else if (!productList && lstCheck) {
4375 // already handled list case but size 0, need to try next method
4376 }
4377 } else {
4378 // Not a List, try size/get heuristic
4379 jclass rc2 = env->GetObjectClass(res);
4380 jmethodID sz2 = env->GetMethodID(rc2, "size", "()I");
4381 if (env->ExceptionCheck()) env->ExceptionClear();
4382 jmethodID get2 = env->GetMethodID(rc2, "get", "(I)Ljava/lang/Object;");
4383 if (env->ExceptionCheck()) env->ExceptionClear();
4384 if (sz2 && get2) {
4385 int szTry = env->CallIntMethod(res, sz2);
4386 if (env->ExceptionCheck()) { env->ExceptionClear(); szTry = -1; }
4387 if (szTry > 0) {
4388 jobject first = env->CallObjectMethod(res, get2, 0);
4389 if (!env->ExceptionCheck() && first) {
4390 LOGI(" reflection invoke %s -> list-like wrapper size=%d first=%p ret=%s", mnameC?mnameC:"?", szTry, first, retNameC?retNameC:"?");
4391 env->DeleteLocalRef(first);
4392 productList = res; listCls = rc2; // keep wrapper as list holder
4393 } else { env->ExceptionClear(); env->DeleteLocalRef(rc2); env->DeleteLocalRef(res); }
4394 } else { env->DeleteLocalRef(rc2); env->DeleteLocalRef(res); }
4395 } else { if (rc2) env->DeleteLocalRef(rc2); env->DeleteLocalRef(res); }
4396 if (lstCheck) env->DeleteLocalRef(lstCheck);
4397 }
4398 }
4399 env->DeleteLocalRef(emptyArgs);
4400 }
4401 if (mnameC) env->ReleaseStringUTFChars(mnameJ, mnameC);
4402 if (retNameC) env->ReleaseStringUTFChars(retNameJ, retNameC);
4403 if (mnameJ) env->DeleteLocalRef(mnameJ);
4404 if (retNameJ) env->DeleteLocalRef(retNameJ);
4405 if (retCls) env->DeleteLocalRef(retCls);
4406 env->DeleteLocalRef(m);
4407 }
4408 env->DeleteLocalRef(methods);
4409 if (methodCls) env->DeleteLocalRef(methodCls);
4410 } else env->ExceptionClear();
4411 if (classCls) env->DeleteLocalRef(classCls);
4412 }
4413 }
4414 if (productList && listCls) {
4415 jmethodID sizeMid = env->GetMethodID(listCls, "size", "()I");
4416 jmethodID getMid = env->GetMethodID(listCls, "get", "(I)Ljava/lang/Object;");
4417 if (!sizeMid || !getMid) {
4418 // try interfaces: List.class
4419 jclass lst = env->FindClass("java/util/List");
4420 if (lst) {
4421 if (!sizeMid) sizeMid = env->GetMethodID(lst, "size", "()I");
4422 if (!getMid) getMid = env->GetMethodID(lst, "get", "(I)Ljava/lang/Object;");
4423 env->DeleteLocalRef(lst);
4424 }
4425 }
4426 int sz = sizeMid ? env->CallIntMethod(productList, sizeMid) : 0;
4427 if (env->ExceptionCheck()) { env->ExceptionClear(); sz = 0; }
4428 LOGI("queryProductDetailsAsync: productList size=%d", sz);
4429 const char* idCandidates[] = {"getProductId", "zza", "zzb", "zzc", "getProduct", "getId", "getProductIdString", nullptr};
4430 for (int idx = 0; idx < sz; idx++) {
4431 jobject prod = getMid ? env->CallObjectMethod(productList, getMid, idx) : nullptr;
4432 if (env->ExceptionCheck()) { env->ExceptionClear(); prod = nullptr; }
4433 if (!prod) { LOGI(" [%d] null", idx); continue; }
4434 jclass pc2 = env->GetObjectClass(prod);
4435 // log prod class
4436 {
4437 jclass cc = env->FindClass("java/lang/Class");
4438 jmethodID gn = cc ? env->GetMethodID(cc, "getName", "()Ljava/lang/String;") : nullptr;
4439 jstring pn = gn ? (jstring)env->CallObjectMethod(pc2, gn) : nullptr;
4440 if (pn && !env->ExceptionCheck()) {
4441 const char *pnc = env->GetStringUTFChars(pn, nullptr);
4442 LOGI(" [%d] prod class=%s", idx, pnc?pnc:"null");
4443 if (pnc) env->ReleaseStringUTFChars(pn, pnc);
4444 env->DeleteLocalRef(pn);
4445 } else env->ExceptionClear();
4446 if (cc) env->DeleteLocalRef(cc);
4447 }
4448 jstring id = nullptr;
4449 const char* foundVia = nullptr;
4450 for (int ci = 0; idCandidates[ci] && !id; ci++) {
4451 jmethodID mid = env->GetMethodID(pc2, idCandidates[ci], "()Ljava/lang/String;");
4452 if (env->ExceptionCheck()) env->ExceptionClear();
4453 if (!mid) continue;
4454 jstring res = (jstring)env->CallObjectMethod(prod, mid);
4455 if (env->ExceptionCheck()) { env->ExceptionClear(); res = nullptr; continue; }
4456 if (res) { id = res; foundVia = idCandidates[ci]; LOGI(" [%d] get via %s ok", idx, foundVia); }
4457 }
4458 // field fallback
4459 if (!id) {
4460 jclass cc = env->FindClass("java/lang/Class");
4461 jmethodID gdm = cc ? env->GetMethodID(cc, "getDeclaredFields", "()[Ljava/lang/reflect/Field;") : nullptr;
4462 if (gdm) {
4463 jobjectArray fields = (jobjectArray)env->CallObjectMethod(pc2, gdm);
4464 if (!env->ExceptionCheck() && fields) {
4465 int flen = env->GetArrayLength(fields);
4466 jclass fieldCls = env->FindClass("java/lang/reflect/Field");
4467 jmethodID getNameMid = fieldCls ? env->GetMethodID(fieldCls, "getName", "()Ljava/lang/String;") : nullptr;
4468 jmethodID getTypeMid = fieldCls ? env->GetMethodID(fieldCls, "getType", "()Ljava/lang/Class;") : nullptr;
4469 jmethodID getMid2 = fieldCls ? env->GetMethodID(fieldCls, "get", "(Ljava/lang/Object;)Ljava/lang/Object;") : nullptr;
4470 jmethodID setAccMid = fieldCls ? env->GetMethodID(fieldCls, "setAccessible", "(Z)V") : nullptr;
4471 for (int fi = 0; fi < flen && !id; fi++) {
4472 jobject f = env->GetObjectArrayElement(fields, fi);
4473 if (!f) continue;
4474 jstring fnJ = getNameMid ? (jstring)env->CallObjectMethod(f, getNameMid) : nullptr;
4475 jobject ft = getTypeMid ? env->CallObjectMethod(f, getTypeMid) : nullptr;
4476 const char *fnC = fnJ ? env->GetStringUTFChars(fnJ, nullptr) : nullptr;
4477 const char *ftC = nullptr;
4478 jstring ftNameJ = nullptr;
4479 if (ft && cc) {
4480 jmethodID gn = env->GetMethodID(cc, "getName", "()Ljava/lang/String;");
4481 ftNameJ = gn ? (jstring)env->CallObjectMethod(ft, gn) : nullptr;
4482 if (ftNameJ && !env->ExceptionCheck()) ftC = env->GetStringUTFChars(ftNameJ, nullptr);
4483 else env->ExceptionClear();
4484 }
4485 bool isString = ftC && strstr(ftC, "java.lang.String");
4486 if (isString && setAccMid) {
4487 env->CallVoidMethod(f, setAccMid, JNI_TRUE);
4488 if (!env->ExceptionCheck()) {
4489 jobject v = getMid2 ? env->CallObjectMethod(f, getMid2, prod) : nullptr;
4490 if (!env->ExceptionCheck() && v) {
4491 id = (jstring)v; // keep
4492 LOGI(" [%d] field %s -> %p", idx, fnC?fnC:"?", id);
4493 } else env->ExceptionClear();
4494 } else env->ExceptionClear();
4495 }
4496 if (fnC) env->ReleaseStringUTFChars(fnJ, fnC);
4497 if (ftC) env->ReleaseStringUTFChars(ftNameJ, ftC);
4498 if (fnJ) env->DeleteLocalRef(fnJ);
4499 if (ftNameJ) env->DeleteLocalRef(ftNameJ);
4500 if (ft) env->DeleteLocalRef(ft);
4501 env->DeleteLocalRef(f);
4502 }
4503 env->DeleteLocalRef(fields);
4504 if (fieldCls) env->DeleteLocalRef(fieldCls);
4505 } else env->ExceptionClear();
4506 }
4507 if (cc) env->DeleteLocalRef(cc);
4508 }
4509 if (id) {
4510 const char *c = env->GetStringUTFChars(id, nullptr);
4511 LOGI(" [%d] productId='%s' via %s", idx, c?c:"null", foundVia?foundVia:"field");
4512 if (c && strcmp(c, "inapp")!=0 && strcmp(c, "subs")!=0 && strlen(c) > 0 && strlen(c) < sizeof(capturedBuf)-1) {
4513 if (strlen(allIdsBuf) + strlen(c) + 2 < sizeof(allIdsBuf)) {
4514 if (strlen(allIdsBuf)) strncat(allIdsBuf, ",", sizeof(allIdsBuf)-strlen(allIdsBuf)-1);
4515 strncat(allIdsBuf, c, sizeof(allIdsBuf)-strlen(allIdsBuf)-1);
4516 }
4517 if (!captured) {
4518 strncpy(capturedBuf, c, sizeof(capturedBuf)-1);
4519 captured = capturedBuf;
4520 if (g_product_id) env->DeleteGlobalRef(g_product_id);
4521 g_product_id = (jstring)env->NewGlobalRef(id);
4522 LOGI("productId captured (first): %s (all=%s)", c, allIdsBuf);
4523 needFake = true;
4524 }
4525 }
4526 if (c) env->ReleaseStringUTFChars(id, c);
4527 env->DeleteLocalRef(id);
4528 } else {
4529 LOGI(" [%d] productId NOT FOUND", idx);
4530 }
4531 env->DeleteLocalRef(pc2);
4532 env->DeleteLocalRef(prod);
4533 }
4534 if (strlen(allIdsBuf)) LOGI("queryProductDetailsAsync: ALL productIds=%s (count=%d)", allIdsBuf, sz);
4535 else LOGI("queryProductDetailsAsync: NO valid productIds in list size=%d", sz);
4536 env->DeleteLocalRef(productList);
4537 env->DeleteLocalRef(listCls);
4538 } else {
4539 LOGE("queryProductDetailsAsync: cannot obtain productList (params class methods failed)");
4540 // log all methods for debug
4541 jclass cc = env->FindClass("java/lang/Class");
4542 jmethodID gms = cc ? env->GetMethodID(cc, "getMethods", "()[Ljava/lang/reflect/Method;") : nullptr;
4543 if (gms && pc) {
4544 jobjectArray ms = (jobjectArray)env->CallObjectMethod(pc, gms);
4545 if (!env->ExceptionCheck() && ms) {
4546 int l = env->GetArrayLength(ms);
4547 LOGI(" dumping %d methods for params class:", l);
4548 jclass mc = env->FindClass("java/lang/reflect/Method");
4549 jmethodID gnm = mc ? env->GetMethodID(mc, "getName", "()Ljava/lang/String;") : nullptr;
4550 for (int i=0;i<l && i<30;i++) {
4551 jobject m = env->GetObjectArrayElement(ms,i);
4552 jstring n = gnm ? (jstring)env->CallObjectMethod(m, gnm) : nullptr;
4553 const char *nc = n ? env->GetStringUTFChars(n,nullptr) : nullptr;
4554 LOGI(" method[%d]=%s", i, nc?nc:"null");
4555 if (nc) env->ReleaseStringUTFChars(n,nc);
4556 if (n) env->DeleteLocalRef(n);
4557 env->DeleteLocalRef(m);
4558 }
4559 env->DeleteLocalRef(ms);
4560 if (mc) env->DeleteLocalRef(mc);
4561 } else env->ExceptionClear();
4562 }
4563 if (cc) env->DeleteLocalRef(cc);
4564 }
4565 env->DeleteLocalRef(pc);
4566 env->ExceptionClear();
4567 }
4568 // Log what we are buying and forward to Play for REAL ProductDetails (no fake, no UI) - use fake JSON only in launchBillingFlow
4569 if (needFake) {
4570 LOGI("STORE_STEP: queryProductDetailsAsync captured productIds=%s (first=%s) -> forwarding to Play for REAL ProductDetails (no fake, no UI)", allIdsBuf, captured?captured:"null");
4571 LOGI("BUY_LOG: productId=%s (all=%s) - query real details from Play", captured?captured:"null", allIdsBuf);
4572 } else {
4573 LOGI("STORE_STEP: queryProductDetailsAsync no productIds captured (allIds=%s) -> forwarding to Play anyway for real details", allIdsBuf);
4574 if (strlen(allIdsBuf)) LOGI("BUY_LOG: will fetch real ProductDetails for %s", allIdsBuf);
4575 }
4576 if (captured) LOGI("BUY_LOG: will fetch real ProductDetails for %s", captured);
4577 // Forward to original to get REAL ProductDetails from Play (query has no UI)
4578 jobject bargs[] = { params, listener };
4579 call_backup(env, g_backup_d, original_this, bargs, 2);
4580 LOGI("STORE_STEP: queryProductDetailsAsync forwarded to Play, real onProductDetailsResponse will follow");
4581 env->DeleteLocalRef(params);
4582 env->DeleteLocalRef(listener);
4583 if (original_this) env->DeleteLocalRef(original_this);
4584 return nullptr;
4585 }
4586 case 4: { // ProxyBillingActivity.onActivityResult(int, int, Intent)
4587 jobject intent = argc > 3 ? env->GetObjectArrayElement(args, 3) : nullptr;
4588 if (env && intent && g_cls_intent && g_cls_arraylist && g_mid_arraylist_init) {
4589 if (g_mid_intent_putextra_int) {
4590 jstring k = env->NewStringUTF("RESPONSE_CODE");
4591 env->CallObjectMethod(intent, g_mid_intent_putextra_int, k, 0);
4592 env->DeleteLocalRef(k);
4593 }
4594 jstring json = make_fake_json_string(env);
4595 if (json) {
4596 jobject dataList = env->NewObject(g_cls_arraylist, g_mid_arraylist_init);
4597 env->CallBooleanMethod(dataList, g_mid_list_add, json);
4598 jobject sigList = env->NewObject(g_cls_arraylist, g_mid_arraylist_init);
4599 jstring sig = env->NewStringUTF("");
4600 env->CallBooleanMethod(sigList, g_mid_list_add, sig);
4601 jobject itemList = env->NewObject(g_cls_arraylist, g_mid_arraylist_init);
4602 if (g_product_id) env->CallBooleanMethod(itemList, g_mid_list_add, g_product_id);
4603 if (g_mid_intent_putsal) {
4604 jstring k1 = env->NewStringUTF("INAPP_PURCHASE_DATA_LIST");
4605 env->CallObjectMethod(intent, g_mid_intent_putsal, k1, dataList);
4606 env->DeleteLocalRef(k1);
4607 jstring k2 = env->NewStringUTF("INAPP_DATA_SIGNATURE_LIST");
4608 env->CallObjectMethod(intent, g_mid_intent_putsal, k2, sigList);
4609 env->DeleteLocalRef(k2);
4610 jstring k3 = env->NewStringUTF("INAPP_PURCHASE_ITEM_LIST");
4611 env->CallObjectMethod(intent, g_mid_intent_putsal, k3, itemList);
4612 env->DeleteLocalRef(k3);
4613 }
4614 env->DeleteLocalRef(dataList);
4615 env->DeleteLocalRef(sigList);
4616 env->DeleteLocalRef(itemList);
4617 env->DeleteLocalRef(sig);
4618 env->DeleteLocalRef(json);
4619 }
4620 env->ExceptionClear();
4621 LOGI("onActivityResult: RESPONSE_CODE=0 + fake extras set");
4622 }
4623 jobject reqCode = argc > 1 ? env->GetObjectArrayElement(args, 1) : nullptr;
4624 jobject resCode = argc > 2 ? env->GetObjectArrayElement(args, 2) : nullptr;
4625 jobject bargs[] = { reqCode, resCode, intent };
4626 call_backup(env, g_backup_e, original_this, bargs, 3);
4627 if (reqCode) env->DeleteLocalRef(reqCode);
4628 if (resCode) env->DeleteLocalRef(resCode);
4629 env->DeleteLocalRef(intent);
4630 if (original_this) env->DeleteLocalRef(original_this);
4631 return nullptr;
4632 }
4633 case 5: { // MaxUnityAdManager.showRewardedAd(String, String, String) — fake events
4634 jobject adUnitId = argc > 1 ? env->GetObjectArrayElement(args, 1) : nullptr;
4635 jobject placement = argc > 2 ? env->GetObjectArrayElement(args, 2) : nullptr;
4636 g_last_ad_reward_ms = eyu_now_ms(); // v4.81: apre finestra B (bonus keys xMULT su eventuale incremento)
4637 send_ad_event(env, "OnRewardedAdDisplayedEvent", adUnitId, placement, nullptr);
4638 send_ad_event(env, "OnRewardedAdReceivedRewardEvent", adUnitId, placement, "reward");
4639 send_ad_event(env, "OnRewardedAdHiddenEvent", adUnitId, placement, nullptr);
4640 LOGI("showRewardedAd: 3 fake events sent (g_last_ad_reward_ms=%lld)", (long long)g_last_ad_reward_ms);
4641 env->DeleteLocalRef(adUnitId);
4642 env->DeleteLocalRef(placement);
4643 if (original_this) env->DeleteLocalRef(original_this);
4644 return nullptr; // skip original
4645 }
4646 case 6: { // MaxUnityAdManager.isRewardedAdReady(String) — force true
4647 if (env && g_cls_boolean && g_mid_boolean_valueof) {
4648 jlong now = 0;
4649 if (g_cls_system && g_mid_system_time) now = env->CallStaticLongMethod(g_cls_system, g_mid_system_time);
4650 static jlong lastRewardMs = 0;
4651 if (now - lastRewardMs > 500) {
4652 jobject adUnitId = argc > 1 ? env->GetObjectArrayElement(args, 1) : nullptr;
4653 if (adUnitId) {
4654 g_last_ad_reward_ms = eyu_now_ms(); // v4.81: apre finestra B (bonus keys xMULT)
4655 send_ad_event(env, "OnRewardedAdDisplayedEvent", adUnitId, nullptr, nullptr);
4656 send_ad_event(env, "OnRewardedAdReceivedRewardEvent", adUnitId, nullptr, "reward");
4657 send_ad_event(env, "OnRewardedAdHiddenEvent", adUnitId, nullptr, nullptr);
4658 lastRewardMs = now;
4659 env->DeleteLocalRef(adUnitId);
4660 }
4661 }
4662 jobject boxed = env->CallStaticObjectMethod(g_cls_boolean, g_mid_boolean_valueof, JNI_TRUE);
4663 env->ExceptionClear();
4664 return boxed;
4665 }
4666 // Fallback: rilancia l'originale (restituisce Boolean già boxed).
4667 jobject adUnitIdFb = argc > 1 ? env->GetObjectArrayElement(args, 1) : nullptr;
4668 jobject bargs[] = { adUnitIdFb };
4669 jobject ret = call_backup(env, g_backup_f, original_this, bargs, 1);
4670 if (adUnitIdFb) env->DeleteLocalRef(adUnitIdFb);
4671 if (original_this) env->DeleteLocalRef(original_this);
4672 return ret;
4673 }
4674 case 7: { // gpbl PurchaseUpdatedListenerProxy.onPurchasesUpdated(Object result, List) — purchase flow
4675 LOGI("STORE_STEP: hookId=7 entry called by %p original_this=%p -> will inject fake", __builtin_return_address(0), original_this);
4676 jobject billingResult = argc > 1 ? env->GetObjectArrayElement(args, 1) : nullptr;
4677 jobject list = argc > 2 ? env->GetObjectArrayElement(args, 2) : nullptr;
4678 if (env && list) {
4679 jobject br = make_success_billing_result(env);
4680 if (br) {
4681 if (billingResult) env->DeleteLocalRef(billingResult);
4682 billingResult = br;
4683 LOGI("STORE_STEP: onPurchasesUpdated BillingResult replaced with builder OK");
4684 }
4685 if (g_cls_purchase && g_mid_list_add && list) {
4686 jobject fake = make_fake_purchase(env);
4687 if (fake) {
4688 env->CallBooleanMethod(list, g_mid_list_add, fake);
4689 if (env->ExceptionCheck()) {
4690 env->ExceptionClear();
4691 LOGI("onPurchasesUpdated: list.add threw (immutable), creating mutable copy");
4692 jobject copy = nullptr;
4693 if (g_cls_arraylist && g_mid_arraylist_init) {
4694 jclass alCls = env->FindClass("java/util/ArrayList");
4695 jmethodID alInitCol = alCls ? env->GetMethodID(alCls, "<init>", "(Ljava/util/Collection;)V") : nullptr;
4696 if (alInitCol) {
4697 copy = env->NewObject(alCls, alInitCol, list);
4698 if (env->ExceptionCheck()) { env->ExceptionClear(); copy = nullptr; }
4699 }
4700 if (alCls) env->DeleteLocalRef(alCls);
4701 if (!copy) {
4702 copy = env->NewObject(g_cls_arraylist, g_mid_arraylist_init);
4703 if (copy && !env->ExceptionCheck()) {
4704 jmethodID addAll = env->GetMethodID(env->GetObjectClass(copy), "addAll", "(Ljava/util/Collection;)Z");
4705 if (addAll) { env->CallBooleanMethod(copy, addAll, list); if (env->ExceptionCheck()) env->ExceptionClear(); }
4706 } else env->ExceptionClear();
4707 }
4708 }
4709 if (copy) {
4710 env->CallBooleanMethod(copy, g_mid_list_add, fake);
4711 if (!env->ExceptionCheck()) {
4712 env->DeleteLocalRef(list);
4713 list = copy;
4714 LOGI("onPurchasesUpdated: fake injected via mutable copy");
4715 } else { env->ExceptionClear(); env->DeleteLocalRef(copy); }
4716 }
4717 } else {
4718 LOGI("STORE_STEP: onPurchasesUpdated fake purchase injected");
4719 }
4720 env->DeleteLocalRef(fake);
4721 }
4722 }
4723 env->ExceptionClear();
4724 }
4725 jobject bargs[] = { billingResult, list };
4726 call_backup(env, g_backup_g, original_this, bargs, 2);
4727 env->DeleteLocalRef(billingResult);
4728 env->DeleteLocalRef(list);
4729 if (original_this) env->DeleteLocalRef(original_this);
4730 return nullptr;
4731 }
4732 case 8: { {
4733 if (env && env->ExceptionCheck()) env->ExceptionClear();
4734 LOGI("STORE_STEP: hookId=8 entry called by %p original_this=%p -> will bypass Play", __builtin_return_address(0), original_this);
4735 jobject activity = argc > 1 ? env->GetObjectArrayElement(args, 1) : nullptr;
4736 jobject params = argc > 2 ? env->GetObjectArrayElement(args, 2) : nullptr;
4737 LOGI("STORE_STEP: launchBillingFlow intercepted activity=%p params=%p", activity, params);
4738 // Find PurchasesUpdatedListener stored in BillingClientImpl.zzf (zzab)
4739 jobject fakeLocal = make_fake_purchase(env);
4740 jobject fake = fakeLocal ? env->NewGlobalRef(fakeLocal) : nullptr;
4741 if (fakeLocal) env->DeleteLocalRef(fakeLocal);
4742 jobject brLocal = make_success_billing_result(env);
4743 jobject br = brLocal ? env->NewGlobalRef(brLocal) : nullptr;
4744 if (brLocal) env->DeleteLocalRef(brLocal);
4745 if (g_product_id) {
4746 const char* _c = env->GetStringUTFChars(g_product_id, nullptr);
4747 LOGI("BUY_LOG: launchBillingFlow purchasing %s (fake JSON, ProductDetails real from Play)", _c?_c:"null");
4748 if (_c) env->ReleaseStringUTFChars(g_product_id, _c);
4749 } else LOGI("BUY_LOG: launchBillingFlow purchasing fallback lootio.gem (g_product_id null)");
4750 // Also try to extract productId directly from BillingFlowParams (if available) for double log
4751 if (params) {
4752 jclass pcls = env->GetObjectClass(params);
4753 jclass cc = env->FindClass("java/lang/Class");
4754 if (cc && pcls && !env->ExceptionCheck()) {
4755 jmethodID gd = env->GetMethodID(cc, "getDeclaredMethods", "()[Ljava/lang/reflect/Method;");
4756 if (gd) {
4757 jobjectArray ms = (jobjectArray)env->CallObjectMethod(pcls, gd);
4758 if (!env->ExceptionCheck() && ms) {
4759 int ml = env->GetArrayLength(ms);
4760 // just log that we have params object, detailed extraction already done in query
4761 LOGI("BUY_LOG: BillingFlowParams has %d methods, params=%p", ml, params);
4762 env->DeleteLocalRef(ms);
4763 } else env->ExceptionClear();
4764 }
4765 env->DeleteLocalRef(cc);
4766 } else env->ExceptionClear();
4767 env->DeleteLocalRef(pcls);
4768 }
4769 bool delivered = false;
4770 if (env && original_this && g_cls_bci) {
4771 jclass bciCls = env->GetObjectClass(original_this);
4772 jclass classCls = env->FindClass("java/lang/Class");
4773 jmethodID getDeclaredFields = classCls ? env->GetMethodID(classCls, "getDeclaredFields", "()[Ljava/lang/reflect/Field;") : nullptr;
4774 jobjectArray fields = getDeclaredFields ? (jobjectArray)env->CallObjectMethod(bciCls, getDeclaredFields) : nullptr;
4775 if (env->ExceptionCheck()) { env->ExceptionClear(); fields = nullptr; }
4776 int fcount = fields ? env->GetArrayLength(fields) : 0;
4777 LOGI("launchBillingFlow: scanning %d fields of BillingClientImpl", fcount);
4778 jclass fieldCls = env->FindClass("java/lang/reflect/Field");
4779 jmethodID getNameMid = fieldCls ? env->GetMethodID(fieldCls, "getName", "()Ljava/lang/String;") : nullptr;
4780 jmethodID getTypeMid = fieldCls ? env->GetMethodID(fieldCls, "getType", "()Ljava/lang/Class;") : nullptr;
4781 jmethodID getMid = fieldCls ? env->GetMethodID(fieldCls, "get", "(Ljava/lang/Object;)Ljava/lang/Object;") : nullptr;
4782 jmethodID setAccMid = fieldCls ? env->GetMethodID(fieldCls, "setAccessible", "(Z)V") : nullptr;
4783 jmethodID getTypeNameMid = classCls ? env->GetMethodID(classCls, "getName", "()Ljava/lang/String;") : nullptr;
4784 jclass pulCls = env->FindClass("com/android/billingclient/api/PurchasesUpdatedListener");
4785 if (env->ExceptionCheck()) env->ExceptionClear();
4786 if (!pulCls) pulCls = load_app_class(env, "com/android/billingclient/api/PurchasesUpdatedListener");
4787 if (env->ExceptionCheck()) env->ExceptionClear();
4788 for (int fi=0; fi<fcount && !delivered; fi++) {
4789 jobject field = env->GetObjectArrayElement(fields, fi);
4790 if (!field) continue;
4791 jstring fnameJ = getNameMid ? (jstring)env->CallObjectMethod(field, getNameMid) : nullptr;
4792 jobject ftype = getTypeMid ? env->CallObjectMethod(field, getTypeMid) : nullptr;
4793 const char* fnameC = fnameJ ? env->GetStringUTFChars(fnameJ, nullptr) : nullptr;
4794 jstring ftypeNameJ = ftype && getTypeNameMid ? (jstring)env->CallObjectMethod(ftype, getTypeNameMid) : nullptr;
4795 const char* ftypeC = ftypeNameJ ? env->GetStringUTFChars(ftypeNameJ, nullptr) : nullptr;
4796 LOGI("launchBillingFlow: scan field[%d/%d] %s (%s)", fi, fcount, fnameC?fnameC:"?", ftypeC?ftypeC:"?");
4797 if (setAccMid) { env->CallVoidMethod(field, setAccMid, JNI_TRUE); if (env->ExceptionCheck()) env->ExceptionClear(); }
4798 jobject fval = nullptr;
4799 if (getMid) {
4800 fval = env->CallObjectMethod(field, getMid, original_this);
4801 if (env->ExceptionCheck()) { env->ExceptionClear(); fval = nullptr; }
4802 }
4803 jobject listener = nullptr;
4804 bool isDirect = false;
4805 if (fval && pulCls && env->IsInstanceOf(fval, pulCls)) {
4806 listener = fval;
4807 isDirect = true;
4808 LOGI("launchBillingFlow: field %s (%s) is direct PurchasesUpdatedListener %p", fnameC?fnameC:"?", ftypeC?ftypeC:"?", listener);
4809 } else if (fval) {
4810 jclass fvalCls = env->GetObjectClass(fval);
4811 jmethodID getDeclaredMethodsMid = classCls ? env->GetMethodID(classCls, "getDeclaredMethods", "()[Ljava/lang/reflect/Method;") : nullptr;
4812 if (env->ExceptionCheck()) env->ExceptionClear();
4813 jobjectArray mths = getDeclaredMethodsMid ? (jobjectArray)env->CallObjectMethod(fvalCls, getDeclaredMethodsMid) : nullptr;
4814 if (env->ExceptionCheck()) { env->ExceptionClear(); mths = nullptr; }
4815 if (mths) {
4816 int mlen = env->GetArrayLength(mths);
4817 jclass methodCls = env->FindClass("java/lang/reflect/Method");
4818 if (env->ExceptionCheck()) env->ExceptionClear();
4819 jmethodID mGetRet = methodCls ? env->GetMethodID(methodCls, "getReturnType", "()Ljava/lang/Class;") : nullptr;
4820 jmethodID mInvoke = methodCls ? env->GetMethodID(methodCls, "invoke", "(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;") : nullptr;
4821 jmethodID mSetAcc = methodCls ? env->GetMethodID(methodCls, "setAccessible", "(Z)V") : nullptr;
4822 for (int mi=0; mi<mlen && !listener; mi++) {
4823 jobject m = env->GetObjectArrayElement(mths, mi);
4824 if (mSetAcc) { env->CallVoidMethod(m, mSetAcc, JNI_TRUE); if (env->ExceptionCheck()) env->ExceptionClear(); }
4825 jobject retType = mGetRet ? env->CallObjectMethod(m, mGetRet) : nullptr;
4826 if (env->ExceptionCheck()) { env->ExceptionClear(); retType = nullptr; }
4827 jstring retNameJ = retType && getTypeNameMid ? (jstring)env->CallObjectMethod(retType, getTypeNameMid) : nullptr;
4828 if (env->ExceptionCheck()) { env->ExceptionClear(); retNameJ = nullptr; }
4829 const char* retNameC = retNameJ ? env->GetStringUTFChars(retNameJ, nullptr) : nullptr;
4830 // SOLO getter che restituiscono esattamente PurchasesUpdatedListener vengono invocati.
4831 // Un invoke generico 0-arg su field come zzbw/provider gms = chiamata Binder sincrona
4832 // sul main thread -> HANG del gioco (osservato: freeze dopo la scan dei 37 field).
4833 // Il getter del listener (es. zzf/zze) ha quel return type ed e' un accesso banale.
4834 jmethodID getParamCountMid = methodCls ? env->GetMethodID(methodCls, "getParameterCount", "()I") : nullptr;
4835 int pcnt = -1;
4836 if (getParamCountMid) { pcnt = env->CallIntMethod(m, getParamCountMid); if (env->ExceptionCheck()) { env->ExceptionClear(); pcnt = -1; } }
4837 bool retIsPUL = retNameC && strcmp(retNameC, "com.android.billingclient.api.PurchasesUpdatedListener") == 0;
4838 bool tryInvoke = (pcnt == 0) && retIsPUL && !listener;
4839 if (tryInvoke) {
4840 jobject empty = env->NewObjectArray(0, env->FindClass("java/lang/Object"), nullptr);
4841 if (env->ExceptionCheck()) env->ExceptionClear();
4842 jobject res = mInvoke ? env->CallObjectMethod(m, mInvoke, fval, empty) : nullptr;
4843 if (env->ExceptionCheck()) { env->ExceptionClear(); res = nullptr; }
4844 if (res && pulCls && env->IsInstanceOf(res, pulCls)) {
4845 listener = res;
4846 LOGI("launchBillingFlow: wrapper %s via %s -> listener %p", fnameC?fnameC:"?", retNameC?retNameC:"unknown", listener);
4847 } else if (res) env->DeleteLocalRef(res);
4848 if (empty) env->DeleteLocalRef(empty);
4849 }
4850 if (retNameC) env->ReleaseStringUTFChars(retNameJ, retNameC);
4851 if (retNameJ) env->DeleteLocalRef(retNameJ);
4852 if (retType) env->DeleteLocalRef(retType);
4853 env->DeleteLocalRef(m);
4854 }
4855 if (methodCls) env->DeleteLocalRef(methodCls);
4856 env->DeleteLocalRef(mths);
4857 } else env->ExceptionClear();
4858 env->DeleteLocalRef(fvalCls);
4859 if (!listener) { env->DeleteLocalRef(fval); fval = nullptr; }
4860 }
4861 if (listener) {
4862 jclass alCls = env->FindClass("java/util/ArrayList");
4863 jmethodID alInit = alCls ? env->GetMethodID(alCls, "<init>", "()V") : nullptr;
4864 jobject list = alInit ? env->NewObject(alCls, alInit) : nullptr;
4865 if (list && fake && g_mid_list_add) { env->CallBooleanMethod(list, g_mid_list_add, fake); if (env->ExceptionCheck()) env->ExceptionClear(); }
4866 jclass lisCls = env->GetObjectClass(listener);
4867 jmethodID mOnPU = env->GetMethodID(lisCls, "onPurchasesUpdated", "(Lcom/android/billingclient/api/BillingResult;Ljava/util/List;)V");
4868 if (env->ExceptionCheck()) env->ExceptionClear();
4869 if (mOnPU) {
4870 if (!br) { jobject tmp = make_success_billing_result(env); if (tmp) { if (br) env->DeleteGlobalRef(br); br = env->NewGlobalRef(tmp); env->DeleteLocalRef(tmp); } }
4871 jclass cl = env->GetObjectClass(listener);
4872 jstring cname = getTypeNameMid ? (jstring)env->CallObjectMethod(cl, getTypeNameMid) : nullptr;
4873 const char* cn = cname ? env->GetStringUTFChars(cname, nullptr) : nullptr;
4874 LOGI("launchBillingFlow: calling onPurchasesUpdated listener=%p class=%s via field %s", listener, cn?cn:"null", fnameC?fnameC:"?");
4875 if (cn) env->ReleaseStringUTFChars(cname, cn);
4876 if (cname) env->DeleteLocalRef(cname);
4877 env->DeleteLocalRef(cl);
4878 env->CallVoidMethod(listener, mOnPU, br, list);
4879 if (env->ExceptionCheck()) { env->ExceptionDescribe(); env->ExceptionClear(); } else { LOGI("STORE_STEP: launchBillingFlow fake delivered via %s", fnameC?fnameC:"?"); delivered = true; }
4880 } else LOGI("launchBillingFlow: onPurchasesUpdated not found on %s", fnameC?fnameC:"?");
4881 if (list) env->DeleteLocalRef(list);
4882 env->DeleteLocalRef(lisCls);
4883 if (alCls) env->DeleteLocalRef(alCls);
4884 if (!isDirect) {
4885 env->DeleteLocalRef(listener);
4886 if (fval) env->DeleteLocalRef(fval);
4887 } else env->DeleteLocalRef(fval);
4888 }
4889 if (fnameC) env->ReleaseStringUTFChars(fnameJ, fnameC);
4890 if (ftypeC) env->ReleaseStringUTFChars(ftypeNameJ, ftypeC);
4891 if (fnameJ) env->DeleteLocalRef(fnameJ);
4892 if (ftypeNameJ) env->DeleteLocalRef(ftypeNameJ);
4893 if (ftype) env->DeleteLocalRef(ftype);
4894 env->DeleteLocalRef(field);
4895 }
4896 if (fields) env->DeleteLocalRef(fields);
4897 if (fieldCls) env->DeleteLocalRef(fieldCls);
4898 if (pulCls) {
4899 if (env->GetObjectRefType(pulCls) == JNIGlobalRefType) env->DeleteGlobalRef(pulCls);
4900 else env->DeleteLocalRef(pulCls);
4901 }
4902 env->DeleteLocalRef(bciCls);
4903 }
4904 // Keep fake and br global alive for app lifetime (prevent Scudo poison / GC)
4905 // if (fake) env->DeleteGlobalRef(fake); // intentionally leak
4906 // if (br) env->DeleteGlobalRef(br);
4907 // Fallback: try Unity StoreBilling proxy if direct zzf delivery failed or as extra
4908 if (!delivered) {
4909 // Try to deliver via PurchaseUpdatedListenerProxy directly by faking a call to its onPurchasesUpdated
4910 // Create a fake BillingResult and list with fake purchase, then try to find any PurchaseUpdatedListenerProxy instance via reflection on g_app_context
4911 LOGI("launchBillingFlow: trying StoreBilling fallback via proxy");
4912 // Search for field of type StoreBilling in Application (brute force)
4913 jclass appCls = env->GetObjectClass(g_app_context);
4914 // Try to get StoreBilling instance via StoreMonitor or similar - simplified: just log and try to call proxy directly via new instance
4915 jclass puProxyCls = g_cls_pu_proxy;
4916 if (puProxyCls) {
4917 // Create a dummy proxy and try to trigger its onPurchasesUpdated with our fake
4918 // Instead, we can directly call the game's PurchaseUpdatedResponseListener if we can find it
4919 LOGI("launchBillingFlow: pu_proxy class available, attempting direct proxy delivery");
4920 // For now, just create a new proxy with a dummy listener and try - not ideal but try to find real instance via app fields
4921 // Brute force: iterate fields of Application
4922 jclass cls = appCls;
4923 // Try to find Field of type StoreBilling
4924 // Use reflection to get declared fields
4925 jclass classCls = env->FindClass("java/lang/Class");
4926 jmethodID getDeclaredFields = env->GetMethodID(classCls, "getDeclaredFields", "()[Ljava/lang/reflect/Field;");
4927 jobjectArray fields = (jobjectArray)env->CallObjectMethod(appCls, getDeclaredFields);
4928 if (!env->ExceptionCheck() && fields) {
4929 int len = env->GetArrayLength(fields);
4930 LOGI("launchBillingFlow: app has %d declared fields", len);
4931 } else { env->ExceptionClear(); }
4932 if (fields) env->DeleteLocalRef(fields);
4933 env->DeleteLocalRef(classCls);
4934 }
4935 env->DeleteLocalRef(appCls);
4936 env->ExceptionClear();
4937 }
4938 // Return success BillingResult without calling original (which would launch Play)
4939 if (!br) br = make_success_billing_result(env);
4940 if (!br && activity) { // fallback: try to get via backup's original? just create dummy via builder failure -> return null?
4941 LOGE("launchBillingFlow: could not create BillingResult, returning null");
4942 }
4943 if (activity) env->DeleteLocalRef(activity);
4944 if (params) env->DeleteLocalRef(params);
4945 if (original_this) env->DeleteLocalRef(original_this);
4946 LOGI("launchBillingFlow: bypassed Play, delivered=%d", delivered);
4947 return br; // return BillingResult directly (boxed)
4948 } }
4949 case 9: { {
4950 jobject params = argc > 1 ? env->GetObjectArrayElement(args, 1) : nullptr;
4951 jobject listener = argc > 2 ? env->GetObjectArrayElement(args, 2) : nullptr;
4952 LOGI("acknowledgePurchase: intercepted, faking success");
4953 if (listener) {
4954 jclass lc = env->GetObjectClass(listener);
4955 jmethodID m = env->GetMethodID(lc, "onAcknowledgePurchaseResponse", "(Lcom/android/billingclient/api/BillingResult;)V");
4956 if (!m) { env->ExceptionClear(); m = env->GetMethodID(lc, "onAcknowledgePurchaseResponse", "(Lcom/android/billingclient/api/BillingResult;)V"); }
4957 if (m) {
4958 jobject br = make_success_billing_result(env);
4959 env->CallVoidMethod(listener, m, br);
4960 if (br) env->DeleteLocalRef(br);
4961 LOGI("acknowledgePurchase: fake success delivered");
4962 if (env->ExceptionCheck()) env->ExceptionClear();
4963 } else { env->ExceptionClear(); LOGE("acknowledgePurchase: listener method not found"); }
4964 env->DeleteLocalRef(lc);
4965 }
4966 if (params) env->DeleteLocalRef(params);
4967 if (listener) env->DeleteLocalRef(listener);
4968 if (original_this) env->DeleteLocalRef(original_this);
4969 return nullptr;
4970 } }
4971 case 10: { {
4972 jobject params = argc > 1 ? env->GetObjectArrayElement(args, 1) : nullptr;
4973 jobject listener = argc > 2 ? env->GetObjectArrayElement(args, 2) : nullptr;
4974 LOGI("consumeAsync: intercepted, faking success");
4975 if (listener) {
4976 jclass lc = env->GetObjectClass(listener);
4977 jmethodID m = env->GetMethodID(lc, "onConsumeResponse", "(Lcom/android/billingclient/api/BillingResult;Ljava/lang/String;)V");
4978 if (m) {
4979 jobject br = make_success_billing_result(env);
4980 jstring token = env->NewStringUTF("mod_token");
4981 env->CallVoidMethod(listener, m, br, token);
4982 if (br) env->DeleteLocalRef(br);
4983 env->DeleteLocalRef(token);
4984 LOGI("consumeAsync: fake success delivered");
4985 if (env->ExceptionCheck()) env->ExceptionClear();
4986 } else { env->ExceptionClear(); LOGE("consumeAsync: listener method not found"); }
4987 env->DeleteLocalRef(lc);
4988 }
4989 if (params) env->DeleteLocalRef(params);
4990 if (listener) env->DeleteLocalRef(listener);
4991 if (original_this) env->DeleteLocalRef(original_this);
4992 return nullptr;
4993 } }
4994 default:
4995 LOGE("nativeDispatch: unknown hookId=%d", hookId);
4996 return nullptr;
4997 }
4998}
4999
5000static void install_art_hooks(JNIEnv *env) {
5001 if (!g_app_context) { LOGE("install_art_hooks: NO app context — aborting"); return; }
5002 LOGI("install_art_hooks: START (app_context=%p)", (void*)g_app_context);
5003
5004 for (int attempt = 0; attempt < 12; attempt++) {
5005 LOGI("install_art_hooks: attempt %d/12", attempt + 1);
5006 bool missing = false;
5007 // deprecated optional classes (Play Billing <8.3.0) - not required, keep for log
5008 if (!g_cls_zzbm) { g_cls_zzbm = load_app_class(env, "com/android/billingclient/api/zzbm"); LOGI(" zzbm=%p (optional)", (void*)g_cls_zzbm); }
5009 if (!g_cls_bci) { g_cls_bci = load_app_class(env, "com/android/billingclient/api/BillingClientImpl"); LOGI(" bci=%p", (void*)g_cls_bci); }
5010 if (!g_cls_proxy) { g_cls_proxy = load_app_class(env, "com/android/billingclient/api/ProxyBillingActivity"); LOGI(" proxy=%p", (void*)g_cls_proxy); }
5011 if (!g_cls_gpbl_proxy){ g_cls_gpbl_proxy= load_app_class(env, "com/unity3d/services/store/gpbl/proxies/PurchasesResponseListenerProxy"); LOGI(" gpbl_proxy=%p", (void*)g_cls_gpbl_proxy); }
5012 if (!g_cls_pu_proxy) { g_cls_pu_proxy = load_app_class(env, "com/unity3d/services/store/gpbl/proxies/PurchaseUpdatedListenerProxy"); LOGI(" pu_proxy=%p", (void*)g_cls_pu_proxy); }
5013 if (!g_cls_max) { g_cls_max = load_app_class(env, "com/applovin/mediation/unity/MaxUnityAdManager"); LOGI(" max=%p", (void*)g_cls_max); }
5014 if (!g_cls_purchase) { g_cls_purchase = load_app_class(env, "com/android/billingclient/api/Purchase"); LOGI(" purchase=%p", (void*)g_cls_purchase); }
5015 if (!g_cls_zzcj) { g_cls_zzcj = load_app_class(env, "com/android/billingclient/api/zzdc"); LOGI(" zzdc=%p (optional deprecated)", (void*)g_cls_zzcj); }
5016 if (!g_cls_billingresult) { g_cls_billingresult = load_app_class(env, "com/android/billingclient/api/BillingResult"); LOGI(" billingresult=%p", (void*)g_cls_billingresult); }
5017 if (!g_cls_productdetails) { g_cls_productdetails = load_app_class(env, "com/android/billingclient/api/ProductDetails"); LOGI(" productDetails=%p", (void*)g_cls_productdetails); }
5018 if (!g_cls_queryresult) { g_cls_queryresult = load_app_class(env, "com/android/billingclient/api/QueryProductDetailsResult"); LOGI(" queryResult=%p", (void*)g_cls_queryresult); }
5019 if (!g_cls_arraylist) { g_cls_arraylist = load_app_class(env, "java/util/ArrayList"); LOGI(" arraylist=%p", (void*)g_cls_arraylist); }
5020 if (!g_cls_intent) { g_cls_intent = load_app_class(env, "android/content/Intent"); LOGI(" intent=%p", (void*)g_cls_intent); }
5021 if (!g_cls_jsonobject) { g_cls_jsonobject = load_app_class(env, "org/json/JSONObject"); LOGI(" jsonobject=%p", (void*)g_cls_jsonobject); }
5022 if (!g_cls_system) { g_cls_system = load_app_class(env, "java/lang/System"); LOGI(" system=%p", (void*)g_cls_system); }
5023
5024 if (!g_cls_bci || !g_cls_proxy || !g_cls_gpbl_proxy || !g_cls_pu_proxy || !g_cls_max || !g_cls_purchase ||
5025 !g_cls_billingresult || !g_cls_productdetails || !g_cls_queryresult || !g_cls_arraylist || !g_cls_intent || !g_cls_jsonobject || !g_cls_system) {
5026 missing = true;
5027 }
5028
5029 if (missing) {
5030 LOGI("install_art_hooks: some classes NOT ready, retry %d/12", attempt + 1);
5031 sleep(5);
5032 continue;
5033 }
5034 LOGI("install_art_hooks: all classes loaded OK");
5035
5036 LOGI("install_art_hooks: resolving helper methods...");
5037 g_mid_purchase_init = env->GetMethodID(g_cls_purchase, "<init>", "(Ljava/lang/String;Ljava/lang/String;)V");
5038 LOGI(" purchase_init=%p", (void*)g_mid_purchase_init);
5039 jclass list_cls = env->FindClass("java/util/List");
5040 LOGI(" List.class=%p", (void*)list_cls);
5041 g_mid_list_add = env->GetMethodID(list_cls, "add", "(Ljava/lang/Object;)Z");
5042 LOGI(" list_add=%p", (void*)g_mid_list_add);
5043 g_mid_arraylist_init = env->GetMethodID(g_cls_arraylist, "<init>", "()V");
5044 LOGI(" arraylist_init=%p", (void*)g_mid_arraylist_init);
5045 g_mid_intent_putextra_int = env->GetMethodID(g_cls_intent, "putExtra", "(Ljava/lang/String;I)Landroid/content/Intent;");
5046 LOGI(" intent_putextra_int=%p", (void*)g_mid_intent_putextra_int);
5047 g_mid_intent_putsal = env->GetMethodID(g_cls_intent, "putStringArrayListExtra", "(Ljava/lang/String;Ljava/util/ArrayList;)Landroid/content/Intent;");
5048 LOGI(" intent_putsal=%p", (void*)g_mid_intent_putsal);
5049 g_mid_forward = env->GetStaticMethodID(g_cls_max, "forwardUnityEvent", "(Lorg/json/JSONObject;)V");
5050 LOGI(" max_forward=%p", (void*)g_mid_forward);
5051 g_mid_jsonobj_init = env->GetMethodID(g_cls_jsonobject, "<init>", "()V");
5052 LOGI(" jsonobj_init=%p", (void*)g_mid_jsonobj_init);
5053 g_mid_jsonobj_put = env->GetMethodID(g_cls_jsonobject, "put", "(Ljava/lang/String;Ljava/lang/Object;)Lorg/json/JSONObject;");
5054 LOGI(" jsonobj_put=%p", (void*)g_mid_jsonobj_put);
5055 g_mid_system_time = env->GetStaticMethodID(g_cls_system, "currentTimeMillis", "()J");
5056 LOGI(" system_time=%p", (void*)g_mid_system_time);
5057 g_cls_boolean = (jclass)env->NewGlobalRef(env->FindClass("java/lang/Boolean"));
5058 g_mid_boolean_valueof = env->GetStaticMethodID(g_cls_boolean, "valueOf", "(Z)Ljava/lang/Boolean;");
5059 LOGI(" boolean=%p valueOf=%p", (void*)g_cls_boolean, (void*)g_mid_boolean_valueof);
5060 // deprecated zzdc field - optional, try but not required
5061 if (g_cls_zzcj) {
5062 g_fid_zzcj_zzl = env->GetStaticFieldID(g_cls_zzcj, "zzi", "Lcom/android/billingclient/api/BillingResult;");
5063 LOGI(" zzdc_zzi=%p (optional)", (void*)g_fid_zzcj_zzl);
5064 env->ExceptionClear();
5065 } else {
5066 LOGI(" zzdc_zzi skipped (g_cls_zzcj null, optional)");
5067 }
5068 // BillingResult v8.3.0+ builder (required) - use app classloader (FindClass fails for app classes)
5069 g_mid_billing_newbuilder = env->GetStaticMethodID(g_cls_billingresult, "newBuilder", "()Lcom/android/billingclient/api/BillingResult$Builder;");
5070 LOGI(" billing_newBuilder=%p", (void*)g_mid_billing_newbuilder);
5071 if (!g_cls_billingbuilder) {
5072 g_cls_billingbuilder = load_app_class(env, "com/android/billingclient/api/BillingResult$Builder");
5073 LOGI(" billingbuilder load_app_class=%p", (void*)g_cls_billingbuilder);
5074 }
5075 if (g_cls_billingbuilder) {
5076 g_mid_builder_setResponseCode = env->GetMethodID(g_cls_billingbuilder, "setResponseCode", "(I)Lcom/android/billingclient/api/BillingResult$Builder;");
5077 g_mid_builder_setDebugMessage = env->GetMethodID(g_cls_billingbuilder, "setDebugMessage", "(Ljava/lang/String;)Lcom/android/billingclient/api/BillingResult$Builder;");
5078 g_mid_builder_build = env->GetMethodID(g_cls_billingbuilder, "build", "()Lcom/android/billingclient/api/BillingResult;");
5079 LOGI(" builder cls=%p setResponseCode=%p setDebugMessage=%p build=%p", (void*)g_cls_billingbuilder, (void*)g_mid_builder_setResponseCode, (void*)g_mid_builder_setDebugMessage, (void*)g_mid_builder_build);
5080 } else {
5081 env->ExceptionClear();
5082 LOGE(" BillingResult$Builder class NOT found via load_app_class");
5083 }
5084 env->ExceptionClear();
5085 if (!g_mid_purchase_init || !g_mid_list_add || !g_mid_arraylist_init ||
5086 !g_mid_intent_putextra_int || !g_mid_intent_putsal || !g_mid_forward ||
5087 !g_mid_jsonobj_init || !g_mid_jsonobj_put || !g_mid_system_time ||
5088 !g_mid_billing_newbuilder || !g_mid_builder_setResponseCode || !g_mid_builder_build ||
5089 !g_cls_boolean || !g_mid_boolean_valueof) {
5090 LOGE("install_art_hooks: some methods/fields MISSING — retry");
5091 LOGE(" purchase_init=%p list_add=%p arraylist_init=%p", (void*)g_mid_purchase_init, (void*)g_mid_list_add, (void*)g_mid_arraylist_init);
5092 LOGE(" intent_pi=%p intent_sal=%p forward=%p", (void*)g_mid_intent_putextra_int, (void*)g_mid_intent_putsal, (void*)g_mid_forward);
5093 LOGE(" json_init=%p json_put=%p sys_time=%p billing_newBuilder=%p builder_setRC=%p builder_build=%p bool=%p", (void*)g_mid_jsonobj_init, (void*)g_mid_jsonobj_put, (void*)g_mid_system_time, (void*)g_mid_billing_newbuilder, (void*)g_mid_builder_setResponseCode, (void*)g_mid_builder_build, (void*)g_cls_boolean);
5094 env->ExceptionClear();
5095 sleep(5);
5096 continue;
5097 }
5098 LOGI("install_art_hooks: all helper methods/fields resolved OK");
5099
5100 cache_reflection();
5101
5102 if (!lsplant_init(env)) {
5103 LOGE("install_art_hooks: lsplant_init FAILED");
5104 return;
5105 }
5106
5107 jclass hc_cls = load_class(env, "com/drmod/HookerCallback");
5108 if (!hc_cls) { LOGE("HookerCallback class NOT found"); return; }
5109 JNINativeMethod nm[] = {
5110 {(char *)"nativeDispatch",
5111 (char *)"(ILcom/drmod/HookerCallback;[Ljava/lang/Object;)Ljava/lang/Object;",
5112 (void *)Java_com_drmod_HookerCallback_nativeDispatch}
5113 };
5114 if (env->RegisterNatives(hc_cls, nm, 1) != 0) {
5115 env->ExceptionClear();
5116 LOGE("install_art_hooks: RegisterNatives nativeDispatch failed");
5117 } else {
5118 LOGI("install_art_hooks: nativeDispatch registered");
5119 }
5120 g_hooker_cls = (jclass)env->NewGlobalRef(hc_cls);
5121 g_hooker_ctor = env->GetMethodID(hc_cls, "<init>", "(I)V");
5122 jmethodID cb_mid = env->GetMethodID(hc_cls, "callback", "([Ljava/lang/Object;)Ljava/lang/Object;");
5123 g_hooker_callback_method = env->ToReflectedMethod(hc_cls, cb_mid, false);
5124 g_hooker_callback_method = env->NewGlobalRef(g_hooker_callback_method);
5125 env->DeleteLocalRef(hc_cls);
5126 if (!g_hooker_ctor || !g_hooker_callback_method) {
5127 env->ExceptionClear();
5128 LOGE("install_art_hooks: HookerCallback ctor/callback NOT found");
5129 return;
5130 }
5131 LOGI("install_art_hooks: HookerCallback class OK (ctor=%p cb=%p)", (void*)g_hooker_ctor, (void*)g_hooker_callback_method);
5132
5133 LOGI("install_art_hooks: installing hooks via LSPlant...");
5134 // Store: hook l'unico punto game-facing che riceve {BillingResult, List<Purchase>} nella
5135 // query path di Unity gpbl (PurchasesResponseListenerProxy). I vecchi hook su zzbm (Callable)
5136 // e BillingClientImpl.zzaI non esistono in Play Billing 8.3.0 → rimossi.
5137 g_backup_a = lsplant_hook_method(env, g_cls_gpbl_proxy, "onQueryPurchasesResponse",
5138 "(Ljava/lang/Object;Ljava/util/List;)V", false, 0);
5139 g_backup_g = lsplant_hook_method(env, g_cls_pu_proxy, "onPurchasesUpdated",
5140 "(Ljava/lang/Object;Ljava/util/List;)V", false, 7);
5141 g_backup_h = lsplant_hook_method(env, g_cls_bci, "launchBillingFlow",
5142 "(Landroid/app/Activity;Lcom/android/billingclient/api/BillingFlowParams;)Lcom/android/billingclient/api/BillingResult;", false, 8);
5143 g_backup_i = lsplant_hook_method(env, g_cls_bci, "acknowledgePurchase",
5144 "(Lcom/android/billingclient/api/AcknowledgePurchaseParams;Lcom/android/billingclient/api/AcknowledgePurchaseResponseListener;)V", false, 9);
5145 g_backup_j = lsplant_hook_method(env, g_cls_bci, "consumeAsync",
5146 "(Lcom/android/billingclient/api/ConsumeParams;Lcom/android/billingclient/api/ConsumeResponseListener;)V", false, 10);
5147 g_backup_d = lsplant_hook_method(env, g_cls_bci, "queryProductDetailsAsync",
5148 "(Lcom/android/billingclient/api/QueryProductDetailsParams;Lcom/android/billingclient/api/ProductDetailsResponseListener;)V", false, 3);
5149 g_backup_e = lsplant_hook_method(env, g_cls_proxy, "onActivityResult",
5150 "(IILandroid/content/Intent;)V", false, 4);
5151 lsplant_hook_method(env, g_cls_max, "showRewardedAd",
5152 "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V", false, 5);
5153 // Ad: forza isRewardedAdReady=true così il gioco procede in showRewardedAd (hook 5 -> reward).
5154 g_backup_f = lsplant_hook_method(env, g_cls_max, "isRewardedAdReady",
5155 "(Ljava/lang/String;)Z", false, 6);
5156 LOGI("install_art_hooks: ALL LSPlant HOOKS DONE");
5157 return;
5158 }
5159 LOGE("install_art_hooks: gave up after 12 attempts");
5160}
5161
5162// ==================== Process verification ====================
5163
5164static bool is_exact_target(JNIEnv *env, jstring nice_name) {
5165 if (!env || !nice_name)
5166 return false;
5167
5168 const char *name = env->GetStringUTFChars(nice_name, nullptr);
5169 if (!name)
5170 return false;
5171
5172 bool match = strcmp(name, GAME_PKG) == 0;
5173
5174 LOGI("process=[%s], target=%d", name, match);
5175
5176 env->ReleaseStringUTFChars(nice_name, name);
5177 return match;
5178}
5179
5180static uintptr_t find_lib_base(const char *lib_name) {
5181 LOGI("find_lib_base: searching for %s", lib_name);
5182 FILE *f = fopen("/proc/self/maps", "r");
5183 if (!f) { LOGE("find_lib_base: fopen /proc/self/maps failed errno=%d (%s)", errno, strerror(errno)); return 0; }
5184 char line[512];
5185 uintptr_t best = 0;
5186 while (fgets(line, sizeof(line), f)) {
5187 if (!strstr(line, lib_name)) continue;
5188 uintptr_t start = 0;
5189 unsigned long foff = 0;
5190 char perms[8] = {};
5191 if (sscanf(line, "%lx-%*x %7s %lx", &start, perms, &foff) != 3) {
5192 LOGE("find_lib_base: sscanf FAILED on line: %.200s", line);
5193 continue;
5194 }
5195 LOGI("find_lib_base: mapping %s start=0x%lx perms=%s foff=0x%lx", lib_name, (unsigned long)start, perms, foff);
5196 if (strchr(perms, 'x')) {
5197 uintptr_t b = start - (uintptr_t)foff;
5198 LOGI("find_lib_base: exec mapping 0x%lx foff 0x%lx -> base 0x%lx",
5199 (unsigned long)start, foff, (unsigned long)b);
5200 fclose(f);
5201 return b;
5202 }
5203 if (foff == 0 && start && (best == 0 || start < best)) best = start;
5204 }
5205 fclose(f);
5206 LOGI("find_lib_base: no exec mapping found, fallback best=0x%lx", (unsigned long)best);
5207 return best;
5208}
5209
5210static bool is_target_process() {
5211 char name[256] = {};
5212 FILE *f = fopen("/proc/self/cmdline", "r");
5213 if (!f) { LOGE("is_target_process: fopen /proc/self/cmdline failed errno=%d (%s)", errno, strerror(errno)); return false; }
5214 size_t n = fread(name, 1, sizeof(name) - 1, f);
5215 fclose(f);
5216 if (n == 0) { LOGE("is_target_process: cmdline read returned 0 bytes"); return false; }
5217 name[n] = '\0';
5218 bool match = strcmp(name, GAME_PKG_FULL) == 0;
5219 LOGI("is_target_process: cmdline=[%s] match=%d", name, match);
5220 return match;
5221}
5222
5223// ==================== Boot marker ====================
5224
5225// ==================== Worker ====================
5226
5227static void *worker(void *) {
5228 if (!g_is_game) {
5229 LOGE("Worker refused: not target");
5230 return nullptr;
5231 }
5232 LOGI("Worker confirmed target process, delay %ds", DELAY_SEC);
5233
5234 sleep(DELAY_SEC);
5235 LOGI("Worker: delay elapsed, starting work");
5236
5237 g_immortality = false;
5238 g_forced_crits = false;
5239 g_minigame_loot = false;
5240 g_divine_forge = false;
5241 g_overdrive_divine = false;
5242 g_forge_exp = false;
5243 LOGI("Worker: toggle defaults FORCED OFF - imm=%d crit=%d forge=%d od=%d mg=%d fexp=%d",
5244 g_immortality, g_forced_crits, g_divine_forge, g_overdrive_divine, g_minigame_loot, g_forge_exp);
5245 LOGI("Worker: toggle addrs - imm=%p crit=%p forge=%p od=%p mg=%p fexp=%p",
5246 &g_immortality, &g_forced_crits, &g_divine_forge, &g_overdrive_divine, &g_minigame_loot, &g_forge_exp);
5247
5248 uintptr_t base = 0;
5249 for (int r = 0; r < 12; r++) {
5250 base = find_lib_base("libil2cpp.so");
5251 if (base) break;
5252 LOGI("Worker: libil2cpp not found, retry %d/12 in 5s", r+1);
5253 sleep(5);
5254 }
5255 if (!base) { LOGE("libil2cpp.so not found after retries"); return nullptr; }
5256 g_il2cpp_base = base;
5257 LOGI("libil2cpp base: %p", (void *)base);
5258
5259 LOGI("Worker: startup restore — checking toggleable patches already applied...");
5260 int restored = 0;
5261 for (int i = 0; i < num_patches; i++) {
5262 if (!patches[i].toggleable || !patches[i].repl || !patches[i].orig) continue;
5263 uintptr_t a = base + patches[i].off;
5264 if (memcmp((void *)a, patches[i].repl, patches[i].len) == 0) {
5265 LOGI(" RESTORE: %s @0x%X already patched, reverting to original", patches[i].name, patches[i].off);
5266 if (unpatch_bytes(base, patches[i])) { restored++; LOGI(" RESTORE OK: %s", patches[i].name); }
5267 else { LOGE(" RESTORE FAIL: %s", patches[i].name); }
5268 }
5269 }
5270 LOGI("Worker: startup restore DONE — %d reverted", restored);
5271
5272 LOGI("Worker: applying byte patches (always-on only)...");
5273 int ok = 0, fail = 0;
5274 for (int i = 0; i < num_patches; i++) {
5275 if (patches[i].toggleable) continue;
5276 if (patch_bytes(base, patches[i])) ok++; else fail++;
5277 }
5278 LOGI("Worker: byte patches DONE — %d ok, %d fail", ok, fail);
5279
5280 LOGI("Worker: installing immortality hook...");
5281 install_immortality(base);
5282
5283 LOGI("Worker: installing store hook (ProcessPurchase -> calls ORIGINAL so grant runs)");
5284 install_store_hook(base);
5285 LOGI("Worker: installing dnf hook (dnf -> returns true, passes verification)");
5286 install_dnf_hook(base);
5287 LOGI("Worker: installing tbz fix #1 (ProcessPurchase GATE1 entry+0x118 -> NOP, fall-through a path TRUE che spara l'evento Action<Product> = GRANT)");
5288 install_tbz_force_grant();
5289 LOGI("Worker: installing tbz fix #2 (ProcessPurchase GATE2 entry+0x1D0 -> unconditional b to GRANT PATH con eao/dne; NON raggiungibile con GATE1=NOP)");
5290 install_fallback_grant_force();
5291 LOGI("Worker: installing dsq redirect (v4.77: dsq@0x28B68A4 -> b dsu@0x28B6924: la callback del path TRUE esegue il GRANT COMPLETO, non il wrapper vuoto)");
5292 install_dsq_redirect();
5293 LOGI("Worker: installing club bridge (v4.78: entry hook su dsu@0x28B6924 -> attiva ClubTier localmente via esj per vip/elite club)");
5294 install_club_bridge();
5295 LOGI("Worker: installing STORE CHAIN FULL TRACE (v4.71: logga OGNI movimento della catena store in tempo reale)");
5296 install_store_chain_trace();
5297 LOGI("Worker: smart-grant CHECK bypass DISABLED in v56 (non applicabile: v56 non ha chain validator; metodo finisce ~entry+0x2D8)");
5298 // install_smart_grant_check_nop();
5299 LOGI("Worker: dxn entry trace DISABLED in v56 (off 0x28BE238 era v55; in v56 il grant avviene dentro ProcessPurchase via eao 0x2991924)");
5300 // install_dxn_entry_trace();
5301 LOGI("Worker: x20-gate probe DISABLED in v56 (offset entry+0x90 era v55)");
5302 // install_x20_gate_probe();
5303 LOGI("Worker: grant chain probes DISABLED in v56 (offsets v55: fallback entry 0x28a92e8, chain 0x298f448/0x28a8f1c/0x28a94d4/0x28bc3e8)");
5304 // install_fallback_entry_probe();
5305 // install_grant_chain_probes();
5306 LOGI("Worker: engine dispatch probe DISABLED in v56 (0x2a7ed18 era v55)");
5307 // install_engine_dispatch_probe();
5308 LOGI("Worker: POST trace probes DISABLED in v56 (dli 0x27dabbc / dze 0x298f548 erano v55)");
5309 // install_post_trace_probes();
5310 LOGI("Worker: grant callback invoke probe DISABLED in v56 (0x2379940 era v55)");
5311 // install_grant_callback_invoke_probe();
5312 LOGI("Worker: fc.dwu + apply-grant trace probes DISABLED in v56 (0x2A7C700 / 0x28bc158 erano v55)");
5313 // install_fc_dwu_grant_trace_probes();
5314 LOGI("Worker: VERIFY IAP probes + Code=0 FORCE DISABLED in v56 (verifyiap 0x28ad5c4 / dnv 0x28ad894 / bl patch 0x28ad928 erano v55)");
5315 // install_verify_iap_probes();
5316 LOGI("Worker: google purchase hook DISABLED (ART launchBillingFlow already handles, native interferes)");
5317 // GooglePurchaseUpdatedListener hook disabled
5318 LOGI("Worker: dw store REAL hooks DISABLED (overwrites g_orig vars per method -> wrong trampoline, causes crashes)");
5319 // install_dw_store_real_hooks(base);
5320
5321 LOGI("Worker: installing User.eyu hook (dungeon keys A/B/C: chiavi mai giu', ad x%d, ads illimitate)", KEY_BONUS_MULT);
5322 install_user_eyu_probe();
5323
5324 LOGI("Worker: granting SYSTEM_ALERT_WINDOW...");
5325 int ret = system("/vendor/bin/su -c 'appops set " GAME_PKG_FULL " SYSTEM_ALERT_WINDOW allow'");
5326 LOGI("SYSTEM_ALERT_WINDOW grant: ret=%d", ret);
5327
5328 LOGI("Worker: starting JNI phase...");
5329 {
5330 JNIEnv *env = nullptr;
5331 if (g_jvm->AttachCurrentThread(&env, nullptr) != 0 || !env) {
5332 LOGE("worker: AttachCurrentThread failed");
5333 } else {
5334 LOGI("worker: JNI attached (env=%p)", (void*)env);
5335 jclass at_cls = env->FindClass("android/app/ActivityThread");
5336 LOGI("worker: ActivityThread.class=%p", (void*)at_cls);
5337 if (at_cls) {
5338 jmethodID current_at = env->GetStaticMethodID(at_cls,
5339 "currentActivityThread", "()Landroid/app/ActivityThread;");
5340 jobject at = env->CallStaticObjectMethod(at_cls, current_at);
5341 if (at) {
5342 jmethodID get_app = env->GetMethodID(at_cls,
5343 "getApplication", "()Landroid/app/Application;");
5344 jobject app = env->CallObjectMethod(at, get_app);
5345 if (app) {
5346 g_app_context = env->NewGlobalRef(app);
5347 env->DeleteLocalRef(app);
5348 LOGI("App context captured");
5349 } else {
5350 env->ExceptionClear();
5351 LOGE("worker: getApplication returned null");
5352 }
5353 env->DeleteLocalRef(at);
5354 } else {
5355 env->ExceptionClear();
5356 LOGE("worker: currentActivityThread returned null");
5357 }
5358 env->DeleteLocalRef(at_cls);
5359 } else {
5360 env->ExceptionClear();
5361 LOGE("worker: ActivityThread not found");
5362 }
5363
5364 LOGI("worker: calling load_dex...");
5365 load_dex(env);
5366
5367 LOGI("worker: calling install_art_hooks...");
5368 install_art_hooks(env);
5369
5370 LOGI("worker: calling create_overlay...");
5371 create_overlay(env);
5372 LOGI("worker: create_overlay done, detaching JNI...");
5373 g_jvm->DetachCurrentThread();
5374 LOGI("worker: JNI detached — all worker steps complete");
5375 }
5376 }
5377
5378 g_activated = true;
5379 LOGI("All mods active");
5380 return nullptr;
5381}
5382
5383// ==================== Zygisk module ====================
5384
5385class DRMod : public zygisk::ModuleBase {
5386public:
5387 void onLoad(zygisk::Api *api, JNIEnv *env) override {
5388 this->api = api;
5389 this->env = env;
5390 env->GetJavaVM(&g_jvm);
5391 LOGI("onLoad");
5392 }
5393
5394 void preAppSpecialize(zygisk::AppSpecializeArgs *args) override {
5395 g_is_game = false;
5396 g_activated = false;
5397 g_il2cpp_base = 0;
5398 g_main = nullptr;
5399 g_orig_fop = nullptr;
5400 g_dex_loader = nullptr;
5401 g_immortality = false;
5402 g_forced_crits = false;
5403 g_minigame_loot = false;
5404 g_divine_forge = false;
5405 g_overdrive_divine = false;
5406 g_forge_exp = false;
5407
5408 if (!args || !args->nice_name) {
5409 LOGI("preAppSpecialize: no nice_name, dlclose");
5410 api->setOption(zygisk::DLCLOSE_MODULE_LIBRARY);
5411 return;
5412 }
5413
5414 if (!is_exact_target(env, args->nice_name)) {
5415 LOGI("preAppSpecialize: not target, dlclose");
5416 api->setOption(zygisk::DLCLOSE_MODULE_LIBRARY);
5417 return;
5418 }
5419
5420 g_is_game = true;
5421 LOGI("TARGET: %s", GAME_PKG);
5422 }
5423
5424 void postAppSpecialize(const zygisk::AppSpecializeArgs *) override {
5425 if (!g_is_game) {
5426 // No force-stop here: it would run in every app process.
5427 LOGI("postAppSpecialize: not target, returning");
5428 return;
5429 }
5430
5431 LOGI("postAppSpecialize: target process");
5432
5433 pthread_t t;
5434 if (pthread_create(&t, nullptr, worker, nullptr) == 0) {
5435 pthread_detach(t);
5436 LOGI("postAppSpecialize: worker thread created");
5437 } else {
5438 LOGE("pthread_create failed");
5439 }
5440 }
5441
5442 void preServerSpecialize(zygisk::ServerSpecializeArgs *args) override {
5443 LOGI("preServerSpecialize");
5444 (void)args;
5445 }
5446
5447 void postServerSpecialize(const zygisk::ServerSpecializeArgs *args) override {
5448 LOGI("postServerSpecialize");
5449 (void)args;
5450 }
5451
5452private:
5453 zygisk::Api *api = nullptr;
5454 JNIEnv *env = nullptr;
5455};
5456
5457REGISTER_ZYGISK_MODULE(DRMod)