chore: 备份 TNG 注册/captcha 逆向与 MariBank SG bypass 进展

TngRootBypassHook 增强 captcha 诊断、TigerTally/JNIC 分层与 HWUI 策略;新增逆向脚本、Frida 工具与 UI dump;同步 MariBank SG hook 与 tng_exit_guard 更新。
This commit is contained in:
mars
2026-08-03 15:23:02 +08:00
parent 193c04a24b
commit 609635aba1
185 changed files with 60843 additions and 231 deletions

View File

@@ -0,0 +1,466 @@
#!/usr/bin/env python3
"""TNG 全流程自动化:注册区号 + 登录 PIN/区号;两次冷启动,互不干扰。"""
import re
import subprocess
import sys
import time
import xml.etree.ElementTree as ET
PKG = "my.com.tngdigital.ewallet"
SPLASH = f"{PKG}/.ui.SplashActivity"
ADB = ["adb"]
TNG_LOG = re.compile(
r"TngRoot|F HWUI|GraphicBuffer|Runtime abort|signal 6|exited due to signal"
r"|UserSearchCallingCode|UserLogin|UserRegistration|SecurityError"
r"|Dialog\.show|skip loading|registration flow",
re.I,
)
CRASH_LOG = re.compile(
r"signal 6|Runtime aborting|F HWUI|F GraphicBuffer|gralloc-mapper is missing",
re.I,
)
def adb(*args, timeout=60):
r = subprocess.run(ADB + list(args), capture_output=True, text=True, timeout=timeout, errors="ignore")
return r.returncode, (r.stdout or "") + (r.stderr or "")
def is_tng_foreground():
_, out = adb("shell", "dumpsys", "activity", "activities")
for line in out.splitlines():
if "topResumedActivity=" in line:
return PKG in line
return False
def bring_tng_foreground():
adb("shell", "input", "keyevent", "KEYCODE_WAKEUP")
if is_tng_foreground():
return
adb("shell", "am", "start", "-n", SPLASH)
time.sleep(2)
def dump_ui(path="/sdcard/tng_ui.xml", retries=2):
adb("shell", "input", "keyevent", "KEYCODE_WAKEUP")
for _ in range(retries):
adb("shell", "uiautomator", "dump", path)
code, xml = adb("shell", "cat", path)
if code == 0 and xml.strip().startswith("<?xml"):
try:
return ET.fromstring(xml)
except ET.ParseError:
pass
time.sleep(1)
return None
def center(bounds_str):
m = re.match(r"\[(\d+),(\d+)\]\[(\d+),(\d+)\]", bounds_str or "")
if not m:
return None
x1, y1, x2, y2 = map(int, m.groups())
return (x1 + x2) // 2, (y1 + y2) // 2
def find_pin_login_row(root):
candidates = []
for node in root.iter("node"):
t = node.get("text") or ""
if "忘记" in t:
continue
pt = center(node.get("bounds"))
if pt is None:
continue
# 登录方式页 PIN 行 y≈600780
if 600 <= pt[1] <= 780 and (
"PIN" in t.upper() or "6位数" in t or "6位" in t or (len(t) >= 4 and "PIN" in t)
):
candidates.append((pt[1], node))
if not candidates:
return None
candidates.sort(key=lambda x: x[0])
return candidates[0][1]
def wait_phone_page(timeout=20):
return wait_for(lambda: is_phone_page(dump_ui()), timeout=timeout, interval=1.5, desc="phone page")
def find_nodes(root, **kwargs):
out = []
for node in root.iter("node"):
text = (node.get("text") or "").strip()
rid = node.get("resource-id") or ""
cls = node.get("class") or ""
clickable = node.get("clickable") == "true"
ok = True
if "text_contains" in kwargs and kwargs["text_contains"] not in text:
ok = False
if "text_excludes" in kwargs:
for ex in kwargs["text_excludes"]:
if ex in text:
ok = False
if "rid_contains" in kwargs and kwargs["rid_contains"] not in rid:
ok = False
if "class_contains" in kwargs and kwargs["class_contains"] not in cls:
ok = False
if kwargs.get("clickable") and not clickable:
ok = False
if ok:
out.append(node)
return out
def tap_node(node, label=""):
pt = center(node.get("bounds"))
if not pt:
print(f" skip tap {label}: bad bounds")
return False
x, y = pt
print(f" tap {label!r} at {x},{y}")
adb("shell", "input", "tap", str(x), str(y))
return True
def find_country_control(root, min_y=0, max_y=9999):
nodes = find_nodes(root, rid_contains="ll_country", clickable=True)
for node in nodes:
pt = center(node.get("bounds"))
if pt and min_y <= pt[1] <= max_y:
return node
nodes = find_nodes(root, rid_contains="tv_left", clickable=True)
for node in nodes:
pt = center(node.get("bounds"))
if pt and min_y <= pt[1] <= max_y:
return node
return None
def find_register_btn(root):
for node in root.iter("node"):
if node.get("clickable") != "true":
continue
t = node.get("text") or ""
if ("注册" in t or "Register" in t.lower()) and "已经" not in t and "已注" not in t:
return node
pt = center(node.get("bounds"))
if pt and 1050 < pt[1] < 1220 and 430 < pt[0] < 660:
return node
return None
def is_phone_page(root):
return bool(find_nodes(root, rid_contains="userContinueBtn"))
def is_register_page(root):
return bool(
find_nodes(root, rid_contains="ftv_register_title")
or find_nodes(root, rid_contains="userRegisterContinueBtn")
)
def has_country_list(root):
if root is None:
return False
for node in root.iter("node"):
t = node.get("text") or ""
if any(k in t for k in ("Malaysia", "Singapore", "Australia", "China", "+86", "+61")):
return True
return False
def pid():
code, out = adb("shell", "pidof", PKG)
return out.strip() if code == 0 and out.strip() else ""
def top_activity():
_, out = adb("shell", "dumpsys", "activity", "activities")
for line in out.splitlines():
if PKG not in line:
continue
if "topResumedActivity=" in line:
m = re.search(r"/([^/}\s]+)", line)
return m.group(1) if m else "?"
for line in out.splitlines():
if "ResumedActivity:" in line and PKG in line:
m = re.search(r"/([^/}\s]+)", line)
if m:
return m.group(1)
return "?"
def wait_for(fn, timeout=35, interval=1.5, desc=""):
deadline = time.time() + timeout
while time.time() < deadline:
if fn():
return True
time.sleep(interval)
print(f" timeout: {desc}")
return False
def wait_ui_widgets(timeout=30):
"""等待 Compose 控件出现在 dumpSplash WebView 退场后)。"""
def ready():
root = dump_ui()
if root is None:
return False
return bool(
find_pin_login_row(root)
or find_register_btn(root)
or is_phone_page(root)
or find_nodes(root, rid_contains="ftv_content")
or find_nodes(root, rid_contains="ftv_register_title")
)
return wait_for(ready, timeout=timeout, interval=2, desc="login/register widgets")
def wait_login_method(timeout=40):
def ready():
if not pid():
return False
if not is_tng_foreground():
bring_tng_foreground()
return "UserLoginActivity" in top_activity()
ok = wait_for(ready, timeout=timeout, interval=2, desc="UserLoginActivity ready")
if ok:
wait_ui_widgets(timeout=25)
return ok
def wait_login_method_register(timeout=40):
return wait_login_method(timeout=timeout)
def wait_country_list(timeout=30):
def activity_has_country_list():
act = top_activity()
if "UserSearchCallingCode" in act:
return True
_, out = adb("shell", "dumpsys", "activity", "activities")
return "UserSearchCallingCodeActivity" in out and PKG in out
def ready():
if activity_has_country_list():
return True
root = dump_ui()
return has_country_list(root)
return wait_for(ready, timeout=timeout, interval=1.5, desc="country list")
def cold_start(clear_log=False):
if clear_log:
adb("logcat", "-c")
adb("shell", "am", "force-stop", PKG)
time.sleep(3)
adb("shell", "input", "keyevent", "KEYCODE_WAKEUP")
adb("shell", "am", "start", "-W", "-n", SPLASH)
ok = wait_for(
lambda: bool(pid()) and "UserLoginActivity" in top_activity(),
timeout=60,
interval=2,
desc="UserLoginActivity after cold start",
)
if not ok:
return False
bring_tng_foreground()
time.sleep(3)
return True
def cold_start_with_retry(clear_log=False, attempts=3):
for i in range(attempts):
if cold_start(clear_log and i == 0):
return True
print(f" cold start retry {i + 1}/{attempts}")
adb("shell", "am", "force-stop", PKG)
time.sleep(5)
return bool(pid()) and "UserLoginActivity" in top_activity()
def tap_pin_row(root):
if root is None:
print(" PIN fallback (no dump) 561,677")
adb("shell", "input", "tap", "561", "677")
return
pin_text = find_pin_login_row(root)
if pin_text is None:
if find_nodes(root, rid_contains="ftv_content") or find_nodes(root, rid_contains="ftv_title"):
print(" PIN layout fallback 561,677")
adb("shell", "input", "tap", "561", "677")
return
print(" PIN fallback tap 540,677")
adb("shell", "input", "tap", "540", "677")
return
pt = center(pin_text.get("bounds"))
print(f" tap PIN at {pt[0]},{pt[1]}")
adb("shell", "input", "tap", str(pt[0]), str(pt[1]))
def collect_logs(n=3000):
_, out = adb("shell", "logcat", "-d", "-t", str(n))
return [ln for ln in out.splitlines() if TNG_LOG.search(ln)]
def collect_crashes(n=4000):
_, out = adb("shell", "logcat", "-d", "-t", str(n))
return [ln for ln in out.splitlines() if CRASH_LOG.search(ln) and "digital.ewallet" in ln]
def step(name, fn):
print(f"\n=== {name} ===")
bring_tng_foreground()
ok = fn()
print(f" pid={pid() or 'DEAD'} activity={top_activity()}")
return ok and bool(pid())
def wait_register_page(timeout=20):
def ready():
root = dump_ui()
if root is None or not is_register_page(root):
return False
ctrl = find_country_control(root, min_y=680)
if ctrl is None:
return False
pt = center(ctrl.get("bounds"))
# 注册页 ll_country 中心 y 通常 > 680
return pt is not None and pt[1] >= 680
ok = wait_for(ready, timeout=timeout, interval=1.5, desc="register page settled")
if ok:
time.sleep(1)
return ok
def flow_register():
time.sleep(5)
if not cold_start_with_retry():
return False
if not wait_login_method_register():
print(" register entry not visible")
return False
root = dump_ui()
reg = find_register_btn(root)
if reg is None:
print(" register fallback 540,1137")
adb("shell", "input", "tap", "540", "1137")
else:
tap_node(reg, "register")
if not wait_register_page():
return False
root2 = dump_ui()
target = find_country_control(root2, min_y=680)
if target is None:
print(" no country control on register page")
return False
tap_node(target, "register country")
if not wait_country_list(timeout=25):
return False
root3 = dump_ui()
if root3 is not None:
for node in root3.iter("node"):
t = node.get("text") or ""
if "Malaysia" in t or "Singapore" in t:
print(f" country UI: {t[:40]}")
break
logs = collect_logs(300)
for ln in logs:
if "skip loading" in ln or "UserSearchCallingCode" in ln:
print(f" log: {ln[:120]}")
break
return True
def flow_login():
if not cold_start_with_retry(clear_log=True):
return False
if not wait_login_method():
return False
root = dump_ui()
if is_phone_page(root):
print(" already on phone page")
else:
for attempt in range(2):
root = dump_ui()
if root is None:
time.sleep(2)
continue
if is_phone_page(root):
print(" phone page ready")
break
tap_pin_row(root)
if wait_phone_page(timeout=15):
break
print(f" phone page retry {attempt + 1}/2")
time.sleep(2)
else:
return False
root2 = dump_ui()
target = find_country_control(root2, min_y=450, max_y=650)
if target is None:
print(" no country control on phone page")
return False
tap_node(target, "login country")
if not wait_country_list():
return False
root3 = dump_ui()
if root3 is not None:
for node in root3.iter("node"):
t = node.get("text") or ""
if "Malaysia" in t or "Singapore" in t:
print(f" country UI: {t[:40]}")
break
logs = collect_logs(300)
for ln in logs:
if "skip loading" in ln:
print(f" log: {ln[:120]}")
break
return "UserSearchCallingCode" in top_activity() or has_country_list(root3)
def main():
adb("shell", "svc", "power", "stayon", "true")
adb("logcat", "-c")
adb("shell", "am", "force-stop", PKG)
time.sleep(5)
# 先登录后注册:首次冷启动最稳定
results = [
("login_country", step("A. login PIN → country list", flow_login)),
("register_country", step("B. register → country list", flow_register)),
]
crashes = collect_crashes()
p = pid()
print("\n=== SUMMARY ===")
for name, ok in results:
print(f" {'PASS' if ok else 'FAIL'} {name}")
print(f" pid={p or 'DEAD'} activity={top_activity()}")
print(f" crashes={len(crashes)}")
if crashes:
for ln in crashes[-5:]:
print(" ", ln[:150])
print("\n=== TngRoot (last 20) ===")
for ln in collect_logs(2000)[-20:]:
if "TngRoot" in ln:
print(ln[:180])
ok = bool(p) and all(r[1] for r in results)
if crashes and not ok:
print(" (crash lines may include prior process; see pid)")
return 0 if ok else 1
if __name__ == "__main__":
sys.exit(main())