From 13e407623b31f3b2a84ece9d46c322391ae0ccb8 Mon Sep 17 00:00:00 2001 From: mars <3361409208@qq.com> Date: Fri, 31 Jul 2026 17:55:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(tng):=20TNG=20eWallet=20Promon=20bypass?= =?UTF-8?q?=E3=80=81Zygisk=20=E4=BF=A1=E5=8F=B7=E5=AE=88=E5=8D=AB=E4=B8=8E?= =?UTF-8?q?=E6=B3=A8=E5=86=8C=E9=93=BE=E4=BF=9D=E6=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 TngRootBypassHook 与 tng_exit_guard 模块,修复 BAL 强拉、SIGABRT/pc==lr SEGV 崩溃,验证可进入注册页。 --- docs/TNG_自杀链逆向笔记.md | 85 + magisk-modules/tng_exit_guard/jni/Android.mk | 11 + .../tng_exit_guard/jni/Application.mk | 4 + magisk-modules/tng_exit_guard/jni/main.cpp | 390 +++ magisk-modules/tng_exit_guard/jni/zygisk.hpp | 391 +++ magisk-modules/tng_exit_guard/module.prop | 6 + .../tng_exit_guard/zygisk/arm64-v8a.so | Bin 0 -> 186384 bytes scripts/build-install-tng-exit-guard.ps1 | 55 + scripts/diag-tng-exit-guard.sh | 21 + scripts/logcat-tng.ps1 | 14 + scripts/pull-tng-log.sh | 4 + scripts/test-tng-survive.sh | 16 + .../smsmessage/xposed/MainHook.java | 6 + .../xposed/hook/ProcMapsFilterHook.java | 370 +++ .../xposed/hook/RootBypassHelper.java | 35 +- .../xposed/hook/TngRootBypassHook.java | 2569 +++++++++++++++++ 16 files changed, 3960 insertions(+), 17 deletions(-) create mode 100644 docs/TNG_自杀链逆向笔记.md create mode 100644 magisk-modules/tng_exit_guard/jni/Android.mk create mode 100644 magisk-modules/tng_exit_guard/jni/Application.mk create mode 100644 magisk-modules/tng_exit_guard/jni/main.cpp create mode 100644 magisk-modules/tng_exit_guard/jni/zygisk.hpp create mode 100644 magisk-modules/tng_exit_guard/module.prop create mode 100644 magisk-modules/tng_exit_guard/zygisk/arm64-v8a.so create mode 100644 scripts/build-install-tng-exit-guard.ps1 create mode 100644 scripts/diag-tng-exit-guard.sh create mode 100644 scripts/logcat-tng.ps1 create mode 100644 scripts/pull-tng-log.sh create mode 100644 scripts/test-tng-survive.sh create mode 100644 xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/hook/ProcMapsFilterHook.java create mode 100644 xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/hook/TngRootBypassHook.java diff --git a/docs/TNG_自杀链逆向笔记.md b/docs/TNG_自杀链逆向笔记.md new file mode 100644 index 0000000..12a1c2a --- /dev/null +++ b/docs/TNG_自杀链逆向笔记.md @@ -0,0 +1,85 @@ +# TNG eWallet 逆向笔记 — 自杀链(2026-07-31) + +## 运行时铁证 + +| 现象 | 含义 | +|------|------| +| `Fatal signal 6 (SIGABRT), code 0 (SI_USER)` | 用户态 `kill(pid, SIGABRT)`,**不是** `exit_group`,也不是 `abort()` 的 `SI_TKILL` | +| PLT hook `kill/raise` **从不打日志** | Promon 走 **内联 SVC**,不经 PLT | +| `E Report : Exiting:` + `xwwqazamx.W: 16` + `bl.a` | Java 层检测入口仍是 `xwwqazamx.bl#a/#b` | +| `Displayed ...UserLoginActivity` | UI 能到登录页;随后数秒内被杀 | +| `Lcom/aliyun/TigerTally/t/B` 反射 `Thread.dispatchUncaughtException` | 另有一套阿里 TigerTally SDK | + +## 自杀阶梯(当前理解) + +``` +Promon native 检测 (Thread-N 扫 /proc、selinux、wifi prop…) + → xwwqazamx.bl#a / #b + → 抛 W:16(Report 打 Exiting) + → 同时/随后 inline kill(SIGABRT, SI_USER) + → KillApplicationHandler(Xposed 可拦) + → 若 ABRT 被 ignore:可能改 SIGKILL / BRK(待证) +``` + +并行: + +- `AppSecurityManager.handle*Callback`(Xposed 已拦) +- `openSecurityUrl` Root FAQ(已拦) +- `UnhandledEvent` → `finishAllActivityAndKillApp`(已 hook) +- ForceExitCountdown(已 hook) +- TigerTally / SecurityGuard `JNICLibrary.doCommand`(当前 stub,可能过度) + +## 静态 SO + +`libtngdigital_ewallet.so`(~7.7MB,打包态): + +- `brk` 假阳性/加密指令很多(imm 离散) +- 真实 `svc #0` 极少;运行时解密后才出现 exit 序列 +- **禁止**在 `JNI_OnLoad` 前/中 patch SO(会 Bad JNI / SIGILL) + +## 已验证无效/有害手段 + +- seccomp 拦 `exit_group` → Promon 改 UDF/SIGILL +- 改 libc exit SVC→RET → 同上 +- 改 Promon SO 字节 → Bad JNI / SIGILL +- 高频 `sigaction` 重装 → 易被当成 hook 指纹 + +## 较有效手段 + +- Xposed:拦 FAQ / KillApplicationHandler / handle*Callback / ForceExit / UnhandledEvent +- Xposed:`bl#a/#b` **beforeHook short-circuit**(避免跑进 native 杀进程) +- Zygisk:PLT 观察 + `SIGABRT` ignore(对 SI_USER 理论有效;Promon 可能复位 handler) + +## 新发现(2026-07-31 续) + +### `libtiger_tally.so`(~4.6MB) +- 与 Promon `libtngdigital_ewallet.so` **并行加载** +- 动态依赖:`abort@LIBC`、`signal@LIBC`(**走 PLT**,可被 Zygisk PLT hook) +- 内嵌少量 syscall stub(openat/read/faccessat…),**无** kill/exit stub +- `BIND_NOW` + +### 打包态 SO +- `libtngdigital_ewallet.so` / `libtiger_tally.so` 静态 **0** 处 `movz x8,#129 + svc` +- 自杀用的 inline `kill` 仍可能在 **运行时解密** 后出现 → Zygisk **延迟 2s** 扫描 patch + +### 日志结论(2026-07-31 11:37–11:47) + +| 现象 | 含义 | +|------|------| +| `exited cleanly (1)` | inline `exit_group(1)`(PLT 拦不住) | +| kill SVC→RET 后 `signal 9` | SO 完整性/备用路径 → **SIGKILL** | +| 过早 patch exit_group | 启动即崩(CEM) | +| seccomp 拦 `exit`+`exit_group` | ~1s 死(误伤线程 exit) | +| seccomp **只拦 exit_group** | 存活 **~16s**,随后 **SIGILL 风暴**(count 7000+) | +| `caught sig=4 si_code=1` | Promon UDF 兜底;+4 skip 无效 | + +### Rest 配置(2026-07-31 12:55,已装机) + +| 层 | 策略 | +|----|------| +| Zygisk | PLT 拦 exit/abort/kill;**exit_group-only seccomp @2s**;**禁止**改 SO(会 SIGKILL) | +| 信号 | ABRT ignore;SEGV/ILL **+4 skip**;**不** freeze 线程(freeze 会 ANR) | +| Xposed | Splash `reportFullyDrawn` + **2.5s 强拉 UserLogin** + finish Splash;bl SC / 自杀 Java / JNIC stub | + +实测:进程 **≥40s** 存活,焦点 `UserLoginActivity`;`phase done seccomp=1 caught=0`。 +注意:点亮屏幕后再开 App(`svc power stayon true`);勿把 TNG 放进 Magisk DenyList。 diff --git a/magisk-modules/tng_exit_guard/jni/Android.mk b/magisk-modules/tng_exit_guard/jni/Android.mk new file mode 100644 index 0000000..c672e30 --- /dev/null +++ b/magisk-modules/tng_exit_guard/jni/Android.mk @@ -0,0 +1,11 @@ +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) +LOCAL_MODULE := tng_exit_guard +LOCAL_SRC_FILES := main.cpp +LOCAL_C_INCLUDES := $(LOCAL_PATH) +LOCAL_LDLIBS := -llog -ldl +LOCAL_CFLAGS := -Wall -Wextra -fno-rtti -fvisibility=hidden +LOCAL_CPPFLAGS := -std=c++17 +LOCAL_LDFLAGS := -Wl,--exclude-libs,ALL +include $(BUILD_SHARED_LIBRARY) diff --git a/magisk-modules/tng_exit_guard/jni/Application.mk b/magisk-modules/tng_exit_guard/jni/Application.mk new file mode 100644 index 0000000..06dd49c --- /dev/null +++ b/magisk-modules/tng_exit_guard/jni/Application.mk @@ -0,0 +1,4 @@ +APP_ABI := arm64-v8a +APP_PLATFORM := android-24 +APP_STL := c++_static +APP_CPPFLAGS := -std=c++17 diff --git a/magisk-modules/tng_exit_guard/jni/main.cpp b/magisk-modules/tng_exit_guard/jni/main.cpp new file mode 100644 index 0000000..3b6c09c --- /dev/null +++ b/magisk-modules/tng_exit_guard/jni/main.cpp @@ -0,0 +1,390 @@ +/* + * TNG eWallet — Zygisk companion. + * + * stable: PLT + ABRT/SIGTRAP swallow + exit_group seccomp@400ms. + * Promon worker SIGSEGV (libtngdigital_ewallet.so null deref): LR-return skip (cap N). + * pc==lr 循环 SEGV 也 skip;libc++abi __cxa_guard_acquire → SIGABRT 吞掉。 + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "zygisk.hpp" + +#define SC_RET_ALLOW 0x7fff0000U +#define SC_RET_ERRNO_EPERM (0x00050000U | 1U) + +#define LOG_TAG "TngExitGuard" +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) + +static constexpr const char *kTargetPkg = "my.com.tngdigital.ewallet"; +static constexpr const char *kPromonSo = "libtngdigital_ewallet.so"; +static bool g_enabled = false; +static std::atomic g_seccomp_ok{0}; +static std::atomic g_stack_chk{0}; +static std::atomic g_promon_segv{0}; +static std::atomic g_promon_start{0}; +static std::atomic g_promon_end{0}; +/* 隔离进程 :goacqowmmt 会循环 SEGV;cap=16 打满后 freeze 反拖累主进程 */ +static constexpr int kMaxPromonSegvSkip = 0; /* 0 = unlimited LR-return skip */ +static std::atomic g_soft_sig_logged{0}; + +static void freeze_forever() { + for (;;) pause(); +} + +static void refresh_promon_so_range() { + FILE *fp = fopen("/proc/self/maps", "r"); + if (!fp) return; + char line[1024]; + uintptr_t start = 0, end = 0; + while (fgets(line, sizeof(line), fp)) { + unsigned long s = 0, e = 0; + char path[512] = {}; + int n = sscanf(line, "%lx-%lx %*s %*s %*s %*s %511[^\n]", &s, &e, path); + if (n < 3) continue; + char *p = path; + while (*p == ' ') ++p; + if (strstr(p, kPromonSo) == nullptr) continue; + if (start == 0 || s < start) start = s; + if (e > end) end = e; + } + fclose(fp); + if (start != 0 && end > start) { + g_promon_start.store(start); + g_promon_end.store(end); + } +} + +static bool pc_in_promon_so(uintptr_t pc) { + uintptr_t start = g_promon_start.load(); + uintptr_t end = g_promon_end.load(); + return start != 0 && pc >= start && pc < end; +} + +static void promon_segv_handler(int sig, siginfo_t *info, void *ctx) { + (void)sig; + (void)info; + ucontext_t *uc = reinterpret_cast(ctx); +#if defined(__aarch64__) + uintptr_t pc = uc->uc_mcontext.pc; + uintptr_t lr = uc->uc_mcontext.regs[30]; +#else + uintptr_t pc = 0; + uintptr_t lr = 0; +#endif + refresh_promon_so_range(); + /* Promon 典型 pc==lr 自旋 null deref;maps 尚未刷新时也按此 skip */ + if (pc != 0 && pc == lr) { + int n = ++g_promon_segv; + if (n <= 3 || n % 50 == 0) { + LOGI("promon pc==lr SIGSEGV tid=%d pc=%lx n=%d — skip to pc+4", + (int)gettid(), (unsigned long)pc, n); + } + uc->uc_mcontext.pc = pc + 4; + return; + } + if (pc_in_promon_so(pc) || pc_in_promon_so(lr)) { + int n = ++g_promon_segv; + uintptr_t target = lr; + if (target == 0 || target == pc) { + target = pc + 4; + } + if (kMaxPromonSegvSkip <= 0 || n <= kMaxPromonSegvSkip) { + if (n <= 3 || n % 50 == 0) { + LOGI("promon SIGSEGV tid=%d pc=%lx lr=%lx n=%d — skip to %lx", + (int)gettid(), (unsigned long)pc, (unsigned long)lr, n, + (unsigned long)target); + } + uc->uc_mcontext.pc = target; + return; + } + LOGI("promon SIGSEGV tid=%d n=%d — cap hit, freeze", (int)gettid(), n); + freeze_forever(); + } + signal(SIGSEGV, SIG_DFL); + raise(SIGSEGV); +} + +/** libc++abi __cxa_guard_acquire 递归初始化 → abort;跳回 LR 继续而非杀进程。 */ +static void fatal_skip_handler(int sig, siginfo_t *info, void *ctx) { + (void)info; + ucontext_t *uc = reinterpret_cast(ctx); +#if defined(__aarch64__) + uintptr_t pc = uc->uc_mcontext.pc; + uintptr_t lr = uc->uc_mcontext.regs[30]; + LOGI("swallowed signal %d tid=%d pc=%lx lr=%lx", sig, (int)gettid(), + (unsigned long)pc, (unsigned long)lr); + if (lr != 0) { + uc->uc_mcontext.pc = lr; + return; + } + if (pc != 0) { + uc->uc_mcontext.pc = pc + 4; + return; + } +#endif + freeze_forever(); +} + +static void install_fatal_skip_handlers() { + struct sigaction sa {}; + sa.sa_sigaction = fatal_skip_handler; + sa.sa_flags = SA_SIGINFO | SA_ONSTACK; + sigemptyset(&sa.sa_mask); + sigaction(SIGABRT, &sa, nullptr); + sigaction(SIGTRAP, &sa, nullptr); + LOGI("fatal skip handlers (ABRT+TRAP→LR)"); +} + +static void install_promon_segv_handler() { + refresh_promon_so_range(); + struct sigaction sa {}; + sa.sa_sigaction = promon_segv_handler; + sa.sa_flags = SA_SIGINFO | SA_ONSTACK; + sigemptyset(&sa.sa_mask); + if (sigaction(SIGSEGV, &sa, nullptr) != 0) { + LOGE("SIGSEGV handler install failed errno=%d", errno); + return; + } + LOGI("promon SIGSEGV handler armed range=%lx-%lx", + (unsigned long)g_promon_start.load(), + (unsigned long)g_promon_end.load()); +} + +static void install_soft_signals() { + struct sigaction sa {}; + sa.sa_sigaction = fatal_skip_handler; + sa.sa_flags = SA_SIGINFO | SA_ONSTACK; + sigemptyset(&sa.sa_mask); + sigaction(SIGABRT, &sa, nullptr); + sigaction(SIGTRAP, &sa, nullptr); + if (g_soft_sig_logged.fetch_add(1) == 0) { + LOGI("soft signals (ABRT+TRAP skip→LR)"); + } +} + +#if defined(__aarch64__) +#define _BPFI(code, jt, jf, k) \ + ((struct sock_filter){(unsigned short)(code), (jt), (jf), (unsigned int)(k)}) + +static int install_seccomp_exit_group_only() { + struct sock_filter filter[] = { + _BPFI(BPF_LD | BPF_W | BPF_ABS, 0, 0, offsetof(struct seccomp_data, arch)), + _BPFI(BPF_JMP | BPF_JEQ | BPF_K, 1, 0, AUDIT_ARCH_AARCH64), + _BPFI(BPF_RET | BPF_K, 0, 0, SC_RET_ALLOW), + _BPFI(BPF_LD | BPF_W | BPF_ABS, 0, 0, offsetof(struct seccomp_data, nr)), + _BPFI(BPF_JMP | BPF_JEQ | BPF_K, 0, 1, 94), + _BPFI(BPF_RET | BPF_K, 0, 0, SC_RET_ERRNO_EPERM), + _BPFI(BPF_RET | BPF_K, 0, 0, SC_RET_ALLOW), + }; + struct sock_fprog prog = { + .len = (unsigned short)(sizeof(filter) / sizeof(filter[0])), + .filter = filter, + }; + prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); + long rc = syscall(__NR_seccomp, SECCOMP_SET_MODE_FILTER, + SECCOMP_FILTER_FLAG_TSYNC, &prog); + if (rc != 0) { + rc = prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog); + if (rc != 0) { + LOGE("seccomp failed errno=%d", errno); + return -1; + } + LOGI("seccomp exit_group via prctl"); + } else { + LOGI("seccomp exit_group via TSYNC"); + } + g_seccomp_ok.store(1); + return 0; +} +#else +static int install_seccomp_exit_group_only() { return -1; } +#endif + +using exit_fn = void (*)(int); +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); + +static exit_fn orig_exit = nullptr; +static exit_fn orig__exit = nullptr; +static void (*orig_abort)() = nullptr; +static void (*orig_stack_chk_fail)() = nullptr; +static kill_fn orig_kill = nullptr; +static tgkill_fn orig_tgkill = nullptr; +static raise_fn orig_raise = nullptr; +static pthread_kill_fn orig_pthread_kill = nullptr; + +static bool deadly(int sig) { + return sig == SIGKILL || sig == SIGABRT || sig == SIGTERM || + sig == SIGTRAP || sig == SIGILL || + sig == 9 || sig == 6 || sig == 5 || sig == 4 || sig == 15; +} + +static void hooked_exit(int code) { LOGI("blocked exit(%d)", code); } +static void hooked__exit(int code) { LOGI("blocked _exit(%d)", code); } +static void hooked_abort() { + LOGI("blocked abort() tid=%d", (int)gettid()); +} +static void hooked_stack_chk_fail() { + int n = ++g_stack_chk; + LOGI("blocked __stack_chk_fail tid=%d n=%d", (int)gettid(), n); +} +static int hooked_raise(int sig) { + if (deadly(sig)) { + LOGI("blocked raise(%d)", sig); + return 0; + } + return orig_raise ? orig_raise(sig) : -1; +} +static int hooked_kill(pid_t pid, int sig) { + if (deadly(sig)) { + LOGI("blocked kill(%d,%d)", (int)pid, sig); + return 0; + } + return orig_kill ? orig_kill(pid, sig) : -1; +} +static int hooked_tgkill(int tgid, int tid, int sig) { + if (deadly(sig)) { + LOGI("blocked tgkill(%d,%d,%d)", tgid, tid, sig); + return 0; + } + return orig_tgkill ? orig_tgkill(tgid, tid, sig) : -1; +} +static int hooked_pthread_kill(pthread_t thread, int sig) { + if (deadly(sig)) { + LOGI("blocked pthread_kill(sig=%d)", sig); + return 0; + } + return orig_pthread_kill ? orig_pthread_kill(thread, sig) : -1; +} + +static bool find_libc(dev_t *dev, ino_t *ino) { + FILE *fp = fopen("/proc/self/maps", "r"); + if (!fp) return false; + char line[1024]; + bool ok = false; + while (fgets(line, sizeof(line), fp)) { + uintptr_t start = 0, end = 0; + char perms[8] = {}; + unsigned long long offset = 0; + char deststr[32] = {}; + 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); + if (n < 7 || inode == 0) continue; + char *p = path; + while (*p == ' ') ++p; + if (strstr(p, "libc.so") == nullptr) continue; + unsigned maj = 0, min = 0; + if (sscanf(deststr, "%x:%x", &maj, &min) != 2) continue; + *dev = makedev(maj, min); + *ino = inode; + ok = true; + break; + } + fclose(fp); + return ok; +} + +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); + bool ok = api->pltHookCommit(); + LOGI("PLT commit=%d", ok ? 1 : 0); +} + +static void *phase_thread(void *) { + install_promon_segv_handler(); + usleep(400 * 1000); + install_seccomp_exit_group_only(); + LOGI("seccomp armed @400ms ok=%d", g_seccomp_ok.load()); + for (int i = 0; i < 40; i++) { + usleep(1000 * 1000); + install_soft_signals(); + if (i % 5 == 0) refresh_promon_so_range(); + } + LOGI("phase done seccomp=%d stack_chk=%d promon_segv=%d", + g_seccomp_ok.load(), g_stack_chk.load(), g_promon_segv.load()); + return nullptr; +} + +static void install_all(zygisk::Api *api) { + LOGI("install pid=%d (PLT+ABRT/TRAP-skip+pc==lr-SEGV+exit_group@400ms)", getpid()); + install_fatal_skip_handlers(); + install_soft_signals(); + install_plt(api); + pthread_t th; + if (pthread_create(&th, nullptr, phase_thread, nullptr) == 0) { + pthread_detach(th); + } + LOGI("ready"); +} + +class TngExitGuardModule : public zygisk::ModuleBase { +public: + void onLoad(zygisk::Api *api, JNIEnv *env) override { + this->api = api; + this->env = env; + } + + void preAppSpecialize(zygisk::AppSpecializeArgs *args) override { + const char *nice = nullptr; + if (args->nice_name) { + nice = env->GetStringUTFChars(args->nice_name, nullptr); + } + bool match = nice && ( + std::strncmp(nice, kTargetPkg, std::strlen(kTargetPkg)) == 0); + if (nice) env->ReleaseStringUTFChars(args->nice_name, nice); + g_enabled = match; + if (!match) { + api->setOption(zygisk::Option::DLCLOSE_MODULE_LIBRARY); + return; + } + LOGI("preAppSpecialize matched"); + } + + void postAppSpecialize(const zygisk::AppSpecializeArgs *args) override { + (void)args; + if (!g_enabled) return; + install_all(api); + } + +private: + zygisk::Api *api = nullptr; + JNIEnv *env = nullptr; +}; + +REGISTER_ZYGISK_MODULE(TngExitGuardModule) diff --git a/magisk-modules/tng_exit_guard/jni/zygisk.hpp b/magisk-modules/tng_exit_guard/jni/zygisk.hpp new file mode 100644 index 0000000..7c861ad --- /dev/null +++ b/magisk-modules/tng_exit_guard/jni/zygisk.hpp @@ -0,0 +1,391 @@ +/* Copyright 2022-2023 John "topjohnwu" Wu + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + +// This is the public API for Zygisk modules. +// DO NOT MODIFY ANY CODE IN THIS HEADER. + +#pragma once + +#include + +#define ZYGISK_API_VERSION 4 + +/* + +*************** +* Introduction +*************** + +On Android, all app processes are forked from a special daemon called "Zygote". +For each new app process, zygote will fork a new process and perform "specialization". +This specialization operation enforces the Android security sandbox on the newly forked +process to make sure that 3rd party application code is only loaded after it is being +restricted within a sandbox. + +On Android, there is also this special process called "system_server". This single +process hosts a significant portion of system services, which controls how the +Android operating system and apps interact with each other. + +The Zygisk framework provides a way to allow developers to build modules and run custom +code before and after system_server and any app processes' specialization. +This enable developers to inject code and alter the behavior of system_server and app processes. + +Please note that modules will only be loaded after zygote has forked the child process. +THIS MEANS ALL OF YOUR CODE RUNS IN THE APP/SYSTEM_SERVER PROCESS, NOT THE ZYGOTE DAEMON! + +********************* +* Development Guide +********************* + +Define a class and inherit zygisk::ModuleBase to implement the functionality of your module. +Use the macro REGISTER_ZYGISK_MODULE(className) to register that class to Zygisk. + +Example code: + +static jint (*orig_logger_entry_max)(JNIEnv *env); +static jint my_logger_entry_max(JNIEnv *env) { return orig_logger_entry_max(env); } + +class ExampleModule : public zygisk::ModuleBase { +public: + void onLoad(zygisk::Api *api, JNIEnv *env) override { + this->api = api; + this->env = env; + } + void preAppSpecialize(zygisk::AppSpecializeArgs *args) override { + JNINativeMethod methods[] = { + { "logger_entry_max_payload_native", "()I", (void*) my_logger_entry_max }, + }; + api->hookJniNativeMethods(env, "android/util/Log", methods, 1); + *(void **) &orig_logger_entry_max = methods[0].fnPtr; + } +private: + zygisk::Api *api; + JNIEnv *env; +}; + +REGISTER_ZYGISK_MODULE(ExampleModule) + +----------------------------------------------------------------------------------------- + +Since your module class's code runs with either Zygote's privilege in pre[XXX]Specialize, +or runs in the sandbox of the target process in post[XXX]Specialize, the code in your class +never runs in a true superuser environment. + +If your module require access to superuser permissions, you can create and register +a root companion handler function. This function runs in a separate root companion +daemon process, and an Unix domain socket is provided to allow you to perform IPC between +your target process and the root companion process. + +Example code: + +static void example_handler(int socket) { ... } + +REGISTER_ZYGISK_COMPANION(example_handler) + +*/ + +namespace zygisk { + +struct Api; +struct AppSpecializeArgs; +struct ServerSpecializeArgs; + +class ModuleBase { +public: + + // This method is called as soon as the module is loaded into the target process. + // A Zygisk API handle will be passed as an argument. + virtual void onLoad([[maybe_unused]] Api *api, [[maybe_unused]] JNIEnv *env) {} + + // This method is called before the app process is specialized. + // At this point, the process just got forked from zygote, but no app specific specialization + // is applied. This means that the process does not have any sandbox restrictions and + // still runs with the same privilege of zygote. + // + // All the arguments that will be sent and used for app specialization is passed as a single + // AppSpecializeArgs object. You can read and overwrite these arguments to change how the app + // process will be specialized. + // + // If you need to run some operations as superuser, you can call Api::connectCompanion() to + // get a socket to do IPC calls with a root companion process. + // See Api::connectCompanion() for more info. + virtual void preAppSpecialize([[maybe_unused]] AppSpecializeArgs *args) {} + + // This method is called after the app process is specialized. + // At this point, the process has all sandbox restrictions enabled for this application. + // This means that this method runs with the same privilege of the app's own code. + virtual void postAppSpecialize([[maybe_unused]] const AppSpecializeArgs *args) {} + + // This method is called before the system server process is specialized. + // See preAppSpecialize(args) for more info. + virtual void preServerSpecialize([[maybe_unused]] ServerSpecializeArgs *args) {} + + // This method is called after the system server process is specialized. + // At this point, the process runs with the privilege of system_server. + virtual void postServerSpecialize([[maybe_unused]] const ServerSpecializeArgs *args) {} +}; + +struct AppSpecializeArgs { + // Required arguments. These arguments are guaranteed to exist on all Android versions. + jint &uid; + jint &gid; + jintArray &gids; + jint &runtime_flags; + jobjectArray &rlimits; + jint &mount_external; + jstring &se_info; + jstring &nice_name; + jstring &instruction_set; + jstring &app_data_dir; + + // Optional arguments. Please check whether the pointer is null before de-referencing + jintArray *const fds_to_ignore; + jboolean *const is_child_zygote; + jboolean *const is_top_app; + jobjectArray *const pkg_data_info_list; + jobjectArray *const whitelisted_data_info_list; + jboolean *const mount_data_dirs; + jboolean *const mount_storage_dirs; + + AppSpecializeArgs() = delete; +}; + +struct ServerSpecializeArgs { + jint &uid; + jint &gid; + jintArray &gids; + jint &runtime_flags; + jlong &permitted_capabilities; + jlong &effective_capabilities; + + ServerSpecializeArgs() = delete; +}; + +namespace internal { +struct api_table; +template void entry_impl(api_table *, JNIEnv *); +} + +// These values are used in Api::setOption(Option) +enum Option : int { + // Force Magisk's denylist unmount routines to run on this process. + // + // Setting this option only makes sense in preAppSpecialize. + // The actual unmounting happens during app process specialization. + // + // Set this option to force all Magisk and modules' files to be unmounted from the + // mount namespace of the process, regardless of the denylist enforcement status. + FORCE_DENYLIST_UNMOUNT = 0, + + // When this option is set, your module's library will be dlclose-ed after post[XXX]Specialize. + // Be aware that after dlclose-ing your module, all of your code will be unmapped from memory. + // YOU MUST NOT ENABLE THIS OPTION AFTER HOOKING ANY FUNCTIONS IN THE PROCESS. + DLCLOSE_MODULE_LIBRARY = 1, +}; + +// Bit masks of the return value of Api::getFlags() +enum StateFlag : uint32_t { + // The user has granted root access to the current process + PROCESS_GRANTED_ROOT = (1u << 0), + + // The current process was added on the denylist + PROCESS_ON_DENYLIST = (1u << 1), +}; + +// All API methods will stop working after post[XXX]Specialize as Zygisk will be unloaded +// from the specialized process afterwards. +struct Api { + + // Connect to a root companion process and get a Unix domain socket for IPC. + // + // This API only works in the pre[XXX]Specialize methods due to SELinux restrictions. + // + // The pre[XXX]Specialize methods run with the same privilege of zygote. + // If you would like to do some operations with superuser permissions, register a handler + // function that would be called in the root process with REGISTER_ZYGISK_COMPANION(func). + // Another good use case for a companion process is that if you want to share some resources + // across multiple processes, hold the resources in the companion process and pass it over. + // + // The root companion process is ABI aware; that is, when calling this method from a 32-bit + // process, you will be connected to a 32-bit companion process, and vice versa for 64-bit. + // + // Returns a file descriptor to a socket that is connected to the socket passed to your + // module's companion request handler. Returns -1 if the connection attempt failed. + int connectCompanion(); + + // Get the file descriptor of the root folder of the current module. + // + // This API only works in the pre[XXX]Specialize methods. + // Accessing the directory returned is only possible in the pre[XXX]Specialize methods + // or in the root companion process (assuming that you sent the fd over the socket). + // Both restrictions are due to SELinux and UID. + // + // Returns -1 if errors occurred. + int getModuleDir(); + + // Set various options for your module. + // Please note that this method accepts one single option at a time. + // Check zygisk::Option for the full list of options available. + void setOption(Option opt); + + // Get information about the current process. + // Returns bitwise-or'd zygisk::StateFlag values. + uint32_t getFlags(); + + // Exempt the provided file descriptor from being automatically closed. + // + // This API only make sense in preAppSpecialize; calling this method in any other situation + // is either a no-op (returns true) or an error (returns false). + // + // When false is returned, the provided file descriptor will eventually be closed by zygote. + bool exemptFd(int fd); + + // Hook JNI native methods for a class + // + // Lookup all registered JNI native methods and replace it with your own methods. + // The original function pointer will be saved in each JNINativeMethod's fnPtr. + // If no matching class, method name, or signature is found, that specific JNINativeMethod.fnPtr + // will be set to nullptr. + void hookJniNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *methods, int numMethods); + + // Hook functions in the PLT (Procedure Linkage Table) of ELFs loaded in memory. + // + // Parsing /proc/[PID]/maps will give you the memory map of a process. As an example: + // + //
+ // 56b4346000-56b4347000 r-xp 00002000 fe:00 235 /system/bin/app_process64 + // (More details: https://man7.org/linux/man-pages/man5/proc.5.html) + // + // The `dev` and `inode` pair uniquely identifies a file being mapped into memory. + // For matching ELFs loaded in memory, replace function `symbol` with `newFunc`. + // If `oldFunc` is not nullptr, the original function pointer will be saved to `oldFunc`. + void pltHookRegister(dev_t dev, ino_t inode, const char *symbol, void *newFunc, void **oldFunc); + + // Commit all the hooks that was previously registered. + // Returns false if an error occurred. + bool pltHookCommit(); + +private: + internal::api_table *tbl; + template friend void internal::entry_impl(internal::api_table *, JNIEnv *); +}; + +// Register a class as a Zygisk module + +#define REGISTER_ZYGISK_MODULE(clazz) \ +void zygisk_module_entry(zygisk::internal::api_table *table, JNIEnv *env) { \ + zygisk::internal::entry_impl(table, env); \ +} + +// Register a root companion request handler function for your module +// +// The function runs in a superuser daemon process and handles a root companion request from +// your module running in a target process. The function has to accept an integer value, +// which is a Unix domain socket that is connected to the target process. +// See Api::connectCompanion() for more info. +// +// NOTE: the function can run concurrently on multiple threads. +// Be aware of race conditions if you have globally shared resources. + +#define REGISTER_ZYGISK_COMPANION(func) \ +void zygisk_companion_entry(int client) { func(client); } + +/********************************************************* + * The following is internal ABI implementation detail. + * You do not have to understand what it is doing. + *********************************************************/ + +namespace internal { + +struct module_abi { + long api_version; + ModuleBase *impl; + + void (*preAppSpecialize)(ModuleBase *, AppSpecializeArgs *); + void (*postAppSpecialize)(ModuleBase *, const AppSpecializeArgs *); + void (*preServerSpecialize)(ModuleBase *, ServerSpecializeArgs *); + void (*postServerSpecialize)(ModuleBase *, const ServerSpecializeArgs *); + + module_abi(ModuleBase *module) : api_version(ZYGISK_API_VERSION), impl(module) { + preAppSpecialize = [](auto m, auto args) { m->preAppSpecialize(args); }; + postAppSpecialize = [](auto m, auto args) { m->postAppSpecialize(args); }; + preServerSpecialize = [](auto m, auto args) { m->preServerSpecialize(args); }; + postServerSpecialize = [](auto m, auto args) { m->postServerSpecialize(args); }; + } +}; + +struct api_table { + // Base + void *impl; + bool (*registerModule)(api_table *, module_abi *); + + void (*hookJniNativeMethods)(JNIEnv *, const char *, JNINativeMethod *, int); + void (*pltHookRegister)(dev_t, ino_t, const char *, void *, void **); + bool (*exemptFd)(int); + bool (*pltHookCommit)(); + int (*connectCompanion)(void * /* impl */); + void (*setOption)(void * /* impl */, Option); + int (*getModuleDir)(void * /* impl */); + uint32_t (*getFlags)(void * /* impl */); +}; + +template +void entry_impl(api_table *table, JNIEnv *env) { + static Api api; + api.tbl = table; + static T module; + ModuleBase *m = &module; + static module_abi abi(m); + if (!table->registerModule(table, &abi)) return; + m->onLoad(&api, env); +} + +} // namespace internal + +inline int Api::connectCompanion() { + return tbl->connectCompanion ? tbl->connectCompanion(tbl->impl) : -1; +} +inline int Api::getModuleDir() { + return tbl->getModuleDir ? tbl->getModuleDir(tbl->impl) : -1; +} +inline void Api::setOption(Option opt) { + if (tbl->setOption) tbl->setOption(tbl->impl, opt); +} +inline uint32_t Api::getFlags() { + return tbl->getFlags ? tbl->getFlags(tbl->impl) : 0; +} +inline bool Api::exemptFd(int fd) { + return tbl->exemptFd != nullptr && tbl->exemptFd(fd); +} +inline void Api::hookJniNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *methods, int numMethods) { + if (tbl->hookJniNativeMethods) tbl->hookJniNativeMethods(env, className, methods, numMethods); +} +inline void Api::pltHookRegister(dev_t dev, ino_t inode, const char *symbol, void *newFunc, void **oldFunc) { + if (tbl->pltHookRegister) tbl->pltHookRegister(dev, inode, symbol, newFunc, oldFunc); +} +inline bool Api::pltHookCommit() { + return tbl->pltHookCommit != nullptr && tbl->pltHookCommit(); +} + +} // namespace zygisk + +extern "C" { + +[[gnu::visibility("default"), maybe_unused]] +void zygisk_module_entry(zygisk::internal::api_table *, JNIEnv *); + +[[gnu::visibility("default"), maybe_unused]] +void zygisk_companion_entry(int); + +} // extern "C" diff --git a/magisk-modules/tng_exit_guard/module.prop b/magisk-modules/tng_exit_guard/module.prop new file mode 100644 index 0000000..69e53a3 --- /dev/null +++ b/magisk-modules/tng_exit_guard/module.prop @@ -0,0 +1,6 @@ +id=tng_exit_guard +name=TNG Exit Guard +version=v1.0 +versionCode=1 +author=miraclegarden +description=Zygisk: block Promon native exit_group for my.com.tngdigital.ewallet. Pair with notiMessage Xposed TngRoot hooks. Do NOT put TNG on Magisk DenyList. diff --git a/magisk-modules/tng_exit_guard/zygisk/arm64-v8a.so b/magisk-modules/tng_exit_guard/zygisk/arm64-v8a.so new file mode 100644 index 0000000000000000000000000000000000000000..98a1bdc2e3bf97642f96b7ee16f396b001a57481 GIT binary patch literal 186384 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 zygisk\arm64-v8a.so ($((Get-Item (Join-Path $zygiskDir 'arm64-v8a.so')).Length) bytes)" +Pop-Location + +if ($SkipInstall) { return } + +Write-Host "=== install Magisk module ===" -ForegroundColor Cyan +& $Adb wait-for-device +& $Adb shell "su -c 'mkdir -p /data/adb/modules/tng_exit_guard/zygisk'" +& $Adb push (Join-Path $Mod "module.prop") /data/local/tmp/tng_exit_guard_module.prop +& $Adb push (Join-Path $zygiskDir "arm64-v8a.so") /data/local/tmp/tng_exit_guard.so +& $Adb shell "su -c 'cp /data/local/tmp/tng_exit_guard_module.prop /data/adb/modules/tng_exit_guard/module.prop; cp /data/local/tmp/tng_exit_guard.so /data/adb/modules/tng_exit_guard/zygisk/arm64-v8a.so; chmod 644 /data/adb/modules/tng_exit_guard/module.prop; chmod 755 /data/adb/modules/tng_exit_guard/zygisk/arm64-v8a.so; rm -f /data/adb/modules/tng_exit_guard/disable /data/adb/modules/tng_exit_guard/remove; ls -la /data/adb/modules/tng_exit_guard/ /data/adb/modules/tng_exit_guard/zygisk/'" + +if (-not $NoReboot) { + Write-Host "=== soft reboot zygote (module loads on next specialize) ===" -ForegroundColor Yellow + Write-Host "Magisk Zygisk modules usually need a FULL reboot. Rebooting device..." + & $Adb reboot + Write-Host "Waiting for device..." + & $Adb wait-for-device + Start-Sleep -Seconds 25 + & $Adb shell "getprop sys.boot_completed" +} + +Write-Host "Done. Launch TNG and check: adb logcat -s TngExitGuard:I LSPosed-Bridge:I" -ForegroundColor Green diff --git a/scripts/diag-tng-exit-guard.sh b/scripts/diag-tng-exit-guard.sh new file mode 100644 index 0000000..d8bef96 --- /dev/null +++ b/scripts/diag-tng-exit-guard.sh @@ -0,0 +1,21 @@ +#!/system/bin/sh +set -x +echo "=== magisk ver ===" +magisk -c +magisk -v +echo "=== zygisk setting ===" +magisk --sqlite 'SELECT * FROM settings' +echo "=== path ===" +MAGISK_PATH=$(magisk --path) +echo "MAGISK_PATH=$MAGISK_PATH" +ls -la "$MAGISK_PATH" 2>/dev/null | head -30 +ls -la "$MAGISK_PATH/zygisk" 2>/dev/null +ls -la /data/adb/modules/ +ls -la /data/adb/modules/tng_exit_guard/ +ls -la /data/adb/modules/tng_exit_guard/zygisk/ +echo "=== denylist tng ===" +magisk --denylist ls 2>/dev/null | grep -i tng || echo "TNG not on denylist" +echo "=== processes ===" +ps -A | grep -iE 'magiskd|zygisk|lspd|vector|tngdigital' || true +echo "=== logcat zygisk ===" +logcat -d | grep -iE 'zygisk|TngExit|tng_exit' | tail -40 diff --git a/scripts/logcat-tng.ps1 b/scripts/logcat-tng.ps1 new file mode 100644 index 0000000..da2ed86 --- /dev/null +++ b/scripts/logcat-tng.ps1 @@ -0,0 +1,14 @@ +# Capture TNG eWallet hook logs (clears buffer first if -Clear switch passed) +param([switch]$Clear) +$adb = "C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe" +if (-not (Test-Path $adb)) { + $adb = "adb" +} +if ($Clear) { + & $adb logcat -c + Write-Host "Logcat cleared. Launch TNG eWallet, then run without -Clear:" -ForegroundColor Yellow + Write-Host " .\scripts\logcat-tng.ps1" + exit 0 +} +& $adb logcat -d 2>&1 | Select-String -Pattern "notiMessageHook/TngRoot|LSPosed-Bridge.*TngRoot|LSPosed-Bridge.*notiMessageHook|support.tngdigital|SecurityError|xwwqazamx|UserLogin|blocked intent|blocked Promon" | + Select-Object -Last 80 diff --git a/scripts/pull-tng-log.sh b/scripts/pull-tng-log.sh new file mode 100644 index 0000000..0cc2104 --- /dev/null +++ b/scripts/pull-tng-log.sh @@ -0,0 +1,4 @@ +#!/system/bin/sh +logcat -d > /data/local/tmp/tnglog.txt +grep -E '7500|exited cleanly|Process my.com.tngdigital|TngRoot|UserLogin|FATAL|Abort message|tombstone|blocked syscall|has died' /data/local/tmp/tnglog.txt | tail -120 > /data/local/tmp/tnglog2.txt +wc -l /data/local/tmp/tnglog2.txt diff --git a/scripts/test-tng-survive.sh b/scripts/test-tng-survive.sh new file mode 100644 index 0000000..f7f96e4 --- /dev/null +++ b/scripts/test-tng-survive.sh @@ -0,0 +1,16 @@ +#!/system/bin/sh +logcat -c +am force-stop my.com.tngdigital.ewallet +sleep 1 +monkey -p my.com.tngdigital.ewallet -c android.intent.category.LAUNCHER 1 >/dev/null 2>&1 +sleep 2 +echo "=== @2s ==="; ps -A | grep -i tng || echo NONE +sleep 5 +echo "=== @7s ==="; ps -A | grep -i tng || echo NONE +sleep 8 +echo "=== @15s ==="; ps -A | grep -i tng || echo NONE +logcat -d > /data/local/tmp/tngfull.txt +echo "=== guard ===" +grep TngExitGuard /data/local/tmp/tngfull.txt | tail -30 +echo "=== outcome ===" +grep -E 'exited cleanly|exited due|has died|skipped SIGILL|blocked kill|blocked tgkill|UserLogin|Displayed|PLT blocked' /data/local/tmp/tngfull.txt | tail -30 \ No newline at end of file diff --git a/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/MainHook.java b/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/MainHook.java index 4d3df2e..fa72915 100644 --- a/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/MainHook.java +++ b/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/MainHook.java @@ -3,6 +3,7 @@ package com.miraclegarden.smsmessage.xposed; import com.miraclegarden.smsmessage.xposed.hook.MariBankRootBypassHook; import com.miraclegarden.smsmessage.xposed.hook.MariBankShpsNativeHook; import com.miraclegarden.smsmessage.xposed.hook.SuncorpBankMessageHook; +import com.miraclegarden.smsmessage.xposed.hook.TngRootBypassHook; import com.miraclegarden.smsmessage.xposed.hook.SqliteMessageHook; import com.miraclegarden.smsmessage.xposed.hook.TelegramMessageHook; import com.miraclegarden.smsmessage.xposed.hook.UpBankMessageHook; @@ -63,6 +64,11 @@ public class MainHook implements IXposedHookLoadPackage { return; } + if (TngRootBypassHook.isTargetPackage(lpparam.packageName)) { + TngRootBypassHook.install(lpparam); + return; + } + SqliteMessageHook.install(lpparam); } } diff --git a/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/hook/ProcMapsFilterHook.java b/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/hook/ProcMapsFilterHook.java new file mode 100644 index 0000000..66c679c --- /dev/null +++ b/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/hook/ProcMapsFilterHook.java @@ -0,0 +1,370 @@ +package com.miraclegarden.smsmessage.xposed.hook; + +import android.os.Build; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; +import java.util.WeakHashMap; + +import de.robv.android.xposed.XC_MethodHook; +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.XposedHelpers; +import de.robv.android.xposed.callbacks.XC_LoadPackage; + +/** + * 过滤 /proc/self/maps 等敏感路径,隐藏 Xposed / Magisk / Zygisk 库名。 + */ +public final class ProcMapsFilterHook { + + private static final String TAG = "notiMessageHook/ProcMaps"; + + private static final Set PROC_SENSITIVE = new HashSet<>(Arrays.asList( + "/proc/self/maps", + "/proc/version", + "/proc/self/status", + "/proc/mounts", + "/proc/self/attr/current", + "/proc/self/mountinfo" + )); + + private static final String[] MAPS_HIDE_MARKERS = { + "xposed", "lsposed", "edxposed", "magisk", "frida", "substrate", + "libpine", "pine.so", "zygisk", "riru", "shamiko", "notimessage", + "miraclegarden", "libbytehook", "libgadget", "libfrida", "libriru", + "liblspd", "libzygisk", "libvector", "zygisk_vector", "vector", + }; + + private static final String FAKE_SELINUX_CTX = + "u:r:untrusted_app:s0:c512,c768"; + + private static final WeakHashMap TRACKED_INPUTS = new WeakHashMap<>(); + + private static volatile boolean installed = false; + + private ProcMapsFilterHook() { + } + + public static void install(XC_LoadPackage.LoadPackageParam lpparam) { + if (installed) { + return; + } + installed = true; + hookProcAccess(lpparam); + hookProcViaRandomAccessFile(lpparam); + hookBufferedReader(lpparam); + hookSystemProperties(lpparam); + XposedBridge.log(TAG + " installed for " + lpparam.packageName); + } + + private static void hookProcAccess(XC_LoadPackage.LoadPackageParam lpparam) { + try { + XposedHelpers.findAndHookConstructor( + FileInputStream.class, + String.class, + new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + String path = normalizeProcPath((String) param.args[0]); + if (path != null) { + TRACKED_INPUTS.put(param.getResult(), path); + } + } + } + ); + } catch (Throwable t) { + XposedBridge.log(TAG + " FileInputStream hook failed: " + t.getMessage()); + } + + try { + XposedHelpers.findAndHookConstructor( + FileInputStream.class, + File.class, + new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + File file = (File) param.args[0]; + if (file != null) { + String path = normalizeProcPath(file.getAbsolutePath()); + if (path != null) { + TRACKED_INPUTS.put(param.getResult(), path); + } + } + } + } + ); + } catch (Throwable ignored) { + } + + XC_MethodHook readFilter = new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + String path = TRACKED_INPUTS.get(param.thisObject); + if (path == null || param.getResult() == null) { + return; + } + if (param.getResult() instanceof Integer) { + int read = (Integer) param.getResult(); + if (read <= 0 || param.args.length == 0 || !(param.args[0] instanceof byte[])) { + return; + } + byte[] buf = (byte[]) param.args[0]; + int off = param.args.length > 1 ? (Integer) param.args[1] : 0; + filterProcBytes(path, buf, off, read); + } else if (param.getResult() instanceof byte[]) { + byte[] data = (byte[]) param.getResult(); + param.setResult(filterProcBytesAll(path, data)); + } else if (param.getResult() instanceof String) { + param.setResult(filterProcText(path, (String) param.getResult())); + } + } + }; + + try { + XposedHelpers.findAndHookMethod( + FileInputStream.class, "read", byte[].class, readFilter); + XposedHelpers.findAndHookMethod( + FileInputStream.class, "read", byte[].class, int.class, int.class, readFilter); + } catch (Throwable t) { + XposedBridge.log(TAG + " FileInputStream.read hook failed: " + t.getMessage()); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + try { + XposedHelpers.findAndHookMethod( + "java.nio.file.Files", + lpparam.classLoader, + "readAllBytes", + "java.nio.file.Path", + new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + if (!(param.getResult() instanceof byte[])) { + return; + } + String norm = normalizeProcPath(String.valueOf(param.args[0])); + if (norm != null) { + param.setResult(filterProcBytesAll( + norm, (byte[]) param.getResult())); + } + } + } + ); + } catch (Throwable ignored) { + } + } + } + + private static void hookProcViaRandomAccessFile(XC_LoadPackage.LoadPackageParam lpparam) { + try { + XposedHelpers.findAndHookConstructor( + "java.io.RandomAccessFile", + lpparam.classLoader, + String.class, + String.class, + new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + String path = normalizeProcPath((String) param.args[0]); + if (path != null) { + TRACKED_INPUTS.put(param.getResult(), path); + } + } + } + ); + } catch (Throwable ignored) { + } + try { + XposedHelpers.findAndHookMethod( + "java.io.RandomAccessFile", + lpparam.classLoader, + "read", + byte[].class, + new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + String path = TRACKED_INPUTS.get(param.thisObject); + if (path == null || !(param.getResult() instanceof Integer)) { + return; + } + int read = (Integer) param.getResult(); + if (read > 0 && param.args[0] instanceof byte[]) { + filterProcBytes(path, (byte[]) param.args[0], 0, read); + } + } + } + ); + } catch (Throwable ignored) { + } + } + + private static void hookBufferedReader(XC_LoadPackage.LoadPackageParam lpparam) { + try { + XposedHelpers.findAndHookMethod( + BufferedReader.class, + "readLine", + new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + if (!(param.getResult() instanceof String)) { + return; + } + String line = (String) param.getResult(); + if (shouldHideMapsLine(line)) { + param.setResult(readNextSafeLine((BufferedReader) param.thisObject)); + } + } + } + ); + } catch (Throwable t) { + XposedBridge.log(TAG + " BufferedReader hook failed: " + t.getMessage()); + } + } + + private static String readNextSafeLine(BufferedReader reader) { + try { + String line; + while ((line = reader.readLine()) != null) { + if (!shouldHideMapsLine(line)) { + return line; + } + } + } catch (Throwable ignored) { + } + return ""; + } + + private static void hookSystemProperties(XC_LoadPackage.LoadPackageParam lpparam) { + try { + Class sp = XposedHelpers.findClass("android.os.SystemProperties", lpparam.classLoader); + for (java.lang.reflect.Method method : sp.getDeclaredMethods()) { + if (!"get".equals(method.getName())) { + continue; + } + XposedBridge.hookMethod(method, new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + if (param.args.length == 0 || !(param.args[0] instanceof String)) { + return; + } + String key = (String) param.args[0]; + String spoofed = spoofProperty(key, param.getResult()); + if (spoofed != null) { + param.setResult(spoofed); + } + } + }); + } + } catch (Throwable t) { + XposedBridge.log(TAG + " SystemProperties hook failed: " + t.getMessage()); + } + } + + private static String spoofProperty(String key, Object current) { + if ("ro.debuggable".equals(key)) { + return "0"; + } + if ("init.svc.adbd".equals(key) || "init.svc.adb".equals(key)) { + return "stopped"; + } + if ("service.adb.root".equals(key)) { + return "0"; + } + if ("ro.secure".equals(key)) { + return "1"; + } + if ("ro.build.tags".equals(key)) { + if (current instanceof String && String.valueOf(current).contains("test-keys")) { + return "release-keys"; + } + } + if ("ro.boot.verifiedbootstate".equals(key)) { + return "green"; + } + if ("ro.boot.vbmeta.device_state".equals(key) + || "vendor.boot.vbmeta.device_state".equals(key)) { + return "locked"; + } + return null; + } + + private static String normalizeProcPath(String path) { + if (path == null) { + return null; + } + String norm = path.trim(); + for (String p : PROC_SENSITIVE) { + if (norm.equals(p) || norm.endsWith(p)) { + return p; + } + } + return null; + } + + private static byte[] filterProcBytesAll(String path, byte[] data) { + return filterProcText(path, new String(data)).getBytes(); + } + + private static void filterProcBytes(String path, byte[] buf, int off, int len) { + String text = new String(buf, off, len); + String filtered = filterProcText(path, text); + if (filtered.equals(text)) { + return; + } + byte[] out = filtered.getBytes(); + int copy = Math.min(len, out.length); + System.arraycopy(out, 0, buf, off, copy); + if (copy < len) { + Arrays.fill(buf, off + copy, off + len, (byte) 0); + } + } + + private static String filterProcText(String path, String text) { + if ("/proc/self/maps".equals(path) || "/proc/self/mountinfo".equals(path) + || "/proc/mounts".equals(path)) { + StringBuilder sb = new StringBuilder(); + for (String line : text.split("\n")) { + if (!shouldHideMapsLine(line)) { + if (sb.length() > 0) { + sb.append('\n'); + } + sb.append(line); + } + } + return sb.toString(); + } + if ("/proc/self/attr/current".equals(path)) { + String lower = text.toLowerCase(Locale.US); + if (lower.contains("magisk") || lower.contains("su") || lower.contains("zygisk") + || lower.contains("xposed")) { + return FAKE_SELINUX_CTX; + } + return text; + } + if ("/proc/version".equals(path)) { + return text.replace("dirty", "").replace("test-keys", "release-keys"); + } + if ("/proc/self/status".equals(path)) { + return text.replaceAll("(?m)^TracerPid:\\s*[1-9]\\d*", + "TracerPid:\t0"); + } + return text; + } + + private static boolean shouldHideMapsLine(String line) { + if (line == null || line.isEmpty()) { + return false; + } + String lower = line.toLowerCase(Locale.US); + for (String marker : MAPS_HIDE_MARKERS) { + if (lower.contains(marker)) { + return true; + } + } + return false; + } +} diff --git a/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/hook/RootBypassHelper.java b/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/hook/RootBypassHelper.java index 3903a4b..2f5820f 100644 --- a/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/hook/RootBypassHelper.java +++ b/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/hook/RootBypassHelper.java @@ -131,25 +131,26 @@ public final class RootBypassHelper { } public static void hookRuntimeExec(XC_LoadPackage.LoadPackageParam lpparam) { + XC_MethodHook blockExec = new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + String cmd = null; + if (param.args.length > 0 && param.args[0] instanceof String) { + cmd = (String) param.args[0]; + } else if (param.args.length > 0 && param.args[0] instanceof String[]) { + cmd = String.join(" ", (String[]) param.args[0]); + } + if (ProbeGuard.isBlockedCommand(cmd)) { + param.setResult(ProbeGuard.fakeFailedProcess(cmd)); + } + } + }; try { + XposedHelpers.findAndHookMethod(Runtime.class, "exec", String.class, blockExec); + XposedHelpers.findAndHookMethod(Runtime.class, "exec", String[].class, blockExec); + XposedHelpers.findAndHookMethod(Runtime.class, "exec", String.class, String[].class, blockExec); XposedHelpers.findAndHookMethod( - Runtime.class, - "exec", - String.class, - new XC_MethodHook() { - @Override - protected void beforeHookedMethod(MethodHookParam param) { - String cmd = (String) param.args[0]; - if (cmd == null) { - return; - } - String lower = cmd.toLowerCase(Locale.US); - if (lower.contains("su") || lower.contains("magisk") || lower.contains("which su")) { - throw new SecurityException("blocked root probe"); - } - } - } - ); + Runtime.class, "exec", String[].class, String[].class, blockExec); } catch (Throwable t) { XposedBridge.log(TAG + " Runtime.exec hook failed: " + t.getMessage()); } 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 new file mode 100644 index 0000000..3efbf55 --- /dev/null +++ b/xposed-module/src/main/java/com/miraclegarden/smsmessage/xposed/hook/TngRootBypassHook.java @@ -0,0 +1,2569 @@ +package com.miraclegarden.smsmessage.xposed.hook; + +import android.app.Activity; +import android.app.Instrumentation; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.app.Application; +import android.app.Dialog; +import android.app.ActivityManager; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Process; + +import android.os.Message; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; +import java.util.Locale; + +import de.robv.android.xposed.XC_MethodHook; +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.XposedHelpers; +import de.robv.android.xposed.callbacks.XC_LoadPackage; + +/** + * TNG eWallet Root / Promon / JailBroken 检测绕过。 + * 包名:my.com.tngdigital.ewallet(v1.9.9+) + */ +public final class TngRootBypassHook { + + public static final String PACKAGE = "my.com.tngdigital.ewallet"; + private static final String TAG = "notiMessageHook/TngRoot"; + + private static final String SECURITY_ERROR_ACTIVITY = + "my.com.tngdigital.common.security.ui.SecurityErrorActivity"; + + private static final String[] BOOLEAN_HOOK_CLASSES = { + "my.com.tngdigital.common.jailbrkendetect.JailBrokenApiImpl", + "my.com.tngdigital.common.jailbrkendetect.JailBrokenManager", + }; + + private static final String[] BLOCKED_SUPPORT_MARKERS = { + "36616543382169-rooting", + "36616508159769-emulator", + "36616480108697-malicious", + "36616444815257-multiboxing", + "45485757513113-private-space", + "48493749288857-malware", + "/articles/", + "support.tngdigital.com.my/hc/", + }; + private static volatile long lastBlockedSuicideAt = 0L; + private static final long SOFT_CRASH_GUARD_MS = 10000L; + /** 用户已进入注册/登录后续页时,禁止 Splash 强拉回 Login。 */ + private static volatile boolean registrationFlowActive = false; + + private static final String[] REGISTRATION_FLOW_MARKERS = { + "GuideActivity", + "UserRegistrationMobileActivity", + "UserOtpVerificationActivity", + "UserRegistrationIdentityActivity", + "UserRegistrationSixPinActivity", + "UserRegistrationStrengthenPinActivity", + "UserRegistrationSecurityQuestionActivity", + "UserRegistrationSuccessActivity", + "UserSearchCallingCodeActivity", + "UserPinActivity", + "UserSecurePinActivity", + "UserPinVerifyActivity", + "EmailOtpVerificationActivity", + }; + + private static final String[] REGISTRATION_RPC_MARKERS = { + "phonecheck", "com.abl.wallet.phone", "com.abl.wallet.otp", + "customer.registration", "customer.verify", "customer.login", + "login.options", "callingcode", "pin.token", "module.whitelist", + }; + + private TngRootBypassHook() { + } + + public static boolean isTargetPackage(String packageName) { + return PACKAGE.equals(packageName); + } + + private static final ThreadLocal CURRENT_REQUEST_URL = new ThreadLocal<>(); + + /** 进程启动最早打点,便于确认 LSPosed 是否注入(注册闪退常因 hook 未生效)。 */ + private static void hookEarlyAttachLog(XC_LoadPackage.LoadPackageParam lpparam) { + try { + XposedHelpers.findAndHookMethod(Application.class, "attachBaseContext", Context.class, + new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + Context ctx = (Context) param.args[0]; + if (ctx != null && PACKAGE.equals(ctx.getPackageName())) { + XposedBridge.log(TAG + " attachBaseContext pid=" + + Process.myPid()); + } + } + }); + } catch (Throwable t) { + XposedBridge.log(TAG + " early attach log failed: " + t.getMessage()); + } + } + + /** Splash 优先生效;新 schedule 会取消旧 Runnable(Application 兜底 vs Splash 2000ms)。 */ + private static final Handler FORCE_LOGIN_HANDLER = new Handler(Looper.getMainLooper()); + private static Runnable pendingForceLoginRunnable; + /** Promon native-bridge short-circuit 重入保护,避免 __cxa_guard_acquire 递归 abort。 */ + private static final ThreadLocal PROMON_BRIDGE_DEPTH = new ThreadLocal() { + @Override + protected Integer initialValue() { + return 0; + } + }; + + private static void scheduleForceLoginToUserLogin( + final Context appCtx, final Activity splashAct, final String reason, final long delayMs) { + if (appCtx == null) { + return; + } + if (pendingForceLoginRunnable != null) { + FORCE_LOGIN_HANDLER.removeCallbacks(pendingForceLoginRunnable); + } + final String login = "my.com.tngdigital.user.view.UserLoginActivity"; + pendingForceLoginRunnable = new Runnable() { + @Override + public void run() { + pendingForceLoginRunnable = null; + try { + if (registrationFlowActive || isTopActivityRegistrationFlow(appCtx)) { + XposedBridge.log(TAG + " skip force login (" + reason + ", registration flow)"); + return; + } + ActivityManager am = + (ActivityManager) appCtx.getSystemService(Context.ACTIVITY_SERVICE); + if (am != null) { + for (ActivityManager.AppTask task : am.getAppTasks()) { + ActivityManager.RecentTaskInfo info = task.getTaskInfo(); + if (info == null || info.topActivity == null) { + continue; + } + String top = info.topActivity.getClassName(); + if (top.endsWith(".UserLoginActivity") + || top.endsWith(".UserPinActivity") + || isRegistrationFlowActivity(top)) { + XposedBridge.log(TAG + " skip force login (" + reason + ", on " + top + ")"); + return; + } + } + } + if (!isUiVisibleForForceLogin(appCtx)) { + XposedBridge.log(TAG + " skip force login (" + reason + ", no visible UI / BAL)"); + return; + } + Intent intent = new Intent(); + intent.setClassName(PACKAGE, login); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP + | Intent.FLAG_ACTIVITY_SINGLE_TOP); + if (splashAct != null && !splashAct.isFinishing()) { + splashAct.startActivity(intent); + try { + splashAct.finish(); + } catch (Throwable ignored) { + } + XposedBridge.log(TAG + " forced → UserLogin (" + reason + ", from Splash)"); + } else { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + appCtx.startActivity(intent); + XposedBridge.log(TAG + " forced → UserLogin (" + reason + ", from AppCtx)"); + } + } catch (Throwable t) { + XposedBridge.log(TAG + " force login failed (" + reason + "): " + t.getMessage()); + } + } + }; + FORCE_LOGIN_HANDLER.postDelayed(pendingForceLoginRunnable, delayMs); + } + + /** 后台 Service 重启(如 Firebase SessionLifecycle)无可见 Activity,强拉会被 BAL 拦截。 */ + private static boolean isUiVisibleForForceLogin(Context ctx) { + ActivityManager.RunningAppProcessInfo state = new ActivityManager.RunningAppProcessInfo(); + ActivityManager.getMyMemoryState(state); + if (state.importance > ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE) { + return false; + } + try { + ActivityManager am = (ActivityManager) ctx.getSystemService(Context.ACTIVITY_SERVICE); + if (am != null) { + for (ActivityManager.AppTask task : am.getAppTasks()) { + ActivityManager.RecentTaskInfo info = task.getTaskInfo(); + if (info != null && info.topActivity != null + && PACKAGE.equals(info.topActivity.getPackageName())) { + return true; + } + } + } + } catch (Throwable ignored) { + } + return state.importance <= ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; + } + + private static final String[] HTTP_LOG_MARKERS = { + "otp", "verify", "pin", "register", "auth", "login", "sms", "mobile", + "risk", "token", "error", "code", "unexpected", "reference", + }; + + public static void install(XC_LoadPackage.LoadPackageParam lpparam) { + if (!isTargetPackage(lpparam.packageName)) { + return; + } + XposedBridge.log(TAG + " install for " + lpparam.packageName + + " pid=" + Process.myPid() + + " proc=" + getProcessName()); + + // Splash 常卡死;Instrumentation 强拉即可。双通道会抢跑导致 Login 重载/卡死。 + hookEarlyAttachLog(lpparam); + hookRegistrationFlowGuard(lpparam); + hookSplashForceLogin(lpparam); + hookLoginDismissSplash(lpparam); + hookDialogNoHwAccel(); + hookActivityLifecycleDiag(lpparam); + hookHardwareRendererSetName(); + hookBottomSelectDialogSafe(lpparam); + hookPromonApService(lpparam); + hookPromonBroadcastReceiver(lpparam); + hookAppAttachForceLogin(lpparam); + + RootBypassHelper.hookFileExists(lpparam); + RootBypassHelper.hookRuntimeExec(lpparam); + RootBypassHelper.hookSystemGetProperty(lpparam); + ProcMapsFilterHook.install(lpparam); + + hookAntiSuicide(); + hookUncaughtPromonException(lpparam); + hookKillApplicationHandler(lpparam); + hookBlockSecurityErrorLaunch(lpparam); + hookPromonNativeGuard(lpparam); + hookPromonLifecycle(lpparam); + hookJnicLibrary(lpparam); + hookTigerTally(lpparam); + hookActivityThreadExit(lpparam); + hookForceExitFlow(lpparam); + hookFinishAllActivityAndKillApp(lpparam); + hookShowSecurityScreenForState(lpparam); + hookSecurityUrlOpeners(lpparam); + hookJailBroken(lpparam); + hookJailBrokenRpc(lpparam); + hookAppSecurityManager(lpparam); + hookAppSecurityCallbacks(lpparam); + hookPromonNativeBridge(lpparam); + hookSecurityBooleanChecks(lpparam); + hookSecurityErrorActivity(lpparam); + hookNetworkDiag(lpparam); + hookWebViewErrorDiag(lpparam); + } + + private static void logActivityDiag(String phase, Activity activity) { + if (activity == null) { + return; + } + String name = activity.getClass().getName(); + String lower = name.toLowerCase(Locale.US); + if (lower.contains("userpin") + || lower.contains("userlogin") + || lower.contains("registration") + || lower.contains("otp") + || lower.contains("verify") + || lower.contains("sms") + || lower.contains("webview") + || lower.contains("issue") + || lower.contains("guide") + || lower.contains("error") + || lower.contains("dialog")) { + XposedBridge.log(TAG + " ACT " + phase + " " + name); + } + } + + /** 记录 OTP/登录相关 HTTP 请求与响应体,定位验证码提交失败原因。 */ + private static void hookNetworkDiag(XC_LoadPackage.LoadPackageParam lpparam) { + try { + XposedHelpers.findAndHookMethod( + "okhttp3.Request$Builder", + lpparam.classLoader, + "build", + new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + try { + Object url = XposedHelpers.callMethod(param.getResult(), "url"); + if (url != null) { + CURRENT_REQUEST_URL.set(String.valueOf(url)); + } + } catch (Throwable ignored) { + } + } + }); + } catch (Throwable t) { + XposedBridge.log(TAG + " diag Request.Builder.build failed: " + t.getMessage()); + } + try { + XposedHelpers.findAndHookMethod( + "okhttp3.ResponseBody", + lpparam.classLoader, + "string", + new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + String body = (String) param.getResult(); + String url = CURRENT_REQUEST_URL.get(); + CURRENT_REQUEST_URL.remove(); + if (body == null) { + return; + } + if (!shouldLogHttp(url, body)) { + return; + } + String snippet = body.length() > 800 + ? body.substring(0, 800) + "..." : body; + XposedBridge.log(TAG + " HTTP rsp" + + (url != null ? " " + url : "") + + " body=" + snippet); + } + }); + } catch (Throwable t) { + XposedBridge.log(TAG + " diag ResponseBody.string failed: " + t.getMessage()); + } + XC_MethodHook callRequestHook = new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + try { + Object req = XposedHelpers.callMethod(param.thisObject, "request"); + logHttpRequest(lpparam.classLoader, req); + } catch (Throwable ignored) { + } + } + }; + XC_MethodHook enqueueHook = new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + try { + Object req = XposedHelpers.callMethod(param.thisObject, "request"); + logHttpRequest(lpparam.classLoader, req); + } catch (Throwable ignored) { + } + } + }; + for (String className : new String[]{ + "okhttp3.RealCall", "okhttp3.internal.connection.RealCall"}) { + try { + XposedHelpers.findAndHookMethod( + className, lpparam.classLoader, "execute", callRequestHook); + XposedHelpers.findAndHookMethod( + className, lpparam.classLoader, "enqueue", + "okhttp3.Callback", enqueueHook); + } catch (Throwable ignored) { + } + } + XposedBridge.log(TAG + " hooked network diag (okhttp)"); + } + + private static void logHttpRequest(ClassLoader loader, Object req) { + if (req == null) { + return; + } + Object url = XposedHelpers.callMethod(req, "url"); + if (url == null) { + return; + } + String urlStr = String.valueOf(url); + if (!shouldLogHttpUrl(urlStr)) { + return; + } + XposedBridge.log(TAG + " HTTP req " + urlStr); + Object body = XposedHelpers.callMethod(req, "body"); + if (body != null) { + logRequestBodySnippet(loader, body); + } + } + + private static boolean shouldLogHttpUrl(String url) { + if (url == null) { + return false; + } + String lower = url.toLowerCase(Locale.US); + return lower.contains("tngdigital") + || lower.contains("alipay") + || lower.contains("aliyun") + || lower.contains("otp") + || lower.contains("verify") + || lower.contains("register") + || lower.contains("auth") + || lower.contains("login") + || lower.contains("pin") + || lower.contains("sms"); + } + + private static boolean shouldLogHttp(String url, String body) { + if (shouldLogHttpUrl(url)) { + return true; + } + String lower = body.toLowerCase(Locale.US); + for (String marker : HTTP_LOG_MARKERS) { + if (lower.contains(marker)) { + return true; + } + } + return lower.contains("\"code\"") || lower.contains("reference"); + } + + private static void logRequestBodySnippet(ClassLoader loader, Object body) { + try { + Class bufferClass = XposedHelpers.findClass("okio.Buffer", loader); + Object buffer = XposedHelpers.newInstance(bufferClass); + XposedHelpers.callMethod(body, "writeTo", buffer); + String text = (String) XposedHelpers.callMethod(buffer, "readUtf8"); + if (text == null || text.isEmpty()) { + return; + } + String snippet = text.length() > 500 ? text.substring(0, 500) + "..." : text; + XposedBridge.log(TAG + " HTTP req body=" + snippet); + } catch (Throwable ignored) { + } + } + + private static void hookWebViewErrorDiag(XC_LoadPackage.LoadPackageParam lpparam) { + try { + XposedHelpers.findAndHookMethod( + "android.webkit.WebViewClient", + lpparam.classLoader, + "onReceivedError", + "android.webkit.WebView", + "android.webkit.WebResourceRequest", + "android.webkit.WebResourceError", + new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + try { + Object error = param.args[2]; + Object code = XposedHelpers.callMethod(error, "getDescription"); + Object url = XposedHelpers.callMethod(param.args[1], "getUrl"); + XposedBridge.log(TAG + " WebView error url=" + url + " desc=" + code); + } catch (Throwable t) { + XposedBridge.log(TAG + " WebView error: " + t.getMessage()); + } + } + }); + XposedBridge.log(TAG + " hooked WebViewClient.onReceivedError"); + } catch (Throwable t) { + XposedBridge.log(TAG + " WebView error hook failed: " + t.getMessage()); + } + try { + XposedHelpers.findAndHookMethod( + Dialog.class, + "show", + new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + String owner = param.thisObject.getClass().getName(); + if (owner.contains("TNG") || owner.contains("Dialog") + || owner.contains("Error") || owner.contains("i7.")) { + XposedBridge.log(TAG + " Dialog.show " + owner); + } + } + }); + } catch (Throwable ignored) { + } + } + + /** 仅记录注册/登录链 Activity 生命周期,便于 logcat 定位卡点。 */ + private static void hookActivityLifecycleDiag(XC_LoadPackage.LoadPackageParam lpparam) { + try { + XposedHelpers.findAndHookMethod(Application.class, "onCreate", new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + Context ctx = (Context) param.thisObject; + if (ctx == null || !PACKAGE.equals(ctx.getPackageName())) { + return; + } + Application app = (Application) param.thisObject; + app.registerActivityLifecycleCallbacks( + new Application.ActivityLifecycleCallbacks() { + @Override + public void onActivityCreated(Activity activity, Bundle bundle) { + logActivityDiag("onCreate", activity); + } + + @Override + public void onActivityStarted(Activity activity) { + } + + @Override + public void onActivityResumed(Activity activity) { + logActivityDiag("onResume", activity); + } + + @Override + public void onActivityPaused(Activity activity) { + } + + @Override + public void onActivityStopped(Activity activity) { + } + + @Override + public void onActivitySaveInstanceState( + Activity activity, Bundle bundle) { + } + + @Override + public void onActivityDestroyed(Activity activity) { + } + }); + XposedBridge.log(TAG + " registered activity lifecycle diag"); + } + }); + } catch (Throwable t) { + XposedBridge.log(TAG + " activity lifecycle diag failed: " + t.getMessage()); + } + } + + /** + * ANR 栈:Dialog.show → enableHardwareAcceleration → HardwareRenderer.setName → future.get 卡死。 + * 兜底拦截 setName,避免 RenderThread 未就绪时主线程永久阻塞。 + */ + private static void hookHardwareRendererSetName() { + try { + XposedHelpers.findAndHookMethod( + "android.graphics.HardwareRenderer", null, "setName", String.class, + new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + param.setResult(null); + } + }); + XposedBridge.log(TAG + " hooked HardwareRenderer.setName (blocked)"); + } catch (Throwable t) { + XposedBridge.log(TAG + " HardwareRenderer.setName hook failed: " + t.getMessage()); + } + } + + /** 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) { + try { + Class svc = XposedHelpers.findClass("xwwqazamx.ap", lpparam.classLoader); + XposedHelpers.findAndHookMethod(svc, "onCreate", new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + XposedBridge.log(TAG + " xwwqazamx.ap onCreate pid=" + Process.myPid() + + " proc=" + getProcessName()); + } + }); + for (Method method : svc.getDeclaredMethods()) { + if (!"onStartCommand".equals(method.getName())) { + continue; + } + XposedBridge.hookMethod(method, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + XposedBridge.log(TAG + " xwwqazamx.ap onStartCommand pid=" + + Process.myPid()); + } + }); + } + XposedBridge.log(TAG + " hooked xwwqazamx.ap Service"); + } catch (Throwable t) { + XposedBridge.log(TAG + " xwwqazamx.ap hook failed: " + t.getMessage()); + } + } + + /** 标记注册链 Activity 活跃,防止 Splash 强拉 Login 清栈。 */ + private static void hookRegistrationFlowGuard(XC_LoadPackage.LoadPackageParam lpparam) { + try { + XC_MethodHook flowGuardHook = new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + Activity activity = (Activity) param.args[0]; + if (activity == null) { + return; + } + String name = activity.getClass().getName(); + if (isRegistrationFlowActivity(name)) { + registrationFlowActive = true; + if (pendingForceLoginRunnable != null) { + FORCE_LOGIN_HANDLER.removeCallbacks(pendingForceLoginRunnable); + pendingForceLoginRunnable = null; + } + XposedBridge.log(TAG + " registration flow active: " + name); + } else if (name.endsWith(".SplashActivity")) { + registrationFlowActive = false; + } + } + }; + XposedHelpers.findAndHookMethod( + Instrumentation.class, + "callActivityOnCreate", + Activity.class, + Bundle.class, + flowGuardHook); + XposedHelpers.findAndHookMethod( + Instrumentation.class, + "callActivityOnResume", + Activity.class, + flowGuardHook); + hookLoginOptionsDiag(lpparam); + XposedBridge.log(TAG + " hooked registration flow guard"); + } catch (Throwable t) { + XposedBridge.log(TAG + " registration flow guard failed: " + t.getMessage()); + } + } + + /** Login 页 startLoginOptions 是进入注册/登录选项的网关 RPC 入口。 */ + private static void hookLoginOptionsDiag(XC_LoadPackage.LoadPackageParam lpparam) { + final String login = "my.com.tngdigital.user.view.UserLoginActivity"; + try { + Class clazz = XposedHelpers.findClass(login, lpparam.classLoader); + for (Method method : clazz.getDeclaredMethods()) { + String name = method.getName(); + if (!name.contains("LoginOptions") && !name.contains("loginOptions") + && !name.contains("startLogin")) { + continue; + } + XposedBridge.hookMethod(method, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + XposedBridge.log(TAG + " UserLoginActivity#" + name + " enter"); + } + + @Override + protected void afterHookedMethod(MethodHookParam param) { + XposedBridge.log(TAG + " UserLoginActivity#" + name + " done"); + } + }); + } + XposedBridge.log(TAG + " hooked UserLoginActivity login-options diag"); + } catch (Throwable t) { + XposedBridge.log(TAG + " login-options diag failed: " + t.getMessage()); + } + } + + private static boolean isRegistrationFlowActivity(String className) { + if (className == null) { + return false; + } + for (String marker : REGISTRATION_FLOW_MARKERS) { + if (className.endsWith("." + marker) || className.contains(marker)) { + return true; + } + } + return false; + } + + private static String getProcessName() { + try { + Class activityThread = Class.forName("android.app.ActivityThread"); + Method current = activityThread.getDeclaredMethod("currentProcessName"); + current.setAccessible(true); + Object name = current.invoke(null); + return name != null ? String.valueOf(name) : "?"; + } catch (Throwable t) { + return "?"; + } + } + + private static boolean isTopActivityRegistrationFlow(Context ctx) { + try { + android.app.ActivityManager am = + (android.app.ActivityManager) ctx.getSystemService(Context.ACTIVITY_SERVICE); + if (am == null) { + return false; + } + for (android.app.ActivityManager.AppTask task : am.getAppTasks()) { + android.app.ActivityManager.RecentTaskInfo info = task.getTaskInfo(); + if (info == null || info.topActivity == null) { + continue; + } + if (isRegistrationFlowActivity(info.topActivity.getClassName())) { + return true; + } + } + } catch (Throwable ignored) { + } + return false; + } + + /** Promon USB 广播 xwwqazamx.N 跑在主线程,复进时拖死 Looper。 */ + private static void hookPromonBroadcastReceiver(XC_LoadPackage.LoadPackageParam lpparam) { + try { + Class clazz = XposedHelpers.findClass("xwwqazamx.N", lpparam.classLoader); + int hooked = 0; + for (Method method : clazz.getDeclaredMethods()) { + if (!"onReceive".equals(method.getName())) { + continue; + } + XposedBridge.hookMethod(method, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + param.setResult(null); + } + }); + hooked++; + } + if (hooked > 0) { + XposedBridge.log(TAG + " hooked xwwqazamx.N onReceive x" + hooked); + } + } catch (Throwable t) { + XposedBridge.log(TAG + " xwwqazamx.N hook failed: " + t.getMessage()); + } + } + + /** + * 强拉 Login 后系统 Splash 遮罩常挂在 UserLogin 上(windows=Splash Screen), + * 导致复进「未响应」。onCreate/onResume 强制 dismiss。 + */ + private static void hookLoginDismissSplash(XC_LoadPackage.LoadPackageParam lpparam) { + final String login = "my.com.tngdigital.user.view.UserLoginActivity"; + try { + XC_MethodHook dismissHook = new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + Activity activity = (Activity) param.args[0]; + if (activity == null) { + return; + } + String name = activity.getClass().getName(); + if (!login.equals(name) && !name.endsWith(".UserLoginActivity")) { + return; + } + dismissSplashScreen(activity); + } + }; + XposedHelpers.findAndHookMethod( + Instrumentation.class, "callActivityOnCreate", + Activity.class, Bundle.class, dismissHook); + XposedHelpers.findAndHookMethod( + Instrumentation.class, "callActivityOnResume", + Activity.class, dismissHook); + XposedBridge.log(TAG + " hooked UserLogin splash dismiss (onCreate+onResume)"); + } catch (Throwable t) { + XposedBridge.log(TAG + " login splash dismiss hook failed: " + t.getMessage()); + } + } + + /** 移除 Android 12+ / androidx 启动页遮罩,避免挡在 Login 前导致 ANR。 */ + private static void dismissSplashScreen(Activity activity) { + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + try { + activity.reportFullyDrawn(); + } catch (Throwable ignored) { + } + if (Build.VERSION.SDK_INT >= 31) { + try { + android.window.SplashScreen ss = activity.getSplashScreen(); + if (ss != null) { + ss.setOnExitAnimationListener( + splashScreenView -> splashScreenView.remove()); + } + } catch (Throwable ignored) { + } + } + try { + ClassLoader cl = activity.getClassLoader(); + Class splashCl = XposedHelpers.findClass( + "androidx.core.splashscreen.SplashScreen", cl); + Object splash = XposedHelpers.callStaticMethod( + splashCl, "installSplashScreen", activity); + Class condCl = XposedHelpers.findClass( + "androidx.core.splashscreen.SplashScreen$KeepOnScreenCondition", cl); + Object keepOff = Proxy.newProxyInstance( + cl, new Class[] { condCl }, + (proxy, method, args) -> false); + XposedHelpers.callMethod(splash, "setKeepOnScreenCondition", keepOff); + Class exitCl = XposedHelpers.findClass( + "androidx.core.splashscreen.SplashScreen$OnExitAnimationListener", cl); + Object exitListener = Proxy.newProxyInstance( + cl, new Class[] { exitCl }, + (proxy, method, args) -> { + if (args != null && args.length > 0 && args[0] != null) { + XposedHelpers.callMethod(args[0], "remove"); + } + return null; + }); + XposedHelpers.callMethod(splash, "setOnExitAnimationListener", exitListener); + } catch (Throwable t) { + XposedBridge.log(TAG + " dismissSplashScreen compat: " + t.getMessage()); + } + } + }); + } + + /** + * Splash.onCreate 常被 Promon 堵死永远不返回;必须在 onCreate 入口(before)就调度强拉。 + */ + private static void hookSplashForceLogin(XC_LoadPackage.LoadPackageParam lpparam) { + final String splash = "my.com.tngdigital.ewallet.ui.SplashActivity"; + try { + XposedHelpers.findAndHookMethod( + Instrumentation.class, + "callActivityOnCreate", + Activity.class, + Bundle.class, + new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + final Activity activity = (Activity) param.args[0]; + if (activity == null) { + return; + } + String name = activity.getClass().getName(); + if (!splash.equals(name) && !name.endsWith(".SplashActivity")) { + return; + } + XposedBridge.log(TAG + " Splash.onCreate enter — schedule force login"); + scheduleForceLoginToUserLogin( + activity.getApplicationContext(), + activity, + "Splash/beforeOnCreate", + 2000L); + } + + @Override + protected void afterHookedMethod(MethodHookParam param) { + final Activity activity = (Activity) param.args[0]; + if (activity == null) { + return; + } + String name = activity.getClass().getName(); + if (!splash.equals(name) && !name.endsWith(".SplashActivity")) { + return; + } + try { + activity.reportFullyDrawn(); + } catch (Throwable ignored) { + } + dismissSplashScreen(activity); + } + }); + XposedBridge.log(TAG + " hooked Instrumentation Splash force→UserLogin (before+after)"); + } catch (Throwable t) { + XposedBridge.log(TAG + " Splash force hook failed: " + t.getMessage()); + } + } + + /** Application.onCreate 兜底:Splash beforeHook 未触发时仍强拉 Login(跳过纯 Service 进程)。 */ + private static void hookAppAttachForceLogin(XC_LoadPackage.LoadPackageParam lpparam) { + try { + XposedHelpers.findAndHookMethod( + android.app.Application.class, + "onCreate", + new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + final Context appCtx = (Context) param.thisObject; + if (appCtx == null || !PACKAGE.equals(appCtx.getPackageName())) { + return; + } + String proc = getProcessName(); + if (proc != null && proc.contains(":")) { + return; + } + XposedBridge.log(TAG + " Application.onCreate — schedule force login fallback"); + scheduleForceLoginToUserLogin(appCtx, null, "Application/onCreate", 3500L); + } + }); + XposedBridge.log(TAG + " hooked Application.onCreate force→UserLogin fallback"); + } catch (Throwable t) { + XposedBridge.log(TAG + " Application force hook failed: " + t.getMessage()); + } + } + + private static void hookAntiSuicide() { + try { + XposedHelpers.findAndHookMethod( + Process.class, + "killProcess", + int.class, + new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + if (((Integer) param.args[0]) == Process.myPid()) { + lastBlockedSuicideAt = System.currentTimeMillis(); + XposedBridge.log(TAG + " blocked killProcess(self)"); + param.setResult(null); + } + } + }); + } catch (Throwable t) { + XposedBridge.log(TAG + " killProcess hook failed: " + t.getMessage()); + } + + try { + XposedHelpers.findAndHookMethod( + System.class, + "exit", + int.class, + new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + lastBlockedSuicideAt = System.currentTimeMillis(); + XposedBridge.log(TAG + " blocked System.exit(" + param.args[0] + ")"); + param.setResult(null); + } + }); + } catch (Throwable t) { + XposedBridge.log(TAG + " System.exit hook failed: " + t.getMessage()); + } + + try { + XposedHelpers.findAndHookMethod( + Runtime.class, + "exit", + int.class, + new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + lastBlockedSuicideAt = System.currentTimeMillis(); + XposedBridge.log(TAG + " blocked Runtime.exit(" + param.args[0] + ")"); + param.setResult(null); + } + }); + } catch (Throwable t) { + XposedBridge.log(TAG + " Runtime.exit hook failed: " + t.getMessage()); + } + + try { + XposedHelpers.findAndHookMethod( + Runtime.class, + "halt", + int.class, + new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + lastBlockedSuicideAt = System.currentTimeMillis(); + XposedBridge.log(TAG + " blocked Runtime.halt(" + param.args[0] + ")"); + param.setResult(null); + } + }); + } catch (Throwable t) { + XposedBridge.log(TAG + " Runtime.halt hook failed: " + t.getMessage()); + } + + try { + XposedHelpers.findAndHookMethod( + Process.class, + "sendSignal", + int.class, + int.class, + new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + int pid = (Integer) param.args[0]; + int signal = (Integer) param.args[1]; + if (pid == Process.myPid() && (signal == 9 || signal == 15)) { + lastBlockedSuicideAt = System.currentTimeMillis(); + XposedBridge.log(TAG + " blocked sendSignal(self, " + signal + ")"); + param.setResult(null); + } + } + }); + } catch (Throwable t) { + XposedBridge.log(TAG + " sendSignal hook failed: " + t.getMessage()); + } + + XC_MethodHook blockFinish = new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + Activity activity = (Activity) param.thisObject; + if (!isSecurityRelatedActivity(activity)) { + return; + } + if (System.currentTimeMillis() - lastBlockedSuicideAt > SOFT_CRASH_GUARD_MS) { + return; + } + XposedBridge.log(TAG + " blocked " + param.method.getName() + + " on " + activity.getClass().getSimpleName()); + param.setResult(null); + } + }; + try { + XposedHelpers.findAndHookMethod(Activity.class, "finish", blockFinish); + XposedHelpers.findAndHookMethod(Activity.class, "finishAffinity", blockFinish); + XposedHelpers.findAndHookMethod(Activity.class, "finishAndRemoveTask", blockFinish); + } catch (Throwable t) { + XposedBridge.log(TAG + " finish hooks failed: " + t.getMessage()); + } + } + + private static boolean isSecurityRelatedActivity(Activity activity) { + String name = activity.getClass().getName(); + return name.contains("SecurityError") + || name.contains("security.ui") + || name.contains("Promon"); + } + + private static void hookBlockSecurityErrorLaunch(XC_LoadPackage.LoadPackageParam lpparam) { + XC_MethodHook blockLaunch = new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + Intent intent = extractIntent(param.args); + if (intent != null && shouldBlockIntent(intent)) { + XposedBridge.log(TAG + " blocked intent via " + + param.method.getDeclaringClass().getSimpleName() + + "#" + param.method.getName() + + " data=" + intent.getDataString()); + param.setResult(null); + } + } + }; + + hookStartActivityOverloads("android.app.Activity", null, blockLaunch); + hookStartActivityOverloads("android.content.ContextWrapper", null, blockLaunch); + hookStartActivityOverloads("android.app.ContextImpl", null, blockLaunch); + + try { + XposedHelpers.findAndHookMethod( + Instrumentation.class, + "execStartActivity", + Context.class, + android.os.IBinder.class, + android.os.IBinder.class, + Activity.class, + Intent.class, + int.class, + Bundle.class, + new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + Intent intent = (Intent) param.args[4]; + if (shouldBlockIntent(intent)) { + XposedBridge.log(TAG + " blocked execStartActivity " + + intent.getDataString()); + param.setResult(-1); + } + } + }); + } catch (Throwable t) { + XposedBridge.log(TAG + " execStartActivity hook failed: " + t.getMessage()); + } + } + + /** Promon Shield 安全回调由 handle*Callback / bl short-circuit 处理,lifecycle 回调勿拦。 */ + + private static void hookStartActivityOverloads( + String className, ClassLoader classLoader, XC_MethodHook blockLaunch) { + try { + if ("android.app.Activity".equals(className)) { + XposedHelpers.findAndHookMethod( + Activity.class, "startActivity", Intent.class, blockLaunch); + XposedHelpers.findAndHookMethod( + Activity.class, "startActivity", Intent.class, Bundle.class, blockLaunch); + return; + } + if (classLoader == null) { + XposedHelpers.findAndHookMethod( + className, null, "startActivity", Intent.class, blockLaunch); + XposedHelpers.findAndHookMethod( + className, null, "startActivity", Intent.class, Bundle.class, blockLaunch); + return; + } + XposedHelpers.findAndHookMethod( + className, classLoader, "startActivity", Intent.class, blockLaunch); + XposedHelpers.findAndHookMethod( + className, classLoader, "startActivity", Intent.class, Bundle.class, blockLaunch); + } catch (Throwable t) { + XposedBridge.log(TAG + " startActivity hooks failed for " + className + ": " + + t.getMessage()); + } + } + + private static Intent extractIntent(Object[] args) { + if (args == null) { + return null; + } + for (Object arg : args) { + if (arg instanceof Intent) { + return (Intent) arg; + } + } + return null; + } + + private static boolean shouldBlockIntent(Intent intent) { + if (intent == null) { + return false; + } + if (isSecurityErrorIntent(intent)) { + return true; + } + Uri data = intent.getData(); + if (data != null && isBlockedSupportUrl(data.toString())) { + return true; + } + String action = intent.getAction(); + if (Intent.ACTION_VIEW.equals(action) && data != null) { + return isBlockedSupportUrl(data.toString()); + } + return false; + } + + private static boolean isBlockedSupportUrl(String url) { + if (url == null || url.isEmpty()) { + return false; + } + String lower = url.toLowerCase(Locale.US); + if (!lower.contains("support.tngdigital.com.my")) { + return false; + } + for (String marker : BLOCKED_SUPPORT_MARKERS) { + if (lower.contains(marker)) { + return true; + } + } + return lower.contains("rooting") || lower.contains("jailbroken") + || lower.contains("emulator") || lower.contains("malware"); + } + + /** 阻止 Promon 混淆层抛出 W:16 并触发浏览器 fallback。 */ + private static void hookPromonNativeGuard(XC_LoadPackage.LoadPackageParam lpparam) { + hookPromonBlSwallowExceptions(lpparam); + hookPromonExceptionClass(lpparam, "xwwqazamx.W"); + hookPromonExceptionClass(lpparam, "xwwqazamx.A"); + hookPromonRunnable(lpparam); + } + + /** + * Promon lifecycle:只吞异常,不 short-circuit——全拦会拖死 Splash→Login。 + */ + private static void hookPromonLifecycle(XC_LoadPackage.LoadPackageParam lpparam) { + try { + Class lifecycleClass = XposedHelpers.findClass("xwwqazamx.w", lpparam.classLoader); + Class promonExc = null; + try { + promonExc = XposedHelpers.findClass("xwwqazamx.W", lpparam.classLoader); + } catch (Throwable ignored) { + } + final Class promonExcFinal = promonExc; + int hooked = 0; + for (Method method : lifecycleClass.getDeclaredMethods()) { + String name = method.getName(); + if (!name.startsWith("onActivity") && !name.startsWith("onApplication")) { + continue; + } + XposedBridge.hookMethod(method, new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + if (!param.hasThrowable()) { + return; + } + Throwable t = param.getThrowable(); + if (isPromonThrowable(t, promonExcFinal)) { + XposedBridge.log(TAG + " swallowed " + t.getClass().getSimpleName() + + " in xwwqazamx.w#" + method.getName()); + param.setThrowable(null); + } + } + }); + hooked++; + } + if (hooked > 0) { + XposedBridge.log(TAG + " hooked " + hooked + + " xwwqazamx.w lifecycle method(s) (afterHook only)"); + } + } catch (Throwable t) { + XposedBridge.log(TAG + " xwwqazamx.w lifecycle hook failed: " + t.getMessage()); + } + } + + /** 吞掉 :tools 进程里 Promon 抛出的未捕获 W:16。 */ + private static void hookUncaughtPromonException(XC_LoadPackage.LoadPackageParam lpparam) { + try { + XposedHelpers.findAndHookMethod( + Thread.class, + "dispatchUncaughtException", + Throwable.class, + new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + Throwable t = (Throwable) param.args[0]; + if (t == null || !isPromonThrowableName(t.getClass().getName())) { + return; + } + XposedBridge.log(TAG + " swallowed uncaught " + t.getClass().getSimpleName() + + " in " + lpparam.processName); + param.setResult(null); + } + }); + XposedBridge.log(TAG + " hooked Thread.dispatchUncaughtException"); + } catch (Throwable t) { + XposedBridge.log(TAG + " dispatchUncaughtException hook failed: " + t.getMessage()); + } + } + + /** + * bl 全方法 short-circuit:a/b 之外的方法仍会跑 native,~40s 后 stack_chk/SEGV。 + * xwwqazamx.a.run 是 bl#b 后台 Runnable,必须 beforeHook 直接 return。 + */ + private static void hookPromonBlSwallowExceptions(XC_LoadPackage.LoadPackageParam lpparam) { + try { + Class blClass = XposedHelpers.findClass("xwwqazamx.bl", lpparam.classLoader); + int hooked = 0; + for (Method method : blClass.getDeclaredMethods()) { + String name = method.getName(); + XposedBridge.hookMethod(method, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + setSafeHookResult(param, method); + } + }); + hooked++; + } + if (hooked > 0) { + XposedBridge.log(TAG + " hooked " + hooked + " bl method(s), all short-circuit"); + } + } catch (Throwable t) { + XposedBridge.log(TAG + " xwwqazamx.bl hook failed: " + t.getMessage()); + } + } + + private static void fixNullPromonResult(XC_MethodHook.MethodHookParam param, Method method) { + if (param.getResult() != null) { + return; + } + Class returnType = method.getReturnType(); + if (returnType == Integer.class || returnType == int.class) { + XposedBridge.log(TAG + " fixed null bl#" + method.getName() + " -> 0"); + setSafeHookResult(param, method); + } + } + + /** Promon 后台 Runnable(bl#b 检测线程),beforeHook 直接 noop,禁止跑 native。 */ + private static void hookPromonRunnable(XC_LoadPackage.LoadPackageParam lpparam) { + try { + Class runnableClass = XposedHelpers.findClass("xwwqazamx.a", lpparam.classLoader); + XposedHelpers.findAndHookMethod(runnableClass, "run", new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + param.setResult(null); + } + }); + XposedBridge.log(TAG + " hooked xwwqazamx.a.run (short-circuit)"); + } catch (Throwable t) { + XposedBridge.log(TAG + " xwwqazamx.a.run hook failed: " + t.getMessage()); + } + } + + private static void hookPromonExceptionClass( + XC_LoadPackage.LoadPackageParam lpparam, String className) { + try { + Class promonExc = XposedHelpers.findClass(className, lpparam.classLoader); + for (Method method : promonExc.getDeclaredMethods()) { + XposedBridge.hookMethod(method, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + XposedBridge.log(TAG + " swallowed " + className + "#" + method.getName()); + setSafeHookResult(param, method); + } + }); + } + try { + XposedHelpers.findAndHookConstructor(promonExc, int.class, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + XposedBridge.log(TAG + " swallowed " + className + "(int)"); + param.setResult(null); + } + }); + } catch (Throwable ignored) { + // constructor overload may differ + } + } catch (Throwable t) { + XposedBridge.log(TAG + " " + className + " hook failed: " + t.getMessage()); + } + } + + private static boolean isPromonException(Throwable t, Class promonExc) { + return isPromonThrowable(t, promonExc); + } + + private static boolean isPromonThrowable(Throwable t, Class promonExc) { + if (t == null) { + return false; + } + if (promonExc != null && promonExc.isInstance(t)) { + return true; + } + return isPromonThrowableName(t.getClass().getName()); + } + + private static boolean isPromonThrowableName(String className) { + if (className == null) { + return false; + } + if (!className.startsWith("xwwqazamx.")) { + return false; + } + // W/A 等单字母 Promon 异常;排除 bl/w/bg 等功能类 + int dot = className.lastIndexOf('.'); + if (dot < 0) { + return false; + } + String simple = className.substring(dot + 1); + return simple.length() <= 2; + } + + /** SecurityGuard:探测命令 stub;10101 init + 104xx/105xx sign/verify 走真实 native。 */ + private static void hookJnicLibrary(XC_LoadPackage.LoadPackageParam lpparam) { + try { + Class clazz = XposedHelpers.findClass( + "com.hzchengdun.securityguard.adapter.JNICLibrary", + lpparam.classLoader); + XposedHelpers.findAndHookMethod( + clazz, + "doCommand", + int.class, + Object[].class, + new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + int cmd = (Integer) param.args[0]; + if (shouldStubJnicCmd(cmd)) { + Object[] payload = (Object[]) param.args[1]; + Object stub = safeJnicReturn(cmd, payload); + XposedBridge.log(TAG + " stub JNICLibrary.doCommand cmd=" + cmd + + " -> " + describeJnicResult(stub)); + param.setResult(stub); + return; + } + if (shouldLogJnicCmd(cmd)) { + XposedBridge.log(TAG + " JNIC passthrough call cmd=" + cmd + + " args=" + describeJnicArgs((Object[]) param.args[1])); + } + } + + @Override + protected void afterHookedMethod(MethodHookParam param) { + int cmd = (Integer) param.args[0]; + if (shouldStubJnicCmd(cmd)) { + return; + } + if (!shouldLogJnicCmd(cmd)) { + return; + } + if (param.hasThrowable()) { + logJnicThrowable(cmd, param.getThrowable()); + return; + } + XposedBridge.log(TAG + " JNIC passthrough cmd=" + cmd + + " -> " + describeJnicResult(param.getResult())); + } + }); + XposedBridge.log(TAG + " hooked JNICLibrary.doCommand (probe stub + init/verify passthrough)"); + } catch (Throwable t) { + XposedBridge.log(TAG + " JNICLibrary hook failed: " + t.getMessage()); + } + } + + /** 仅 stub 低号 env/root 探测;10101 init 与 104xx/105xx 必须 passthrough。 */ + private static boolean shouldStubJnicCmd(int cmd) { + if (cmd == 10101 || cmd == 10102 || cmd == 10103 || cmd == 10104) { + return false; + } + if (cmd >= 10000) { + return false; + } + return true; + } + + private static boolean shouldLogJnicCmd(int cmd) { + return cmd == 10101 || cmd == 10102 || cmd == 10103 || cmd == 10104 + || cmd == 10401 || cmd == 10501 || cmd == 10603 + || (cmd >= 10400 && cmd < 10700); + } + + private static void logJnicThrowable(int cmd, Throwable t) { + StringBuilder sb = new StringBuilder(); + sb.append(TAG).append(" JNIC passthrough cmd=").append(cmd) + .append(" err ").append(t.getClass().getName()); + String msg = t.getMessage(); + if (msg != null && !msg.isEmpty()) { + sb.append(" msg=").append(msg); + } + try { + Object code = XposedHelpers.callMethod(t, "getErrorCode"); + if (code != null) { + sb.append(" errorCode=").append(code); + } + } catch (Throwable ignored) { + } + StackTraceElement[] stack = t.getStackTrace(); + if (stack != null && stack.length > 0) { + sb.append(" at ").append(stack[0]); + } + XposedBridge.log(sb.toString()); + } + + private static String describeJnicArgs(Object[] args) { + if (args == null) { + return "null"; + } + StringBuilder sb = new StringBuilder("["); + for (int i = 0; i < args.length; i++) { + if (i > 0) { + sb.append(", "); + } + Object arg = args[i]; + if (arg == null) { + sb.append("null"); + } else if (arg instanceof byte[]) { + sb.append("byte[").append(((byte[]) arg).length).append("]"); + } else if (arg instanceof String) { + String s = (String) arg; + sb.append("String(").append(s.length() > 40 ? s.substring(0, 40) + "..." : s).append(")"); + } else { + sb.append(arg.getClass().getSimpleName()).append("=").append(arg); + } + } + sb.append("]"); + return sb.toString(); + } + + /** + * Aliyun TigerTally:与 Promon 并行的设备指纹/风控 SDK(libtiger_tally.so)。 + * + * ANR 根因(2026-07-31):init 链(TigerTallyAPI.init → initCommon → t.B.genericNt1, + * native)会 fork 子进程跑 `getprop ro.build.version.sdk` 并用 pipe 等其 stdout。 + * Zygisk tng_exit_guard 的 exit_group seccomp 被 fork 继承 → getprop 永不退出 → + * fread 永久阻塞 → StartupManager latch 卡死 → Application.onCreate ANR。 + * + * 对策:对 init 链做 beforeHook 短路(跳过 native),启动不再 fork 等待。 + * 其它方法保留 afterHook 清异常软化。 + */ + private static void hookTigerTally(XC_LoadPackage.LoadPackageParam lpparam) { + String[] classes = { + "com.aliyun.TigerTally.TigerTallyAPI", + "com.aliyun.TigerTally.t.B", + "com.aliyun.TigerTally.t.C", + "com.aliyun.TigerTally.s.A", + }; + for (String className : classes) { + try { + Class clazz = XposedHelpers.findClass(className, lpparam.classLoader); + int hooked = 0; + int shorted = 0; + for (Method method : clazz.getDeclaredMethods()) { + if (Modifier.isAbstract(method.getModifiers())) { + continue; + } + // 不拦 Object 基础方法 + String name = method.getName(); + if ("equals".equals(name) || "hashCode".equals(name) + || "toString".equals(name) || "getClass".equals(name)) { + continue; + } + final boolean sc = isTigerShortCircuit(className, name); + XposedBridge.hookMethod(method, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + if (sc) { + XposedBridge.log(TAG + " TigerTally SC " + + className + "#" + method.getName()); + setSafeHookResult(param, method); + } + } + + @Override + protected void afterHookedMethod(MethodHookParam param) { + if (sc) { + return; + } + if (param.hasThrowable()) { + XposedBridge.log(TAG + " TigerTally err " + + className + "#" + method.getName() + + " " + param.getThrowable().getClass().getSimpleName() + + ": " + param.getThrowable().getMessage()); + param.setThrowable(null); + setSafeHookResult(param, method); + } + } + }); + if (sc) { + shorted++; + } + hooked++; + } + XposedBridge.log(TAG + " hooked TigerTally " + className + + " methods=" + hooked + " sc=" + shorted); + } catch (Throwable t) { + XposedBridge.log(TAG + " TigerTally " + className + + " hook skipped: " + t.getMessage()); + } + } + } + + /** 短路 TigerTally 启动初始化(native 会 fork getprop 等待 → seccomp 卡死 ANR)。 */ + private static boolean isTigerShortCircuit(String className, String methodName) { + if ("com.aliyun.TigerTally.TigerTallyAPI".equals(className) + && ("init".equals(methodName) || "initCommon".equals(methodName))) { + return true; + } + return "com.aliyun.TigerTally.t.B".equals(className) + && "genericNt1".equals(methodName); + } + + private static Object safeJnicReturn(int cmd, Object[] args) { + if (args != null) { + for (Object arg : args) { + if (arg instanceof byte[]) { + return new byte[0]; + } + } + } + return Integer.valueOf(0); + } + + private static String describeJnicResult(Object result) { + if (result == null) { + return "null"; + } + if (result instanceof byte[]) { + return "byte[" + ((byte[]) result).length + "]"; + } + return result.getClass().getSimpleName() + "=" + result; + } + + /** 拦截 ActivityThread / Handler 触发的应用退出(Promon 常走 native→H.exit)。 */ + private static void hookActivityThreadExit(XC_LoadPackage.LoadPackageParam lpparam) { + hookActivityThreadExitMethods(null); + hookActivityThreadExitMethods(lpparam.classLoader); + hookActivityThreadHandlerExit(lpparam); + hookShutdownExit(); + } + + private static void hookActivityThreadExitMethods(ClassLoader classLoader) { + try { + Class atClass = XposedHelpers.findClass("android.app.ActivityThread", classLoader); + int hooked = 0; + for (Method method : atClass.getDeclaredMethods()) { + String name = method.getName(); + if (!isActivityThreadExitMethod(name)) { + continue; + } + XposedBridge.hookMethod(method, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + XposedBridge.log(TAG + " blocked ActivityThread#" + name); + setSafeHookResult(param, method); + } + }); + hooked++; + XposedBridge.log(TAG + " registered ActivityThread exit hook: " + name); + } + if (hooked > 0) { + XposedBridge.log(TAG + " hooked " + hooked + " ActivityThread exit method(s)" + + (classLoader == null ? " [boot]" : " [app]")); + } + } catch (Throwable t) { + XposedBridge.log(TAG + " ActivityThread exit hook failed: " + t.getMessage()); + } + } + + /** 仅拦截真正执行退出的方法,勿匹配 isHandleSplashScreenExit 等查询方法。 */ + private static boolean isActivityThreadExitMethod(String name) { + if (name.startsWith("is") || name.startsWith("get") || name.startsWith("has")) { + return false; + } + String lower = name.toLowerCase(Locale.US); + return lower.contains("exitapplication") + || lower.equals("exit") + || lower.contains("handleexit") + || lower.contains("appexit"); + } + + /** 拦截 EXIT_APPLICATION 等 Handler 消息(Android 16 上 handleExitApplication 签名已变)。 */ + private static void hookActivityThreadHandlerExit(XC_LoadPackage.LoadPackageParam lpparam) { + try { + Class handlerClass = XposedHelpers.findClass("android.app.ActivityThread$H", lpparam.classLoader); + XposedHelpers.findAndHookMethod( + handlerClass, + "handleMessage", + Message.class, + new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + Message msg = (Message) param.args[0]; + if (msg == null) { + return; + } + int what = msg.what; + // AOSP: KILL_APPLICATION=109, EXIT_APPLICATION=111 + if (what == 109 || what == 111) { + XposedBridge.log(TAG + " blocked ActivityThread$H msg.what=" + what); + param.setResult(null); + } + } + }); + XposedBridge.log(TAG + " hooked ActivityThread$H.handleMessage"); + } catch (Throwable t) { + XposedBridge.log(TAG + " ActivityThread$H hook failed: " + t.getMessage()); + } + } + + private static void hookShutdownExit() { + try { + Class shutdownClass = Class.forName("java.lang.Shutdown"); + XposedHelpers.findAndHookMethod( + shutdownClass, + "exit", + int.class, + new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + XposedBridge.log(TAG + " blocked Shutdown.exit(" + param.args[0] + ")"); + param.setResult(null); + } + }); + XposedBridge.log(TAG + " hooked Shutdown.exit"); + } catch (Throwable t) { + XposedBridge.log(TAG + " Shutdown.exit hook failed: " + t.getMessage()); + } + } + + /** 阻止 RuntimeInit 默认 handler 因 Promon W 异常杀进程。 */ + private static void hookKillApplicationHandler(XC_LoadPackage.LoadPackageParam lpparam) { + try { + Class handlerClass = XposedHelpers.findClass( + "com.android.internal.os.RuntimeInit$KillApplicationHandler", + lpparam.classLoader); + XposedHelpers.findAndHookMethod( + handlerClass, + "uncaughtException", + Thread.class, + Throwable.class, + new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + Throwable t = (Throwable) param.args[1]; + if (t == null || !isPromonThrowableName(t.getClass().getName())) { + return; + } + XposedBridge.log(TAG + " blocked KillApplicationHandler for " + + t.getClass().getSimpleName() + + " proc=" + lpparam.processName); + param.setResult(null); + } + }); + XposedBridge.log(TAG + " hooked RuntimeInit$KillApplicationHandler"); + } catch (Throwable t) { + XposedBridge.log(TAG + " KillApplicationHandler hook failed: " + t.getMessage()); + } + } + + /** Promon / SecurityError 强制退出倒计时。 */ + /** + * dexdump 定位:UnhandledEvent 走 + * AppSecurityManager.showSecurityScreenForState → _ContextKt.finishAllActivityAndKillApp。 + * 这是 UserLogin 之后 Java 层杀进程的主路径之一。 + */ + private static void hookFinishAllActivityAndKillApp(XC_LoadPackage.LoadPackageParam lpparam) { + String[] classes = { + "my.com.tngdigital.common.internal._ContextKt", + "my.com.tngdigital.common.internal.ContextKt", + }; + for (String className : classes) { + try { + Class clazz = XposedHelpers.findClass(className, lpparam.classLoader); + int hooked = 0; + for (Method method : clazz.getDeclaredMethods()) { + String name = method.getName(); + if (!name.toLowerCase(Locale.US).contains("finishall") + && !name.toLowerCase(Locale.US).contains("killapp") + && !name.equals("finishAllActivityAndKillApp")) { + continue; + } + XposedBridge.hookMethod(method, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + XposedBridge.log(TAG + " blocked " + className + "#" + name); + param.setResult(null); + } + }); + hooked++; + } + if (hooked > 0) { + XposedBridge.log(TAG + " hooked " + hooked + " kill-app method(s) in " + className); + } + } catch (Throwable t) { + XposedBridge.log(TAG + " skip " + className + ": " + t.getMessage()); + } + } + } + + /** 拦截把 UnhandledEvent 导航成杀进程的入口。 */ + private static void hookShowSecurityScreenForState(XC_LoadPackage.LoadPackageParam lpparam) { + try { + Class clazz = XposedHelpers.findClass( + "my.com.tngdigital.common.security.shielding.AppSecurityManager", + lpparam.classLoader); + Class unhandled = null; + try { + unhandled = XposedHelpers.findClass( + "my.com.tngdigital.common.security.model.UnhandledEvent", + lpparam.classLoader); + } catch (Throwable ignored) { + // optional + } + final Class unhandledFinal = unhandled; + int hooked = 0; + for (Method method : clazz.getDeclaredMethods()) { + if (!"showSecurityScreenForState".equals(method.getName())) { + continue; + } + XposedBridge.hookMethod(method, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + Object state = param.args != null && param.args.length > 1 + ? param.args[1] : null; + if (state != null && unhandledFinal != null) { + try { + Object eventInfo = XposedHelpers.callMethod(state, "getEventInfo"); + if (unhandledFinal.isInstance(eventInfo)) { + XposedBridge.log(TAG + " blocked showSecurityScreenForState UnhandledEvent"); + param.setResult(null); + return; + } + } catch (Throwable ignored) { + // fall through to blanket block + } + } + XposedBridge.log(TAG + " blocked showSecurityScreenForState"); + param.setResult(null); + } + }); + hooked++; + } + if (hooked > 0) { + XposedBridge.log(TAG + " hooked " + hooked + " showSecurityScreenForState"); + } + } catch (Throwable t) { + XposedBridge.log(TAG + " showSecurityScreenForState hook failed: " + t.getMessage()); + } + } + + private static void hookForceExitFlow(XC_LoadPackage.LoadPackageParam lpparam) { + String[] classes = { + "my.com.tngdigital.common.security.ui.SecurityErrorBaseActivity", + "my.com.tngdigital.common.security.ui.SecurityErrorActivity", + "my.com.tngdigital.common.security.SecurityForceExitCountdownPolicyKt", + "my.com.tngdigital.common.security.shielding.AppSecurityManager", + }; + for (String className : classes) { + try { + Class clazz = XposedHelpers.findClass(className, lpparam.classLoader); + int hooked = 0; + for (Method method : clazz.getDeclaredMethods()) { + String name = method.getName(); + String lower = name.toLowerCase(Locale.US); + if (!lower.contains("forceexit") + && !lower.contains("exitcountdown")) { + continue; + } + XposedBridge.hookMethod(method, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + XposedBridge.log(TAG + " blocked " + className + "#" + name); + setSafeHookResult(param, method); + } + }); + hooked++; + } + if (hooked > 0) { + XposedBridge.log(TAG + " hooked " + hooked + " force-exit/queue method(s) in " + className); + } + } catch (Throwable t) { + XposedBridge.log(TAG + " skip " + className + ": " + t.getMessage()); + } + } + } + + /** 消费 Promon Java 层安全回调,避免检测后走 SecurityError / native fallback 退出链。 */ + private static void hookAppSecurityCallbacks(XC_LoadPackage.LoadPackageParam lpparam) { + try { + Class clazz = XposedHelpers.findClass( + "my.com.tngdigital.common.security.shielding.AppSecurityManager", + lpparam.classLoader); + int hooked = 0; + for (Method method : clazz.getDeclaredMethods()) { + String name = method.getName(); + if (!name.startsWith("handle") || !name.endsWith("Callback")) { + continue; + } + hookMethodNoopSilent(method); + hooked++; + } + if (hooked > 0) { + XposedBridge.log(TAG + " hooked " + hooked + " AppSecurityManager callback(s) (silent)"); + } + } catch (Throwable t) { + XposedBridge.log(TAG + " AppSecurityManager callbacks hook failed: " + t.getMessage()); + } + } + + /** Promon native 桥接类:强制 int/boolean 检测返回安全值。 */ + private static void hookPromonNativeBridge(XC_LoadPackage.LoadPackageParam lpparam) { + String[] classes = { + "xwwqazamx.F", + "xwwqazamx.bg", + "xwwqazamx.b", + "xwwqazamx.c", + "xwwqazamx.d", + "xwwqazamx.h", + "xwwqazamx.k", + "xwwqazamx.l", + "xwwqazamx.m", + "xwwqazamx.o", + "xwwqazamx.s", + "xwwqazamx.t", + "xwwqazamx.z", + }; + int total = 0; + for (String className : classes) { + total += hookPromonIntBooleanMethods(lpparam, className); + } + if (total > 0) { + XposedBridge.log(TAG + " Promon native-bridge total hooks=" + total); + } + } + + private static int hookPromonIntBooleanMethods( + XC_LoadPackage.LoadPackageParam lpparam, String className) { + int count = 0; + try { + Class clazz = XposedHelpers.findClass(className, lpparam.classLoader); + for (Method method : clazz.getDeclaredMethods()) { + Class returnType = method.getReturnType(); + if (returnType != boolean.class && returnType != Boolean.class + && returnType != int.class && returnType != Integer.class) { + continue; + } + if (method.getParameterTypes().length > 6) { + continue; + } + XposedBridge.hookMethod(method, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + int depth = PROMON_BRIDGE_DEPTH.get(); + if (depth > 0) { + return; + } + PROMON_BRIDGE_DEPTH.set(depth + 1); + try { + if (returnType == boolean.class || returnType == Boolean.class) { + param.setResult(false); + } else { + param.setResult(0); + } + } finally { + PROMON_BRIDGE_DEPTH.set(depth); + } + } + }); + count++; + } + } catch (Throwable ignored) { + // class may be absent in this process + } + return count; + } + + private static void hookSecurityUrlOpeners(XC_LoadPackage.LoadPackageParam lpparam) { + String[] classes = { + "my.com.tngdigital.common.security.shielding.AppSecurityManager", + "my.com.tngdigital.common.security.ui.SecurityErrorBaseActivity", + "my.com.tngdigital.common.security.ui.SecurityErrorActivity", + "my.com.tngdigital.common.security.shielding.utils.HelperKt", + "my.com.tngdigital.app.launcher.initializer.AppSecurityInitializer", + }; + String[] methods = { + "openSecurityUrl", "openUrlByBrowser", "openBrowser", "openUrl", + "openWebUrl", + }; + for (String className : classes) { + for (String methodName : methods) { + hookUrlOrCallbackMethod(lpparam, className, methodName); + } + } + } + + private static void hookUrlOrCallbackMethod( + XC_LoadPackage.LoadPackageParam lpparam, String className, String methodName) { + try { + Class clazz = XposedHelpers.findClass(className, lpparam.classLoader); + int hooked = 0; + for (Method method : clazz.getDeclaredMethods()) { + if (!methodName.equals(method.getName())) { + continue; + } + XposedBridge.hookMethod(method, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + if (param.args != null && param.args.length > 0 + && param.args[0] instanceof String) { + String url = (String) param.args[0]; + if (isBlockedSupportUrl(url)) { + XposedBridge.log(TAG + " blocked " + className + + "#" + methodName + " url=" + url); + setSafeHookResult(param, method); + return; + } + } + if ("openSecurityUrl".equals(methodName) + || "openUrlByBrowser".equals(methodName) + || "openBrowser".equals(methodName)) { + XposedBridge.log(TAG + " blocked " + className + "#" + methodName); + setSafeHookResult(param, method); + } + } + }); + hooked++; + } + if (hooked > 0) { + XposedBridge.log(TAG + " hooked " + hooked + " " + className + "#" + methodName); + } + } catch (Throwable ignored) { + // class/method may not exist in this APK split + } + } + + private static void hookMethodNoop(Method method, String label) { + XposedBridge.hookMethod(method, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + XposedBridge.log(TAG + " blocked " + label); + setSafeHookResult(param, method); + } + }); + XposedBridge.log(TAG + " hooked " + label); + } + + /** 高频回调(如 handleTapjackingCallback)禁止逐次打 log,避免复进主线程 ANR。 */ + private static void hookMethodNoopSilent(Method method) { + XposedBridge.hookMethod(method, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + setSafeHookResult(param, method); + } + }); + } + + /** methodName 为 null 时 hook 类内全部方法(用于 Promon 混淆类)。 */ + private static void hookAllMethodsNoop( + XC_LoadPackage.LoadPackageParam lpparam, String className, String methodName) { + try { + Class clazz = XposedHelpers.findClass(className, lpparam.classLoader); + int hooked = 0; + for (Method method : clazz.getDeclaredMethods()) { + if (methodName != null && !methodName.equals(method.getName())) { + continue; + } + XposedBridge.hookMethod(method, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + XposedBridge.log(TAG + " blocked " + className + "#" + method.getName()); + setSafeHookResult(param, method); + } + }); + hooked++; + } + if (hooked > 0) { + XposedBridge.log(TAG + " hooked " + hooked + " method(s) in " + className + + (methodName != null ? "#" + methodName : "")); + } + } catch (Throwable t) { + XposedBridge.log(TAG + " skip " + className + ": " + t.getMessage()); + } + } + + private static boolean isSecurityErrorIntent(Intent intent) { + if (intent == null) { + return false; + } + if (intent.getComponent() != null) { + String cls = intent.getComponent().getClassName(); + if (cls != null && cls.contains("SecurityError")) { + return true; + } + } + String target = intent.getStringExtra("targetActivity"); + return target != null && target.contains("SecurityError"); + } + + private static void hookJailBroken(XC_LoadPackage.LoadPackageParam lpparam) { + hookNoArgVoid(lpparam, "my.com.tngdigital.common.jailbrkendetect.JailBrokenManager", + "showJailBrokenAlert"); + hookNoArgVoid(lpparam, "my.com.tngdigital.common.jailbrkendetect.JailBrokenManager", + "showJailBrokenAlert$lambda$15"); + + hookReturnFalse(lpparam, "my.com.tngdigital.common.jailbrkendetect.JailBrokenApiImpl", + "isJailBroken"); + hookReturnFalse(lpparam, "my.com.tngdigital.common.jailbrkendetect.JailBrokenApiImpl", + "detectJailBroken"); + hookReturnFalse(lpparam, "my.com.tngdigital.common.jailbrkendetect.JailBrokenManager", + "isJailBroken"); + hookReturnFalse(lpparam, "my.com.tngdigital.common.jailbrkendetect.JailBrokenManager", + "detectJailBroken"); + + // 以下为 void 回调(非 boolean);触发时弹窗/杀进程,必须 noop。 + hookNoArgVoid(lpparam, "my.com.tngdigital.common.jailbrkendetect.JailBrokenApiImpl", + "onBlockStaticCheck"); + hookNoArgVoid(lpparam, "my.com.tngdigital.common.jailbrkendetect.JailBrokenApiImpl", + "onShowPopupDisable"); + hookNoArgVoid(lpparam, "my.com.tngdigital.common.jailbrkendetect.JailBrokenApiImpl", + "onEmptyToken"); + hookNoArgVoid(lpparam, "my.com.tngdigital.common.jailbrkendetect.JailBrokenApiImpl", + "onRpcCheckValid"); + + hookJailBrokenResult(lpparam); + } + + /** 拦截 jail.broken.detect RPC:本地直接回调「未越狱」结果,不发网关。 */ + private static void hookJailBrokenRpc(XC_LoadPackage.LoadPackageParam lpparam) { + String implClass = "my.com.tngdigital.common.jailbrkendetect.JailBrokenApiImpl"; + String[] shortCircuitMethods = { + "rpcCheckJailBroken", + "jailBrokenDetect", + "rpcCheck", + }; + try { + Class clazz = XposedHelpers.findClass(implClass, lpparam.classLoader); + int hooked = 0; + for (Method method : clazz.getDeclaredMethods()) { + String name = method.getName(); + boolean match = false; + for (String target : shortCircuitMethods) { + if (target.equals(name)) { + match = true; + break; + } + } + if (!match) { + continue; + } + XposedBridge.hookMethod(method, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + XposedBridge.log(TAG + " skip jail RPC " + implClass + "#" + name); + Object clean = buildCleanJailBrokenResult(lpparam.classLoader); + invokeJailBrokenCallbacks(param.args, clean); + setSafeHookResult(param, method); + } + }); + hooked++; + } + if (hooked > 0) { + XposedBridge.log(TAG + " hooked jail RPC short-circuit methods=" + hooked); + } + } catch (Throwable t) { + XposedBridge.log(TAG + " jail RPC short-circuit failed: " + t.getMessage()); + } + + hookRpcInvocationJailBypass(lpparam); + } + + private static void hookRpcInvocationJailBypass(XC_LoadPackage.LoadPackageParam lpparam) { + String[] rpcClasses = { + "my.com.tngdigital.common.aliservice.quake.TngdRpcInvocationHandlerHost", + "com.alipay.imobile.network.quake.rpc.RpcInvocationHandler", + }; + XC_MethodHook rpcHook = new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + String op = extractRpcOperation(param.args); + if (op != null && isRegistrationRpc(op)) { + XposedBridge.log(TAG + " RPC reg/login op=" + op + " " + + describeRpcInvocation(param.args)); + } + if (!isJailBrokenRpcInvocation(param.args)) { + return; + } + XposedBridge.log(TAG + " block jail.broken RPC invoke " + + describeRpcInvocation(param.args)); + Object clean = buildCleanJailBrokenResult(lpparam.classLoader); + if (clean != null) { + param.setResult(clean); + return; + } + param.setResult(null); + } + }; + for (String className : rpcClasses) { + try { + Class clazz = XposedHelpers.findClass(className, lpparam.classLoader); + int hooked = 0; + for (Method method : clazz.getDeclaredMethods()) { + if (!"invoke".equals(method.getName())) { + continue; + } + XposedBridge.hookMethod(method, rpcHook); + hooked++; + } + if (hooked > 0) { + XposedBridge.log(TAG + " hooked RPC guard+log " + className + + " invoke=" + hooked); + } + } catch (Throwable t) { + XposedBridge.log(TAG + " skip RPC guard " + className + + ": " + t.getMessage()); + } + } + } + + private static String extractRpcOperation(Object[] args) { + if (args == null) { + return null; + } + for (Object arg : args) { + if (!(arg instanceof String)) { + continue; + } + String text = (String) arg; + if (text.startsWith("com.abl.") + || text.startsWith("ap.tngd") + || text.startsWith("alipayplus.")) { + return text; + } + } + return null; + } + + private static boolean isRegistrationRpc(String op) { + if (op == null) { + return false; + } + String lower = op.toLowerCase(Locale.US); + for (String marker : REGISTRATION_RPC_MARKERS) { + if (lower.contains(marker)) { + return true; + } + } + return false; + } + + private static boolean isJailBrokenRpcInvocation(Object[] args) { + if (args == null) { + return false; + } + for (Object arg : args) { + if (arg == null) { + continue; + } + if (arg instanceof Method) { + String name = ((Method) arg).getName().toLowerCase(Locale.US); + if (name.contains("jail")) { + return true; + } + continue; + } + if (arg instanceof String) { + String text = ((String) arg).toLowerCase(Locale.US); + if (text.contains("jail.broken") + || text.contains("jailbroken") + || text.contains("jail_broken")) { + return true; + } + continue; + } + // 勿对 RPC 动态代理 toString:会再次进入 invoke 导致栈溢出。 + if (Proxy.isProxyClass(arg.getClass())) { + continue; + } + Class clazz = arg.getClass(); + if (clazz.isArray()) { + continue; + } + String simple = clazz.getSimpleName().toLowerCase(Locale.US); + if (simple.contains("jailbroken") || simple.contains("jailbrokenrequest")) { + return true; + } + } + return false; + } + + private static String describeRpcInvocation(Object[] args) { + if (args == null || args.length == 0) { + return "[]"; + } + StringBuilder sb = new StringBuilder("["); + for (int i = 0; i < args.length; i++) { + if (i > 0) { + sb.append(", "); + } + Object arg = args[i]; + if (arg instanceof Method) { + sb.append("Method=").append(((Method) arg).getName()); + } else if (arg instanceof String) { + String s = (String) arg; + sb.append(s.length() > 120 ? s.substring(0, 120) + "..." : s); + } else if (Proxy.isProxyClass(arg.getClass())) { + sb.append("Proxy=").append(arg.getClass().getInterfaces()[0].getSimpleName()); + } else { + sb.append(arg.getClass().getSimpleName()); + } + } + sb.append("]"); + return sb.toString(); + } + + private static Object buildCleanJailBrokenResult(ClassLoader loader) { + try { + Class clazz = XposedHelpers.findClass( + "my.com.tngdigital.common.jailbrkendetect.JailBrokenResult", loader); + for (Constructor constructor : clazz.getDeclaredConstructors()) { + Class[] types = constructor.getParameterTypes(); + Object[] args = new Object[types.length]; + boolean ok = true; + for (int i = 0; i < types.length; i++) { + Class type = types[i]; + if (type == boolean.class || type == Boolean.class) { + args[i] = Boolean.FALSE; + } else if (type == String.class) { + args[i] = ""; + } else if (type == int.class) { + args[i] = 0; + } else if (type == Integer.class) { + args[i] = Integer.valueOf(0); + } else if (!type.isPrimitive()) { + args[i] = null; + } else { + ok = false; + break; + } + } + if (!ok) { + continue; + } + try { + constructor.setAccessible(true); + Object result = constructor.newInstance(args); + sanitizeJailBrokenResult(result); + return result; + } catch (Throwable ignored) { + } + } + } catch (Throwable t) { + XposedBridge.log(TAG + " buildCleanJailBrokenResult failed: " + t.getMessage()); + } + return null; + } + + private static void sanitizeJailBrokenResult(Object result) { + if (result == null) { + return; + } + try { + for (java.lang.reflect.Field field : result.getClass().getDeclaredFields()) { + field.setAccessible(true); + String name = field.getName().toLowerCase(Locale.US); + Class type = field.getType(); + if ((type == boolean.class || type == Boolean.class) + && (name.contains("jail") || name.contains("root"))) { + field.set(result, Boolean.FALSE); + } else if (type == String.class && name.contains("msg")) { + field.set(result, ""); + } + } + } catch (Throwable ignored) { + } + } + + private static void invokeJailBrokenCallbacks(Object[] args, Object cleanResult) { + if (args == null || cleanResult == null) { + return; + } + for (Object arg : args) { + if (arg == null) { + continue; + } + try { + for (Method method : arg.getClass().getDeclaredMethods()) { + if (method.getParameterTypes().length != 1) { + continue; + } + Class paramType = method.getParameterTypes()[0]; + if (!paramType.getName().contains("JailBrokenResult")) { + continue; + } + method.setAccessible(true); + method.invoke(arg, cleanResult); + XposedBridge.log(TAG + " delivered clean JailBrokenResult via " + + arg.getClass().getSimpleName() + "#" + method.getName()); + } + } catch (Throwable ignored) { + } + try { + XposedHelpers.callMethod(arg, "invoke", cleanResult); + } catch (Throwable ignored) { + } + } + } + + private static void hookJailBrokenResult(XC_LoadPackage.LoadPackageParam lpparam) { + try { + Class resultClass = XposedHelpers.findClass( + "my.com.tngdigital.common.jailbrkendetect.JailBrokenResult", + lpparam.classLoader); + for (Method method : resultClass.getDeclaredMethods()) { + String name = method.getName(); + Class returnType = method.getReturnType(); + if (returnType == boolean.class || returnType == Boolean.class) { + if (name.toLowerCase(Locale.US).contains("jail") + || name.toLowerCase(Locale.US).contains("root")) { + XposedBridge.hookMethod(method, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + param.setResult(false); + } + }); + XposedBridge.log(TAG + " hooked JailBrokenResult#" + name); + } + } else if (returnType == String.class + && name.toLowerCase(Locale.US).contains("msg")) { + XposedBridge.hookMethod(method, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + param.setResult(""); + } + }); + XposedBridge.log(TAG + " hooked JailBrokenResult#" + name); + } + } + XposedHelpers.findAndHookConstructor(resultClass, new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + sanitizeJailBrokenResult(param.getResult()); + } + }); + } catch (Throwable t) { + XposedBridge.log(TAG + " JailBrokenResult hooks failed: " + t.getMessage()); + } + } + + private static void hookAppSecurityManager(XC_LoadPackage.LoadPackageParam lpparam) { + // addIntoQueue 勿拦:拦截后 Promon 会走 native _exit fallback + hookReturnFalse(lpparam, + "my.com.tngdigital.common.security.malwarescan.MalwareScanUtils", + "isShowErrorScreen"); + } + + private static void hookSecurityBooleanChecks(XC_LoadPackage.LoadPackageParam lpparam) { + for (String className : BOOLEAN_HOOK_CLASSES) { + RootBypassHelper.hookSecurityClass(lpparam, className); + } + } + + private static void hookSecurityErrorActivity(XC_LoadPackage.LoadPackageParam lpparam) { + hookSecurityErrorLaunch(lpparam); + hookGenericSecurityErrorFinish(); + XC_MethodHook closeSecurityScreen = new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + Activity activity = (Activity) param.thisObject; + XposedBridge.log(TAG + " closing " + activity.getClass().getSimpleName()); + activity.finish(); + } + }; + for (String activityClass : new String[]{ + SECURITY_ERROR_ACTIVITY, + "my.com.tngdigital.common.security.ui.SecurityErrorBaseActivity", + }) { + hookAllOnCreateMethods(lpparam, activityClass, closeSecurityScreen); + } + } + + private static void hookAllOnCreateMethods( + XC_LoadPackage.LoadPackageParam lpparam, + String className, + XC_MethodHook hook) { + try { + Class clazz = XposedHelpers.findClass(className, lpparam.classLoader); + int hooked = 0; + for (Method method : clazz.getDeclaredMethods()) { + if (!"onCreate".equals(method.getName())) { + continue; + } + XposedBridge.hookMethod(method, hook); + hooked++; + } + if (hooked > 0) { + XposedBridge.log(TAG + " hooked " + hooked + " onCreate overload(s) in " + className); + } else { + XposedBridge.log(TAG + " no onCreate in " + className); + } + } catch (Throwable t) { + XposedBridge.log(TAG + " " + className + " onCreate hook failed: " + t.getMessage()); + } + } + + /** SecurityErrorActivity 为 Compose,无独立 onCreate;在 Activity 基类统一 finish。 */ + private static void hookGenericSecurityErrorFinish() { + try { + XposedHelpers.findAndHookMethod( + Activity.class, + "onCreate", + Bundle.class, + new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) { + Activity activity = (Activity) param.thisObject; + String name = activity.getClass().getName(); + if (name.contains("SecurityError")) { + XposedBridge.log(TAG + " finishing " + name); + activity.finish(); + } + } + }); + XposedBridge.log(TAG + " hooked Activity.onCreate SecurityError finish"); + } catch (Throwable t) { + XposedBridge.log(TAG + " generic SecurityError finish failed: " + t.getMessage()); + } + } + + /** 拦截 Security 状态机启动 SecurityErrorActivity。 */ + private static void hookSecurityErrorLaunch(XC_LoadPackage.LoadPackageParam lpparam) { + // launchProcessNextSecurityStateIfIdle / addIntoQueue 勿拦: + // 拦截后 Promon 会立刻走 native exit_group(1)。 + // 杀进程改由 finishAllActivityAndKillApp / showSecurityScreenForState 兜底。 + XposedBridge.log(TAG + " skip launchProcessNext noop (avoid native exit fallback)"); + } + + private static void hookReturnFalse( + XC_LoadPackage.LoadPackageParam lpparam, String className, String methodName) { + try { + Class clazz = XposedHelpers.findClass(className, lpparam.classLoader); + int hooked = 0; + for (Method method : clazz.getDeclaredMethods()) { + if (!methodName.equals(method.getName())) { + continue; + } + Class returnType = method.getReturnType(); + if (returnType != boolean.class && returnType != Boolean.class) { + continue; + } + XposedBridge.hookMethod(method, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + param.setResult(false); + } + }); + hooked++; + } + if (hooked > 0) { + XposedBridge.log(TAG + " hooked " + hooked + " overload(s) " + + className + "#" + methodName + " -> false"); + } else { + XposedBridge.log(TAG + " no boolean method " + className + "#" + methodName); + } + } catch (Throwable t) { + XposedBridge.log(TAG + " skip " + className + "#" + methodName + ": " + t.getMessage()); + } + } + + private static void hookNoArgVoid( + XC_LoadPackage.LoadPackageParam lpparam, String className, String methodName) { + hookAllMethodsNoop(lpparam, className, methodName); + } + + private static void setSafeHookResult(XC_MethodHook.MethodHookParam param, Method method) { + Class returnType = method.getReturnType(); + if (returnType == void.class) { + param.setResult(null); + } else if (returnType == boolean.class) { + param.setResult(false); + } else if (returnType == int.class) { + param.setResult(0); + } else if (returnType == long.class) { + param.setResult(0L); + } else if (returnType == float.class) { + param.setResult(0f); + } else if (returnType == double.class) { + param.setResult(0d); + } else if (returnType == byte.class) { + param.setResult((byte) 0); + } else if (returnType == short.class) { + param.setResult((short) 0); + } else if (returnType == char.class) { + param.setResult((char) 0); + } else if (returnType == Boolean.class) { + param.setResult(Boolean.FALSE); + } else if (returnType == Integer.class) { + param.setResult(Integer.valueOf(0)); + } else if (returnType == Long.class) { + param.setResult(Long.valueOf(0L)); + } else if (returnType == Float.class) { + param.setResult(Float.valueOf(0f)); + } else if (returnType == Double.class) { + param.setResult(Double.valueOf(0d)); + } else if (returnType == String.class) { + param.setResult(""); + } else if (returnType == byte[].class) { + param.setResult(new byte[0]); + } else { + param.setResult(null); + } + } +}