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

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

View File

@@ -0,0 +1,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)

View 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)

View 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)

View 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}")

View 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()

View 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()

View 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))

View 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()

View 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))

View 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}")

View 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)

View 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)

View 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()

View 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)

View 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())

View 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("/", "."))

View 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("/", "."))

View 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("/", "."))

View 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)

View 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())

View 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)

View 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)

View 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))

View 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])

View 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)

View 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 ""))

View 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("/", "."))

View 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)

View 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))

View 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)

View 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()

View 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)

View 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)

View 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)

View 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)

View 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)

View 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])

View 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)

View 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())

View 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())

View 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)

View 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

View 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()

View 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()

View 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)

View 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())

View 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)

View 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()

View 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))

View 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')}"
)

View 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()

View 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()

View 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()

View 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)

View File

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