Compare commits

...

3 Commits

Author SHA1 Message Date
mars
193c04a24b fix(tng): 修复地区选择黑屏并加固注册链 native abort 防护 2026-08-03 11:22:51 +08:00
mars
18ae42ec63 fix(tng): 适配 Promon 1.9.10 vhvlnqgy 包名并加固 Zygisk 信号跳过
1.9.10 将 xwwqazamx 重命名为 vhvlnqgy,Login 闪退为 vhvlnqgy.bd:16;Xposed 双包名解析并 short-circuit R/bl、拦截 bd 异常。Zygisk 扩展 non-promon SEGV pc+4 与 ABRT/TRAP 一律 pc+4,修复 Login 稳定;注册页 stack_chk abort 待续攻。
2026-08-03 11:02:46 +08:00
mars
818b2f4f51 fix(tng): 防止 SIGABRT 跳 LR 死循环,改 pc+4 与 streak freeze
吞 ABRT 时若跳回近距 LR 会瞬间再 abort 刷屏;改为优先 pc+4,同 tid+pc 超阈值后冻结工作线程,主线程仅 bail。
2026-08-03 10:26:48 +08:00
11 changed files with 616 additions and 147 deletions

View File

@@ -37,13 +37,21 @@
static constexpr const char *kTargetPkg = "my.com.tngdigital.ewallet";
static constexpr const char *kPromonSo = "libtngdigital_ewallet.so";
static bool g_enabled = false;
static zygisk::Api *g_api = nullptr;
static std::atomic<int> g_cxx_plt{0};
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 会循环 SEGVcap=16 打满后 freeze 反拖累主进程 */
static constexpr int kMaxPromonSegvSkip = 0; /* 0 = unlimited LR-return skip */
static std::atomic<pid_t> g_main_tid{0};
static std::atomic<int> g_abrt_swallow{0};
static std::atomic<pid_t> g_abrt_last_tid{0};
static std::atomic<uintptr_t> g_abrt_last_pc{0};
static std::atomic<int> g_abrt_streak{0};
/* 隔离进程 :goacqowmmt 会循环 SEGV主进程 worker 也狂刷。cap 后 freeze 该线程 */
static constexpr int kMaxPromonSegvSkip = 200;
static constexpr int kMaxAbrtStreak = 3;
static std::atomic<int> g_soft_sig_logged{0};
static void freeze_forever() {
@@ -119,25 +127,67 @@ static void promon_segv_handler(int sig, siginfo_t *info, void *ctx) {
LOGI("promon SIGSEGV tid=%d n=%d — cap hit, freeze", (int)gettid(), n);
freeze_forever();
}
signal(SIGSEGV, SIG_DFL);
raise(SIGSEGV);
/* 1.9.10Login 后出现 Promon so 外 SEGV → 旧逻辑 re-raise 直接闪退 */
{
int n = ++g_promon_segv;
pid_t tid = gettid();
if (n <= 5 || n % 50 == 0) {
LOGI("non-promon SIGSEGV tid=%d pc=%lx lr=%lx n=%d — pc+4",
(int)tid, (unsigned long)pc, (unsigned long)lr, n);
}
if (n > 200 && tid != g_main_tid.load()) {
LOGI("non-promon SEGV storm tid=%d — freeze", (int)tid);
freeze_forever();
}
if (pc != 0) {
uc->uc_mcontext.pc = pc + 4;
return;
}
}
freeze_forever();
}
/** libc++abi __cxa_guard_acquire 递归初始化 → abort跳回 LR 继续而非杀进程。 */
/**
* ABRT/TRAP一律 pc+4。跳远距 LR 会弄坏主线程 Looper闪退观感
* 工作线程同 PC 连 abort 超限 → freeze主线程始终 pc+4。
*/
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;
pid_t tid = gettid();
if (sig == SIGABRT) {
int streak = 1;
if (g_abrt_last_tid.load() == tid && g_abrt_last_pc.load() == pc) {
streak = g_abrt_streak.fetch_add(1) + 1;
} else {
g_abrt_last_tid.store(tid);
g_abrt_last_pc.store(pc);
g_abrt_streak.store(1);
}
if (streak > kMaxAbrtStreak && tid != g_main_tid.load()) {
LOGI("ABRT streak cap tid=%d pc=%lx n=%d — freeze worker", (int)tid,
(unsigned long)pc, streak);
freeze_forever();
}
int n = ++g_abrt_swallow;
if (n <= 5 || n % 50 == 0) {
LOGI("ABRT pc+4 tid=%d pc=%lx lr=%lx streak=%d", (int)tid,
(unsigned long)pc, (unsigned long)lr, streak);
}
uc->uc_mcontext.pc = pc + 4;
return;
}
if (pc != 0) {
uc->uc_mcontext.pc = pc + 4;
if (sig == SIGTRAP) {
int n = ++g_abrt_swallow;
if (n <= 5 || n % 50 == 0) {
LOGI("TRAP pc+4 tid=%d pc=%lx", (int)tid, (unsigned long)pc);
}
uc->uc_mcontext.pc = pc != 0 ? pc + 4 : lr;
return;
}
#endif
@@ -151,7 +201,7 @@ static void install_fatal_skip_handlers() {
sigemptyset(&sa.sa_mask);
sigaction(SIGABRT, &sa, nullptr);
sigaction(SIGTRAP, &sa, nullptr);
LOGI("fatal skip handlers (ABRT+TRAP→LR)");
LOGI("fatal skip handlers (ABRT/TRAP always pc+4)");
}
static void install_promon_segv_handler() {
@@ -177,7 +227,7 @@ static void install_soft_signals() {
sigaction(SIGABRT, &sa, nullptr);
sigaction(SIGTRAP, &sa, nullptr);
if (g_soft_sig_logged.fetch_add(1) == 0) {
LOGI("soft signals (ABRT+TRAP skip→LR)");
LOGI("soft signals (ABRT/TRAP always pc+4)");
}
}
@@ -224,6 +274,13 @@ using kill_fn = int (*)(pid_t, int);
using tgkill_fn = int (*)(int, int, int);
using raise_fn = int (*)(int);
using pthread_kill_fn = int (*)(pthread_t, int);
using cxa_guard_acquire_fn = int (*)(void *);
using cxa_guard_abort_fn = void (*)();
using dlopen_fn = void *(*)(const char *, int);
static cxa_guard_acquire_fn orig_cxa_guard_acquire = nullptr;
static cxa_guard_abort_fn orig_cxa_guard_abort = nullptr;
static dlopen_fn orig_dlopen = nullptr;
static exit_fn orig_exit = nullptr;
static exit_fn orig__exit = nullptr;
@@ -278,7 +335,40 @@ static int hooked_pthread_kill(pthread_t thread, int sig) {
return orig_pthread_kill ? orig_pthread_kill(thread, sig) : -1;
}
static bool find_libc(dev_t *dev, ino_t *ino) {
/** Promon/libc++ 静态局部量递归初始化会 abort 主进程Registration 页 HWUI 线程)。 */
static int hooked_cxa_guard_acquire(void *guard) {
(void)guard;
return 1;
}
static void hooked_cxa_guard_abort() {
LOGI("blocked __cxa_guard_abort tid=%d", (int)gettid());
}
static void *hooked_dlopen(const char *name, int flags) {
void *handle = orig_dlopen ? orig_dlopen(name, flags) : nullptr;
if (handle != nullptr || name == nullptr) {
return handle;
}
if (strstr(name, "libandroid.so") != nullptr) {
static const char *kFallbacks[] = {
"/system/lib64/libandroid.so",
"/system/lib/libandroid.so",
"libandroid.so",
};
for (const char *path : kFallbacks) {
handle = orig_dlopen ? orig_dlopen(path, flags) : nullptr;
if (handle != nullptr) {
LOGI("dlopen fallback %s -> %p (from %s)", path, handle, name);
return handle;
}
}
LOGI("dlopen libandroid.so failed tid=%d", (int)gettid());
}
return handle;
}
static bool find_lib_match(const char *suffix, const char *contains,
dev_t *dev, ino_t *ino) {
FILE *fp = fopen("/proc/self/maps", "r");
if (!fp) return false;
char line[1024];
@@ -291,11 +381,19 @@ static bool find_libc(dev_t *dev, ino_t *ino) {
unsigned long inode = 0;
char path[512] = {};
int n = sscanf(line, "%lx-%lx %7s %llx %31s %lu %511[^\n]",
&start, &end, perms, &offset, deststr, &inode, path);
&start, &end, perms, &offset, &deststr, &inode, path);
if (n < 7 || inode == 0) continue;
char *p = path;
while (*p == ' ') ++p;
if (strstr(p, "libc.so") == nullptr) continue;
bool match = false;
if (suffix != nullptr) {
size_t plen = strlen(p);
size_t slen = strlen(suffix);
match = plen >= slen && strcmp(p + plen - slen, suffix) == 0;
} else if (contains != nullptr) {
match = strstr(p, contains) != nullptr;
}
if (!match) continue;
unsigned maj = 0, min = 0;
if (sscanf(deststr, "%x:%x", &maj, &min) != 2) continue;
*dev = makedev(maj, min);
@@ -307,25 +405,72 @@ static bool find_libc(dev_t *dev, ino_t *ino) {
return ok;
}
static bool find_lib_by_suffix(const char *suffix, dev_t *dev, ino_t *ino) {
return find_lib_match(suffix, nullptr, dev, ino);
}
static bool find_lib_contains(const char *needle, dev_t *dev, ino_t *ino) {
return find_lib_match(nullptr, needle, dev, ino);
}
static bool find_libc(dev_t *dev, ino_t *ino) {
return find_lib_by_suffix("libc.so", dev, ino);
}
static void register_plt(zygisk::Api *api, dev_t dev, ino_t ino,
const char *sym, void *hook, void **orig) {
if (!api || dev == 0 || ino == 0) return;
api->pltHookRegister(dev, ino, sym, hook, orig);
}
static void install_plt(zygisk::Api *api) {
if (!api) return;
dev_t dev = 0;
ino_t ino = 0;
if (!find_libc(&dev, &ino)) return;
api->pltHookRegister(dev, ino, "exit", (void *)hooked_exit, (void **)&orig_exit);
api->pltHookRegister(dev, ino, "_exit", (void *)hooked__exit, (void **)&orig__exit);
api->pltHookRegister(dev, ino, "abort", (void *)hooked_abort, (void **)&orig_abort);
api->pltHookRegister(dev, ino, "__stack_chk_fail",
(void *)hooked_stack_chk_fail, (void **)&orig_stack_chk_fail);
api->pltHookRegister(dev, ino, "raise", (void *)hooked_raise, (void **)&orig_raise);
api->pltHookRegister(dev, ino, "kill", (void *)hooked_kill, (void **)&orig_kill);
api->pltHookRegister(dev, ino, "tgkill", (void *)hooked_tgkill, (void **)&orig_tgkill);
api->pltHookRegister(dev, ino, "pthread_kill",
(void *)hooked_pthread_kill, (void **)&orig_pthread_kill);
if (find_libc(&dev, &ino)) {
register_plt(api, dev, ino, "exit", (void *)hooked_exit, (void **)&orig_exit);
register_plt(api, dev, ino, "_exit", (void *)hooked__exit, (void **)&orig__exit);
register_plt(api, dev, ino, "abort", (void *)hooked_abort, (void **)&orig_abort);
register_plt(api, dev, ino, "__stack_chk_fail",
(void *)hooked_stack_chk_fail, (void **)&orig_stack_chk_fail);
register_plt(api, dev, ino, "raise", (void *)hooked_raise, (void **)&orig_raise);
register_plt(api, dev, ino, "kill", (void *)hooked_kill, (void **)&orig_kill);
register_plt(api, dev, ino, "tgkill", (void *)hooked_tgkill, (void **)&orig_tgkill);
register_plt(api, dev, ino, "pthread_kill",
(void *)hooked_pthread_kill, (void **)&orig_pthread_kill);
register_plt(api, dev, ino, "dlopen", (void *)hooked_dlopen, (void **)&orig_dlopen);
}
bool ok = api->pltHookCommit();
LOGI("PLT commit=%d", ok ? 1 : 0);
}
static void try_install_cxx_guard_plt() {
if (g_cxx_plt.load() || !g_api) return;
dev_t dev = 0;
ino_t ino = 0;
bool any = false;
if (find_lib_by_suffix("libc++_shared.so", &dev, &ino)
|| find_lib_contains("libc++", &dev, &ino)) {
register_plt(g_api, dev, ino, "__cxa_guard_acquire",
(void *)hooked_cxa_guard_acquire, (void **)&orig_cxa_guard_acquire);
register_plt(g_api, dev, ino, "__cxa_guard_abort",
(void *)hooked_cxa_guard_abort, (void **)&orig_cxa_guard_abort);
any = true;
}
if (find_lib_by_suffix("libtngdigital_ewallet.so", &dev, &ino)) {
register_plt(g_api, dev, ino, "__cxa_guard_acquire",
(void *)hooked_cxa_guard_acquire, (void **)&orig_cxa_guard_acquire);
register_plt(g_api, dev, ino, "__cxa_guard_abort",
(void *)hooked_cxa_guard_abort, (void **)&orig_cxa_guard_abort);
any = true;
}
if (!any) return;
if (g_api->pltHookCommit()) {
g_cxx_plt.store(1);
LOGI("PLT cxx guards committed");
}
}
static void *phase_thread(void *) {
install_promon_segv_handler();
usleep(400 * 1000);
@@ -334,6 +479,8 @@ static void *phase_thread(void *) {
for (int i = 0; i < 40; i++) {
usleep(1000 * 1000);
install_soft_signals();
install_promon_segv_handler();
try_install_cxx_guard_plt();
if (i % 5 == 0) refresh_promon_so_range();
}
LOGI("phase done seccomp=%d stack_chk=%d promon_segv=%d",
@@ -342,7 +489,10 @@ static void *phase_thread(void *) {
}
static void install_all(zygisk::Api *api) {
LOGI("install pid=%d (PLT+ABRT/TRAP-skip+pc==lr-SEGV+exit_group@400ms)", getpid());
g_api = api;
g_main_tid.store(gettid());
LOGI("install pid=%d main_tid=%d (PLT+cxx-guard+ABRT-pc+4+SEGV-skip+exit_group@400ms)",
getpid(), (int)g_main_tid.load());
install_fatal_skip_handlers();
install_soft_signals();
install_plt(api);

View File

@@ -0,0 +1,14 @@
import zipfile
import re
z = zipfile.ZipFile(r"reverse/dumps/tng_1.9.10_base.apk")
names = [n for n in z.namelist() if n.endswith(".dex")]
pat = re.compile(rb"Lvhvlnqgy/([^;\s]{1,80});")
found = set()
for n in names:
data = z.read(n)
for m in pat.findall(data):
found.add(m.decode("ascii", errors="ignore"))
print("vhvlnqgy classes", len(found))
for c in sorted(found):
print(f"vhvlnqgy.{c.replace('/', '.')}")

View File

@@ -0,0 +1,24 @@
"""Dump vhvlnqgy bl/R/bd method refs from TNG 1.9.10 dex."""
import re
import zipfile
APK = r"reverse/dumps/tng_1.9.10_base.apk"
with zipfile.ZipFile(APK) as z:
data = b"".join(z.read(n) for n in z.namelist() if n.endswith(".dex"))
for cls in ["Lvhvlnqgy/bl;", "Lvhvlnqgy/R;", "Lvhvlnqgy/bd;", "Lvhvlnqgy/w;", "Lvhvlnqgy/W;", "Lvhvlnqgy/a;"]:
print("===", cls, "===")
refs = sorted(set(re.findall(cls.encode() + rb"->[^\x00]{1,80}", data)))
for r in refs[:30]:
print(r.decode("ascii", "ignore"))
print()
print("=== R callers (who invokes R.a/R.b) ===")
for m in sorted(set(re.findall(rb"Lvhvlnqgy/[^;]+;->[a-zA-Z]+[^\x00]{0,40}Lvhvlnqgy/R;", data))):
print(m.decode("ascii", "ignore"))
print("\n=== bd throw sites ===")
for m in sorted(set(re.findall(rb"[^\x00]{0,40}Lvhvlnqgy/bd;", data))):
s = m.decode("ascii", "ignore")
if "vhvlnqgy" in s:
print(s)

View File

@@ -0,0 +1,15 @@
import re
import zipfile
APK = r"reverse/dumps/tng_1.9.10_base.apk"
with zipfile.ZipFile(APK) as z:
data = b"".join(z.read(n) for n in z.namelist() if n.endswith(".dex"))
for cls in ["Lvhvlnqgy/w;", "Lvhvlnqgy/u;"]:
print("===", cls, "refs ===")
pat = cls.encode() + rb"[^\x00]{0,120}"
hits = sorted(set(re.findall(pat, data)))
for h in hits[:40]:
print(h.decode("ascii", "ignore"))
print("count", len(hits))
print()

View File

@@ -0,0 +1,14 @@
import sys
path = sys.argv[1]
lines = open(path, encoding="utf-8", errors="ignore").read().splitlines()
keys = (
"Runtime aborting", "Aborting", "stack corruption", "blocked __stack",
"vhvlnqgy.u", "UserRegistration", "F libc", "has died",
)
for i, line in enumerate(lines):
if any(k in line for k in keys):
start = max(0, i - 2)
end = min(len(lines), i + 6)
print("---")
print("\n".join(lines[start:end]))

View File

@@ -0,0 +1,20 @@
import sys
import xml.etree.ElementTree as ET
path = sys.argv[1] if len(sys.argv) > 1 else "reverse/dumps/ui_login.xml"
root = ET.parse(path).getroot()
keywords = [
"register", "sign", "login", "mobile", "phone", "continue", "next",
"create", "otp", "skip", "started", "email", "password", "pin",
]
for node in root.iter("node"):
text = node.get("text", "")
desc = node.get("content-desc", "")
clickable = node.get("clickable", "")
bounds = node.get("bounds", "")
label = (text or desc).strip()
if not label and clickable != "true":
continue
hay = (text + " " + desc).lower()
if clickable == "true" or any(k in hay for k in keywords):
print(f"{label!r} bounds={bounds} clickable={clickable}")

View File

@@ -0,0 +1,13 @@
import sys
import xml.etree.ElementTree as ET
path = sys.argv[1]
root = ET.parse(path).getroot()
for node in root.iter("node"):
text = (node.get("text") or "").strip()
desc = (node.get("content-desc") or "").strip()
rid = node.get("resource-id") or ""
bounds = node.get("bounds") or ""
clickable = node.get("clickable") or "false"
if text or desc or "login" in rid.lower() or "register" in rid.lower():
print(f"text={text!r} desc={desc!r} id={rid} bounds={bounds} click={clickable}")

View File

@@ -0,0 +1,89 @@
import re
import subprocess
import sys
import time
import xml.etree.ElementTree as ET
ADB = ["adb"]
def adb(*args):
subprocess.run(ADB + list(args), check=False)
def main():
adb("shell", "am", "force-stop", "my.com.tngdigital.ewallet")
time.sleep(2)
adb("shell", "am", "start", "-n", "my.com.tngdigital.ewallet/.ui.SplashActivity")
time.sleep(16)
adb("shell", "uiautomator", "dump", "/sdcard/ui_cc.xml")
xml = subprocess.check_output(
ADB + ["shell", "cat", "/sdcard/ui_cc.xml"], text=True, errors="ignore")
root = ET.fromstring(xml)
target = None
register_btn = None
for node in root.iter("node"):
rid = node.get("resource-id") or ""
text = node.get("text") or ""
if "tv_left" in rid or (text.strip().startswith("+") and len(text.strip()) < 8):
target = node
if "注册" in text and node.get("clickable") == "true":
register_btn = node
if target is None and register_btn is not None:
bounds = register_btn.get("bounds", "")
m = re.match(r"\[(\d+),(\d+)\]\[(\d+),(\d+)\]", bounds)
if m:
x1, y1, x2, y2 = map(int, m.groups())
x, y = (x1 + x2) // 2, (y1 + y2) // 2
print(f"tap register at {x},{y}")
adb("shell", "input", "tap", str(x), str(y))
time.sleep(4)
adb("shell", "uiautomator", "dump", "/sdcard/ui_cc.xml")
xml = subprocess.check_output(
ADB + ["shell", "cat", "/sdcard/ui_cc.xml"], text=True, errors="ignore")
root = ET.fromstring(xml)
for node in root.iter("node"):
rid = node.get("resource-id") or ""
text = node.get("text") or ""
if "tv_left" in rid or (text.strip().startswith("+") and len(text.strip()) < 8):
target = node
break
if target is None:
for node in root.iter("node"):
text = node.get("text") or ""
if "注册" in text or "Register" in text.lower():
print("login screen text:", text[:40])
print("ERROR: country code control not found")
return 1
bounds = target.get("bounds", "")
m = re.match(r"\[(\d+),(\d+)\]\[(\d+),(\d+)\]", bounds)
if not m:
print("bad bounds", bounds)
return 1
x1, y1, x2, y2 = map(int, m.groups())
x, y = (x1 + x2) // 2, (y1 + y2) // 2
print(f"tap country {target.get('text','')} at {x},{y}")
adb("shell", "input", "tap", str(x), str(y))
time.sleep(3)
adb("shell", "uiautomator", "dump", "/sdcard/ui_cc2.xml")
xml2 = subprocess.check_output(
ADB + ["shell", "cat", "/sdcard/ui_cc2.xml"], text=True, errors="ignore")
root2 = ET.fromstring(xml2)
countries = []
for node in root2.iter("node"):
text = (node.get("text") or "").strip()
if "+61" in text or "+86" in text or "Australia" in text or "Malaysia" in text:
countries.append(text)
pid = subprocess.check_output(
ADB + ["shell", "pidof", "my.com.tngdigital.ewallet"], text=True).strip()
print("pid:", pid or "DEAD")
print("countries visible:", countries[:8])
if countries:
print("OK region picker visible")
return 0
print("FAIL region picker empty/black")
return 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,110 @@
#!/usr/bin/env python3
"""Automate TNG login/register tap test and report crashes."""
import re
import subprocess
import sys
import time
PKG = "my.com.tngdigital.ewallet"
TAGS = re.compile(
r"FATAL EXCEPTION|AndroidRuntime.*Process: " + PKG
+ r"|has died|exited due to signal|vhvlnqgy\.bd|blocked killProcess|blocked System\.exit"
+ r"|UserRegistration|UserOtp|UserLogin|Displayed.*tngdigital|ACT on(Create|Resume)"
+ r"|TngExitGuard.*ABRT|TngRoot hooked 6 vhvlnqgy\.R",
re.I,
)
TAPS = [
("register_continue", 540, 959, 8),
("maybe_otp_continue", 540, 959, 6),
("back_to_login", None, None, 2),
("login_tab", 540, 1168, 5),
]
def adb(*args, timeout=30):
cmd = ["adb"] + list(args)
r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
out = (r.stdout or "") + (r.stderr or "")
return r.returncode, out.strip()
def pid_alive():
code, out = adb("shell", "pidof", PKG)
return code == 0 and out.strip().split()
def top_activity():
code, out = adb("shell", "dumpsys", "activity", "activities")
if code != 0:
return "?"
for line in out.splitlines():
if "topResumedActivity=" in line:
m = re.search(r"/([^/]+)$", line.strip())
if m:
return m.group(1).rstrip("}")
return line.strip()
return "?"
def logcat_since(start):
code, out = adb("shell", "logcat", "-d", "-t", start)
hits = []
if code == 0:
for line in out.splitlines():
if TAGS.search(line):
hits.append(line)
return hits
def main():
adb("logcat", "-c")
adb("shell", "am", "force-stop", PKG)
time.sleep(1)
adb("shell", "am", "start", "-n", f"{PKG}/.ui.SplashActivity")
time.sleep(12)
print("=== after cold start ===")
print("pids:", pid_alive())
print("activity:", top_activity())
# dismiss country picker if open
adb("shell", "input", "keyevent", "KEYCODE_BACK")
time.sleep(1)
results = []
for name, x, y, wait_s in TAPS:
if x is not None:
adb("shell", "input", "tap", str(x), str(y))
print(f"\n=== tap {name} ({x},{y}) ===")
else:
adb("shell", "input", "keyevent", "KEYCODE_BACK")
print(f"\n=== {name} ===")
time.sleep(wait_s)
pids = pid_alive()
act = top_activity()
crash_buf = adb("shell", "logcat", "-d", "-b", "crash", "-t", "50")[1]
fatal = [l for l in crash_buf.splitlines() if PKG in l or "vhvlnqgy" in l]
results.append((name, pids, act, fatal))
print("pids:", pids or "DEAD")
print("activity:", act)
if fatal:
print("CRASH:", fatal[-3:])
print("\n=== summary ===")
ok = True
for name, pids, act, fatal in results:
status = "OK" if pids and not fatal else "FAIL"
if status == "FAIL":
ok = False
print(f"{status} {name}: pids={pids} activity={act} crash_lines={len(fatal)}")
hits = logcat_since("2000")
print(f"\n=== key log lines ({len(hits)}) ===")
for line in hits[-40:]:
print(line)
return 0 if ok else 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -36,6 +36,9 @@ public final class TngRootBypassHook {
public static final String PACKAGE = "my.com.tngdigital.ewallet";
private static final String TAG = "notiMessageHook/TngRoot";
/** Promon 混淆包名1.9.10 为 vhvlnqgy旧版为 xwwqazamx。 */
private static final String[] PROMON_PKG_PREFIXES = {"vhvlnqgy", "xwwqazamx"};
private static final String SECURITY_ERROR_ACTIVITY =
"my.com.tngdigital.common.security.ui.SecurityErrorActivity";
@@ -224,10 +227,9 @@ public final class TngRootBypassHook {
hookRegistrationFlowGuard(lpparam);
hookSplashForceLogin(lpparam);
hookLoginDismissSplash(lpparam);
hookDialogNoHwAccel();
hookActivityLifecycleDiag(lpparam);
hookHardwareRendererSetName();
hookBottomSelectDialogSafe(lpparam);
hookBottomSelectDialogDiag(lpparam);
hookActivityLifecycleDiag(lpparam);
hookPromonApService(lpparam);
hookPromonBroadcastReceiver(lpparam);
hookAppAttachForceLogin(lpparam);
@@ -525,9 +527,30 @@ public final class TngRootBypassHook {
}
}
/**
* 区号选择弹窗诊断。勿关 HW 加速——Android 16 上会导致列表黑屏。
* ANR 由 HardwareRenderer.setName 拦截兜底。
*/
private static void hookBottomSelectDialogDiag(XC_LoadPackage.LoadPackageParam lpparam) {
try {
Class<?> clazz = XposedHelpers.findClass(
"my.com.tngdigital.common.widget.BottomSelectDialogFragment",
lpparam.classLoader);
XposedHelpers.findAndHookMethod(clazz, "onStart", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log(TAG + " BottomSelectDialogFragment.onStart");
}
});
XposedBridge.log(TAG + " hooked BottomSelectDialogFragment.onStart (diag only)");
} catch (Throwable t) {
XposedBridge.log(TAG + " BottomSelectDialog hook failed: " + t.getMessage());
}
}
/**
* ANR 栈Dialog.show → enableHardwareAcceleration → HardwareRenderer.setName → future.get 卡死。
* 兜底拦截 setName,避免 RenderThread 未就绪时主线程永久阻塞
* 拦截 setName;勿全局关 Dialog HW区号选择会黑屏
*/
private static void hookHardwareRendererSetName() {
try {
@@ -545,71 +568,19 @@ public final class TngRootBypassHook {
}
}
/** Dialog.show 走 HW 加速会等 RenderThread复进时 RenderThread 易 abort → 主线程 ANR。 */
private static void hookDialogNoHwAccel() {
try {
XposedHelpers.findAndHookMethod(Dialog.class, "show", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
try {
Dialog dialog = (Dialog) param.thisObject;
android.view.Window window = dialog.getWindow();
if (window != null) {
window.setFlags(
0,
android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
}
} catch (Throwable ignored) {
}
}
});
XposedBridge.log(TAG + " hooked Dialog.show (no HW accel)");
} catch (Throwable t) {
XposedBridge.log(TAG + " Dialog.show hook failed: " + t.getMessage());
}
}
/**
* 注册/登录页点「下一步」会弹国家区号 BottomSelectDialogFragment。
* HardwareRenderer.setName 已拦截 ANR此处仅关 Dialog HW 加速,保留区号选择。
*/
private static void hookBottomSelectDialogSafe(XC_LoadPackage.LoadPackageParam lpparam) {
try {
Class<?> clazz = XposedHelpers.findClass(
"my.com.tngdigital.common.widget.BottomSelectDialogFragment",
lpparam.classLoader);
XposedHelpers.findAndHookMethod(clazz, "onStart", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
try {
Object dialog = XposedHelpers.callMethod(param.thisObject, "getDialog");
if (dialog instanceof Dialog) {
Dialog d = (Dialog) dialog;
if (d.getWindow() != null) {
d.getWindow().setFlags(
0,
android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
}
}
} catch (Throwable ignored) {
}
XposedBridge.log(TAG + " BottomSelectDialogFragment.onStart (safe, no HW)");
}
});
XposedBridge.log(TAG + " hooked BottomSelectDialogFragment.onStart (safe)");
} catch (Throwable t) {
XposedBridge.log(TAG + " BottomSelectDialog hook failed: " + t.getMessage());
}
}
/** Promon 隔离 Service打点确认 :goacqowmmt 进程 hook 已注入。 */
private static void hookPromonApService(XC_LoadPackage.LoadPackageParam lpparam) {
Class<?> svc = findPromonClass(lpparam.classLoader, "ap");
if (svc == null) {
XposedBridge.log(TAG + " Promon ap Service not found");
return;
}
try {
Class<?> svc = XposedHelpers.findClass("xwwqazamx.ap", lpparam.classLoader);
final String svcName = svc.getName();
XposedHelpers.findAndHookMethod(svc, "onCreate", new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
XposedBridge.log(TAG + " xwwqazamx.ap onCreate pid=" + Process.myPid()
XposedBridge.log(TAG + " " + svcName + " onCreate pid=" + Process.myPid()
+ " proc=" + getProcessName());
}
});
@@ -620,14 +591,14 @@ public final class TngRootBypassHook {
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log(TAG + " xwwqazamx.ap onStartCommand pid="
XposedBridge.log(TAG + " " + svcName + " onStartCommand pid="
+ Process.myPid());
}
});
}
XposedBridge.log(TAG + " hooked xwwqazamx.ap Service");
XposedBridge.log(TAG + " hooked " + svcName + " Service");
} catch (Throwable t) {
XposedBridge.log(TAG + " xwwqazamx.ap hook failed: " + t.getMessage());
XposedBridge.log(TAG + " Promon ap Service hook failed: " + t.getMessage());
}
}
@@ -746,10 +717,14 @@ public final class TngRootBypassHook {
return false;
}
/** Promon USB 广播 xwwqazamx.N 跑在主线程,复进时拖死 Looper。 */
/** Promon USB 广播 N 跑在主线程,复进时拖死 Looper。 */
private static void hookPromonBroadcastReceiver(XC_LoadPackage.LoadPackageParam lpparam) {
Class<?> clazz = findPromonClass(lpparam.classLoader, "N");
if (clazz == null) {
XposedBridge.log(TAG + " Promon N receiver not found");
return;
}
try {
Class<?> clazz = XposedHelpers.findClass("xwwqazamx.N", lpparam.classLoader);
int hooked = 0;
for (Method method : clazz.getDeclaredMethods()) {
if (!"onReceive".equals(method.getName())) {
@@ -764,10 +739,10 @@ public final class TngRootBypassHook {
hooked++;
}
if (hooked > 0) {
XposedBridge.log(TAG + " hooked xwwqazamx.N onReceive x" + hooked);
XposedBridge.log(TAG + " hooked " + clazz.getName() + " onReceive x" + hooked);
}
} catch (Throwable t) {
XposedBridge.log(TAG + " xwwqazamx.N hook failed: " + t.getMessage());
XposedBridge.log(TAG + " Promon N hook failed: " + t.getMessage());
}
}
@@ -1179,26 +1154,39 @@ public final class TngRootBypassHook {
|| lower.contains("emulator") || lower.contains("malware");
}
/** 阻止 Promon 混淆层抛出 W:16 并触发浏览器 fallback。 */
/** 阻止 Promon 混淆层抛出 W/bd:16 并触发浏览器 fallback。 */
private static void hookPromonNativeGuard(XC_LoadPackage.LoadPackageParam lpparam) {
hookPromonBlSwallowExceptions(lpparam);
hookPromonExceptionClass(lpparam, "xwwqazamx.W");
hookPromonExceptionClass(lpparam, "xwwqazamx.A");
for (String simple : new String[]{"W", "A", "bd"}) {
hookPromonExceptionClass(lpparam, simple);
}
hookPromonRunnable(lpparam);
}
/**
* Promon lifecycle只吞异常,不 short-circuit——全拦会拖死 Splash→Login
* Promon lifecycle1.9.10 用 vhvlnqgy.u旧版用 w。只吞异常不全拦
*/
private static void hookPromonLifecycle(XC_LoadPackage.LoadPackageParam lpparam) {
Class<?> promonExc = findPromonClass(lpparam.classLoader, "W");
if (promonExc == null) {
promonExc = findPromonClass(lpparam.classLoader, "bd");
}
final Class<?> promonExcFinal = promonExc;
for (String simple : new String[]{"w", "u"}) {
hookPromonLifecycleClass(lpparam, simple, promonExcFinal);
}
}
private static void hookPromonLifecycleClass(
XC_LoadPackage.LoadPackageParam lpparam,
String simpleName,
Class<?> promonExc) {
Class<?> lifecycleClass = findPromonClass(lpparam.classLoader, simpleName);
if (lifecycleClass == null) {
return;
}
final String lifecycleName = lifecycleClass.getName();
try {
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();
@@ -1212,9 +1200,9 @@ public final class TngRootBypassHook {
return;
}
Throwable t = param.getThrowable();
if (isPromonThrowable(t, promonExcFinal)) {
if (isPromonThrowable(t, promonExc)) {
XposedBridge.log(TAG + " swallowed " + t.getClass().getSimpleName()
+ " in xwwqazamx.w#" + method.getName());
+ " in " + lifecycleName + "#" + method.getName());
param.setThrowable(null);
}
}
@@ -1223,10 +1211,11 @@ public final class TngRootBypassHook {
}
if (hooked > 0) {
XposedBridge.log(TAG + " hooked " + hooked
+ " xwwqazamx.w lifecycle method(s) (afterHook only)");
+ " " + lifecycleName + " lifecycle method(s) (afterHook only)");
}
} catch (Throwable t) {
XposedBridge.log(TAG + " xwwqazamx.w lifecycle hook failed: " + t.getMessage());
XposedBridge.log(TAG + " " + lifecycleName + " lifecycle hook failed: "
+ t.getMessage());
}
}
@@ -1256,15 +1245,24 @@ public final class TngRootBypassHook {
}
/**
* bl 全方法 short-circuita/b 之外的方法仍会跑 native~40s 后 stack_chk/SEGV。
* xwwqazamx.a.run 是 bl#b 后台 Runnable必须 beforeHook 直接 return。
* bl/R 全方法 short-circuita/b 之外的方法仍会跑 native~40s 后 stack_chk/SEGV。
* a.run 是 bl#b 后台 Runnable必须 beforeHook 直接 return。
*/
private static void hookPromonBlSwallowExceptions(XC_LoadPackage.LoadPackageParam lpparam) {
for (String simple : new String[]{"bl", "R"}) {
hookPromonClassShortCircuit(lpparam, simple);
}
}
private static void hookPromonClassShortCircuit(
XC_LoadPackage.LoadPackageParam lpparam, String simpleName) {
Class<?> clazz = findPromonClass(lpparam.classLoader, simpleName);
if (clazz == null) {
return;
}
try {
Class<?> blClass = XposedHelpers.findClass("xwwqazamx.bl", lpparam.classLoader);
int hooked = 0;
for (Method method : blClass.getDeclaredMethods()) {
String name = method.getName();
for (Method method : clazz.getDeclaredMethods()) {
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
@@ -1274,10 +1272,12 @@ public final class TngRootBypassHook {
hooked++;
}
if (hooked > 0) {
XposedBridge.log(TAG + " hooked " + hooked + " bl method(s), all short-circuit");
XposedBridge.log(TAG + " hooked " + hooked + " " + clazz.getName()
+ " method(s), all short-circuit");
}
} catch (Throwable t) {
XposedBridge.log(TAG + " xwwqazamx.bl hook failed: " + t.getMessage());
XposedBridge.log(TAG + " " + clazz.getName() + " short-circuit failed: "
+ t.getMessage());
}
}
@@ -1294,24 +1294,32 @@ public final class TngRootBypassHook {
/** Promon 后台 Runnablebl#b 检测线程beforeHook 直接 noop禁止跑 native。 */
private static void hookPromonRunnable(XC_LoadPackage.LoadPackageParam lpparam) {
Class<?> runnableClass = findPromonClass(lpparam.classLoader, "a");
if (runnableClass == null) {
XposedBridge.log(TAG + " Promon a runnable not found");
return;
}
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)");
XposedBridge.log(TAG + " hooked " + runnableClass.getName() + ".run (short-circuit)");
} catch (Throwable t) {
XposedBridge.log(TAG + " xwwqazamx.a.run hook failed: " + t.getMessage());
XposedBridge.log(TAG + " Promon a.run hook failed: " + t.getMessage());
}
}
private static void hookPromonExceptionClass(
XC_LoadPackage.LoadPackageParam lpparam, String className) {
XC_LoadPackage.LoadPackageParam lpparam, String simpleName) {
Class<?> promonExc = findPromonClass(lpparam.classLoader, simpleName);
if (promonExc == null) {
return;
}
final String className = promonExc.getName();
try {
Class<?> promonExc = XposedHelpers.findClass(className, lpparam.classLoader);
for (Method method : promonExc.getDeclaredMethods()) {
XposedBridge.hookMethod(method, new XC_MethodHook() {
@Override
@@ -1332,6 +1340,7 @@ public final class TngRootBypassHook {
} catch (Throwable ignored) {
// constructor overload may differ
}
XposedBridge.log(TAG + " hooked Promon exception " + className);
} catch (Throwable t) {
XposedBridge.log(TAG + " " + className + " hook failed: " + t.getMessage());
}
@@ -1355,16 +1364,38 @@ public final class TngRootBypassHook {
if (className == null) {
return false;
}
if (!className.startsWith("xwwqazamx.")) {
if (!isPromonPackageClass(className)) {
return false;
}
// W/A 等单字母 Promon 异常;排除 bl/w/bg 等功能类
// W/A/bd 等 Promon 异常;排除 bl/w/bg/R 等功能类
int dot = className.lastIndexOf('.');
if (dot < 0) {
return false;
}
String simple = className.substring(dot + 1);
return simple.length() <= 2;
if ("bd".equals(simple)) {
return true;
}
return simple.length() == 1;
}
private static boolean isPromonPackageClass(String className) {
for (String prefix : PROMON_PKG_PREFIXES) {
if (className.startsWith(prefix + ".")) {
return true;
}
}
return false;
}
private static Class<?> findPromonClass(ClassLoader loader, String simpleName) {
for (String prefix : PROMON_PKG_PREFIXES) {
try {
return XposedHelpers.findClass(prefix + "." + simpleName, loader);
} catch (Throwable ignored) {
}
}
return null;
}
/** SecurityGuard探测命令 stub10101 init + 104xx/105xx sign/verify 走真实 native。 */
@@ -1866,35 +1897,24 @@ public final class TngRootBypassHook {
/** 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",
String[] simpleNames = {
"F", "bg", "b", "c", "d", "h", "k", "l", "m", "o", "s", "t", "z",
};
int total = 0;
for (String className : classes) {
total += hookPromonIntBooleanMethods(lpparam, className);
for (String simpleName : simpleNames) {
Class<?> clazz = findPromonClass(lpparam.classLoader, simpleName);
if (clazz != null) {
total += hookPromonIntBooleanMethods(clazz);
}
}
if (total > 0) {
XposedBridge.log(TAG + " Promon native-bridge total hooks=" + total);
}
}
private static int hookPromonIntBooleanMethods(
XC_LoadPackage.LoadPackageParam lpparam, String className) {
private static int hookPromonIntBooleanMethods(Class<?> clazz) {
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