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 0000000..98a1bdc Binary files /dev/null and b/magisk-modules/tng_exit_guard/zygisk/arm64-v8a.so differ diff --git a/scripts/build-install-tng-exit-guard.ps1 b/scripts/build-install-tng-exit-guard.ps1 new file mode 100644 index 0000000..e0f088a --- /dev/null +++ b/scripts/build-install-tng-exit-guard.ps1 @@ -0,0 +1,55 @@ +# Build + install TNG Zygisk exit guard +param( + [switch]$SkipInstall, + [switch]$NoReboot +) + +$ErrorActionPreference = "Stop" +$Root = Split-Path -Parent $PSScriptRoot +$Mod = Join-Path $Root "magisk-modules\tng_exit_guard" +$Ndk = "C:\Users\Administrator\AppData\Local\Android\Sdk\ndk\21.4.7075529" +$NdkBuild = Join-Path $Ndk "ndk-build.cmd" +$Adb = "C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe" + +if (-not (Test-Path $NdkBuild)) { + throw "ndk-build not found: $NdkBuild" +} + +Write-Host "=== ndk-build ===" -ForegroundColor Cyan +Push-Location $Mod +& $NdkBuild NDK_PROJECT_PATH=. APP_BUILD_SCRIPT=jni/Android.mk NDK_APPLICATION_MK=jni/Application.mk -j8 +if ($LASTEXITCODE -ne 0) { Pop-Location; throw "ndk-build failed" } + +$built = Join-Path $Mod "libs\arm64-v8a\libtng_exit_guard.so" +if (-not (Test-Path $built)) { + # some ndk versions omit lib prefix based on LOCAL_MODULE + $built = Get-ChildItem (Join-Path $Mod "libs\arm64-v8a") -Filter "*.so" | Select-Object -First 1 -ExpandProperty FullName +} +if (-not $built -or -not (Test-Path $built)) { Pop-Location; throw "built .so missing" } + +$zygiskDir = Join-Path $Mod "zygisk" +New-Item -ItemType Directory -Force -Path $zygiskDir | Out-Null +Copy-Item $built (Join-Path $zygiskDir "arm64-v8a.so") -Force +Write-Host "built -> 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); + } + } +}