Files
notiMessage/reverse/scripts/test_tng_full_flow.py
mars 609635aba1 chore: 备份 TNG 注册/captcha 逆向与 MariBank SG bypass 进展
TngRootBypassHook 增强 captcha 诊断、TigerTally/JNIC 分层与 HWUI 策略;新增逆向脚本、Frida 工具与 UI dump;同步 MariBank SG hook 与 tng_exit_guard 更新。
2026-08-03 15:23:02 +08:00

467 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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())