fix(tng): 修复地区选择黑屏并加固注册链 native abort 防护

This commit is contained in:
mars
2026-08-03 11:22:51 +08:00
parent 18ae42ec63
commit 193c04a24b
6 changed files with 274 additions and 84 deletions

View File

@@ -37,6 +37,8 @@
static constexpr const char *kTargetPkg = "my.com.tngdigital.ewallet";
static constexpr const char *kPromonSo = "libtngdigital_ewallet.so";
static bool g_enabled = false;
static zygisk::Api *g_api = nullptr;
static std::atomic<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};
@@ -272,6 +274,13 @@ using kill_fn = int (*)(pid_t, int);
using tgkill_fn = int (*)(int, int, int);
using raise_fn = int (*)(int);
using pthread_kill_fn = int (*)(pthread_t, int);
using cxa_guard_acquire_fn = int (*)(void *);
using cxa_guard_abort_fn = void (*)();
using dlopen_fn = void *(*)(const char *, int);
static cxa_guard_acquire_fn orig_cxa_guard_acquire = nullptr;
static cxa_guard_abort_fn orig_cxa_guard_abort = nullptr;
static dlopen_fn orig_dlopen = nullptr;
static exit_fn orig_exit = nullptr;
static exit_fn orig__exit = nullptr;
@@ -326,7 +335,40 @@ static int hooked_pthread_kill(pthread_t thread, int sig) {
return orig_pthread_kill ? orig_pthread_kill(thread, sig) : -1;
}
static bool find_libc(dev_t *dev, ino_t *ino) {
/** Promon/libc++ 静态局部量递归初始化会 abort 主进程Registration 页 HWUI 线程)。 */
static int hooked_cxa_guard_acquire(void *guard) {
(void)guard;
return 1;
}
static void hooked_cxa_guard_abort() {
LOGI("blocked __cxa_guard_abort tid=%d", (int)gettid());
}
static void *hooked_dlopen(const char *name, int flags) {
void *handle = orig_dlopen ? orig_dlopen(name, flags) : nullptr;
if (handle != nullptr || name == nullptr) {
return handle;
}
if (strstr(name, "libandroid.so") != nullptr) {
static const char *kFallbacks[] = {
"/system/lib64/libandroid.so",
"/system/lib/libandroid.so",
"libandroid.so",
};
for (const char *path : kFallbacks) {
handle = orig_dlopen ? orig_dlopen(path, flags) : nullptr;
if (handle != nullptr) {
LOGI("dlopen fallback %s -> %p (from %s)", path, handle, name);
return handle;
}
}
LOGI("dlopen libandroid.so failed tid=%d", (int)gettid());
}
return handle;
}
static bool find_lib_match(const char *suffix, const char *contains,
dev_t *dev, ino_t *ino) {
FILE *fp = fopen("/proc/self/maps", "r");
if (!fp) return false;
char line[1024];
@@ -339,11 +381,19 @@ static bool find_libc(dev_t *dev, ino_t *ino) {
unsigned long inode = 0;
char path[512] = {};
int n = sscanf(line, "%lx-%lx %7s %llx %31s %lu %511[^\n]",
&start, &end, perms, &offset, deststr, &inode, path);
&start, &end, perms, &offset, &deststr, &inode, path);
if (n < 7 || inode == 0) continue;
char *p = path;
while (*p == ' ') ++p;
if (strstr(p, "libc.so") == nullptr) continue;
bool match = false;
if (suffix != nullptr) {
size_t plen = strlen(p);
size_t slen = strlen(suffix);
match = plen >= slen && strcmp(p + plen - slen, suffix) == 0;
} else if (contains != nullptr) {
match = strstr(p, contains) != nullptr;
}
if (!match) continue;
unsigned maj = 0, min = 0;
if (sscanf(deststr, "%x:%x", &maj, &min) != 2) continue;
*dev = makedev(maj, min);
@@ -355,25 +405,72 @@ static bool find_libc(dev_t *dev, ino_t *ino) {
return ok;
}
static bool find_lib_by_suffix(const char *suffix, dev_t *dev, ino_t *ino) {
return find_lib_match(suffix, nullptr, dev, ino);
}
static bool find_lib_contains(const char *needle, dev_t *dev, ino_t *ino) {
return find_lib_match(nullptr, needle, dev, ino);
}
static bool find_libc(dev_t *dev, ino_t *ino) {
return find_lib_by_suffix("libc.so", dev, ino);
}
static void register_plt(zygisk::Api *api, dev_t dev, ino_t ino,
const char *sym, void *hook, void **orig) {
if (!api || dev == 0 || ino == 0) return;
api->pltHookRegister(dev, ino, sym, hook, orig);
}
static void install_plt(zygisk::Api *api) {
if (!api) return;
dev_t dev = 0;
ino_t ino = 0;
if (!find_libc(&dev, &ino)) return;
api->pltHookRegister(dev, ino, "exit", (void *)hooked_exit, (void **)&orig_exit);
api->pltHookRegister(dev, ino, "_exit", (void *)hooked__exit, (void **)&orig__exit);
api->pltHookRegister(dev, ino, "abort", (void *)hooked_abort, (void **)&orig_abort);
api->pltHookRegister(dev, ino, "__stack_chk_fail",
(void *)hooked_stack_chk_fail, (void **)&orig_stack_chk_fail);
api->pltHookRegister(dev, ino, "raise", (void *)hooked_raise, (void **)&orig_raise);
api->pltHookRegister(dev, ino, "kill", (void *)hooked_kill, (void **)&orig_kill);
api->pltHookRegister(dev, ino, "tgkill", (void *)hooked_tgkill, (void **)&orig_tgkill);
api->pltHookRegister(dev, ino, "pthread_kill",
(void *)hooked_pthread_kill, (void **)&orig_pthread_kill);
if (find_libc(&dev, &ino)) {
register_plt(api, dev, ino, "exit", (void *)hooked_exit, (void **)&orig_exit);
register_plt(api, dev, ino, "_exit", (void *)hooked__exit, (void **)&orig__exit);
register_plt(api, dev, ino, "abort", (void *)hooked_abort, (void **)&orig_abort);
register_plt(api, dev, ino, "__stack_chk_fail",
(void *)hooked_stack_chk_fail, (void **)&orig_stack_chk_fail);
register_plt(api, dev, ino, "raise", (void *)hooked_raise, (void **)&orig_raise);
register_plt(api, dev, ino, "kill", (void *)hooked_kill, (void **)&orig_kill);
register_plt(api, dev, ino, "tgkill", (void *)hooked_tgkill, (void **)&orig_tgkill);
register_plt(api, dev, ino, "pthread_kill",
(void *)hooked_pthread_kill, (void **)&orig_pthread_kill);
register_plt(api, dev, ino, "dlopen", (void *)hooked_dlopen, (void **)&orig_dlopen);
}
bool ok = api->pltHookCommit();
LOGI("PLT commit=%d", ok ? 1 : 0);
}
static void try_install_cxx_guard_plt() {
if (g_cxx_plt.load() || !g_api) return;
dev_t dev = 0;
ino_t ino = 0;
bool any = false;
if (find_lib_by_suffix("libc++_shared.so", &dev, &ino)
|| find_lib_contains("libc++", &dev, &ino)) {
register_plt(g_api, dev, ino, "__cxa_guard_acquire",
(void *)hooked_cxa_guard_acquire, (void **)&orig_cxa_guard_acquire);
register_plt(g_api, dev, ino, "__cxa_guard_abort",
(void *)hooked_cxa_guard_abort, (void **)&orig_cxa_guard_abort);
any = true;
}
if (find_lib_by_suffix("libtngdigital_ewallet.so", &dev, &ino)) {
register_plt(g_api, dev, ino, "__cxa_guard_acquire",
(void *)hooked_cxa_guard_acquire, (void **)&orig_cxa_guard_acquire);
register_plt(g_api, dev, ino, "__cxa_guard_abort",
(void *)hooked_cxa_guard_abort, (void **)&orig_cxa_guard_abort);
any = true;
}
if (!any) return;
if (g_api->pltHookCommit()) {
g_cxx_plt.store(1);
LOGI("PLT cxx guards committed");
}
}
static void *phase_thread(void *) {
install_promon_segv_handler();
usleep(400 * 1000);
@@ -382,6 +479,8 @@ static void *phase_thread(void *) {
for (int i = 0; i < 40; i++) {
usleep(1000 * 1000);
install_soft_signals();
install_promon_segv_handler();
try_install_cxx_guard_plt();
if (i % 5 == 0) refresh_promon_so_range();
}
LOGI("phase done seccomp=%d stack_chk=%d promon_segv=%d",
@@ -390,8 +489,9 @@ static void *phase_thread(void *) {
}
static void install_all(zygisk::Api *api) {
g_api = api;
g_main_tid.store(gettid());
LOGI("install pid=%d main_tid=%d (PLT+ABRT-pc+4+SEGV-all-skip+exit_group@400ms)",
LOGI("install pid=%d main_tid=%d (PLT+cxx-guard+ABRT-pc+4+SEGV-skip+exit_group@400ms)",
getpid(), (int)g_main_tid.load());
install_fatal_skip_handlers();
install_soft_signals();

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,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

@@ -227,10 +227,9 @@ public final class TngRootBypassHook {
hookRegistrationFlowGuard(lpparam);
hookSplashForceLogin(lpparam);
hookLoginDismissSplash(lpparam);
hookDialogNoHwAccel();
hookActivityLifecycleDiag(lpparam);
hookHardwareRendererSetName();
hookBottomSelectDialogSafe(lpparam);
hookBottomSelectDialogDiag(lpparam);
hookActivityLifecycleDiag(lpparam);
hookPromonApService(lpparam);
hookPromonBroadcastReceiver(lpparam);
hookAppAttachForceLogin(lpparam);
@@ -528,9 +527,30 @@ public final class TngRootBypassHook {
}
}
/**
* 区号选择弹窗诊断。勿关 HW 加速——Android 16 上会导致列表黑屏。
* ANR 由 HardwareRenderer.setName 拦截兜底。
*/
private static void hookBottomSelectDialogDiag(XC_LoadPackage.LoadPackageParam lpparam) {
try {
Class<?> clazz = XposedHelpers.findClass(
"my.com.tngdigital.common.widget.BottomSelectDialogFragment",
lpparam.classLoader);
XposedHelpers.findAndHookMethod(clazz, "onStart", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log(TAG + " BottomSelectDialogFragment.onStart");
}
});
XposedBridge.log(TAG + " hooked BottomSelectDialogFragment.onStart (diag only)");
} catch (Throwable t) {
XposedBridge.log(TAG + " BottomSelectDialog hook failed: " + t.getMessage());
}
}
/**
* ANR 栈Dialog.show → enableHardwareAcceleration → HardwareRenderer.setName → future.get 卡死。
* 兜底拦截 setName,避免 RenderThread 未就绪时主线程永久阻塞
* 拦截 setName;勿全局关 Dialog HW区号选择会黑屏
*/
private static void hookHardwareRendererSetName() {
try {
@@ -548,63 +568,6 @@ public final class TngRootBypassHook {
}
}
/** Dialog.show 走 HW 加速会等 RenderThread复进时 RenderThread 易 abort → 主线程 ANR。 */
private static void hookDialogNoHwAccel() {
try {
XposedHelpers.findAndHookMethod(Dialog.class, "show", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
try {
Dialog dialog = (Dialog) param.thisObject;
android.view.Window window = dialog.getWindow();
if (window != null) {
window.setFlags(
0,
android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
}
} catch (Throwable ignored) {
}
}
});
XposedBridge.log(TAG + " hooked Dialog.show (no HW accel)");
} catch (Throwable t) {
XposedBridge.log(TAG + " Dialog.show hook failed: " + t.getMessage());
}
}
/**
* 注册/登录页点「下一步」会弹国家区号 BottomSelectDialogFragment。
* HardwareRenderer.setName 已拦截 ANR此处仅关 Dialog HW 加速,保留区号选择。
*/
private static void hookBottomSelectDialogSafe(XC_LoadPackage.LoadPackageParam lpparam) {
try {
Class<?> clazz = XposedHelpers.findClass(
"my.com.tngdigital.common.widget.BottomSelectDialogFragment",
lpparam.classLoader);
XposedHelpers.findAndHookMethod(clazz, "onStart", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
try {
Object dialog = XposedHelpers.callMethod(param.thisObject, "getDialog");
if (dialog instanceof Dialog) {
Dialog d = (Dialog) dialog;
if (d.getWindow() != null) {
d.getWindow().setFlags(
0,
android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
}
}
} catch (Throwable ignored) {
}
XposedBridge.log(TAG + " BottomSelectDialogFragment.onStart (safe, no HW)");
}
});
XposedBridge.log(TAG + " hooked BottomSelectDialogFragment.onStart (safe)");
} catch (Throwable t) {
XposedBridge.log(TAG + " BottomSelectDialog hook failed: " + t.getMessage());
}
}
/** Promon 隔离 Service打点确认 :goacqowmmt 进程 hook 已注入。 */
private static void hookPromonApService(XC_LoadPackage.LoadPackageParam lpparam) {
Class<?> svc = findPromonClass(lpparam.classLoader, "ap");
@@ -1201,19 +1164,27 @@ public final class TngRootBypassHook {
}
/**
* Promon lifecycle只吞异常,不 short-circuit——全拦会拖死 Splash→Login
* Promon lifecycle1.9.10 用 vhvlnqgy.u旧版用 w。只吞异常不全拦
*/
private static void hookPromonLifecycle(XC_LoadPackage.LoadPackageParam lpparam) {
Class<?> lifecycleClass = findPromonClass(lpparam.classLoader, "w");
if (lifecycleClass == null) {
XposedBridge.log(TAG + " Promon w lifecycle not found");
return;
}
Class<?> promonExc = findPromonClass(lpparam.classLoader, "W");
if (promonExc == null) {
promonExc = findPromonClass(lpparam.classLoader, "bd");
}
final Class<?> promonExcFinal = promonExc;
for (String simple : new String[]{"w", "u"}) {
hookPromonLifecycleClass(lpparam, simple, promonExcFinal);
}
}
private static void hookPromonLifecycleClass(
XC_LoadPackage.LoadPackageParam lpparam,
String simpleName,
Class<?> promonExc) {
Class<?> lifecycleClass = findPromonClass(lpparam.classLoader, simpleName);
if (lifecycleClass == null) {
return;
}
final String lifecycleName = lifecycleClass.getName();
try {
int hooked = 0;
@@ -1229,7 +1200,7 @@ public final class TngRootBypassHook {
return;
}
Throwable t = param.getThrowable();
if (isPromonThrowable(t, promonExcFinal)) {
if (isPromonThrowable(t, promonExc)) {
XposedBridge.log(TAG + " swallowed " + t.getClass().getSimpleName()
+ " in " + lifecycleName + "#" + method.getName());
param.setThrowable(null);
@@ -1243,7 +1214,8 @@ public final class TngRootBypassHook {
+ " " + lifecycleName + " lifecycle method(s) (afterHook only)");
}
} catch (Throwable t) {
XposedBridge.log(TAG + " Promon w lifecycle hook failed: " + t.getMessage());
XposedBridge.log(TAG + " " + lifecycleName + " lifecycle hook failed: "
+ t.getMessage());
}
}