feat(tng): TNG eWallet Promon bypass、Zygisk 信号守卫与注册链保护
新增 TngRootBypassHook 与 tng_exit_guard 模块,修复 BAL 强拉、SIGABRT/pc==lr SEGV 崩溃,验证可进入注册页。
This commit is contained in:
85
docs/TNG_自杀链逆向笔记.md
Normal file
85
docs/TNG_自杀链逆向笔记.md
Normal file
@@ -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。
|
||||
11
magisk-modules/tng_exit_guard/jni/Android.mk
Normal file
11
magisk-modules/tng_exit_guard/jni/Android.mk
Normal file
@@ -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)
|
||||
4
magisk-modules/tng_exit_guard/jni/Application.mk
Normal file
4
magisk-modules/tng_exit_guard/jni/Application.mk
Normal file
@@ -0,0 +1,4 @@
|
||||
APP_ABI := arm64-v8a
|
||||
APP_PLATFORM := android-24
|
||||
APP_STL := c++_static
|
||||
APP_CPPFLAGS := -std=c++17
|
||||
390
magisk-modules/tng_exit_guard/jni/main.cpp
Normal file
390
magisk-modules/tng_exit_guard/jni/main.cpp
Normal file
@@ -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 <android/log.h>
|
||||
#include <errno.h>
|
||||
#include <linux/audit.h>
|
||||
#include <linux/filter.h>
|
||||
#include <linux/seccomp.h>
|
||||
#include <pthread.h>
|
||||
#include <signal.h>
|
||||
#include <stddef.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/prctl.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <sys/sysmacros.h>
|
||||
#include <ucontext.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#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<int> g_seccomp_ok{0};
|
||||
static std::atomic<int> g_stack_chk{0};
|
||||
static std::atomic<int> g_promon_segv{0};
|
||||
static std::atomic<uintptr_t> g_promon_start{0};
|
||||
static std::atomic<uintptr_t> g_promon_end{0};
|
||||
/* 隔离进程 :goacqowmmt 会循环 SEGV;cap=16 打满后 freeze 反拖累主进程 */
|
||||
static constexpr int kMaxPromonSegvSkip = 0; /* 0 = unlimited LR-return skip */
|
||||
static std::atomic<int> 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<ucontext_t *>(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<ucontext_t *>(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)
|
||||
391
magisk-modules/tng_exit_guard/jni/zygisk.hpp
Normal file
391
magisk-modules/tng_exit_guard/jni/zygisk.hpp
Normal file
@@ -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 <jni.h>
|
||||
|
||||
#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 <class T> 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:
|
||||
//
|
||||
// <address> <perms> <offset> <dev> <inode> <pathname>
|
||||
// 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 <class T> 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<clazz>(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 <class T>
|
||||
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"
|
||||
6
magisk-modules/tng_exit_guard/module.prop
Normal file
6
magisk-modules/tng_exit_guard/module.prop
Normal file
@@ -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.
|
||||
BIN
magisk-modules/tng_exit_guard/zygisk/arm64-v8a.so
Normal file
BIN
magisk-modules/tng_exit_guard/zygisk/arm64-v8a.so
Normal file
Binary file not shown.
55
scripts/build-install-tng-exit-guard.ps1
Normal file
55
scripts/build-install-tng-exit-guard.ps1
Normal file
@@ -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
|
||||
21
scripts/diag-tng-exit-guard.sh
Normal file
21
scripts/diag-tng-exit-guard.sh
Normal file
@@ -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
|
||||
14
scripts/logcat-tng.ps1
Normal file
14
scripts/logcat-tng.ps1
Normal file
@@ -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
|
||||
4
scripts/pull-tng-log.sh
Normal file
4
scripts/pull-tng-log.sh
Normal file
@@ -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
|
||||
16
scripts/test-tng-survive.sh
Normal file
16
scripts/test-tng-survive.sh
Normal file
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> 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<Object, String> 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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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<String> 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<Integer> PROMON_BRIDGE_DEPTH = new ThreadLocal<Integer>() {
|
||||
@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 + "<init>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user