diff --git a/magisk-modules/tng_exit_guard/jni/main.cpp b/magisk-modules/tng_exit_guard/jni/main.cpp index 8ad34ec..b770707 100644 --- a/magisk-modules/tng_exit_guard/jni/main.cpp +++ b/magisk-modules/tng_exit_guard/jni/main.cpp @@ -37,6 +37,8 @@ static constexpr const char *kTargetPkg = "my.com.tngdigital.ewallet"; static constexpr const char *kPromonSo = "libtngdigital_ewallet.so"; static bool g_enabled = false; +static zygisk::Api *g_api = nullptr; +static std::atomic g_cxx_plt{0}; static std::atomic g_seccomp_ok{0}; static std::atomic g_stack_chk{0}; static std::atomic g_promon_segv{0}; @@ -272,6 +274,13 @@ using kill_fn = int (*)(pid_t, int); using tgkill_fn = int (*)(int, int, int); using raise_fn = int (*)(int); using pthread_kill_fn = int (*)(pthread_t, int); +using cxa_guard_acquire_fn = int (*)(void *); +using cxa_guard_abort_fn = void (*)(); +using dlopen_fn = void *(*)(const char *, int); + +static cxa_guard_acquire_fn orig_cxa_guard_acquire = nullptr; +static cxa_guard_abort_fn orig_cxa_guard_abort = nullptr; +static dlopen_fn orig_dlopen = nullptr; static exit_fn orig_exit = nullptr; static exit_fn orig__exit = nullptr; @@ -326,7 +335,40 @@ static int hooked_pthread_kill(pthread_t thread, int sig) { return orig_pthread_kill ? orig_pthread_kill(thread, sig) : -1; } -static bool find_libc(dev_t *dev, ino_t *ino) { +/** Promon/libc++ 静态局部量递归初始化会 abort 主进程(Registration 页 HWUI 线程)。 */ +static int hooked_cxa_guard_acquire(void *guard) { + (void)guard; + return 1; +} +static void hooked_cxa_guard_abort() { + LOGI("blocked __cxa_guard_abort tid=%d", (int)gettid()); +} + +static void *hooked_dlopen(const char *name, int flags) { + void *handle = orig_dlopen ? orig_dlopen(name, flags) : nullptr; + if (handle != nullptr || name == nullptr) { + return handle; + } + if (strstr(name, "libandroid.so") != nullptr) { + static const char *kFallbacks[] = { + "/system/lib64/libandroid.so", + "/system/lib/libandroid.so", + "libandroid.so", + }; + for (const char *path : kFallbacks) { + handle = orig_dlopen ? orig_dlopen(path, flags) : nullptr; + if (handle != nullptr) { + LOGI("dlopen fallback %s -> %p (from %s)", path, handle, name); + return handle; + } + } + LOGI("dlopen libandroid.so failed tid=%d", (int)gettid()); + } + return handle; +} + +static bool find_lib_match(const char *suffix, const char *contains, + dev_t *dev, ino_t *ino) { FILE *fp = fopen("/proc/self/maps", "r"); if (!fp) return false; char line[1024]; @@ -339,11 +381,19 @@ static bool find_libc(dev_t *dev, ino_t *ino) { unsigned long inode = 0; char path[512] = {}; int n = sscanf(line, "%lx-%lx %7s %llx %31s %lu %511[^\n]", - &start, &end, perms, &offset, deststr, &inode, path); + &start, &end, perms, &offset, &deststr, &inode, path); if (n < 7 || inode == 0) continue; char *p = path; while (*p == ' ') ++p; - if (strstr(p, "libc.so") == nullptr) continue; + bool match = false; + if (suffix != nullptr) { + size_t plen = strlen(p); + size_t slen = strlen(suffix); + match = plen >= slen && strcmp(p + plen - slen, suffix) == 0; + } else if (contains != nullptr) { + match = strstr(p, contains) != nullptr; + } + if (!match) continue; unsigned maj = 0, min = 0; if (sscanf(deststr, "%x:%x", &maj, &min) != 2) continue; *dev = makedev(maj, min); @@ -355,25 +405,72 @@ static bool find_libc(dev_t *dev, ino_t *ino) { return ok; } +static bool find_lib_by_suffix(const char *suffix, dev_t *dev, ino_t *ino) { + return find_lib_match(suffix, nullptr, dev, ino); +} + +static bool find_lib_contains(const char *needle, dev_t *dev, ino_t *ino) { + return find_lib_match(nullptr, needle, dev, ino); +} + +static bool find_libc(dev_t *dev, ino_t *ino) { + return find_lib_by_suffix("libc.so", dev, ino); +} + +static void register_plt(zygisk::Api *api, dev_t dev, ino_t ino, + const char *sym, void *hook, void **orig) { + if (!api || dev == 0 || ino == 0) return; + api->pltHookRegister(dev, ino, sym, hook, orig); +} + static void install_plt(zygisk::Api *api) { if (!api) return; dev_t dev = 0; ino_t ino = 0; - if (!find_libc(&dev, &ino)) return; - api->pltHookRegister(dev, ino, "exit", (void *)hooked_exit, (void **)&orig_exit); - api->pltHookRegister(dev, ino, "_exit", (void *)hooked__exit, (void **)&orig__exit); - api->pltHookRegister(dev, ino, "abort", (void *)hooked_abort, (void **)&orig_abort); - api->pltHookRegister(dev, ino, "__stack_chk_fail", - (void *)hooked_stack_chk_fail, (void **)&orig_stack_chk_fail); - api->pltHookRegister(dev, ino, "raise", (void *)hooked_raise, (void **)&orig_raise); - api->pltHookRegister(dev, ino, "kill", (void *)hooked_kill, (void **)&orig_kill); - api->pltHookRegister(dev, ino, "tgkill", (void *)hooked_tgkill, (void **)&orig_tgkill); - api->pltHookRegister(dev, ino, "pthread_kill", - (void *)hooked_pthread_kill, (void **)&orig_pthread_kill); + if (find_libc(&dev, &ino)) { + register_plt(api, dev, ino, "exit", (void *)hooked_exit, (void **)&orig_exit); + register_plt(api, dev, ino, "_exit", (void *)hooked__exit, (void **)&orig__exit); + register_plt(api, dev, ino, "abort", (void *)hooked_abort, (void **)&orig_abort); + register_plt(api, dev, ino, "__stack_chk_fail", + (void *)hooked_stack_chk_fail, (void **)&orig_stack_chk_fail); + register_plt(api, dev, ino, "raise", (void *)hooked_raise, (void **)&orig_raise); + register_plt(api, dev, ino, "kill", (void *)hooked_kill, (void **)&orig_kill); + register_plt(api, dev, ino, "tgkill", (void *)hooked_tgkill, (void **)&orig_tgkill); + register_plt(api, dev, ino, "pthread_kill", + (void *)hooked_pthread_kill, (void **)&orig_pthread_kill); + register_plt(api, dev, ino, "dlopen", (void *)hooked_dlopen, (void **)&orig_dlopen); + } bool ok = api->pltHookCommit(); LOGI("PLT commit=%d", ok ? 1 : 0); } +static void try_install_cxx_guard_plt() { + if (g_cxx_plt.load() || !g_api) return; + dev_t dev = 0; + ino_t ino = 0; + bool any = false; + if (find_lib_by_suffix("libc++_shared.so", &dev, &ino) + || find_lib_contains("libc++", &dev, &ino)) { + register_plt(g_api, dev, ino, "__cxa_guard_acquire", + (void *)hooked_cxa_guard_acquire, (void **)&orig_cxa_guard_acquire); + register_plt(g_api, dev, ino, "__cxa_guard_abort", + (void *)hooked_cxa_guard_abort, (void **)&orig_cxa_guard_abort); + any = true; + } + if (find_lib_by_suffix("libtngdigital_ewallet.so", &dev, &ino)) { + register_plt(g_api, dev, ino, "__cxa_guard_acquire", + (void *)hooked_cxa_guard_acquire, (void **)&orig_cxa_guard_acquire); + register_plt(g_api, dev, ino, "__cxa_guard_abort", + (void *)hooked_cxa_guard_abort, (void **)&orig_cxa_guard_abort); + any = true; + } + if (!any) return; + if (g_api->pltHookCommit()) { + g_cxx_plt.store(1); + LOGI("PLT cxx guards committed"); + } +} + static void *phase_thread(void *) { install_promon_segv_handler(); usleep(400 * 1000); @@ -382,6 +479,8 @@ static void *phase_thread(void *) { for (int i = 0; i < 40; i++) { usleep(1000 * 1000); install_soft_signals(); + install_promon_segv_handler(); + try_install_cxx_guard_plt(); if (i % 5 == 0) refresh_promon_so_range(); } LOGI("phase done seccomp=%d stack_chk=%d promon_segv=%d", @@ -390,8 +489,9 @@ static void *phase_thread(void *) { } static void install_all(zygisk::Api *api) { + g_api = api; g_main_tid.store(gettid()); - LOGI("install pid=%d main_tid=%d (PLT+ABRT-pc+4+SEGV-all-skip+exit_group@400ms)", + LOGI("install pid=%d main_tid=%d (PLT+cxx-guard+ABRT-pc+4+SEGV-skip+exit_group@400ms)", getpid(), (int)g_main_tid.load()); install_fatal_skip_handlers(); install_soft_signals(); diff --git a/magisk-modules/tng_exit_guard/zygisk/arm64-v8a.so b/magisk-modules/tng_exit_guard/zygisk/arm64-v8a.so index af139d9..516f4e1 100644 Binary files a/magisk-modules/tng_exit_guard/zygisk/arm64-v8a.so and b/magisk-modules/tng_exit_guard/zygisk/arm64-v8a.so differ diff --git a/reverse/scripts/_scan_tng_vhv_u.py b/reverse/scripts/_scan_tng_vhv_u.py new file mode 100644 index 0000000..810ad56 --- /dev/null +++ b/reverse/scripts/_scan_tng_vhv_u.py @@ -0,0 +1,15 @@ +import re +import zipfile + +APK = r"reverse/dumps/tng_1.9.10_base.apk" +with zipfile.ZipFile(APK) as z: + data = b"".join(z.read(n) for n in z.namelist() if n.endswith(".dex")) + +for cls in ["Lvhvlnqgy/w;", "Lvhvlnqgy/u;"]: + print("===", cls, "refs ===") + pat = cls.encode() + rb"[^\x00]{0,120}" + hits = sorted(set(re.findall(pat, data))) + for h in hits[:40]: + print(h.decode("ascii", "ignore")) + print("count", len(hits)) + print() diff --git a/reverse/scripts/parse_reg_crash_log.py b/reverse/scripts/parse_reg_crash_log.py new file mode 100644 index 0000000..93b59f5 --- /dev/null +++ b/reverse/scripts/parse_reg_crash_log.py @@ -0,0 +1,14 @@ +import sys + +path = sys.argv[1] +lines = open(path, encoding="utf-8", errors="ignore").read().splitlines() +keys = ( + "Runtime aborting", "Aborting", "stack corruption", "blocked __stack", + "vhvlnqgy.u", "UserRegistration", "F libc", "has died", +) +for i, line in enumerate(lines): + if any(k in line for k in keys): + start = max(0, i - 2) + end = min(len(lines), i + 6) + print("---") + print("\n".join(lines[start:end])) diff --git a/reverse/scripts/tap_country_code.py b/reverse/scripts/tap_country_code.py new file mode 100644 index 0000000..dad749c --- /dev/null +++ b/reverse/scripts/tap_country_code.py @@ -0,0 +1,89 @@ +import re +import subprocess +import sys +import time +import xml.etree.ElementTree as ET + +ADB = ["adb"] + + +def adb(*args): + subprocess.run(ADB + list(args), check=False) + + +def main(): + adb("shell", "am", "force-stop", "my.com.tngdigital.ewallet") + time.sleep(2) + adb("shell", "am", "start", "-n", "my.com.tngdigital.ewallet/.ui.SplashActivity") + time.sleep(16) + adb("shell", "uiautomator", "dump", "/sdcard/ui_cc.xml") + xml = subprocess.check_output( + ADB + ["shell", "cat", "/sdcard/ui_cc.xml"], text=True, errors="ignore") + root = ET.fromstring(xml) + target = None + register_btn = None + for node in root.iter("node"): + rid = node.get("resource-id") or "" + text = node.get("text") or "" + if "tv_left" in rid or (text.strip().startswith("+") and len(text.strip()) < 8): + target = node + if "注册" in text and node.get("clickable") == "true": + register_btn = node + if target is None and register_btn is not None: + bounds = register_btn.get("bounds", "") + m = re.match(r"\[(\d+),(\d+)\]\[(\d+),(\d+)\]", bounds) + if m: + x1, y1, x2, y2 = map(int, m.groups()) + x, y = (x1 + x2) // 2, (y1 + y2) // 2 + print(f"tap register at {x},{y}") + adb("shell", "input", "tap", str(x), str(y)) + time.sleep(4) + adb("shell", "uiautomator", "dump", "/sdcard/ui_cc.xml") + xml = subprocess.check_output( + ADB + ["shell", "cat", "/sdcard/ui_cc.xml"], text=True, errors="ignore") + root = ET.fromstring(xml) + for node in root.iter("node"): + rid = node.get("resource-id") or "" + text = node.get("text") or "" + if "tv_left" in rid or (text.strip().startswith("+") and len(text.strip()) < 8): + target = node + break + if target is None: + for node in root.iter("node"): + text = node.get("text") or "" + if "注册" in text or "Register" in text.lower(): + print("login screen text:", text[:40]) + print("ERROR: country code control not found") + return 1 + bounds = target.get("bounds", "") + m = re.match(r"\[(\d+),(\d+)\]\[(\d+),(\d+)\]", bounds) + if not m: + print("bad bounds", bounds) + return 1 + x1, y1, x2, y2 = map(int, m.groups()) + x, y = (x1 + x2) // 2, (y1 + y2) // 2 + print(f"tap country {target.get('text','')} at {x},{y}") + adb("shell", "input", "tap", str(x), str(y)) + time.sleep(3) + adb("shell", "uiautomator", "dump", "/sdcard/ui_cc2.xml") + xml2 = subprocess.check_output( + ADB + ["shell", "cat", "/sdcard/ui_cc2.xml"], text=True, errors="ignore") + root2 = ET.fromstring(xml2) + countries = [] + for node in root2.iter("node"): + text = (node.get("text") or "").strip() + if "+61" in text or "+86" in text or "Australia" in text or "Malaysia" in text: + countries.append(text) + pid = subprocess.check_output( + ADB + ["shell", "pidof", "my.com.tngdigital.ewallet"], text=True).strip() + print("pid:", pid or "DEAD") + print("countries visible:", countries[:8]) + if countries: + print("OK region picker visible") + return 0 + print("FAIL region picker empty/black") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/hook/TngRootBypassHook.java b/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/hook/TngRootBypassHook.java index 652d5bd..de700c0 100644 --- a/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/hook/TngRootBypassHook.java +++ b/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/hook/TngRootBypassHook.java @@ -227,10 +227,9 @@ public final class TngRootBypassHook { hookRegistrationFlowGuard(lpparam); hookSplashForceLogin(lpparam); hookLoginDismissSplash(lpparam); - hookDialogNoHwAccel(); - hookActivityLifecycleDiag(lpparam); hookHardwareRendererSetName(); - hookBottomSelectDialogSafe(lpparam); + hookBottomSelectDialogDiag(lpparam); + hookActivityLifecycleDiag(lpparam); hookPromonApService(lpparam); hookPromonBroadcastReceiver(lpparam); hookAppAttachForceLogin(lpparam); @@ -528,9 +527,30 @@ public final class TngRootBypassHook { } } + /** + * 区号选择弹窗诊断。勿关 HW 加速——Android 16 上会导致列表黑屏。 + * ANR 由 HardwareRenderer.setName 拦截兜底。 + */ + private static void hookBottomSelectDialogDiag(XC_LoadPackage.LoadPackageParam lpparam) { + try { + Class clazz = XposedHelpers.findClass( + "my.com.tngdigital.common.widget.BottomSelectDialogFragment", + lpparam.classLoader); + XposedHelpers.findAndHookMethod(clazz, "onStart", new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + XposedBridge.log(TAG + " BottomSelectDialogFragment.onStart"); + } + }); + XposedBridge.log(TAG + " hooked BottomSelectDialogFragment.onStart (diag only)"); + } catch (Throwable t) { + XposedBridge.log(TAG + " BottomSelectDialog hook failed: " + t.getMessage()); + } + } + /** * ANR 栈:Dialog.show → enableHardwareAcceleration → HardwareRenderer.setName → future.get 卡死。 - * 兜底拦截 setName,避免 RenderThread 未就绪时主线程永久阻塞。 + * 仅拦截 setName;勿全局关 Dialog HW(区号选择会黑屏)。 */ private static void hookHardwareRendererSetName() { try { @@ -548,63 +568,6 @@ public final class TngRootBypassHook { } } - /** Dialog.show 走 HW 加速会等 RenderThread;复进时 RenderThread 易 abort → 主线程 ANR。 */ - private static void hookDialogNoHwAccel() { - try { - XposedHelpers.findAndHookMethod(Dialog.class, "show", new XC_MethodHook() { - @Override - protected void beforeHookedMethod(MethodHookParam param) { - try { - Dialog dialog = (Dialog) param.thisObject; - android.view.Window window = dialog.getWindow(); - if (window != null) { - window.setFlags( - 0, - android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED); - } - } catch (Throwable ignored) { - } - } - }); - XposedBridge.log(TAG + " hooked Dialog.show (no HW accel)"); - } catch (Throwable t) { - XposedBridge.log(TAG + " Dialog.show hook failed: " + t.getMessage()); - } - } - - /** - * 注册/登录页点「下一步」会弹国家区号 BottomSelectDialogFragment。 - * HardwareRenderer.setName 已拦截 ANR;此处仅关 Dialog HW 加速,保留区号选择。 - */ - private static void hookBottomSelectDialogSafe(XC_LoadPackage.LoadPackageParam lpparam) { - try { - Class clazz = XposedHelpers.findClass( - "my.com.tngdigital.common.widget.BottomSelectDialogFragment", - lpparam.classLoader); - XposedHelpers.findAndHookMethod(clazz, "onStart", new XC_MethodHook() { - @Override - protected void beforeHookedMethod(MethodHookParam param) { - try { - Object dialog = XposedHelpers.callMethod(param.thisObject, "getDialog"); - if (dialog instanceof Dialog) { - Dialog d = (Dialog) dialog; - if (d.getWindow() != null) { - d.getWindow().setFlags( - 0, - android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED); - } - } - } catch (Throwable ignored) { - } - XposedBridge.log(TAG + " BottomSelectDialogFragment.onStart (safe, no HW)"); - } - }); - XposedBridge.log(TAG + " hooked BottomSelectDialogFragment.onStart (safe)"); - } catch (Throwable t) { - XposedBridge.log(TAG + " BottomSelectDialog hook failed: " + t.getMessage()); - } - } - /** Promon 隔离 Service;打点确认 :goacqowmmt 进程 hook 已注入。 */ private static void hookPromonApService(XC_LoadPackage.LoadPackageParam lpparam) { Class svc = findPromonClass(lpparam.classLoader, "ap"); @@ -1201,19 +1164,27 @@ public final class TngRootBypassHook { } /** - * Promon lifecycle:只吞异常,不 short-circuit——全拦会拖死 Splash→Login。 + * Promon lifecycle:1.9.10 用 vhvlnqgy.u,旧版用 w。只吞异常,不全拦。 */ private static void hookPromonLifecycle(XC_LoadPackage.LoadPackageParam lpparam) { - Class lifecycleClass = findPromonClass(lpparam.classLoader, "w"); - if (lifecycleClass == null) { - XposedBridge.log(TAG + " Promon w lifecycle not found"); - return; - } Class promonExc = findPromonClass(lpparam.classLoader, "W"); if (promonExc == null) { promonExc = findPromonClass(lpparam.classLoader, "bd"); } final Class promonExcFinal = promonExc; + for (String simple : new String[]{"w", "u"}) { + hookPromonLifecycleClass(lpparam, simple, promonExcFinal); + } + } + + private static void hookPromonLifecycleClass( + XC_LoadPackage.LoadPackageParam lpparam, + String simpleName, + Class promonExc) { + Class lifecycleClass = findPromonClass(lpparam.classLoader, simpleName); + if (lifecycleClass == null) { + return; + } final String lifecycleName = lifecycleClass.getName(); try { int hooked = 0; @@ -1229,7 +1200,7 @@ public final class TngRootBypassHook { return; } Throwable t = param.getThrowable(); - if (isPromonThrowable(t, promonExcFinal)) { + if (isPromonThrowable(t, promonExc)) { XposedBridge.log(TAG + " swallowed " + t.getClass().getSimpleName() + " in " + lifecycleName + "#" + method.getName()); param.setThrowable(null); @@ -1243,7 +1214,8 @@ public final class TngRootBypassHook { + " " + lifecycleName + " lifecycle method(s) (afterHook only)"); } } catch (Throwable t) { - XposedBridge.log(TAG + " Promon w lifecycle hook failed: " + t.getMessage()); + XposedBridge.log(TAG + " " + lifecycleName + " lifecycle hook failed: " + + t.getMessage()); } }