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:
20
reverse/scripts/_debug_pin.py
Normal file
20
reverse/scripts/_debug_pin.py
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env python3
|
||||
import re
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
path = sys.argv[1]
|
||||
root = ET.parse(path).getroot()
|
||||
|
||||
def center(b):
|
||||
m = re.match(r"\[(\d+),(\d+)\]\[(\d+),(\d+)\]", b or "")
|
||||
if not m:
|
||||
return None
|
||||
x1, y1, x2, y2 = map(int, m.groups())
|
||||
return (x1 + x2) // 2, (y1 + y2) // 2
|
||||
|
||||
for n in root.iter("node"):
|
||||
t = n.get("text") or ""
|
||||
pt = center(n.get("bounds"))
|
||||
if pt and 600 <= pt[1] <= 800:
|
||||
print(repr(t), pt, "PIN" in t.upper(), "6位" in t)
|
||||
41
reverse/scripts/_dump_captcha_methods.py
Normal file
41
reverse/scripts/_dump_captcha_methods.py
Normal file
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Dump aliyun captcha method signatures from TNG APK."""
|
||||
import zipfile
|
||||
import re
|
||||
import sys
|
||||
|
||||
apk = r"C:\Users\Administrator\Desktop\notiMessage\reverse\dumps\tng_1.9.10_base.apk"
|
||||
targets = [
|
||||
b"Lcom/aliyun/captcha/Captcha;",
|
||||
b"Lcom/aliyun/captcha/CaptchaWebViewDialog;",
|
||||
b"Lcom/aliyun/captcha/Captcha$VerificationCallback;",
|
||||
b"Lcom/aliyun/captcha/CaptchaWebViewDialog$CaptchaCompletionListener;",
|
||||
b"Lcom/aliyun/captcha/a;",
|
||||
b"Lcom/aliyun/captcha/b;",
|
||||
b"Lcom/aliyun/captcha/c;",
|
||||
]
|
||||
|
||||
# Method refs: Lclass;->name(args)ret
|
||||
pat = re.compile(
|
||||
rb"(Lcom/aliyun/captcha/[A-Za-z0-9_/$]+;)->([A-Za-z0-9_<>$]+)\(([^)]*)\)([A-Za-z0-9_/;$[\]-]+)"
|
||||
)
|
||||
|
||||
found = {t.decode(): set() for t in targets}
|
||||
with zipfile.ZipFile(apk) as z:
|
||||
for n in z.namelist():
|
||||
if not n.endswith(".dex"):
|
||||
continue
|
||||
data = z.read(n)
|
||||
for m in pat.finditer(data):
|
||||
clazz = m.group(1).decode()
|
||||
if clazz not in found:
|
||||
continue
|
||||
name = m.group(2).decode()
|
||||
args = m.group(3).decode()
|
||||
ret = m.group(4).decode()
|
||||
found[clazz].add(f"{name}({args}){ret}")
|
||||
|
||||
for clazz, methods in found.items():
|
||||
print("====", clazz, "n=", len(methods))
|
||||
for s in sorted(methods):
|
||||
print(" ", s)
|
||||
27
reverse/scripts/_parse_country_ui.py
Normal file
27
reverse/scripts/_parse_country_ui.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import xml.etree.ElementTree as ET
|
||||
import re
|
||||
import sys
|
||||
|
||||
path = sys.argv[1] if len(sys.argv) > 1 else r"reverse/dumps/ui_login_now.xml"
|
||||
root = ET.parse(path).getroot()
|
||||
for n in root.iter("node"):
|
||||
rid = n.attrib.get("resource-id", "")
|
||||
text = n.attrib.get("text", "")
|
||||
desc = n.attrib.get("content-desc", "")
|
||||
click = n.attrib.get("clickable")
|
||||
bounds = n.attrib.get("bounds")
|
||||
blob = (rid + " " + text + " " + desc).lower()
|
||||
if any(k in blob for k in ("country", "ll_country", "+60", "calling", "flag")):
|
||||
print("HIT", rid, repr(text), repr(desc), click, bounds)
|
||||
if text and (("+" in text[:3]) or text.strip() in ("PIN",) or "Malaysia" in text):
|
||||
print("TEXT", repr(text), bounds, click, rid)
|
||||
|
||||
# print clickable centers for ll_country
|
||||
for n in root.iter("node"):
|
||||
rid = n.attrib.get("resource-id", "")
|
||||
if "ll_country" in rid and n.attrib.get("clickable") == "true":
|
||||
m = re.match(r"\[(\d+),(\d+)\]\[(\d+),(\d+)\]", n.attrib.get("bounds", ""))
|
||||
if m:
|
||||
x1, y1, x2, y2 = map(int, m.groups())
|
||||
print("TAP", (x1 + x2) // 2, (y1 + y2) // 2, rid)
|
||||
15
reverse/scripts/_parse_ui.py
Normal file
15
reverse/scripts/_parse_ui.py
Normal file
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
path = sys.argv[1] if len(sys.argv) > 1 else "/sdcard/tng_ui.xml"
|
||||
root = ET.parse(path).getroot()
|
||||
for n in root.iter("node"):
|
||||
t = (n.get("text") or "").strip()
|
||||
rid = n.get("resource-id") or ""
|
||||
desc = (n.get("content-desc") or "").strip()
|
||||
c = n.get("clickable") == "true"
|
||||
b = n.get("bounds") or ""
|
||||
if t or rid or desc:
|
||||
if t or "tv_" in rid or "country" in rid.lower() or "PIN" in (t + desc).upper():
|
||||
print(f"text={t!r} rid={rid} desc={desc!r} click={c} {b}")
|
||||
81
reverse/scripts/_scan_calling_code.py
Normal file
81
reverse/scripts/_scan_calling_code.py
Normal file
@@ -0,0 +1,81 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Scan TNG APK for CallingCode / country UI / HW-related classes."""
|
||||
import re
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\dumps\tng_base_scan.apk"
|
||||
|
||||
NEEDLES = [
|
||||
b"UserSearchCallingCodeActivity",
|
||||
b"CallingCode",
|
||||
b"ll_country",
|
||||
b"BottomSelect",
|
||||
b"BottomSelectDialogFragment",
|
||||
b"ftv_title",
|
||||
b"i7.l",
|
||||
b"enableHardwareAcceleration",
|
||||
b"FLAG_HARDWARE_ACCELERATED",
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
z = zipfile.ZipFile(APK)
|
||||
print("=== DEX STRING HITS ===")
|
||||
for name in sorted(z.namelist()):
|
||||
if not name.endswith(".dex"):
|
||||
continue
|
||||
data = z.read(name)
|
||||
hits = [s.decode() for s in NEEDLES if s in data]
|
||||
if hits:
|
||||
print("%s -> %s" % (name, hits))
|
||||
|
||||
print("\n=== CLASS NAMES (CallingCode / BottomSelect / country) ===")
|
||||
pat = re.compile(
|
||||
rb"L[a-zA-Z0-9_$/]*(?:CallingCode|BottomSelect|Country|country)[a-zA-Z0-9_$/]*;"
|
||||
)
|
||||
found = set()
|
||||
for name in sorted(z.namelist()):
|
||||
if not name.endswith(".dex"):
|
||||
continue
|
||||
data = z.read(name)
|
||||
for m in pat.findall(data):
|
||||
found.add(m.decode("ascii", "ignore")[1:-1].replace("/", "."))
|
||||
for c in sorted(found):
|
||||
print(" ", c)
|
||||
|
||||
print("\n=== CONTEXT AROUND UserSearchCallingCodeActivity ===")
|
||||
target = b"UserSearchCallingCodeActivity"
|
||||
for name in sorted(z.namelist()):
|
||||
if not name.endswith(".dex"):
|
||||
continue
|
||||
data = z.read(name)
|
||||
start = 0
|
||||
n = 0
|
||||
while True:
|
||||
idx = data.find(target, start)
|
||||
if idx < 0:
|
||||
break
|
||||
s = max(0, idx - 80)
|
||||
e = min(len(data), idx + len(target) + 120)
|
||||
chunk = re.sub(rb"[^\x20-\x7e]+", b".", data[s:e])
|
||||
print("[%s @%d] %s" % (name, idx, chunk.decode("ascii", "ignore")))
|
||||
start = idx + 1
|
||||
n += 1
|
||||
if n >= 8:
|
||||
break
|
||||
|
||||
# Manifest component
|
||||
print("\n=== ANDROIDMANIFEST snippets ===")
|
||||
try:
|
||||
# binary manifest — just search utf16/utf8 remnants in apk
|
||||
data = z.read("AndroidManifest.xml")
|
||||
for key in (b"CallingCode", b"hardwareAccelerated", b"user.view"):
|
||||
if key in data or key.decode().encode("utf-16le") in data:
|
||||
print(" manifest contains", key)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
68
reverse/scripts/_scan_calling_code_deep.py
Normal file
68
reverse/scripts/_scan_calling_code_deep.py
Normal file
@@ -0,0 +1,68 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Deeper scan: UserSearchCallingCodeActivity methods / Compose / launch."""
|
||||
import re
|
||||
import zipfile
|
||||
|
||||
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\dumps\tng_base_scan.apk"
|
||||
|
||||
def strings_near(data, needle, radius=200, limit=15):
|
||||
out = []
|
||||
start = 0
|
||||
while len(out) < limit:
|
||||
idx = data.find(needle, start)
|
||||
if idx < 0:
|
||||
break
|
||||
s = max(0, idx - radius)
|
||||
e = min(len(data), idx + len(needle) + radius)
|
||||
chunk = re.sub(rb"[^\x20-\x7e]+", b".", data[s:e])
|
||||
out.append(chunk.decode("ascii", "ignore"))
|
||||
start = idx + 1
|
||||
return out
|
||||
|
||||
def main():
|
||||
z = zipfile.ZipFile(APK)
|
||||
# Collect interesting strings from classes that have CallingCode
|
||||
keys = [
|
||||
b"CallingListScreen",
|
||||
b"setContent",
|
||||
b"ComposeView",
|
||||
b"AbstractComposeView",
|
||||
b"ComponentActivity",
|
||||
b"getCallingCodeList",
|
||||
b"CountryListRepository",
|
||||
b"startActivity",
|
||||
b"UserSearchCallingCodeActivity",
|
||||
b"ll_country",
|
||||
b"hardwareAccelerated",
|
||||
b"RecyclerView",
|
||||
b"LazyColumn",
|
||||
b"androidx/compose",
|
||||
]
|
||||
for dex in sorted(n for n in z.namelist() if n.endswith(".dex")):
|
||||
data = z.read(dex)
|
||||
if b"UserSearchCallingCodeActivity" not in data and b"CallingListScreen" not in data and b"ll_country" not in data:
|
||||
continue
|
||||
print("\n========", dex, "========")
|
||||
for k in keys:
|
||||
if k in data:
|
||||
print("HAS", k.decode())
|
||||
if b"CallingListScreen" in data:
|
||||
print("--- CallingListScreen ctx ---")
|
||||
for c in strings_near(data, b"CallingListScreen", 120, 6):
|
||||
print(" ", c[:240])
|
||||
if b"ll_country" in data:
|
||||
print("--- ll_country ctx ---")
|
||||
for c in strings_near(data, b"ll_country", 100, 8):
|
||||
print(" ", c[:240])
|
||||
|
||||
# Who references UserSearchCallingCodeActivity (launchers)
|
||||
print("\n=== who references UserSearchCallingCodeActivity class desc ===")
|
||||
desc = b"Lmy/com/tngdigital/user/view/UserSearchCallingCodeActivity;"
|
||||
for dex in sorted(n for n in z.namelist() if n.endswith(".dex")):
|
||||
data = z.read(dex)
|
||||
count = data.count(desc)
|
||||
if count:
|
||||
print(dex, "count=", count)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
16
reverse/scripts/_scan_captcha.py
Normal file
16
reverse/scripts/_scan_captcha.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import re
|
||||
import zipfile
|
||||
|
||||
apk = r"C:\Users\Administrator\Desktop\notiMessage\reverse\dumps\tng_1.9.10_base.apk"
|
||||
z = zipfile.ZipFile(apk)
|
||||
pat = re.compile(rb"Lcom/aliyun/captcha/[A-Za-z0-9_/$]+;")
|
||||
found = set()
|
||||
for n in z.namelist():
|
||||
if not n.endswith(".dex"):
|
||||
continue
|
||||
data = z.read(n)
|
||||
for m in pat.findall(data):
|
||||
found.add(m.decode())
|
||||
for c in sorted(found):
|
||||
print(c)
|
||||
print("total", len(found))
|
||||
59
reverse/scripts/_scan_country_select.py
Normal file
59
reverse/scripts/_scan_country_select.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Compare CallingCode vs CountrySelect activities / intent extras."""
|
||||
import re
|
||||
import zipfile
|
||||
|
||||
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\dumps\tng_base_scan.apk"
|
||||
|
||||
|
||||
def near(data, needle, r=180, lim=12):
|
||||
out = []
|
||||
start = 0
|
||||
while len(out) < lim:
|
||||
i = data.find(needle, start)
|
||||
if i < 0:
|
||||
break
|
||||
chunk = re.sub(rb"[^\x20-\x7e]+", b".", data[max(0, i - r): i + len(needle) + r])
|
||||
out.append(chunk.decode("ascii", "ignore"))
|
||||
start = i + 1
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
z = zipfile.ZipFile(APK)
|
||||
for dex in sorted(n for n in z.namelist() if n.endswith(".dex")):
|
||||
data = z.read(dex)
|
||||
if b"UserCountrySelectActivity" not in data and b"UserSearchCallingCodeActivity" not in data:
|
||||
continue
|
||||
print("\n====", dex, "====")
|
||||
for n in (b"UserCountrySelectActivity", b"AbsCountrySelectActivity",
|
||||
b"newIntent", b"CallingListScreen"):
|
||||
if n in data:
|
||||
print("HAS", n.decode())
|
||||
for n in (b"UserSearchCallingCodeActivity", b"UserCountrySelectActivity"):
|
||||
if n not in data:
|
||||
continue
|
||||
print("--", n.decode(), "--")
|
||||
for c in near(data, n, 100, 6):
|
||||
if "Hilt_" in c and ".java" in c:
|
||||
continue
|
||||
print(" ", c[:220])
|
||||
|
||||
print("\n=== calling/country intent-like strings ===")
|
||||
seen = set()
|
||||
for dex in sorted(n for n in z.namelist() if n.endswith(".dex")):
|
||||
data = z.read(dex)
|
||||
if b"CallingCode" not in data and b"CountrySelect" not in data:
|
||||
continue
|
||||
for m in re.findall(
|
||||
rb"(?:EXTRA_|KEY_|arg_|ARG_)[A-Za-z0-9_]{2,40}|"
|
||||
rb"[A-Za-z0-9_]{0,15}(?:calling_code|CallingCode|country_code|CountryCode|countryList)[A-Za-z0-9_]{0,20}",
|
||||
data):
|
||||
s = m.decode("ascii", "ignore")
|
||||
if s not in seen and len(s) > 6:
|
||||
seen.add(s)
|
||||
print(" ", s)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
15
reverse/scripts/_scan_tiger_methods.py
Normal file
15
reverse/scripts/_scan_tiger_methods.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import re
|
||||
import zipfile
|
||||
|
||||
apk = r"C:\Users\Administrator\Desktop\notiMessage\reverse\dumps\tng_1.9.10_base.apk"
|
||||
z = zipfile.ZipFile(apk)
|
||||
pat = re.compile(rb"Lcom/aliyun/TigerTally/[a-zA-Z0-9_/\$]+;->([A-Za-z0-9_<>\$]+)")
|
||||
found = set()
|
||||
for n in z.namelist():
|
||||
if not n.endswith(".dex"):
|
||||
continue
|
||||
for m in pat.findall(z.read(n)):
|
||||
found.add(m.decode())
|
||||
for m in sorted(found):
|
||||
print(m)
|
||||
print("total", len(found))
|
||||
41
reverse/scripts/_scan_tng_promon_pkg.py
Normal file
41
reverse/scripts/_scan_tng_promon_pkg.py
Normal file
@@ -0,0 +1,41 @@
|
||||
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")]
|
||||
|
||||
# Find packages that look like Promon (short random package + few classes)
|
||||
# Also search string markers
|
||||
markers = [
|
||||
b"promon",
|
||||
b"Promon",
|
||||
b"PROMON",
|
||||
b"xwwqazamx",
|
||||
b"Rooting",
|
||||
b"jailbroken",
|
||||
b"JailBroken",
|
||||
b"W:16",
|
||||
b"libtngdigital_ewallet",
|
||||
]
|
||||
|
||||
for m in markers:
|
||||
hits = 0
|
||||
for n in names:
|
||||
hits += z.read(n).count(m)
|
||||
print(f"marker {m!r}: {hits}")
|
||||
|
||||
# Extract L.../...; type descriptors that contain 'promon' case-insensitive or weird short pkgs
|
||||
pkg_re = re.compile(rb"L([a-z]{6,12})/([A-Za-z0-9_$]{1,20});")
|
||||
pkg_counts = {}
|
||||
for n in names:
|
||||
data = z.read(n)
|
||||
for m in pkg_re.findall(data):
|
||||
pkg = m[0].decode("ascii", errors="ignore")
|
||||
pkg_counts[pkg] = pkg_counts.get(pkg, 0) + 1
|
||||
|
||||
# Show rare short packages (likely obfuscated)
|
||||
cands = [(p, c) for p, c in pkg_counts.items() if 5 <= c <= 500 and p.isalpha() and len(p) <= 12]
|
||||
cands.sort(key=lambda x: -x[1])
|
||||
print("\ncandidate obfuscated packages:")
|
||||
for p, c in cands[:40]:
|
||||
print(f" {p}: {c}")
|
||||
41
reverse/scripts/_scan_tng_structure.py
Normal file
41
reverse/scripts/_scan_tng_structure.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "tng" / "base.apk"
|
||||
if not APK.exists():
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "tng" / "tng.apk"
|
||||
|
||||
acts = set()
|
||||
ops = set()
|
||||
xww = set()
|
||||
tng = set()
|
||||
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
for name in zf.namelist():
|
||||
if not name.endswith(".dex"):
|
||||
continue
|
||||
data = zf.read(name)
|
||||
for m in re.finditer(rb"my/com/tngdigital/[a-zA-Z0-9_/]+Activity", data):
|
||||
acts.add(m.group().decode().replace("/", "."))
|
||||
for m in re.finditer(rb"com\.(?:abl|tngd|zoloz|alipayplus)\.[a-z0-9.]+", data):
|
||||
s = m.group().decode("ascii", "ignore")
|
||||
if any(k in s for k in ("wallet", "otp", "login", "register", "phone", "member", "jail", "customer", "pin", "mobile")):
|
||||
ops.add(s)
|
||||
for m in re.finditer(rb"xwwqazamx/[a-zA-Z0-9_]+", data):
|
||||
xww.add(m.group().decode().replace("/", "."))
|
||||
|
||||
print("=== User flow Activities ===")
|
||||
for a in sorted(acts):
|
||||
if any(k in a for k in ("User", "Splash", "Guide", "Registration", "Login", "Otp", "Pin", "WebView", "Security")):
|
||||
print(a)
|
||||
|
||||
print("\n=== Promon xwwqazamx classes (sample) ===")
|
||||
for c in sorted(xww)[:40]:
|
||||
print(c)
|
||||
print(f"... total {len(xww)}")
|
||||
|
||||
print("\n=== RPC operationTypes (sample) ===")
|
||||
for o in sorted(ops)[:50]:
|
||||
print(o)
|
||||
15
reverse/scripts/_scan_tng_xww.py
Normal file
15
reverse/scripts/_scan_tng_xww.py
Normal file
@@ -0,0 +1,15 @@
|
||||
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")]
|
||||
print("dex files", names)
|
||||
pat = re.compile(rb"Lxwwqazamx/[^;\s]{1,60};")
|
||||
found = set()
|
||||
for n in names:
|
||||
data = z.read(n)
|
||||
for m in pat.findall(data):
|
||||
found.add(m.decode("ascii", errors="ignore"))
|
||||
print("xwwqazamx classes", len(found))
|
||||
for c in sorted(found):
|
||||
print(c)
|
||||
68
reverse/scripts/clash_switch_sg.py
Normal file
68
reverse/scripts/clash_switch_sg.py
Normal file
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import time
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
BASE = "http://127.0.0.1:9090"
|
||||
|
||||
|
||||
def wait_api(retries=15):
|
||||
for _ in range(retries):
|
||||
try:
|
||||
with urllib.request.urlopen(BASE + "/proxies", timeout=3) as r:
|
||||
return json.loads(r.read())
|
||||
except Exception:
|
||||
time.sleep(1)
|
||||
raise SystemExit("clash api not ready")
|
||||
|
||||
|
||||
def put_proxy(group: str, target: str):
|
||||
enc_g = urllib.parse.quote(group, safe="")
|
||||
req = urllib.request.Request(
|
||||
BASE + "/proxies/" + enc_g,
|
||||
data=json.dumps({"name": target}).encode(),
|
||||
method="PUT",
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=5) as r:
|
||||
print("switched", group, "->", target, "status", r.status)
|
||||
|
||||
|
||||
def main():
|
||||
data = wait_api()
|
||||
proxies = data.get("proxies", {})
|
||||
group = None
|
||||
for gname in ["🚀节点选择", "GLOBAL"]:
|
||||
if gname in proxies:
|
||||
group = gname
|
||||
break
|
||||
if not group:
|
||||
raise SystemExit("selector group not found")
|
||||
|
||||
print("group=", group, "now=", proxies.get(group, {}).get("now"))
|
||||
|
||||
candidates = [
|
||||
"🇸🇬狮城节点",
|
||||
"🇸🇬AWS新加坡01 | 电信移动联通推荐",
|
||||
"🇸🇬新加坡01 | 电信联通推荐",
|
||||
"🇸🇬新加坡 | 高速专线-hy2",
|
||||
]
|
||||
target = next((c for c in candidates if c in proxies), None)
|
||||
if not target:
|
||||
for k in proxies:
|
||||
if "新加坡" in k or "AWS新加坡" in k:
|
||||
target = k
|
||||
break
|
||||
if not target:
|
||||
raise SystemExit("no SG proxy found")
|
||||
|
||||
put_proxy(group, target)
|
||||
|
||||
enc_g = urllib.parse.quote(group, safe="")
|
||||
with urllib.request.urlopen(BASE + "/proxies/" + enc_g, timeout=3) as r:
|
||||
print("verify now=", json.loads(r.read()).get("now"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
110
reverse/scripts/dump_tng_nativelib_methods.py
Normal file
110
reverse/scripts/dump_tng_nativelib_methods.py
Normal file
@@ -0,0 +1,110 @@
|
||||
"""Minimal DEX parser: dump methods for target classes."""
|
||||
import struct
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
TARGET = {
|
||||
"Lcom/tngd/networksdk/common/NativeLib;",
|
||||
"Lcom/tngd/networksdk/common/ApiSixSecretKeys;",
|
||||
"Lmy/com/tngdigital/common/internal/libs/RetrieveFromNativeLibs;",
|
||||
}
|
||||
|
||||
|
||||
def uleb(data, i):
|
||||
result = 0
|
||||
shift = 0
|
||||
while True:
|
||||
b = data[i]
|
||||
i += 1
|
||||
result |= (b & 0x7F) << shift
|
||||
if (b & 0x80) == 0:
|
||||
break
|
||||
shift += 7
|
||||
return result, i
|
||||
|
||||
|
||||
def parse_dex(data: bytes, label: str):
|
||||
if data[:4] != b"dex\n":
|
||||
return
|
||||
string_ids_size, string_ids_off = struct.unpack_from("<II", data, 56)
|
||||
type_ids_size, type_ids_off = struct.unpack_from("<II", data, 64)
|
||||
proto_ids_size, proto_ids_off = struct.unpack_from("<II", data, 72)
|
||||
field_ids_size, field_ids_off = struct.unpack_from("<II", data, 80)
|
||||
method_ids_size, method_ids_off = struct.unpack_from("<II", data, 88)
|
||||
class_defs_size, class_defs_off = struct.unpack_from("<II", data, 96)
|
||||
|
||||
def string_at(idx):
|
||||
off = struct.unpack_from("<I", data, string_ids_off + idx * 4)[0]
|
||||
size, p = uleb(data, off)
|
||||
return data[p : p + size].decode("utf-8", "replace")
|
||||
|
||||
def type_at(idx):
|
||||
return string_at(struct.unpack_from("<I", data, type_ids_off + idx * 4)[0])
|
||||
|
||||
def proto_at(idx):
|
||||
shorty_idx, return_type_idx, parameters_off = struct.unpack_from(
|
||||
"<III", data, proto_ids_off + idx * 12
|
||||
)
|
||||
ret = type_at(return_type_idx)
|
||||
params = []
|
||||
if parameters_off:
|
||||
size = struct.unpack_from("<I", data, parameters_off)[0]
|
||||
for i in range(size):
|
||||
tidx = struct.unpack_from("<H", data, parameters_off + 4 + i * 2)[0]
|
||||
params.append(type_at(tidx))
|
||||
return ret, params
|
||||
|
||||
def method_at(idx):
|
||||
class_idx, proto_idx, name_idx = struct.unpack_from(
|
||||
"<HHI", data, method_ids_off + idx * 8
|
||||
)
|
||||
ret, params = proto_at(proto_idx)
|
||||
return type_at(class_idx), string_at(name_idx), ret, params
|
||||
|
||||
print(f"\n===== {label} =====")
|
||||
for c in range(class_defs_size):
|
||||
class_idx, access_flags, superclass_idx, interfaces_off, source_file_idx, annotations_off, class_data_off, static_values_off = struct.unpack_from(
|
||||
"<IIIIIIII", data, class_defs_off + c * 32
|
||||
)
|
||||
cname = type_at(class_idx)
|
||||
if cname not in TARGET:
|
||||
continue
|
||||
print(f"\nCLASS {cname} access=0x{access_flags:x}")
|
||||
if not class_data_off:
|
||||
print(" (no class_data)")
|
||||
continue
|
||||
p = class_data_off
|
||||
static_fields_size, p = uleb(data, p)
|
||||
instance_fields_size, p = uleb(data, p)
|
||||
direct_methods_size, p = uleb(data, p)
|
||||
virtual_methods_size, p = uleb(data, p)
|
||||
# skip fields
|
||||
for _ in range(static_fields_size + instance_fields_size):
|
||||
_, p = uleb(data, p)
|
||||
_, p = uleb(data, p)
|
||||
mid = 0
|
||||
for kind, count in (("direct", direct_methods_size), ("virtual", virtual_methods_size)):
|
||||
mid = 0
|
||||
for _ in range(count):
|
||||
diff, p = uleb(data, p)
|
||||
access, p = uleb(data, p)
|
||||
code_off, p = uleb(data, p)
|
||||
mid += diff
|
||||
cls, name, ret, params = method_at(mid)
|
||||
flags = []
|
||||
if access & 0x100:
|
||||
flags.append("native")
|
||||
if access & 0x8:
|
||||
flags.append("static")
|
||||
if access & 0x10000:
|
||||
flags.append("constructor")
|
||||
print(f" [{kind}] {' '.join(flags)} {name}({', '.join(params)}){ret} code=0x{code_off:x}")
|
||||
|
||||
|
||||
apk = Path(r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\tng\base.apk")
|
||||
with zipfile.ZipFile(apk) as z:
|
||||
for n in z.namelist():
|
||||
if n.endswith(".dex"):
|
||||
data = z.read(n)
|
||||
if any(t.encode() in data for t in ("NativeLib;", "ApiSixSecretKeys;", "RetrieveFromNativeLibs;")):
|
||||
parse_dex(data, n)
|
||||
167
reverse/scripts/dump_tng_runtime_kill.py
Normal file
167
reverse/scripts/dump_tng_runtime_kill.py
Normal file
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Race-window: dump TNG executable maps and hunt kill+SVC after launch."""
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ADB = r"C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
|
||||
PKG = "my.com.tngdigital.ewallet"
|
||||
OUT = Path(__file__).resolve().parents[1] / "dumps" / "tng_runtime"
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# aarch64 movz x8/w8,#129/130/131 + svc #0
|
||||
KILL_IMMS = [
|
||||
bytes.fromhex("281080d2"),
|
||||
bytes.fromhex("28108052"),
|
||||
bytes.fromhex("481080d2"),
|
||||
bytes.fromhex("48108052"),
|
||||
bytes.fromhex("681080d2"),
|
||||
bytes.fromhex("68108052"),
|
||||
]
|
||||
SVC0 = bytes.fromhex("010000d4")
|
||||
EXIT_IMM = bytes.fromhex("c80b80d2") # movz x8,#94 exit_group
|
||||
EXIT2 = bytes.fromhex("ba0b80d2") # movz x8,#93 exit
|
||||
|
||||
|
||||
def adb(*args, check=False):
|
||||
r = subprocess.run([ADB, *args], capture_output=True)
|
||||
out = (r.stdout or b"") + (r.stderr or b"")
|
||||
if check and r.returncode != 0:
|
||||
raise RuntimeError(out.decode("utf-8", "ignore"))
|
||||
return r.returncode, out.decode("utf-8", "replace")
|
||||
|
||||
|
||||
def su(cmd):
|
||||
return adb("shell", f"su -c '{cmd}'")
|
||||
|
||||
|
||||
def main():
|
||||
adb("shell", "am", "force-stop", PKG)
|
||||
time.sleep(0.5)
|
||||
adb("shell", "monkey", "-p", PKG, "-c", "android.intent.category.LAUNCHER", "1")
|
||||
|
||||
pid = None
|
||||
for _ in range(40):
|
||||
time.sleep(0.25)
|
||||
_, out = adb("shell", "pidof", PKG)
|
||||
toks = out.strip().split()
|
||||
if toks:
|
||||
pid = toks[0]
|
||||
break
|
||||
if not pid:
|
||||
print("FAIL: no pid")
|
||||
return 1
|
||||
print(f"pid={pid}")
|
||||
|
||||
# pull maps
|
||||
_, maps = su(f"cat /proc/{pid}/maps")
|
||||
maps_path = OUT / f"maps_{pid}.txt"
|
||||
maps_path.write_text(maps, encoding="utf-8", errors="replace")
|
||||
print(f"maps -> {maps_path} lines={len(maps.splitlines())}")
|
||||
|
||||
targets = []
|
||||
for line in maps.splitlines():
|
||||
if "r-xp" not in line and "r-x" not in line:
|
||||
# also rw-p with execute rarely; keep x
|
||||
if "x" not in line.split()[1] if len(line.split()) > 1 else "":
|
||||
continue
|
||||
if "tngdigital" in line or "libnative" in line or "ewallet" in line.lower():
|
||||
parts = line.split()
|
||||
rng = parts[0]
|
||||
start_s, end_s = rng.split("-")
|
||||
start, end = int(start_s, 16), int(end_s, 16)
|
||||
path = parts[-1] if len(parts) >= 6 else ""
|
||||
targets.append((start, end, path, line))
|
||||
|
||||
print(f"target segments={len(targets)}")
|
||||
for start, end, path, line in targets[:12]:
|
||||
print(f" {hex(start)}-{hex(end)} {path}")
|
||||
|
||||
# dump via dd from /proc/pid/mem
|
||||
remote = f"/data/local/tmp/tng_rt_{pid}.bin"
|
||||
su(f"rm -f {remote}")
|
||||
total = 0
|
||||
for i, (start, end, path, _) in enumerate(targets):
|
||||
size = end - start
|
||||
if size <= 0 or size > 32 * 1024 * 1024:
|
||||
continue
|
||||
# append dump
|
||||
cmd = (
|
||||
f"dd if=/proc/{pid}/mem bs=4096 skip={start // 4096} "
|
||||
f"count={(size + 4095) // 4096} 2>/dev/null >> {remote}"
|
||||
)
|
||||
# dd skip is in blocks from file start — wrong for /proc/pid/mem!
|
||||
# Use busybox dd with seek on output and skip via python on device instead.
|
||||
cmd = (
|
||||
f"toybox dd if=/proc/{pid}/mem of={remote}.p{i} "
|
||||
f"bs=1 skip={start} count={size} 2>/dev/null"
|
||||
)
|
||||
code, _ = su(cmd)
|
||||
if code == 0:
|
||||
total += size
|
||||
print(f" dumped p{i} size={size} from {path}")
|
||||
else:
|
||||
# fallback: python on device
|
||||
py = (
|
||||
f"python3 -c \"import sys;f=open('/proc/{pid}/mem','rb');"
|
||||
f"f.seek({start});d=f.read({size});open('{remote}.p{i}','wb').write(d)\""
|
||||
)
|
||||
code2, out2 = su(py)
|
||||
if code2 == 0:
|
||||
total += size
|
||||
print(f" dumped p{i} via python size={size}")
|
||||
else:
|
||||
print(f" FAIL dump p{i}: {out2[:120]}")
|
||||
|
||||
# pull pieces and scan
|
||||
local_dir = OUT / f"mem_{pid}"
|
||||
local_dir.mkdir(exist_ok=True)
|
||||
kill_hits = 0
|
||||
exit_hits = 0
|
||||
for i, (start, end, path, _) in enumerate(targets):
|
||||
rem = f"{remote}.p{i}"
|
||||
loc = local_dir / f"seg_{i}_{start:x}.bin"
|
||||
code, _ = adb("shell", f"su -c 'test -f {rem} && echo OK'")
|
||||
if "OK" not in _:
|
||||
continue
|
||||
adb("pull", rem, str(loc))
|
||||
if not loc.exists():
|
||||
continue
|
||||
data = loc.read_bytes()
|
||||
for imm in KILL_IMMS:
|
||||
pos = 0
|
||||
while True:
|
||||
j = data.find(imm, pos)
|
||||
if j < 0:
|
||||
break
|
||||
win = data[j : j + 36]
|
||||
if SVC0 in win:
|
||||
kill_hits += 1
|
||||
delta = win.find(SVC0)
|
||||
print(f"KILL+SVC seg{i} file+0x{j:x} va=0x{start+j:x} delta={delta} path={path}")
|
||||
pos = j + 4
|
||||
for imm in (EXIT_IMM, EXIT2):
|
||||
pos = 0
|
||||
while True:
|
||||
j = data.find(imm, pos)
|
||||
if j < 0:
|
||||
break
|
||||
win = data[j : j + 36]
|
||||
if SVC0 in win:
|
||||
exit_hits += 1
|
||||
if exit_hits <= 15:
|
||||
print(f"EXIT+SVC seg{i} file+0x{j:x} va=0x{start+j:x} path={path}")
|
||||
pos = j + 4
|
||||
# also count raw svc
|
||||
print(f" seg{i} svc0={data.count(SVC0)} size={len(data)}")
|
||||
|
||||
print(f"DONE kill+svc={kill_hits} exit+svc={exit_hits} dumped_bytes~={total}")
|
||||
_, alive = adb("shell", "pidof", PKG)
|
||||
print(f"still alive? {alive.strip() or 'NO'}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
22
reverse/scripts/find_character_crypto.py
Normal file
22
reverse/scripts/find_character_crypto.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "maribank_sg_base.apk"
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
d = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
|
||||
for m in re.finditer(rb"Lcom/[^;]{0,160}CharacterCrypto[^;]{0,40};", d):
|
||||
print(m.group().decode()[1:-1].replace("/", "."))
|
||||
|
||||
for m in re.finditer(rb"Lcom/[^;]{0,160}NativeEncrypt[^;]{0,40};", d):
|
||||
print(m.group().decode()[1:-1].replace("/", "."))
|
||||
|
||||
# find class with getDfpByMMKV - search all Lcom paths and check if followed by getDfp in same method table is hard
|
||||
# instead search for MMKV + dfp strings proximity
|
||||
idx = d.find(b"getDfpByMMKV:")
|
||||
if idx >= 0:
|
||||
chunk = d[max(0, idx - 500) : idx + 500]
|
||||
for m in re.finditer(rb"Lcom/[^;\x00]{5,200};", chunk):
|
||||
print("near getDfpByMMKV:", m.group().decode()[1:-1].replace("/", "."))
|
||||
20
reverse/scripts/find_character_crypto2.py
Normal file
20
reverse/scripts/find_character_crypto2.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "maribank_sg_base.apk"
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
d = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
|
||||
for m in re.finditer(rb"CharacterCrypto[\w$]{0,40}", d):
|
||||
print(m.group().decode())
|
||||
|
||||
for m in re.finditer(rb"getDfp[\w$]{0,20}", d):
|
||||
s = m.group().decode()
|
||||
if s not in ("getDfp",):
|
||||
print("method:", s)
|
||||
|
||||
# utils.d wrapper
|
||||
for m in re.finditer(rb"Lcom/shopee/bke/lib/jni/utils/[\w$]{1,20};", d):
|
||||
print(m.group().decode()[1:-1].replace("/", "."))
|
||||
21
reverse/scripts/find_crypto_manager_class.py
Normal file
21
reverse/scripts/find_crypto_manager_class.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "maribank_sg_base.apk"
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
d = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
|
||||
for needle in [b"CharacterCryptoManager", b"CharacterCryptoManagerWrapper", b"NativeEncryptUtilsWrapper"]:
|
||||
print("\n===", needle.decode(), "===")
|
||||
for m in re.finditer(re.escape(needle) + rb"[\w$]{0,30}", d):
|
||||
name = m.group().decode()
|
||||
if "$" in name or name.endswith("Wrapper") or name.endswith("Manager"):
|
||||
pass
|
||||
for m in re.finditer(rb"Lcom/[^;]{0,200}" + re.escape(needle) + rb"[^;]{0,20};", d):
|
||||
print(m.group().decode()[1:-1].replace("/", "."))
|
||||
|
||||
# also search for class ending with .d
|
||||
for m in re.finditer(rb"Lcom/shopee/bke/lib/jni/utils/[a-z];", d):
|
||||
print("short utils:", m.group().decode()[1:-1].replace("/", "."))
|
||||
23
reverse/scripts/find_dfp_empty_class.py
Normal file
23
reverse/scripts/find_dfp_empty_class.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "maribank_sg_base.apk"
|
||||
needle = b"The dfp is empty in register scene"
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
|
||||
idx = data.find(needle)
|
||||
ctx = data[max(0, idx - 2000) : idx + 2000]
|
||||
classes = sorted(set(m.group().decode()[1:-1].replace("/", ".")
|
||||
for m in re.finditer(rb"Lcom/[^;\x00]{5,200};", ctx)))
|
||||
print("classes near register dfp empty:")
|
||||
for c in classes:
|
||||
print(c)
|
||||
|
||||
print("\nstrings:")
|
||||
for m in re.finditer(rb"[\x20-\x7e]{6,100}", ctx):
|
||||
s = m.group().decode()
|
||||
if any(k in s.lower() for k in ("dfp", "register", "fingerprint", "empty", "scene", "monitor", "iv_")):
|
||||
print(s)
|
||||
27
reverse/scripts/find_getdfp.py
Normal file
27
reverse/scripts/find_getdfp.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "maribank_sg_base.apk"
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
|
||||
for needle in [b"getDfp", b"DeviceFingerprintResp", b"deviceFingerprint", b"/dfp/", b"dfp/v1"]:
|
||||
print("\n===", needle.decode(), "===")
|
||||
for m in re.finditer(re.escape(needle) + rb"[\x00-\xff]{0,80}", data):
|
||||
chunk = data[m.start() : m.start() + 120]
|
||||
s = re.sub(rb"[^\x20-\x7e]+", b"|", chunk).decode("ascii", "ignore")
|
||||
print(s[:140])
|
||||
break
|
||||
|
||||
# classes with fingerprint in name
|
||||
fps = sorted(set(m.group().decode()[1:-1].replace("/", ".")
|
||||
for m in re.finditer(rb"Lcom/shopee/bke/[^;\x00]{0,120}[Ff]ingerprint[^;\x00]{0,40};", data)))
|
||||
print("\n=== fingerprint classes ===")
|
||||
for c in fps[:30]:
|
||||
print(c)
|
||||
|
||||
# RegisterViewModel methods - search string RegisterViewModel in dex
|
||||
for m in re.finditer(rb"Lcom/shopee/bke/biz/user/viewmodel/RegisterViewModel[^;\x00]*;", data):
|
||||
print("\nRegisterViewModel:", m.group().decode())
|
||||
19
reverse/scripts/find_getdfp_by_mmkv_class.py
Normal file
19
reverse/scripts/find_getdfp_by_mmkv_class.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "maribank_sg_base.apk"
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
d = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
|
||||
idx = d.find(b"getDfpByMMKV")
|
||||
print("idx", idx)
|
||||
window = d[max(0, idx - 20000) : idx + 20000]
|
||||
classes = re.findall(rb"Lcom/[a-zA-Z0-9_$/]{5,200};", window)
|
||||
unique = sorted(set(x.decode()[1:-1].replace("/", ".") for x in classes))
|
||||
print("classes in 40k window:", len(unique))
|
||||
for c in unique:
|
||||
cl = c.lower()
|
||||
if any(k in cl for k in ("crypto", "dfp", "finger", "device", "user", "jni", "utils", "manager", "wrapper", "register", "login")):
|
||||
print(c)
|
||||
31
reverse/scripts/find_getdfp_class.py
Normal file
31
reverse/scripts/find_getdfp_class.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "maribank_sg_base.apk"
|
||||
needles = [b"getDfp empty!", b"getDfp onError:", b"getDfpByMMKV:", b"The dfp is empty in register scene"]
|
||||
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
|
||||
all_classes = [m.group().decode()[1:-1].replace("/", ".")
|
||||
for m in re.finditer(rb"Lcom/shopee/bke/[\w$/]{5,160};", data)]
|
||||
|
||||
for needle in needles:
|
||||
print("\n===", needle.decode(), "===")
|
||||
idx = data.find(needle)
|
||||
if idx < 0:
|
||||
print("not found")
|
||||
continue
|
||||
window = data[max(0, idx - 8000) : idx + 8000]
|
||||
nearby = sorted(set(m.group().decode()[1:-1].replace("/", ".")
|
||||
for m in re.finditer(rb"Lcom/shopee/bke/[\w$/]{5,160};", window)))
|
||||
for c in nearby:
|
||||
if any(k in c.lower() for k in ("dfp", "finger", "device", "register", "user", "util", "manager", "helper", "repo", "data", "rn")):
|
||||
print(" ", c)
|
||||
|
||||
dfp_classes = sorted(set(c for c in all_classes if "dfp" in c.lower() or "fingerprint" in c.lower()))
|
||||
print("\n=== dfp/fingerprint class names ===")
|
||||
for c in dfp_classes:
|
||||
print(c)
|
||||
21
reverse/scripts/find_getdfp_class2.py
Normal file
21
reverse/scripts/find_getdfp_class2.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "maribank_sg_base.apk"
|
||||
needle = b"getDfp empty!"
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
|
||||
idx = data.find(needle)
|
||||
window = data[max(0, idx - 12000) : idx + 12000]
|
||||
classes = sorted(set(m.group().decode()[1:-1].replace("/", ".")
|
||||
for m in re.finditer(rb"Lcom/[^;\x00]{5,200};", window)))
|
||||
print("all classes near getDfp empty (filtered):")
|
||||
for c in classes:
|
||||
cl = c.lower()
|
||||
if any(k in cl for k in ("dfp", "finger", "shps", "bke", "jni", "utils", "sdk", "device", "monitor", "crypto", "register")):
|
||||
print(c)
|
||||
|
||||
print("\nall com classes count:", len(classes))
|
||||
17
reverse/scripts/find_unavailable_i18n.py
Normal file
17
reverse/scripts/find_unavailable_i18n.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "maribank_sg_base.apk"
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
for name in zf.namelist():
|
||||
if not name.endswith((".json", ".jsbundle")):
|
||||
continue
|
||||
data = zf.read(name)
|
||||
if b"currently unavailable" not in data and b"system is currently" not in data:
|
||||
continue
|
||||
text = data.decode("utf-8", errors="replace")
|
||||
for m in re.finditer(r".{0,40}currently unavailable.{0,60}", text):
|
||||
print(f"\n[{name}]")
|
||||
print(m.group().replace("\n", " ")[:200])
|
||||
24
reverse/scripts/find_user_register_i18n.py
Normal file
24
reverse/scripts/find_user_register_i18n.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "maribank_sg_base.apk"
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
for name in sorted(zf.namelist()):
|
||||
low = name.lower()
|
||||
if not name.endswith(".json"):
|
||||
continue
|
||||
if "user" not in low and "auth" not in low and "register" not in low and "ekyc" not in low:
|
||||
continue
|
||||
data = zf.read(name).decode("utf-8", errors="replace")
|
||||
hits = []
|
||||
for m in re.finditer(r'"[^"]+"\s*:\s*"[^"]{8,200}"', data):
|
||||
s = m.group()
|
||||
sl = s.lower()
|
||||
if any(k in sl for k in ("unavailable", "register", "dfp", "phone", "otp", "system is")):
|
||||
hits.append(s[:220])
|
||||
if hits:
|
||||
print("\n===", name, "===")
|
||||
for h in hits[:40]:
|
||||
print(h)
|
||||
13
reverse/scripts/inspect_clash_cache.py
Normal file
13
reverse/scripts/inspect_clash_cache.py
Normal file
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
p = sys.argv[1] if len(sys.argv) > 1 else "clash_cache.db"
|
||||
con = sqlite3.connect(p)
|
||||
for (name,) in con.execute("SELECT name FROM sqlite_master WHERE type='table'"):
|
||||
print("TABLE", name)
|
||||
cols = [c[1] for c in con.execute(f"PRAGMA table_info({name})")]
|
||||
print(" cols", cols)
|
||||
for row in con.execute(f"SELECT * FROM {name} LIMIT 8"):
|
||||
s = str(row)
|
||||
print(" ", s[:300] + ("..." if len(s) > 300 else ""))
|
||||
18
reverse/scripts/list_sg_safemode.py
Normal file
18
reverse/scripts/list_sg_safemode.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "maribank_sg_base.apk"
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
d = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
|
||||
classes = sorted(set(m.group().decode()[1:-1].replace("/", ".")
|
||||
for m in re.finditer(rb"Lcom/shopee/bke/lib/safemode/[^;]{1,80};", d)))
|
||||
print("safemode classes:", len(classes))
|
||||
for c in classes[:30]:
|
||||
print(c)
|
||||
|
||||
print("\nrisk classes:")
|
||||
for m in re.finditer(rb"Lcom/shopee/bke/biz/base/risk/[^;]{1,40};", d):
|
||||
print(m.group().decode()[1:-1].replace("/", "."))
|
||||
28
reverse/scripts/list_sg_safemode_obf.py
Normal file
28
reverse/scripts/list_sg_safemode_obf.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "maribank_sg_base.apk"
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
d = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
|
||||
patterns = [
|
||||
rb"Lcom/shopee/bke/lib/safemode/[a-zA-Z$][\w$]{0,30};",
|
||||
rb"Lcom/shopee/bke/lib/safemode/[a-z]+/[a-zA-Z$][\w$]{0,40};",
|
||||
]
|
||||
seen = set()
|
||||
for pat in patterns:
|
||||
for m in re.finditer(pat, d):
|
||||
c = m.group().decode()[1:-1].replace("/", ".")
|
||||
if c.startswith("com.shopee.bke.lib.safemode.R"):
|
||||
continue
|
||||
if c not in seen:
|
||||
seen.add(c)
|
||||
print(c)
|
||||
|
||||
print("\n--- short obfuscated bke classes (root/adb) ---")
|
||||
for m in re.finditer(rb"Lcom/shopee/bke/[a-z]+/[a-z]{1,2};", d):
|
||||
c = m.group().decode()[1:-1].replace("/", ".")
|
||||
if "lib" in c or "safemode" in c or "risk" in c:
|
||||
print(c)
|
||||
21
reverse/scripts/list_sg_shps_classes.py
Normal file
21
reverse/scripts/list_sg_shps_classes.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "maribank_sg_base.apk"
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
d = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
|
||||
for pat in [
|
||||
rb"Lcom/shopee/shpssdk/[\w$/]{3,100};",
|
||||
rb"Lcom/shopee/shpssdkbank/[\w$/]{3,100};",
|
||||
]:
|
||||
cs = sorted(set(m.group().decode()[1:-1].replace("/", ".") for m in re.finditer(pat, d)))
|
||||
print("\n", pat.decode(), len(cs))
|
||||
for c in cs:
|
||||
if "R" != c.split(".")[-1] or "$" in c:
|
||||
print(" ", c)
|
||||
|
||||
for needle in [b"RISK_USB", b"RISK_WIFI", b"RISK_ROOT", b"RISK_HOOK", b"requestDefense"]:
|
||||
print(needle.decode(), d.count(needle))
|
||||
20
reverse/scripts/list_shps_classes.py
Normal file
20
reverse/scripts/list_shps_classes.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "maribank_sg_base.apk"
|
||||
if len(sys.argv) > 1:
|
||||
APK = Path(sys.argv[1])
|
||||
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
|
||||
for pkg in ("shpssdkbank", "shpssdk"):
|
||||
pat = re.compile(rf"Lcom/shopee/{pkg}/[\w$]+;".encode())
|
||||
classes = sorted(set(m.group().decode()[1:-1].replace("/", ".") for m in pat.finditer(data)))
|
||||
print(f"\n=== {pkg} ({len(classes)} classes) ===")
|
||||
short = [c for c in classes if len(c.split(".")[-1]) <= 12 and "shpssdk" in c]
|
||||
for c in short[:40]:
|
||||
print(" ", c)
|
||||
156
reverse/scripts/parse_vuwuuwvw_log.py
Normal file
156
reverse/scripts/parse_vuwuuwvw_log.py
Normal file
@@ -0,0 +1,156 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Parse vuwuuwvw attestation JSON from logcat or raw JSON file."""
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
SUSPICIOUS = re.compile(
|
||||
rb"(root|hook|xposed|lsposed|magisk|frida|substrate|emulator|debug|adb|"
|
||||
rb"selinux|\bsu\b|/proc/|zygisk|riru|shamiko|tamper|integrity|"
|
||||
rb"jailbreak|virtual|mock|proxy|vpn|developer)",
|
||||
re.I,
|
||||
)
|
||||
|
||||
KNOWN_FIELDS = [
|
||||
"root", "hook", "xposed", "lsposed", "magisk", "frida", "adb", "debug",
|
||||
"debuggable", "emulator", "simulator", "vpn", "proxy", "mock",
|
||||
"selinux", "su", "supersu", "zygisk", "riru", "shamiko", "substrate",
|
||||
"integrity", "safetynet", "playIntegrity", "deviceId", "androidId",
|
||||
"serial", "fingerprint", "model", "brand", "manufacturer", "board",
|
||||
"host", "tags", "type", "user", "display", "product", "hardware",
|
||||
"usb", "wifi", "adb_enabled", "development_settings_enabled",
|
||||
"RISK_ROOT", "RISK_HOOK", "RISK_USB_ADB", "RISK_WIFI_ADB", "RISK_ADB",
|
||||
"RISK_EMULATOR", "RISK_DEBUG", "RISK_VPN", "RISK_PROXY", "RISK_MOCK",
|
||||
"rdVerifyInfo", "deviceFingerprint", "data", "dataKey", "riskToken",
|
||||
"isRoot", "isHook", "isDebug", "isAdb", "isEmulator", "isVirtual",
|
||||
"tamper", "jailbreak", "bootloader", "verifiedbootstate", "vbmeta",
|
||||
"init.svc.adbd", "/proc/self/maps", "RealInterceptorChain",
|
||||
]
|
||||
|
||||
# keys seen in 16:29-16:30 Pixel6 logs (from vuwuuwvw head=...)
|
||||
SAMPLE_KEYS = """
|
||||
2535994b 3923d741 68e69650 37132b99 1c5681ce 324f4370 4ea521fa
|
||||
2236b022 5a5532da 1309e885 1bb219c0 3ade7f65 3ade7f66 1c560a56 1c560a55
|
||||
3d33c1b1 1854d9b1 21e5cca2 23a20fae 36f30e66 29e2320e 2652ab1c 122c5826
|
||||
269b494b 22b1f08d 169b85f 1610b055 2baf3770 5bc1a01a 37132b99
|
||||
""".split()
|
||||
|
||||
|
||||
def md5_key(name: str) -> str:
|
||||
return hashlib.md5(name.encode()).hexdigest()[:8]
|
||||
|
||||
|
||||
def guess_keys(keys):
|
||||
table = {md5_key(n): n for n in KNOWN_FIELDS}
|
||||
out = []
|
||||
for k in keys:
|
||||
if k.lower() in table:
|
||||
out.append((k, table[k.lower()]))
|
||||
return out
|
||||
|
||||
|
||||
def scan_value(path, val, hits):
|
||||
if isinstance(val, str):
|
||||
b = val.encode("utf-8", "replace")
|
||||
m = SUSPICIOUS.search(b)
|
||||
if m:
|
||||
hits.append(f"{path} str hit={m.group().decode()} val={val[:120]}")
|
||||
if re.fullmatch(r"[A-Za-z0-9+/=]+", val) and 8 <= len(val) <= 512:
|
||||
try:
|
||||
raw = base64.b64decode(val + "==="[: (4 - len(val) % 4) % 4])
|
||||
if sum(32 <= c < 127 for c in raw) * 100 // max(len(raw), 1) >= 85:
|
||||
inner = raw.decode("utf-8", "replace")
|
||||
m2 = SUSPICIOUS.search(inner.encode())
|
||||
if m2:
|
||||
hits.append(f"{path} b64utf8 hit={m2.group().decode()} val={inner[:120]}")
|
||||
else:
|
||||
hx = raw[:32].hex()
|
||||
hits.append(f"{path} b64 bin len={len(raw)} hex={hx}")
|
||||
except Exception:
|
||||
pass
|
||||
elif isinstance(val, (int, float, bool)):
|
||||
if val in (1, True):
|
||||
hits.append(f"{path} ={val} (flag?)")
|
||||
|
||||
|
||||
def parse_json(text, label=""):
|
||||
obj = json.loads(text)
|
||||
keys = sorted(obj.keys())
|
||||
print(f"\n=== {label} keys={len(keys)} ===")
|
||||
print("first keys:", keys[:12])
|
||||
hits = []
|
||||
for k in keys:
|
||||
scan_value(k, obj[k], hits)
|
||||
if hits:
|
||||
print("SUSPICIOUS:")
|
||||
for h in hits[:30]:
|
||||
print(" ", h)
|
||||
else:
|
||||
print("no plain suspicious strings")
|
||||
matched = guess_keys(keys)
|
||||
if matched:
|
||||
print("MD5 key guesses:")
|
||||
for k, n in matched:
|
||||
print(f" {k} => {n}")
|
||||
return obj
|
||||
|
||||
|
||||
def extract_from_log(path):
|
||||
text = Path(path).read_text(encoding="utf-8", errors="replace")
|
||||
# MariBankCapture chunked: [vuwuuwvw.out REGISTER] 1/N ...
|
||||
chunks = {}
|
||||
current = None
|
||||
for line in text.splitlines():
|
||||
if "vuwuuwvw.out REGISTER" in line or "vuwuuwvw.out]" in line:
|
||||
m = re.search(r"\] (\d+)/(\d+) (.+)$", line)
|
||||
if m:
|
||||
idx, total, part = int(m.group(1)), int(m.group(2)), m.group(3)
|
||||
key = (total, line.split("REGISTER")[0])
|
||||
chunks.setdefault(key, {})[idx] = part
|
||||
elif " len=" in line and " parts=" not in line:
|
||||
m2 = re.search(r"\] len=\d+ (.+)$", line)
|
||||
if m2:
|
||||
current = m2.group(1)
|
||||
elif "vuwuuwvw.out REGISTER] len=" in line and " parts=" not in line:
|
||||
m2 = re.search(r"len=\d+ (.+)$", line)
|
||||
if m2:
|
||||
current = m2.group(1)
|
||||
if current and current.startswith("{"):
|
||||
return [current]
|
||||
out = []
|
||||
for parts in chunks.values():
|
||||
if parts:
|
||||
joined = "".join(parts[i] for i in sorted(parts))
|
||||
if joined.startswith("{"):
|
||||
out.append(joined)
|
||||
# fallback: head= lines won't work for full JSON
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
print("=== MD5 key table (known fields -> 8 hex) ===")
|
||||
for name in KNOWN_FIELDS[:20]:
|
||||
print(f" {md5_key(name):8s} {name}")
|
||||
print(" ...")
|
||||
print("\n=== sample keys from device logs ===")
|
||||
matched = guess_keys(SAMPLE_KEYS)
|
||||
if matched:
|
||||
for k, n in matched:
|
||||
print(f" {k} => {n}")
|
||||
else:
|
||||
print(" (no MD5 match — keys may use different hash algo)")
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
p = Path(sys.argv[1])
|
||||
if p.suffix == ".json":
|
||||
parse_json(p.read_text(encoding="utf-8"), p.name)
|
||||
else:
|
||||
for i, blob in enumerate(extract_from_log(p)):
|
||||
parse_json(blob, f"log#{i+1}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
28
reverse/scripts/scan_dfp_empty_ctx.py
Normal file
28
reverse/scripts/scan_dfp_empty_ctx.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "maribank_sg_base.apk"
|
||||
needle = b"dfp is empty"
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
|
||||
idx = 0
|
||||
while True:
|
||||
idx = data.find(needle, idx)
|
||||
if idx < 0:
|
||||
break
|
||||
ctx = data[max(0, idx - 400) : idx + 400]
|
||||
classes = sorted(set(m.group().decode()[1:-1].replace("/", ".")
|
||||
for m in re.finditer(rb"Lcom/[^;\x00]{5,160};", ctx)))
|
||||
print("\n--- hit at", idx, "---")
|
||||
for c in classes:
|
||||
if any(k in c.lower() for k in ("user", "register", "dfp", "fingerprint", "viewmodel", "rn", "helper")):
|
||||
print(" ", c)
|
||||
# printable strings nearby
|
||||
for m in re.finditer(rb"[\x20-\x7e]{4,80}", ctx):
|
||||
s = m.group().decode()
|
||||
if any(k in s.lower() for k in ("dfp", "register", "empty", "error", "unavailable", "fingerprint")):
|
||||
print(" str:", s)
|
||||
idx += len(needle)
|
||||
28
reverse/scripts/scan_sg_crypto_dfp.py
Normal file
28
reverse/scripts/scan_sg_crypto_dfp.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "maribank_sg_base.apk"
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
d = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
|
||||
for kw in [
|
||||
b"CharacterCrypto",
|
||||
b"IV_Monitor",
|
||||
b"getDfpByMMKV",
|
||||
b"NativeEncryptUtilsWrapper",
|
||||
b"dfp/v1/data/report",
|
||||
b"com/shopee/bke/lib/jni/utils/d",
|
||||
]:
|
||||
print(kw.decode(), d.count(kw))
|
||||
|
||||
print("\ndfp-related classes:")
|
||||
for m in re.finditer(rb"Lcom/[^;]{0,120}[Dd][Ff][Pp][^;]{0,40};", d):
|
||||
print(m.group().decode()[1:-1].replace("/", "."))
|
||||
|
||||
print("\nMonitor classes:")
|
||||
for m in re.finditer(rb"Lcom/[^;]{0,120}Monitor[^;]{0,40};", d):
|
||||
s = m.group().decode()[1:-1].replace("/", ".")
|
||||
if "bke" in s or "shps" in s:
|
||||
print(s)
|
||||
20
reverse/scripts/scan_sg_dfp.py
Normal file
20
reverse/scripts/scan_sg_dfp.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "maribank_sg_base.apk"
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
|
||||
for needle in [b"dfp is empty", b"getDfp", b"DfpManager", b"DeviceFingerprint"]:
|
||||
print("\n===", needle.decode(), "count=", data.count(needle))
|
||||
idx = 0
|
||||
for _ in range(5):
|
||||
idx = data.find(needle, idx)
|
||||
if idx < 0:
|
||||
break
|
||||
ctx = data[max(0, idx - 120) : idx + 200]
|
||||
for m in re.finditer(rb"Lcom/[^;\x00]{5,140};", ctx):
|
||||
print(" ", m.group().decode()[1:-1].replace("/", "."))
|
||||
idx += len(needle)
|
||||
35
reverse/scripts/scan_sg_split_utils.py
Normal file
35
reverse/scripts/scan_sg_split_utils.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Find jni/utils and safemode classes across SG split dex files."""
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "maribank_sg_base.apk"
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
for dex in sorted(n for n in zf.namelist() if n.endswith(".dex")):
|
||||
d = zf.read(dex)
|
||||
needles = [
|
||||
b"Lcom/shopee/bke/lib/jni/utils/",
|
||||
b"Lcom/shopee/bke/lib/safemode/",
|
||||
b"CharacterCrypto",
|
||||
b"rdVerifyInfo",
|
||||
]
|
||||
if not any(n in d for n in needles):
|
||||
continue
|
||||
print("\n===", dex, "===")
|
||||
for pat in [
|
||||
rb"Lcom/shopee/bke/lib/jni/utils/[^;]{1,40};",
|
||||
rb"Lcom/shopee/bke/lib/safemode/[^;]{1,60};",
|
||||
rb"Lcom/shopee/bke/biz/base/risk/[^;]{1,40};",
|
||||
]:
|
||||
cs = sorted(
|
||||
set(
|
||||
m.group().decode()[1:-1].replace("/", ".")
|
||||
for m in re.finditer(pat, d)
|
||||
)
|
||||
)
|
||||
for c in cs:
|
||||
if ".R" in c and c.endswith(".R"):
|
||||
continue
|
||||
if "$" in c or not c.endswith(".R"):
|
||||
print(" ", c)
|
||||
33
reverse/scripts/scan_sg_splits.py
Normal file
33
reverse/scripts/scan_sg_splits.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APKS = Path(__file__).resolve().parent.parent / "apks"
|
||||
for name in ["maribank_sg_base.apk", "maribank_sg_arm64.apk"]:
|
||||
p = APKS / name
|
||||
if not p.exists():
|
||||
print(name, "missing")
|
||||
continue
|
||||
with zipfile.ZipFile(p) as z:
|
||||
dex = [n for n in z.namelist() if n.endswith(".dex")]
|
||||
print("\n", name, "dex:", dex)
|
||||
if not dex:
|
||||
so = [n for n in z.namelist() if n.endswith(".so")][:5]
|
||||
print(" native:", so)
|
||||
continue
|
||||
d = b"".join(z.read(n) for n in dex)
|
||||
for needle in [
|
||||
b"safemode.b",
|
||||
b"safemode/catchs",
|
||||
b"safemode/util",
|
||||
b"USB_ADB",
|
||||
b"RISK_USB",
|
||||
b"lib/safemode/",
|
||||
]:
|
||||
print(" ", needle.decode(), d.count(needle))
|
||||
classes = sorted(set(m.group().decode()[1:-1].replace("/", ".")
|
||||
for m in re.finditer(rb"Lcom/shopee/bke/lib/safemode/[^;]{1,80};", d)))
|
||||
for c in classes:
|
||||
if not c.endswith(".R") and ".R$" not in c:
|
||||
print(" ", c)
|
||||
34
reverse/scripts/scan_sg_unavailable.py
Normal file
34
reverse/scripts/scan_sg_unavailable.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "maribank_sg_base.apk"
|
||||
needles = [
|
||||
b"system is currently unavailable",
|
||||
b"currently unavailable",
|
||||
b"Unexpected error occurred",
|
||||
b"Please try again later",
|
||||
b"3100012",
|
||||
b"deviceFingerprint",
|
||||
b"preCheck",
|
||||
b"preRegister",
|
||||
b"getDfp",
|
||||
b"dfp is empty",
|
||||
b"dfpReady",
|
||||
b"isDfpReady",
|
||||
b"IV_Monitor",
|
||||
b"register scene",
|
||||
b"GlobalAuthError",
|
||||
b"ErrorFlowHelper",
|
||||
]
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
data = b"".join(
|
||||
zf.read(n)
|
||||
for n in zf.namelist()
|
||||
if n.endswith((".dex", ".jsbundle", ".json"))
|
||||
)
|
||||
for n in needles:
|
||||
print(n.decode(), data.count(n))
|
||||
idx = data.find(b"currently unavailable")
|
||||
if idx >= 0:
|
||||
print("\ncontext:", data[max(0, idx - 100) : idx + 150])
|
||||
94
reverse/scripts/scan_tng.py
Normal file
94
reverse/scripts/scan_tng.py
Normal file
@@ -0,0 +1,94 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Scan TNG eWallet APK for AppProtect / root detection artifacts."""
|
||||
import re
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
KEYS = [
|
||||
b"AppProtect", b"appprotect", b"vkey", b"V-Key", b"VKey", b"VGuard",
|
||||
b"jailbroken", b"Jailbroken", b"Rooted Device", b"rooted device",
|
||||
b"Close app", b"tngdigital", b"TNG eWallet", b"How to keep device safe",
|
||||
b"enhanced our security", b"RootBeer", b"SafetyNet", b"PlayIntegrity",
|
||||
b"detectRoot", b"isRooted", b"checkRoot", b"magisk", b"xposed", b"lsposed",
|
||||
b"frida", b"emulator", b"su binary", b"/system/xbin/su",
|
||||
]
|
||||
|
||||
CLASS_KEYS = [
|
||||
b"safemode", b"SafeMode", b"appprotect", b"AppProtect", b"vkey", b"VKey",
|
||||
b"vguard", b"VGuard", b"rooted", b"RootBeer", b"integrity", b"jailbreak",
|
||||
b"security", b"RiskDevice", b"tamper", b"hook", b"frida",
|
||||
]
|
||||
|
||||
|
||||
def scan_strings(apk_path):
|
||||
print("=== STRING SCAN: %s ===" % apk_path)
|
||||
with zipfile.ZipFile(apk_path) as zf:
|
||||
for name in sorted(zf.namelist()):
|
||||
if not name.endswith(".dex"):
|
||||
continue
|
||||
data = zf.read(name)
|
||||
hits = []
|
||||
for key in KEYS:
|
||||
start = 0
|
||||
while True:
|
||||
idx = data.find(key, start)
|
||||
if idx < 0:
|
||||
break
|
||||
s = max(0, idx - 60)
|
||||
e = min(len(data), idx + len(key) + 100)
|
||||
chunk = re.sub(rb"[^\x20-\x7e]+", b" ", data[s:e])
|
||||
chunk = chunk.decode("ascii", "ignore").strip()
|
||||
if chunk and chunk not in hits:
|
||||
hits.append(chunk)
|
||||
start = idx + 1
|
||||
if hits:
|
||||
print("\n--- %s (%d hits) ---" % (name, len(hits)))
|
||||
for h in sorted(set(hits))[:40]:
|
||||
print(" ", h)
|
||||
if len(hits) > 40:
|
||||
print(" ... +%d more" % (len(hits) - 40))
|
||||
|
||||
|
||||
def scan_classes(apk_path):
|
||||
print("\n=== CLASS SCAN: %s ===" % apk_path)
|
||||
with zipfile.ZipFile(apk_path) as zf:
|
||||
for name in sorted(zf.namelist()):
|
||||
if not name.endswith(".dex"):
|
||||
continue
|
||||
data = zf.read(name)
|
||||
classes = set(re.findall(rb"L[a-zA-Z0-9_$/]+;", data))
|
||||
hits = []
|
||||
for raw in classes:
|
||||
low = raw.lower()
|
||||
if any(k.lower() in low for k in CLASS_KEYS):
|
||||
s = raw.decode("ascii", "ignore")[1:-1].replace("/", ".")
|
||||
hits.append(s)
|
||||
if hits:
|
||||
print("\n--- %s (%d classes) ---" % (name, len(hits)))
|
||||
for h in sorted(set(hits))[:60]:
|
||||
print(" ", h)
|
||||
if len(hits) > 60:
|
||||
print(" ... +%d more" % (len(hits) - 60))
|
||||
|
||||
|
||||
def scan_native(apk_path):
|
||||
print("\n=== NATIVE LIB SCAN: %s ===" % apk_path)
|
||||
with zipfile.ZipFile(apk_path) as zf:
|
||||
for name in sorted(zf.namelist()):
|
||||
if not name.endswith(".so"):
|
||||
continue
|
||||
data = zf.read(name)
|
||||
lib = name.split("/")[-1]
|
||||
found = []
|
||||
for key in KEYS + [b"libvos", b"libvkey", b"libvguard", b"libappprotect"]:
|
||||
if key.lower() in data.lower():
|
||||
found.append(key.decode("ascii", "ignore"))
|
||||
if found:
|
||||
print(" %s: %s" % (lib, ", ".join(sorted(set(found)))))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
path = sys.argv[1] if len(sys.argv) > 1 else "reverse/apks/tng/base.apk"
|
||||
scan_strings(path)
|
||||
scan_classes(path)
|
||||
scan_native(path)
|
||||
18
reverse/scripts/scan_tng_bl_methods.py
Normal file
18
reverse/scripts/scan_tng_bl_methods.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Dump xwwqazamx bl/w/A method refs from TNG dex."""
|
||||
import re
|
||||
import zipfile
|
||||
|
||||
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\tng\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 ["Lxwwqazamx/bl;", "Lxwwqazamx/w;", "Lxwwqazamx/W;", "Lxwwqazamx/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("=== lifecycle on xwwqazamx/w ===")
|
||||
for m in sorted(set(re.findall(rb"Lxwwqazamx/w;->on[A-Za-z]+", data))):
|
||||
print(m.decode())
|
||||
25
reverse/scripts/scan_tng_callbacks.py
Normal file
25
reverse/scripts/scan_tng_callbacks.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""Scan TNG AppSecurityManager callbacks and bl methods."""
|
||||
import re
|
||||
import zipfile
|
||||
|
||||
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\tng\base.apk"
|
||||
with zipfile.ZipFile(APK) as z:
|
||||
data = b"".join(z.read(n) for n in z.namelist() if n.endswith(".dex"))
|
||||
|
||||
keys = [
|
||||
b"handleRootingCallback", b"handleEmulatorCallback", b"handleHookingCallback",
|
||||
b"handleMalwareCallback", b"onBlockStaticCheck", b"addIntoQueue",
|
||||
b"ForceExit", b"exitApplication", b"startForceExit", b"Lxwwqazamx/bl;",
|
||||
]
|
||||
for k in keys:
|
||||
i = data.find(k)
|
||||
if i < 0:
|
||||
continue
|
||||
print("---", k.decode(), "---")
|
||||
s = re.sub(rb"[^\x20-\x7e]+", b" ", data[max(0, i - 150) : i + len(k) + 200])
|
||||
print(s.decode("ascii", "ignore")[:400])
|
||||
print()
|
||||
|
||||
print("=== bl method refs ===")
|
||||
for m in sorted(set(re.findall(rb"Lxwwqazamx/bl;->[a-zA-Z0-9_$<>\[\]]+", data))):
|
||||
print(m.decode())
|
||||
37
reverse/scripts/scan_tng_exit.py
Normal file
37
reverse/scripts/scan_tng_exit.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""Scan TNG dex for Promon exit paths and ActivityThread refs."""
|
||||
import re
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
apk = sys.argv[1] if len(sys.argv) > 1 else r"C:\Users\Administrator\AppData\Local\Temp\tng_base.apk"
|
||||
with zipfile.ZipFile(apk) as z:
|
||||
data = b"".join(z.read(n) for n in z.namelist() if n.endswith(".dex"))
|
||||
|
||||
patterns = [
|
||||
rb"handleExitApplication",
|
||||
rb"System;->exit",
|
||||
rb"Runtime;->exit",
|
||||
rb"Process;->killProcess",
|
||||
rb"Runtime;->halt",
|
||||
rb"xwwqazamx/bl",
|
||||
rb"xwwqazamx/w",
|
||||
rb"xwwqazamx/W",
|
||||
rb"addIntoQueue",
|
||||
rb"handleRootingCallback",
|
||||
rb"startForceExit",
|
||||
]
|
||||
for pat in patterns:
|
||||
hits = len(re.findall(pat, data))
|
||||
print(f"{pat.decode('utf-8', 'ignore')}: {hits}")
|
||||
|
||||
print("\n=== xwwqazamx class names (sample) ===")
|
||||
classes = sorted(set(m.group().decode("utf-8", "ignore") for m in re.finditer(rb"Lxwwqazamx/[A-Za-z0-9_$]+;", data)))
|
||||
for c in classes[:60]:
|
||||
print(c)
|
||||
print(f"... total {len(classes)}")
|
||||
|
||||
for pat in [b"startForceExit", b"ForceExit", b"openSecurityUrl", b"Lxwwqazamx/w;", b"Lxwwqazamx/bl;->"]:
|
||||
print("---", pat.decode())
|
||||
hits = sorted(set(m.group().decode("utf-8", "ignore") for m in re.finditer(pat + rb"[^\x00]{0,100}", data)))
|
||||
for h in hits[:20]:
|
||||
print(h)
|
||||
72
reverse/scripts/scan_tng_exit10.py
Normal file
72
reverse/scripts/scan_tng_exit10.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""Find System.exit(10) / killProcess callers and nearby strings in TNG DEX."""
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
apk = Path(r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\tng\base.apk")
|
||||
|
||||
with zipfile.ZipFile(apk) as z:
|
||||
dex_blobs = [(n, z.read(n)) for n in z.namelist() if n.endswith(".dex")]
|
||||
|
||||
needles = [
|
||||
b"finishAllActivityAndKillApp",
|
||||
b"UnhandledEvent detected",
|
||||
b"AppSecurityManager: UnhandledEvent",
|
||||
b"openSecurityUrl",
|
||||
b"startForceExitCountdown",
|
||||
b"ForceExitCountdown",
|
||||
b"killProcess",
|
||||
b"SecurityForceExit",
|
||||
b"handleExitApplication",
|
||||
b"exitApplication",
|
||||
b"Jailbroken/Rooted",
|
||||
b"Detected by AppProtect",
|
||||
]
|
||||
|
||||
print("=== string hits ===")
|
||||
for name, data in dex_blobs:
|
||||
for n in needles:
|
||||
c = data.count(n)
|
||||
if c:
|
||||
print(f"{name}: {n.decode(errors='ignore')} x{c}")
|
||||
|
||||
# Find UTF-16 / UTF-8 contexts around exit-related
|
||||
print("\n=== contexts near 'exit' security strings ===")
|
||||
for name, data in dex_blobs:
|
||||
for m in re.finditer(rb"[\x20-\x7e]{0,30}(exit|KillApp|killApp|ForceExit|Unhandled)[\x20-\x7e]{0,80}", data):
|
||||
s = m.group().decode("ascii", "ignore")
|
||||
if any(k in s.lower() for k in ("force", "kill", "unhandled", "security", "promon", "root")):
|
||||
print(f"{name}: {s}")
|
||||
|
||||
# Smali-ish type refs
|
||||
print("\n=== type refs ===")
|
||||
patterns = [
|
||||
rb"Lmy/com/tngdigital/common/internal/_ContextKt;",
|
||||
rb"Lmy/com/tngdigital/common/security/model/UnhandledEvent;",
|
||||
rb"Lxwwqazamx/W;",
|
||||
rb"Lxwwqazamx/bl;",
|
||||
rb"Landroid/os/Process;->killProcess",
|
||||
rb"Ljava/lang/System;->exit",
|
||||
rb"Ljava/lang/Runtime;->exit",
|
||||
]
|
||||
for name, data in dex_blobs:
|
||||
for pat in patterns:
|
||||
hits = len(re.findall(pat, data))
|
||||
if hits:
|
||||
print(f"{name}: {pat.decode(errors='ignore')} x{hits}")
|
||||
|
||||
# Look for const/16 near exit - hard in raw dex; instead find methods that mention exit code strings
|
||||
print("\n=== classes near ForceExit / KillApp strings ===")
|
||||
for name, data in dex_blobs:
|
||||
for pat in [b"finishAllActivityAndKillApp", b"UnhandledEvent detected", b"startForceExitCountdownIfNeeded"]:
|
||||
i = 0
|
||||
while True:
|
||||
j = data.find(pat, i)
|
||||
if j < 0:
|
||||
break
|
||||
# scan backwards for L...; class descriptor within 2KB
|
||||
window = data[max(0, j - 2048):j]
|
||||
classes = re.findall(rb"L[\w/$]+;", window)
|
||||
if classes:
|
||||
print(f"{name} @{j} near {pat.decode()}: ...{classes[-5:]}")
|
||||
i = j + 1
|
||||
103
reverse/scripts/scan_tng_exiting_deep.py
Normal file
103
reverse/scripts/scan_tng_exiting_deep.py
Normal file
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Deeper TNG reverse: Exiting/Report, TigerTally API, kill-SVC in SO."""
|
||||
from pathlib import Path
|
||||
import re
|
||||
import struct
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
APK_DIR = ROOT / "reverse" / "apks" / "tng"
|
||||
SO = APK_DIR / "libtngdigital_ewallet.so"
|
||||
|
||||
def load_dexes():
|
||||
files = sorted(APK_DIR.glob("classes*.dex"))
|
||||
if not files:
|
||||
# try extracted under other layouts
|
||||
files = sorted((ROOT / "reverse" / "apks").rglob("tng*/classes*.dex"))
|
||||
return files
|
||||
|
||||
def near(data, needle, before=40, after=80):
|
||||
out = []
|
||||
for m in re.finditer(re.escape(needle), data):
|
||||
s = max(0, m.start() - before)
|
||||
e = min(len(data), m.end() + after)
|
||||
chunk = data[s:e]
|
||||
printable = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk)
|
||||
out.append(printable)
|
||||
return out
|
||||
|
||||
def main():
|
||||
dexes = load_dexes()
|
||||
print(f"dex count={len(dexes)}")
|
||||
all_data = b""
|
||||
for d in dexes:
|
||||
data = d.read_bytes()
|
||||
all_data += data
|
||||
hits = []
|
||||
for k in [b"Exiting:", b"Exiting", b"Report", b"W: 16", b"W:16",
|
||||
b"TigerTallyAPI", b"ttInit", b"collect", b"killProcess",
|
||||
b"SIGABRT", b"abort(", b"tgkill"]:
|
||||
c = data.count(k)
|
||||
if c:
|
||||
hits.append(f"{k.decode('latin1')}x{c}")
|
||||
if hits:
|
||||
print(f"{d.name}: {', '.join(hits)}")
|
||||
|
||||
print("\n=== near Exiting ===")
|
||||
for s in near(all_data, b"Exiting")[:15]:
|
||||
print(" ", s)
|
||||
|
||||
print("\n=== TigerTallyAPI method refs ===")
|
||||
for m in sorted(set(re.findall(rb"Lcom/aliyun/TigerTally/TigerTallyAPI;->[A-Za-z0-9_<>$]+", all_data))):
|
||||
print(" ", m.decode())
|
||||
|
||||
print("\n=== TigerTally t/ classes ===")
|
||||
for m in sorted(set(re.findall(rb"Lcom/aliyun/TigerTally/[a-z]/[A-Za-z0-9_/$]*;", all_data))):
|
||||
print(" ", m.decode())
|
||||
|
||||
print("\n=== xwwqazamx/bl method refs ===")
|
||||
for m in sorted(set(re.findall(rb"Lxwwqazamx/bl;->[A-Za-z0-9_<>$]+", all_data)))[:40]:
|
||||
print(" ", m.decode())
|
||||
|
||||
print("\n=== Process.killProcess / Runtime.exit refs near Promon ===")
|
||||
for pat in [rb"Landroid/os/Process;->killProcess", rb"Ljava/lang/System;->exit",
|
||||
rb"Ljava/lang/Runtime;->exit", rb"Ljava/lang/Runtime;->halt"]:
|
||||
print(pat.decode(), "count=", len(re.findall(pat, all_data)))
|
||||
|
||||
if SO.exists():
|
||||
so = SO.read_bytes()
|
||||
print(f"\n=== SO {SO.name} size={len(so)} ===")
|
||||
# movz x8,#129 = D2801028 LE
|
||||
patterns = {
|
||||
"movz_x8_129": bytes.fromhex("281080d2"),
|
||||
"movz_w8_129": bytes.fromhex("28108052"),
|
||||
"movz_x8_130": bytes.fromhex("481080d2"),
|
||||
"movz_x8_131": bytes.fromhex("681080d2"),
|
||||
"svc0": bytes.fromhex("010000d4"),
|
||||
"brk0": bytes.fromhex("000020d4"),
|
||||
}
|
||||
for name, pat in patterns.items():
|
||||
print(f" {name}: {so.count(pat)}")
|
||||
|
||||
# find movz kill + nearby svc within 32 bytes
|
||||
kill_imm = [bytes.fromhex(x) for x in ("281080d2", "28108052", "481080d2", "681080d2")]
|
||||
svc = bytes.fromhex("010000d4")
|
||||
found = 0
|
||||
for imm in kill_imm:
|
||||
start = 0
|
||||
while True:
|
||||
i = so.find(imm, start)
|
||||
if i < 0:
|
||||
break
|
||||
window = so[i:i+36]
|
||||
if svc in window:
|
||||
found += 1
|
||||
if found <= 20:
|
||||
off = window.find(svc)
|
||||
print(f" kill+svc @ file+0x{i:x} svc_delta={off}")
|
||||
start = i + 4
|
||||
print(f" kill+svc pairs (packed): {found}")
|
||||
else:
|
||||
print(f"\nSO missing: {SO}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
70
reverse/scripts/scan_tng_keys.py
Normal file
70
reverse/scripts/scan_tng_keys.py
Normal file
@@ -0,0 +1,70 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
|
||||
APK = "reverse/apks/tng/base.apk"
|
||||
|
||||
KEYS = [
|
||||
b"AppProtect", b"JailBroken", b"jailbroken", b"isRooted", b"isJailbroken",
|
||||
b"VKey", b"Promon", b"promon", b"APSE", b"Close app", b"Rooted Device",
|
||||
b"enhanced our security", b"How to keep device safe", b"Detected by",
|
||||
]
|
||||
|
||||
CLASS_NEEDLES = [
|
||||
b"JailBroken", b"AppProtect", b"ApSecurity", b"Promon", b"VKey", b"VGuard",
|
||||
b"RootDetect", b"DeviceRisk", b"SecurityInitializer", b"libAPSE",
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
with zipfile.ZipFile(APK) as zf:
|
||||
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
|
||||
print("=== KEY STRINGS ===")
|
||||
for k in KEYS:
|
||||
idx = 0
|
||||
while True:
|
||||
idx = data.find(k, idx)
|
||||
if idx < 0:
|
||||
break
|
||||
s = max(0, idx - 80)
|
||||
e = min(len(data), idx + len(k) + 120)
|
||||
chunk = re.sub(rb"[^\x20-\x7e]+", b" ", data[s:e]).decode("ascii", "ignore")
|
||||
print(f"[{k.decode()}] {chunk.strip()}")
|
||||
idx += 1
|
||||
|
||||
print("\n=== KEY CLASSES ===")
|
||||
classes = set(re.findall(rb"L[a-zA-Z0-9_$/]+;", data))
|
||||
hits = []
|
||||
for raw in classes:
|
||||
if any(n in raw for n in CLASS_NEEDLES):
|
||||
hits.append(raw.decode()[1:-1].replace("/", "."))
|
||||
for h in sorted(set(hits)):
|
||||
print(h)
|
||||
|
||||
|
||||
FLOW_KEYS = [
|
||||
b"Detected by", b"Close app", b"AppSecurityManager", b"RootEvent", b"RootI18n",
|
||||
b"PromonError", b"showJailBrokenAlert", b"onShowPopupDisable", b"isJailBroken",
|
||||
b"detectJailBroken", b"APSecuritySdk", b"no/promon/shield", b"HookingFrameworks",
|
||||
b"How to keep device safe", b"onBlockStaticCheck",
|
||||
]
|
||||
|
||||
|
||||
def scan_flow():
|
||||
with zipfile.ZipFile(APK) as zf:
|
||||
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
print("\n=== FLOW STRINGS ===")
|
||||
for k in FLOW_KEYS:
|
||||
idx = data.find(k)
|
||||
if idx < 0:
|
||||
continue
|
||||
s = max(0, idx - 80)
|
||||
e = min(len(data), idx + len(k) + 120)
|
||||
chunk = re.sub(rb"[^\x20-\x7e]+", b" ", data[s:e]).decode("ascii", "ignore")
|
||||
print(f"[{k.decode()}] {chunk.strip()}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
scan_flow()
|
||||
53
reverse/scripts/scan_tng_nativelib.py
Normal file
53
reverse/scripts/scan_tng_nativelib.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""Find NativeLib / loadLibrary targets in TNG APK."""
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
apk = Path(r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\tng\base.apk")
|
||||
split = Path(r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\tng\split_config.arm64_v8a.apk")
|
||||
|
||||
with zipfile.ZipFile(apk) as z:
|
||||
data = b"".join(z.read(n) for n in z.namelist() if n.endswith(".dex"))
|
||||
print("base dex files:", [n for n in z.namelist() if n.endswith(".dex")])
|
||||
print("base lib entries:", [n for n in z.namelist() if "lib/" in n][:40])
|
||||
|
||||
print("\nsplit libs:")
|
||||
with zipfile.ZipFile(split) as z:
|
||||
libs = [n for n in z.namelist() if n.endswith(".so")]
|
||||
for n in libs:
|
||||
print(" ", n)
|
||||
|
||||
# strings related to NativeLib
|
||||
needles = [
|
||||
b"NativeLib",
|
||||
b"tngd.networksdk",
|
||||
b"RetrieveFromNativeLibs",
|
||||
b"getApiSixSecretKeys",
|
||||
b"networksdk",
|
||||
b"libtng",
|
||||
b"loadLibrary",
|
||||
]
|
||||
print("\n=== string hits ===")
|
||||
for n in needles:
|
||||
hits = list(re.finditer(n, data))
|
||||
print(f"{n!r}: {len(hits)}")
|
||||
for h in hits[:5]:
|
||||
ctx = data[max(0, h.start()-30):h.end()+80]
|
||||
ctx = bytes(c if 32 <= c < 127 else 46 for c in ctx)
|
||||
print(" ", ctx)
|
||||
|
||||
# library name candidates near NativeLib
|
||||
print("\n=== lib name-like strings near 'NativeLib' / networksdk ===")
|
||||
for m in re.finditer(rb"[\x20-\x7e]{4,60}", data):
|
||||
s = m.group().decode()
|
||||
if "network" in s.lower() or "tngd" in s.lower() or s.startswith("lib") and "tng" in s.lower():
|
||||
if len(s) < 80:
|
||||
print(" ", s)
|
||||
|
||||
# specific: System.loadLibrary argument often stored as short string without lib/ prefix
|
||||
print("\n=== candidate loadLibrary short names ===")
|
||||
cands = set(re.findall(rb"[\x00]([A-Za-z0-9_]{3,40})[\x00]", data))
|
||||
for c in sorted(cands):
|
||||
s = c.decode()
|
||||
if any(k in s.lower() for k in ("tng", "network", "native", "promon", "shield", "apse")):
|
||||
print(" ", s)
|
||||
19
reverse/scripts/scan_tng_nativelib2.py
Normal file
19
reverse/scripts/scan_tng_nativelib2.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""Find NativeLib method signatures / loadLibrary name via dex string proximity."""
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
apk = Path(r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\tng\base.apk")
|
||||
with zipfile.ZipFile(apk) as z:
|
||||
for name in z.namelist():
|
||||
if not name.endswith(".dex"):
|
||||
continue
|
||||
data = z.read(name)
|
||||
if b"NativeLib" not in data and b"native-lib" not in data:
|
||||
continue
|
||||
print("===", name, "===")
|
||||
for pat in [b"NativeLib", b"native-lib", b"getApiSixSecretKeys", b"RetrieveFromNativeLibs", b"Lcom/tngd/networksdk"]:
|
||||
for m in re.finditer(pat, data):
|
||||
ctx = data[max(0, m.start()-60):m.end()+100]
|
||||
ctx = bytes(c if 32 <= c < 127 else 46 for c in ctx)
|
||||
print(pat.decode(), "@", m.start(), ":", ctx.decode())
|
||||
16
reverse/scripts/scan_tng_promon.py
Normal file
16
reverse/scripts/scan_tng_promon.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import re
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
apk = sys.argv[1] if len(sys.argv) > 1 else r"C:\Users\Administrator\AppData\Local\Temp\tng_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 ["xwwqazamx/bl", "xwwqazamx/W", "xwwqazamx/a", "JNICLibrary", "hzchengdun"]:
|
||||
pattern = cls.encode("utf-8") + rb"[^\x00]{0,120}"
|
||||
hits = sorted(set(
|
||||
m.group().decode("utf-8", "ignore") for m in re.finditer(pattern, data)
|
||||
))
|
||||
print(f"\n=== {cls} ({len(hits)} strings) ===")
|
||||
for h in hits[:40]:
|
||||
print(h)
|
||||
18
reverse/scripts/scan_tng_security_error.py
Normal file
18
reverse/scripts/scan_tng_security_error.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Find SecurityErrorActivity onCreate signature."""
|
||||
import re
|
||||
import zipfile
|
||||
|
||||
APK = r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\tng\base.apk"
|
||||
with zipfile.ZipFile(APK) as z:
|
||||
data = b"".join(z.read(n) for n in z.namelist() if n.endswith(".dex"))
|
||||
|
||||
for pat in [
|
||||
b"SecurityErrorActivity",
|
||||
b"launchProcessNextSecurityState",
|
||||
b"addIntoQueueAndLaunch",
|
||||
b"SecurityErrorBaseActivity;->onCreate",
|
||||
]:
|
||||
print("===", pat.decode(), "===")
|
||||
for m in sorted(set(re.findall(pat + rb"[^\x00]{0,120}", data))):
|
||||
print(m.decode("ascii", "ignore")[:150])
|
||||
print()
|
||||
61
reverse/scripts/scan_tng_so_exit.py
Normal file
61
reverse/scripts/scan_tng_so_exit.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""Scan libtngdigital_ewallet.so for SVC / exit patterns."""
|
||||
from pathlib import Path
|
||||
import struct
|
||||
|
||||
SO = Path(r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\tng\libtngdigital_ewallet.so")
|
||||
if not SO.exists():
|
||||
# try from split apk
|
||||
import zipfile
|
||||
apk = Path(r"C:\Users\Administrator\Desktop\notiMessage\reverse\apks\tng\split_config.arm64_v8a.apk")
|
||||
if apk.exists():
|
||||
with zipfile.ZipFile(apk) as z:
|
||||
for n in z.namelist():
|
||||
if n.endswith("libtngdigital_ewallet.so"):
|
||||
SO.write_bytes(z.read(n))
|
||||
print("extracted from", apk, "->", SO)
|
||||
break
|
||||
|
||||
data = SO.read_bytes()
|
||||
print("size", len(data), SO)
|
||||
|
||||
svc = b"\x01\x00\x00\xd4"
|
||||
idxs = []
|
||||
start = 0
|
||||
while True:
|
||||
i = data.find(svc, start)
|
||||
if i < 0:
|
||||
break
|
||||
idxs.append(i)
|
||||
start = i + 4
|
||||
print("total svc#0:", len(idxs))
|
||||
|
||||
# movz x8,#93 = d2 80 0b a8 ; movz x8,#94 = d2 80 0b c8 (LE)
|
||||
# bytes LE: A8 0B 80 D2 / C8 0B 80 D2
|
||||
exit_setups = [
|
||||
(b"\xa8\x0b\x80\xd2", 93), # movz x8, #93
|
||||
(b"\xc8\x0b\x80\xd2", 94), # movz x8, #94
|
||||
(b"\xa8\x0b\x80\x52", 93), # movz w8, #93
|
||||
(b"\xc8\x0b\x80\x52", 94), # movz w8, #94
|
||||
]
|
||||
for pat, nr in exit_setups:
|
||||
c = data.count(pat)
|
||||
print(f"movz *8,#{nr} pattern count={c}")
|
||||
|
||||
print("\nSVC with nearby exit setup (lookback 32 bytes):")
|
||||
hits = 0
|
||||
for i in idxs[:2000]:
|
||||
window = data[max(0, i - 32) : i]
|
||||
for pat, nr in exit_setups:
|
||||
if pat in window:
|
||||
print(f" off=0x{i:x} exit_group/exit via #{nr}")
|
||||
hits += 1
|
||||
break
|
||||
print("hits", hits)
|
||||
|
||||
# also search brk
|
||||
brk = b"\x00\x00\x20\xd4" # brk #0
|
||||
print("brk#0 count", data.count(brk))
|
||||
|
||||
# string refs
|
||||
for s in [b"_exit", b"exit_group", b"abort", b"frida", b"/proc/self/maps", b"xposed"]:
|
||||
print(s, "->", data.find(s))
|
||||
41
reverse/scripts/scan_tng_so_kill.py
Normal file
41
reverse/scripts/scan_tng_so_kill.py
Normal file
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
from pathlib import Path
|
||||
|
||||
DIR = Path(__file__).resolve().parents[1] / "apks" / "tng"
|
||||
KILL = [bytes.fromhex(x) for x in (
|
||||
"281080d2", "28108052", "481080d2", "48108052", "681080d2", "68108052",
|
||||
)]
|
||||
SVC = bytes.fromhex("010000d4")
|
||||
EXIT = [bytes.fromhex(x) for x in ("c80b80d2", "ba0b80d2")] # exit_group, exit
|
||||
|
||||
for so in sorted(DIR.glob("lib*.so")):
|
||||
data = so.read_bytes()
|
||||
pairs = 0
|
||||
for imm in KILL:
|
||||
i = 0
|
||||
while True:
|
||||
j = data.find(imm, i)
|
||||
if j < 0:
|
||||
break
|
||||
if SVC in data[j:j + 36]:
|
||||
pairs += 1
|
||||
if pairs <= 10:
|
||||
print(f"{so.name} KILL+SVC @0x{j:x} d={data[j:j+36].find(SVC)}")
|
||||
i = j + 4
|
||||
ep = 0
|
||||
for imm in EXIT:
|
||||
i = 0
|
||||
while True:
|
||||
j = data.find(imm, i)
|
||||
if j < 0:
|
||||
break
|
||||
if SVC in data[j:j + 36]:
|
||||
ep += 1
|
||||
if ep <= 8:
|
||||
print(f"{so.name} EXIT+SVC @0x{j:x}")
|
||||
i = j + 4
|
||||
print(
|
||||
f"{so.name}: size={len(data)} svc0={data.count(SVC)} "
|
||||
f"kill+svc={pairs} exit+svc={ep} "
|
||||
f"abort={data.count(b'abort')} kill={data.count(b'kill')}"
|
||||
)
|
||||
40
reverse/scripts/scan_tng_suicide_map.py
Normal file
40
reverse/scripts/scan_tng_suicide_map.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""TNG reverse notes helper — ForceExit / abort / Promon suicide map."""
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parents[1] / "apks" / "tng" / "base.apk"
|
||||
SO = Path(__file__).resolve().parents[1] / "apks" / "tng" / "libtngdigital_ewallet.so"
|
||||
|
||||
|
||||
def main():
|
||||
with zipfile.ZipFile(APK) as z:
|
||||
data = b"".join(z.read(n) for n in z.namelist() if n.endswith(".dex"))
|
||||
|
||||
print("=== suicide ladder (from runtime + static) ===")
|
||||
print("1) Promon root hit -> openSecurityUrl Rooting FAQ (Xposed blocks)")
|
||||
print("2) xwwqazamx.W -> KillApplicationHandler (Xposed blocks)")
|
||||
print("3) native exit_group(1) OR SIGABRT SI_USER via libc abort/raise/tgkill")
|
||||
print("4) AppSecurityManager.startForceExitCountdown* / addIntoQueueAndLaunch")
|
||||
print()
|
||||
|
||||
print("=== ForceExit-related descriptors ===")
|
||||
for m in sorted(set(re.findall(rb"L[A-Za-z0-9_/$]*ForceExit[A-Za-z0-9_/$]*;", data))):
|
||||
print(m.decode())
|
||||
|
||||
print("\n=== AppSecurityManager log strings (detection events) ===")
|
||||
for m in re.finditer(rb"AppSecurityManager: [A-Za-z][^\x00]{5,80}", data):
|
||||
s = m.group().decode("utf-8", "ignore")
|
||||
if any(k in s for k in ("Root", "Hook", "Emulator", "Force", "Unhandled", "Navigat")):
|
||||
print(s)
|
||||
|
||||
if SO.exists():
|
||||
raw = SO.read_bytes()
|
||||
print("\n=== SO imports of interest ===")
|
||||
for s in (b"abort", b"raise", b"tgkill", b"kill", b"exit"):
|
||||
print(s.decode(), "at", hex(raw.find(s)) if raw.find(s) >= 0 else None)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
66
reverse/scripts/scan_tng_tigertally.py
Normal file
66
reverse/scripts/scan_tng_tigertally.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Scan TNG DEX for Aliyun TigerTally / abort / SI_USER suicide helpers."""
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parents[1] / "apks" / "tng" / "base.apk"
|
||||
|
||||
KEYS = [
|
||||
b"TigerTally",
|
||||
b"aliyun",
|
||||
b"Aliyun",
|
||||
b"com/aliyun/TigerTally",
|
||||
b"UnhandledEvent detected",
|
||||
b"finishAllActivityAndKillApp",
|
||||
b"trackUnhandledEvent",
|
||||
b"SI_USER",
|
||||
b"raise",
|
||||
b"SIGABRT",
|
||||
b"pthread_kill",
|
||||
b"dispatchUncaughtException",
|
||||
b"AppProtect",
|
||||
b"promon",
|
||||
b"xwwqazamx",
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
with zipfile.ZipFile(APK) as z:
|
||||
entries = [(n, z.read(n)) for n in z.namelist() if n.endswith(".dex")]
|
||||
|
||||
print("=== key hits ===")
|
||||
for name, data in entries:
|
||||
for k in KEYS:
|
||||
c = data.count(k)
|
||||
if c:
|
||||
print(f"{name}: {k.decode('utf-8','ignore')} x{c}")
|
||||
|
||||
print("\n=== TigerTally class descriptors ===")
|
||||
all_data = b"".join(d for _, d in entries)
|
||||
classes = sorted(set(re.findall(rb"Lcom/aliyun/TigerTally/[A-Za-z0-9_/$]*;", all_data)))
|
||||
for c in classes[:80]:
|
||||
print(c.decode())
|
||||
print("total", len(classes))
|
||||
|
||||
print("\n=== nearby strings TigerTally ===")
|
||||
for m in re.finditer(rb"TigerTally[\x20-\x7e]{0,60}", all_data):
|
||||
print(m.group().decode("ascii", "ignore"))
|
||||
|
||||
print("\n=== finishAll / Unhandled contexts ===")
|
||||
for pat in [
|
||||
b"finishAllActivityAndKillApp",
|
||||
b"UnhandledEvent detected",
|
||||
b"trackUnhandledEvent",
|
||||
b"dispatchUncaughtException",
|
||||
]:
|
||||
idx = all_data.find(pat)
|
||||
if idx < 0:
|
||||
continue
|
||||
ctx = all_data[max(0, idx - 40) : idx + len(pat) + 80]
|
||||
printable = "".join(chr(b) if 32 <= b < 127 else "." for b in ctx)
|
||||
print(pat.decode(), "=>", printable)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
36
reverse/scripts/scan_tng_url.py
Normal file
36
reverse/scripts/scan_tng_url.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import zipfile
|
||||
|
||||
APK = "reverse/apks/tng/base.apk"
|
||||
KEYS = [
|
||||
b"36616543382169", b"support.tngdigital", b"Rooting", b"How to keep device safe",
|
||||
b"showJailBroken", b"openUrl", b"openBrowser", b"launchUrl", b"ACTION_VIEW",
|
||||
b"RootI18n", b"SecurityError", b"startChrome", b"IntentDispatcher",
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
with zipfile.ZipFile(APK) as zf:
|
||||
data = b"".join(zf.read(n) for n in zf.namelist() if n.endswith(".dex"))
|
||||
for k in KEYS:
|
||||
idx = 0
|
||||
while True:
|
||||
idx = data.find(k, idx)
|
||||
if idx < 0:
|
||||
break
|
||||
s = max(0, idx - 80)
|
||||
e = min(len(data), idx + len(k) + 120)
|
||||
chunk = re.sub(rb"[^\x20-\x7e]+", b" ", data[s:e]).decode("ascii", "ignore")
|
||||
print(f"[{k.decode()}] {chunk.strip()}")
|
||||
idx += 1
|
||||
print("\n=== classes with Root/security ===")
|
||||
classes = set(re.findall(rb"Lmy/com/tngdigital/common/security[^;]+;", data))
|
||||
for c in sorted(classes):
|
||||
s = c.decode()[1:-1].replace("/", ".")
|
||||
if any(x in s.lower() for x in ["root", "error", "jail", "shield", "promon"]):
|
||||
print(s)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
24
reverse/scripts/scan_user_bundle.py
Normal file
24
reverse/scripts/scan_user_bundle.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
APK = Path(__file__).resolve().parent.parent / "apks" / "maribank_sg_base.apk"
|
||||
needles = [
|
||||
b"The system is currently unavailable",
|
||||
b"dfp is empty",
|
||||
b"getDfp",
|
||||
b"preCheck",
|
||||
b"register",
|
||||
b"Sign up with mobile",
|
||||
b"msg_ekyc_singpass_service_error",
|
||||
b"general_error",
|
||||
b"GlobalAuth",
|
||||
]
|
||||
with zipfile.ZipFile(str(APK)) as zf:
|
||||
bundles = [n for n in zf.namelist() if n.endswith(".jsbundle")]
|
||||
print("bundles:", len(bundles))
|
||||
for name in bundles:
|
||||
data = zf.read(name)
|
||||
hits = [n.decode() for n in needles if n in data]
|
||||
if hits:
|
||||
print(name, hits)
|
||||
466
reverse/scripts/test_tng_full_flow.py
Normal file
466
reverse/scripts/test_tng_full_flow.py
Normal 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≈600–780
|
||||
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 控件出现在 dump(Splash 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())
|
||||
Reference in New Issue
Block a user