module.cpp
| 1 | |
| 2 | |
| 3 | |
| 4 | |
| 5 | |
| 6 | |
| 7 | |
| 8 | |
| 9 | |
| 10 | |
| 11 | |
| 12 | |
| 13 | |
| 14 | |
| 15 | |
| 16 | |
| 17 | |
| 18 | |
| 19 | |
| 20 | |
| 21 | |
| 22 | |
| 23 | |
| 24 | |
| 25 | |
| 26 | |
| 27 | |
| 28 | |
| 29 | static bool g_is_game = false; |
| 30 | |
| 31 | static FILE *g_logfile = nullptr; |
| 32 | static 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 | |
| 54 | |
| 55 | |
| 56 | |
| 57 | static JavaVM *g_jvm = nullptr; |
| 58 | static bool g_activated = false; |
| 59 | |
| 60 | // ==================== Toggle state (ALL OFF by default) ==================== |
| 61 | |
| 62 | static volatile bool g_immortality = false; |
| 63 | static volatile bool g_forced_crits = false; |
| 64 | static volatile bool g_minigame_loot = false; |
| 65 | static volatile bool g_divine_forge = false; |
| 66 | static volatile bool g_overdrive_divine = false; |
| 67 | static volatile bool g_forge_exp = false; |
| 68 | |
| 69 | // ==================== Shared state ==================== |
| 70 | |
| 71 | static uintptr_t g_il2cpp_base = 0; |
| 72 | |
| 73 | // ==================== Patch table ==================== |
| 74 | |
| 75 | struct 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 | |
| 84 | static const uint8_t NOP4[] = {0x1F,0x20,0x03,0xD5}; |
| 85 | static const uint8_t NOP8[] = {0x1F,0x20,0x03,0xD5,0x1F,0x20,0x03,0xD5}; |
| 86 | static const uint8_t MOV_W0_1[] = {0x20,0x00,0x80,0x52}; |
| 87 | static const uint8_t MOV_W0_0[] = {0x00,0x00,0x80,0x52}; |
| 88 | static const uint8_t MOV_X0_0[] = {0x00,0x00,0x80,0xD2}; |
| 89 | static const uint8_t MOV_W0_9[] = {0x20,0x01,0x80,0x52}; |
| 90 | static const uint8_t RET[] = {0xC0,0x03,0x5F,0xD6}; |
| 91 | static const uint8_t MR_RET[] = {0x20,0x00,0x80,0x52,0xC0,0x03,0x5F,0xD6}; |
| 92 | static const uint8_t M9_RET[] = {0x20,0x01,0x80,0x52,0xC0,0x03,0x5F,0xD6}; |
| 93 | static const uint8_t M2_RET[] = {0x40,0x00,0x80,0x52,0xC0,0x03,0x5F,0xD6}; |
| 94 | static const uint8_t FMOV_S0[] = {0xE0,0x03,0x27,0x1E}; |
| 95 | |
| 96 | // Original bytes (verified against v56 binary) |
| 97 | static const uint8_t O_ctz[] = {0xFE,0x57,0xBE,0xA9,0xF4,0x4F,0x01,0xA9}; // stp x30,x21 / stp x20,x19 |
| 98 | static const uint8_t O_store1[] = {0x41,0x16,0x00,0x54}; // b.ne HTTP 200 check in fh.dyd (TrackingController.fh) |
| 99 | static const uint8_t O_store2[] = {0x41,0x1A,0x00,0x54}; // b.ne HTTP 200 check in fi.dye (TrackingController.fi) |
| 100 | static const uint8_t O_tbz_fc[] = {0x55,0x1C,0x00,0x36}; // tbz w21,#0 -> fi.dye entry flag guard (bit0==0 => FAILURE) |
| 101 | static const uint8_t O_actk_bl[] = {0xC3,0xFF,0xFF,0x97}; |
| 102 | static const uint8_t O_actk_cs[] = {0xE0,0x17,0x9F,0x1A}; |
| 103 | static const uint8_t O_actk_bl2[] = {0xC1,0xFF,0xFF,0x97}; |
| 104 | static const uint8_t O_actk_bl3[] = {0xBF,0xFF,0xFF,0x97}; |
| 105 | static const uint8_t O_mg[] = {0x00,0x28,0x23,0x1E}; |
| 106 | static const uint8_t O_forge[] = {0xE9,0x23,0xBC,0x6D,0xFE,0x0B,0x00,0xF9}; |
| 107 | static const uint8_t O_od[] = {0xFF,0x43,0x02,0xD1,0xFE,0x6F,0x04,0xA9}; |
| 108 | static const uint8_t O_beq_38[] = {0x00,0x07,0x00,0x54}; // b.eq +0xE0 |
| 109 | static const uint8_t B_SYNC_38[] = {0x38,0x00,0x00,0x14}; // b +0xE0 (same target as b.eq) |
| 110 | static const uint8_t O_jme_gate[] = {0x1A,0x02,0x00,0x36}; // tbz w26,#0 (Exceptional gate) in tw.jme @0x28741C8 |
| 111 | static const uint8_t O_jmg[] = {0xE8,0x0F,0x1E,0xFC,0xFE,0x07,0x00,0xF9}; // tw.jmg prologue @0x287478C |
| 112 | |
| 113 | static 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 | }; |
| 148 | static const int num_patches = sizeof(patches) / sizeof(patches[0]); |
| 149 | |
| 150 | // ==================== Helpers ==================== |
| 151 | |
| 152 | static long page_size() { return sysconf(_SC_PAGESIZE); } |
| 153 | |
| 154 | static 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 | |
| 177 | static 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 | |
| 184 | static 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 | |
| 235 | static 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 | |
| 256 | static 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 | |
| 275 | static jclass g_overlay_cls = nullptr; |
| 276 | static jobject g_app_context = nullptr; |
| 277 | static jmethodID g_native_toggle_mid = nullptr; |
| 278 | static jobject g_dex_loader = nullptr; |
| 279 | |
| 280 | static 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 | |
| 286 | static 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 | |
| 293 | static 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 | |
| 354 | static 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 | |
| 385 | static 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 | |
| 403 | static 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 | |
| 441 | |
| 442 | |
| 443 | |
| 444 | |
| 445 | |
| 446 | |
| 447 | typedef void (*fop_fn)(void *self, void *attacker, double dmg, int critTier, bool flag); |
| 448 | static fop_fn g_orig_fop = nullptr; |
| 449 | static void *g_main = nullptr; |
| 450 | |
| 451 | static 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 | |
| 466 | static int g_fop_call_count = 0; |
| 467 | static 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. |
| 519 | static 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. |
| 523 | static uintptr_t g_code_base = 0; |
| 524 | typedef void (*user_eyu_fn)(void *a, void *b, int c); |
| 525 | static user_eyu_fn g_orig_user_eyu = nullptr; |
| 526 | static volatile int g_user_eyu_count = 0; |
| 527 | |
| 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 |
| 534 | struct DungeonFields { uint32_t daily, bonus, reset, watch; }; |
| 535 | static 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). |
| 542 | static uintptr_t g_user_base = 0; |
| 543 | |
| 544 | |
| 545 | static volatile int64_t g_reset_win_until[4] = {0,0,0,0}; // per-dungeon reset window (ms monotonic) |
| 546 | static volatile int64_t g_last_ad_reward_ms = 0; // B: settata nei case 5/6 fake ads (reward event) |
| 547 | |
| 548 | static 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 | |
| 554 | static 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 | |
| 631 | static 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 | |
| 643 | static 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 | |
| 671 | typedef 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 | |
| 688 | typedef 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 | |
| 699 | typedef struct { |
| 700 | int64_t d_tag; |
| 701 | union { uint64_t d_val; uint64_t d_ptr; } d_un; |
| 702 | } Elf64_Dyn; |
| 703 | |
| 704 | typedef 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 | |
| 713 | typedef 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 | |
| 727 | |
| 728 | |
| 729 | |
| 730 | |
| 731 | |
| 732 | |
| 733 | |
| 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). |
| 739 | static 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 | |
| 751 | static 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 | |
| 762 | static 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 | |
| 881 | static 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 | |
| 943 | static bool g_lsplant_initialized = false; |
| 944 | static jclass g_hooker_cls = nullptr; |
| 945 | static jmethodID g_hooker_ctor = nullptr; |
| 946 | static jobject g_hooker_callback_method = nullptr; |
| 947 | |
| 948 | static 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 | } |
| 961 | static 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 | } |
| 1008 | static 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 | |
| 1145 | struct HookRecord { |
| 1146 | void *target; |
| 1147 | uint8_t orig[16]; |
| 1148 | void *backup_exec; |
| 1149 | }; |
| 1150 | static HookRecord g_hook_records[16]; |
| 1151 | static int g_hook_count = 0; |
| 1152 | |
| 1153 | static 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 | |
| 1197 | static 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 | |
| 1215 | static uintptr_t g_libart_base = 0; |
| 1216 | static void *g_libart_handle = nullptr; |
| 1217 | |
| 1218 | static 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 | |
| 1241 | static 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 | |
| 1703 | static 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 | |
| 1739 | static 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 | |
| 1761 | static 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 | |
| 1784 | typedef void* (*fn_domain_get)(); |
| 1785 | typedef const void** (*fn_domain_get_assemblies)(void*, size_t*); |
| 1786 | typedef const void* (*fn_assembly_get_image)(const void*); |
| 1787 | typedef const void* (*fn_class_from_name)(const void*, const char*, const char*); |
| 1788 | typedef const void* (*fn_class_get_methods)(const void*, void**); |
| 1789 | typedef const char* (*fn_method_get_name)(const void*); |
| 1790 | // il2cpp_method_get_pointer removed — Il2CppMethodInfo starts with methodPointer at offset 0 |
| 1791 | typedef void* (*fn_method_get_invoke_ptr)(const void*); |
| 1792 | |
| 1793 | static 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 | |
| 1884 | typedef int (*proc_purchase_fn)(void *self, void *args); |
| 1885 | static proc_purchase_fn g_orig_proc_purchase = nullptr; |
| 1886 | static 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) |
| 1895 | static bool my_dnf(void *self, void *args); |
| 1896 | static void dump_handler_method_name(uintptr_t base); |
| 1897 | static void dump_purchase_types(uintptr_t base); |
| 1898 | static void dump_grant_bl(uintptr_t entry); |
| 1899 | static 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). |
| 1907 | typedef const void* (*fn_object_get_class)(const void* obj); |
| 1908 | typedef const char* (*fn_class_get_name)(const void* klass); |
| 1909 | typedef const char* (*fn_class_get_namespace)(const void* klass); |
| 1910 | static 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 | |
| 1954 | static 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 | } |
| 1988 | static 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 | } |
| 2054 | typedef bool (*dnf_fn)(void *self, void *args); |
| 2055 | static dnf_fn g_orig_dnf = nullptr; |
| 2056 | static 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 | } |
| 2064 | static 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 | |
| 2110 | typedef void* (*fn_runtime_invoke)(const void* method, void* obj, void** params, void** exc); |
| 2111 | static fn_runtime_invoke g_orig_runtime_invoke = nullptr; |
| 2112 | static fn_method_get_name g_runtime_mname = nullptr; |
| 2113 | static thread_local bool g_in_runtime_hook = false; |
| 2114 | static 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 | } |
| 2133 | static 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) |
| 2154 | static void common_dw_logger(const char* name, void* caller) { |
| 2155 | LOGI("REAL_CALL: >>> dw.%s caller=%p", name, caller); |
| 2156 | } |
| 2157 | typedef bool (*dnf_real_fn)(void* self, void* args); |
| 2158 | typedef int (*proc_real_fn)(void* self, void* args); |
| 2159 | static dnf_real_fn g_orig_dnf_real = nullptr; |
| 2160 | static proc_real_fn g_orig_proc_real = nullptr; |
| 2161 | static 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 | } |
| 2167 | static 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 | } |
| 2173 | static 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). |
| 2273 | static 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). |
| 2308 | static 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). |
| 2332 | static 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 | |
| 2370 | |
| 2371 | |
| 2372 | |
| 2373 | |
| 2374 | |
| 2375 | struct 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 | |
| 2384 | static 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 | |
| 2490 | static 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). |
| 2494 | static 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. |
| 2517 | static uint8_t *g_cs_pool = nullptr; |
| 2518 | static size_t g_cs_pool_size = 0; |
| 2519 | static 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). |
| 2525 | static 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 | |
| 2536 | static 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. |
| 2622 | static 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. |
| 2658 | static void dxn_entry_logger(void) { |
| 2659 | LOGI("REWARD_ENTRY: 0x28c2238 (dxn) REACHED - erogation tail-call executes"); |
| 2660 | } |
| 2661 | |
| 2662 | static 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. |
| 2703 | static void grant_probe_298f448_logger(void) { |
| 2704 | LOGI("GRANT_CHAIN_298f448: ENTRY (bl da fallback GRANTBL) — primo grant eseguito"); |
| 2705 | } |
| 2706 | static void grant_probe_28a8f1c_logger(void) { |
| 2707 | LOGI("GRANT_CHAIN_28a8f1c: ENTRY (dw.dlz dispatcher bl @0x28a9410)"); |
| 2708 | } |
| 2709 | static 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. |
| 2719 | static 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 | |
| 2736 | static 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 | |
| 2766 | static 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) ==================== |
| 2775 | static 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 | } |
| 2783 | static 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 | } |
| 2786 | static 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 | |
| 2800 | static 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 |
| 2849 | static void install_post_trace_hook(uintptr_t target, void *wrapper, void **orig_out, const char *which); |
| 2850 | typedef void *(*dispatch_trace_fn)(void *x0, void *x1, void *x2, void *x3, unsigned int x4); |
| 2851 | static void *g_orig_dispatch = nullptr; |
| 2852 | |
| 2853 | static 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 | |
| 2911 | static 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. |
| 2924 | typedef void *(*post_trace_fn)(void *x0, void *x1, void *x2, void *x3, unsigned int x4); |
| 2925 | static void *g_orig_dli = nullptr; |
| 2926 | static void *g_orig_dze = nullptr; |
| 2927 | |
| 2928 | static 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. |
| 2955 | static void *g_cap_cb = nullptr; |
| 2956 | static void *g_cap_hdl = nullptr; |
| 2957 | |
| 2958 | static 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 | |
| 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 | |
| 2972 | } |
| 2973 | |
| 2974 | static 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) |
| 3003 | static 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 | |
| 3017 | static 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 | |
| 3044 | static 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. |
| 3059 | static void *g_orig_delegate_invoke = nullptr; |
| 3060 | |
| 3061 | static 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 | |
| 3075 | static 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. |
| 3088 | static void *g_orig_fc_dwu = nullptr; |
| 3089 | |
| 3090 | static 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 | |
| 3099 | typedef void *(*apply_grant_fn)(void *x0, void *x1, void *x2, void *x3, |
| 3100 | void *x4, void *x5, void *x6, void *x7); |
| 3101 | static void *g_orig_apply_grant = nullptr; |
| 3102 | |
| 3103 | static 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 | |
| 3114 | static 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). |
| 3137 | static 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. |
| 3148 | static int g_club_active = 0; |
| 3149 | static int g_club_tier = 0; |
| 3150 | static int g_club_permanent = 0; |
| 3151 | static int64_t g_club_expiry = 0; |
| 3152 | static int64_t g_club_next_claim = 0; |
| 3153 | static void *g_orig_esj = nullptr; |
| 3154 | |
| 3155 | typedef void *(*esj_full_fn)(void *x0, void *x1, void *x2, void *x3, void *x4); |
| 3156 | |
| 3157 | static 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 | |
| 3171 | static 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). |
| 3183 | static const char *CLUB_STATE_PATH = "/data/data/com.lavalabs.dungeonrush/files/drmod_club.dat"; |
| 3184 | static const char *CLUB_STATE_PATH_ALT= "/data/user/0/com.lavalabs.dungeonrush/files/drmod_club.dat"; |
| 3185 | |
| 3186 | static 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 | |
| 3198 | static 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 | |
| 3214 | static 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 | |
| 3260 | static 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. |
| 3277 | static void *g_orig_verify_iap = nullptr; |
| 3278 | static void *g_orig_dnv = nullptr; |
| 3279 | |
| 3280 | static 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 | |
| 3289 | static 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 | |
| 3299 | static 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 | |
| 3345 | static 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. |
| 3360 | static void grant_probe_fallback_logger(void) { |
| 3361 | LOGI("GRANT_CHAIN_fallback: ENTRY @0x28a92e8 (fallback GRANTBL, patched 4-byte bl)"); |
| 3362 | } |
| 3363 | |
| 3364 | static 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. |
| 3423 | typedef int (*proc00_fn)(void *self, void *args, void *component); |
| 3424 | static proc00_fn g_orig_proc00 = nullptr; |
| 3425 | static uintptr_t g_proc00_entry = 0; |
| 3426 | |
| 3427 | static 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. |
| 3440 | static 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 | |
| 3447 | static 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 | |
| 3464 | static 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. |
| 3502 | static 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 | |
| 3508 | static 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). |
| 3577 | static 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. |
| 3600 | typedef int (*proc01_fn)(void *x0, void *x1, void *x2, void *x3, void *x4, void *x5, void *x6); |
| 3601 | static proc01_fn g_orig_proc01 = nullptr; |
| 3602 | static uintptr_t g_proc01_entry = 0; |
| 3603 | |
| 3604 | static 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 | |
| 3610 | static 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) |
| 3648 | static 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 | |
| 3654 | static 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. |
| 3703 | static 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). |
| 3724 | static 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. |
| 3732 | static 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. |
| 3755 | static 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. |
| 3774 | static 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. |
| 3806 | static 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 | |
| 3818 | typedef void (*google_pu_fn)(void *self, void *billingResult, void *purchases); |
| 3819 | static google_pu_fn g_orig_google_pu = nullptr; |
| 3820 | static 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 | |
| 3830 | static jstring g_product_id = nullptr; |
| 3831 | |
| 3832 | static jclass g_cls_zzbm = nullptr; // deprecated, Play Billing <8.3.0, kept optional |
| 3833 | static jclass g_cls_bci = nullptr; |
| 3834 | static jclass g_cls_proxy = nullptr; |
| 3835 | static jclass g_cls_gpbl_proxy = nullptr; // com.unity3d.services.store.gpbl.proxies.PurchasesResponseListenerProxy |
| 3836 | static jclass g_cls_pu_proxy = nullptr; // com.unity3d.services.store.gpbl.proxies.PurchaseUpdatedListenerProxy |
| 3837 | static jclass g_cls_max = nullptr; |
| 3838 | static jclass g_cls_purchase = nullptr; |
| 3839 | static jclass g_cls_zzcj = nullptr; // deprecated zzdc, kept optional |
| 3840 | static jclass g_cls_billingresult = nullptr; // com/android/billingclient/api/BillingResult (v8.3.0+) |
| 3841 | static jclass g_cls_billingbuilder = nullptr; // BillingResult$Builder |
| 3842 | static jclass g_cls_productdetails = nullptr; // com/android/billingclient/api/ProductDetails |
| 3843 | static jclass g_cls_queryresult = nullptr; // com/android/billingclient/api/QueryProductDetailsResult |
| 3844 | static jclass g_cls_list = nullptr; |
| 3845 | static jclass g_cls_arraylist = nullptr; |
| 3846 | static jclass g_cls_intent = nullptr; |
| 3847 | static jclass g_cls_jsonobject = nullptr; |
| 3848 | static jclass g_cls_system = nullptr; |
| 3849 | static jclass g_cls_boolean = nullptr; // java/lang/Boolean (per boxed return) |
| 3850 | static jmethodID g_mid_purchase_init = nullptr; |
| 3851 | static jmethodID g_mid_list_add = nullptr; |
| 3852 | static jmethodID g_mid_arraylist_init = nullptr; |
| 3853 | static jmethodID g_mid_intent_putextra_int = nullptr; |
| 3854 | static jmethodID g_mid_intent_putsal = nullptr; |
| 3855 | static jmethodID g_mid_forward = nullptr; |
| 3856 | static jmethodID g_mid_jsonobj_init = nullptr; |
| 3857 | static jmethodID g_mid_jsonobj_put = nullptr; |
| 3858 | static jmethodID g_mid_system_time = nullptr; |
| 3859 | static jmethodID g_mid_boolean_valueof = nullptr; |
| 3860 | static jfieldID g_fid_zzcj_zzl = nullptr; // deprecated |
| 3861 | static jmethodID g_mid_billing_newbuilder = nullptr; |
| 3862 | static jmethodID g_mid_builder_setResponseCode = nullptr; |
| 3863 | static jmethodID g_mid_builder_setDebugMessage = nullptr; |
| 3864 | static jmethodID g_mid_builder_build = nullptr; |
| 3865 | |
| 3866 | static jobject g_backup_a = nullptr; // gpbl PurchasesResponseListenerProxy.onQueryPurchasesResponse |
| 3867 | static jobject g_backup_b = nullptr; // (unused, Play Billing 8.3.0 zzbm hooks removed) |
| 3868 | static jobject g_backup_c = nullptr; // (unused, Play Billing 8.3.0 zzaI hook removed) |
| 3869 | static jobject g_backup_d = nullptr; // BillingClientImpl.queryProductDetailsAsync |
| 3870 | static jobject g_backup_e = nullptr; // ProxyBillingActivity.onActivityResult |
| 3871 | static jobject g_backup_f = nullptr; // MaxUnityAdManager.isRewardedAdReady |
| 3872 | static jobject g_backup_g = nullptr; // gpbl PurchaseUpdatedListenerProxy.onPurchasesUpdated |
| 3873 | static jobject g_backup_h = nullptr; // BillingClientImpl.launchBillingFlow |
| 3874 | static jobject g_backup_i = nullptr; // BillingClientImpl.acknowledgePurchase |
| 3875 | static jobject g_backup_j = nullptr; // BillingClientImpl.consumeAsync |
| 3876 | |
| 3877 | static 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 | |
| 3892 | static 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 | |
| 3917 | static 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 | |
| 3939 | static 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 | |
| 3953 | static jclass g_mc_class = nullptr; |
| 3954 | static jmethodID g_mid_invoke = nullptr; |
| 3955 | |
| 3956 | static 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 | |
| 3964 | static 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 | |
| 3981 | static 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 | |
| 4028 | static 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 | |
| 4056 | extern "C" JNIEXPORT jobject JNICALL |
| 4057 | Java_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 | |
| 5000 | static 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 | |
| 5164 | static 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 | |
| 5180 | static 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 | |
| 5210 | static 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 | |
| 5227 | static 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 | |
| 5385 | class DRMod : public zygisk::ModuleBase { |
| 5386 | public: |
| 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 | |
| 5452 | private: |
| 5453 | zygisk::Api *api = nullptr; |
| 5454 | JNIEnv *env = nullptr; |
| 5455 | }; |
| 5456 | |
| 5457 | REGISTER_ZYGISK_MODULE(DRMod) |