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:
@@ -22,16 +22,15 @@ logcat 标签:`NativeEncrypt: loading JNI`、`CharacterCryptoManager`
|
||||
## 运行
|
||||
|
||||
```powershell
|
||||
# 1. 手机启动 frida-server (root)
|
||||
adb push frida-server /data/local/tmp/
|
||||
adb shell su -c 'chmod 755 /data/local/tmp/frida-server && /data/local/tmp/frida-server -D &'
|
||||
# SG 专用 native trace(推荐)
|
||||
.\scripts\run-frida-sg-native.ps1
|
||||
|
||||
# 2. PC 安装 frida-tools 后
|
||||
# 或
|
||||
cd reverse\frida
|
||||
.\run-frida-trace.ps1 -Mode spawn
|
||||
C:\Users\Administrator\AppData\Local\Programs\Python\Python312\python.exe run_frida_sg_native.py attach
|
||||
|
||||
# 3. App 内 Sign up → 输入号码 → Next
|
||||
# 关注 [MB-TRACE] NativeEncryptWrapper / NativeEncryptUtils / HTTP .../register
|
||||
# PH 旧脚本(勿用于 SG)
|
||||
.\run-frida-trace.ps1 -Mode spawn
|
||||
```
|
||||
|
||||
建议测试时**暂时关闭 LSPosed 对 MariBank 的作用域**,避免与 Frida 冲突。
|
||||
|
||||
25
reverse/frida/mini_run.py
Normal file
25
reverse/frida/mini_run.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import subprocess, time
|
||||
import frida
|
||||
ADB = r"C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
|
||||
PKG = "my.com.tngdigital.ewallet"
|
||||
def adb(*a): return subprocess.run([ADB, *a], capture_output=True, text=True)
|
||||
def on_msg(m, d):
|
||||
line = m.get("payload") if m.get("type") in ("send", "log") else str(m)
|
||||
print(line, flush=True)
|
||||
d = frida.get_usb_device(10)
|
||||
adb("shell", "am", "force-stop", PKG); time.sleep(1)
|
||||
pid = d.spawn([PKG])
|
||||
s = d.attach(pid)
|
||||
sc = s.create_script(open("mini_timer.js", encoding="utf-8").read())
|
||||
sc.on("message", on_msg)
|
||||
sc.load()
|
||||
print("resume", pid, flush=True)
|
||||
d.resume(pid)
|
||||
for i in range(20):
|
||||
time.sleep(1)
|
||||
p = adb("shell", "pidof", PKG).stdout.strip()
|
||||
print("t=%d pid=%s" % (i+1, p or "DEAD"), flush=True)
|
||||
if not p: break
|
||||
try: s.detach()
|
||||
except: pass
|
||||
9
reverse/frida/mini_timer.js
Normal file
9
reverse/frida/mini_timer.js
Normal file
@@ -0,0 +1,9 @@
|
||||
"use strict";
|
||||
console.log("[MINI] start");
|
||||
let i = 0;
|
||||
const t = setInterval(() => {
|
||||
i++;
|
||||
const m = Process.findModuleByName("libtiger_tally.so");
|
||||
console.log(`[MINI] tick=${i} tiger_loaded=${!!m}`);
|
||||
if (i > 15) clearInterval(t);
|
||||
}, 1000);
|
||||
143
reverse/frida/run_frida_sg_native.py
Normal file
143
reverse/frida/run_frida_sg_native.py
Normal file
@@ -0,0 +1,143 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Frida SG native trace — attach to sg.com.maribankmobile.digitalbank."""
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import frida
|
||||
|
||||
PKG = "sg.com.maribankmobile.digitalbank"
|
||||
HERE = Path(__file__).resolve().parent
|
||||
LOGS_DIR = HERE.parent / "logs" / "frida"
|
||||
LOGS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
SCRIPT = HERE / "trace_maribank_sg_native.js"
|
||||
LOG = LOGS_DIR / ("sg_native_%s.log" % datetime.now().strftime("%Y%m%d_%H%M%S"))
|
||||
|
||||
|
||||
def on_message(message, data):
|
||||
mtype = message.get("type")
|
||||
if mtype == "send":
|
||||
line = message.get("payload")
|
||||
elif mtype == "log":
|
||||
line = message.get("payload", message.get("description", ""))
|
||||
else:
|
||||
line = str(message)
|
||||
text = line if isinstance(line, str) else repr(line)
|
||||
print(text, flush=True)
|
||||
with open(LOG, "a", encoding="utf-8") as f:
|
||||
f.write(text + "\n")
|
||||
if mtype == "error":
|
||||
with open(str(LOG) + ".err", "a", encoding="utf-8") as f:
|
||||
f.write(text + "\n")
|
||||
|
||||
|
||||
def wait_for_process(device, pkg, timeout_sec=60):
|
||||
deadline = time.time() + timeout_sec
|
||||
while time.time() < deadline:
|
||||
for app in device.enumerate_applications():
|
||||
if app.identifier == pkg and app.pid and app.pid > 0:
|
||||
return app.pid
|
||||
for proc in device.enumerate_processes():
|
||||
if proc.name == pkg:
|
||||
return proc.pid
|
||||
time.sleep(0.5)
|
||||
return None
|
||||
|
||||
|
||||
def launch_app(pkg):
|
||||
import subprocess
|
||||
|
||||
adb = r"C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
|
||||
subprocess.run([adb, "shell", "am", "force-stop", pkg], check=False, capture_output=True)
|
||||
time.sleep(1)
|
||||
subprocess.run(
|
||||
[
|
||||
adb,
|
||||
"shell",
|
||||
"am",
|
||||
"start",
|
||||
"-n",
|
||||
pkg + "/com.shopee.bke.digitalbank.ui.MainActivity",
|
||||
],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
|
||||
def ensure_frida_server():
|
||||
import subprocess
|
||||
|
||||
adb = r"C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
|
||||
out = subprocess.run(
|
||||
[adb, "shell", "su", "-c", "pgrep frida-server"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if out.stdout.strip():
|
||||
return
|
||||
subprocess.run(
|
||||
[adb, "shell", "su", "-c", "/data/local/tmp/frida-server -D &"],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
)
|
||||
time.sleep(2)
|
||||
|
||||
|
||||
def main():
|
||||
mode = "attach"
|
||||
if len(sys.argv) > 1:
|
||||
mode = sys.argv[1]
|
||||
|
||||
if not SCRIPT.is_file():
|
||||
raise SystemExit("missing script: %s" % SCRIPT)
|
||||
|
||||
ensure_frida_server()
|
||||
device = frida.get_usb_device(timeout=10)
|
||||
source = SCRIPT.read_text(encoding="utf-8")
|
||||
|
||||
print("Package: %s" % PKG)
|
||||
print("Script: %s" % SCRIPT)
|
||||
print("Log: %s" % LOG)
|
||||
print("")
|
||||
print("IMPORTANT: keep LSPosed scope ENABLED for SG (bypasses ADB page while tracing)")
|
||||
print("")
|
||||
|
||||
if mode == "spawn":
|
||||
pid = device.spawn([PKG])
|
||||
session = device.attach(pid)
|
||||
else:
|
||||
pid = wait_for_process(device, PKG, 3)
|
||||
if pid is None:
|
||||
print("Launching MariBank SG ...")
|
||||
launch_app(PKG)
|
||||
pid = wait_for_process(device, PKG, 90)
|
||||
if pid is None:
|
||||
raise SystemExit(
|
||||
"SG MariBank not running — open app to Sign up page, then re-run"
|
||||
)
|
||||
print("Attach pid=%s" % pid)
|
||||
session = device.attach(pid)
|
||||
|
||||
script = session.create_script(source)
|
||||
script.on("message", on_message)
|
||||
script.load()
|
||||
|
||||
if mode == "spawn":
|
||||
device.resume(pid)
|
||||
print("Spawn resumed, wait JVM 15s ...")
|
||||
time.sleep(15)
|
||||
else:
|
||||
time.sleep(3)
|
||||
|
||||
print("Trace running. Sign up -> +65 -> Next. Ctrl+C to stop.")
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
print("Stopping...")
|
||||
session.detach()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
113
reverse/frida/run_frida_tng_native.py
Normal file
113
reverse/frida/run_frida_tng_native.py
Normal file
@@ -0,0 +1,113 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Spawn TNG eWallet with Frida native exit blockers."""
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import frida
|
||||
|
||||
PKG = "my.com.tngdigital.ewallet"
|
||||
HERE = Path(__file__).resolve().parent
|
||||
LOGS_DIR = HERE.parent / "logs" / "frida"
|
||||
LOGS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
SCRIPT = HERE / "trace_tng_native_exit.js"
|
||||
LOG = LOGS_DIR / ("tng_native_%s.log" % datetime.now().strftime("%Y%m%d_%H%M%S"))
|
||||
ADB = r"C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
|
||||
HOLD_SEC = 45
|
||||
|
||||
|
||||
def on_message(message, data):
|
||||
mtype = message.get("type")
|
||||
if mtype == "send":
|
||||
line = message.get("payload")
|
||||
elif mtype == "log":
|
||||
line = message.get("payload", "")
|
||||
else:
|
||||
line = str(message)
|
||||
text = line if isinstance(line, str) else repr(line)
|
||||
print(text, flush=True)
|
||||
with open(LOG, "a", encoding="utf-8") as f:
|
||||
f.write(text + "\n")
|
||||
|
||||
|
||||
def adb(*args):
|
||||
return subprocess.run([ADB, *args], capture_output=True, text=True)
|
||||
|
||||
|
||||
def ensure_frida_server():
|
||||
out = adb("shell", "su", "-c", "pgrep -x frida-server")
|
||||
if out.stdout.strip():
|
||||
print("frida-server already running pid=%s" % out.stdout.strip())
|
||||
return
|
||||
print("starting frida-server ...")
|
||||
adb("shell", "su", "-c", "pkill -9 frida-server; true")
|
||||
# run in background via nohup-like
|
||||
subprocess.Popen(
|
||||
[ADB, "shell", "su", "-c", "/data/local/tmp/frida-server -D"],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
time.sleep(2)
|
||||
out = adb("shell", "su", "-c", "pgrep -x frida-server")
|
||||
if not out.stdout.strip():
|
||||
raise RuntimeError("frida-server failed to start")
|
||||
print("frida-server pid=%s" % out.stdout.strip())
|
||||
|
||||
|
||||
def main():
|
||||
ensure_frida_server()
|
||||
adb("shell", "am", "force-stop", PKG)
|
||||
time.sleep(1)
|
||||
|
||||
device = frida.get_usb_device(10)
|
||||
source = SCRIPT.read_text(encoding="utf-8")
|
||||
print("Spawning %s ..." % PKG)
|
||||
print("log=%s" % LOG)
|
||||
|
||||
pid = device.spawn([PKG])
|
||||
session = device.attach(pid)
|
||||
script = session.create_script(source)
|
||||
script.on("message", on_message)
|
||||
script.load()
|
||||
print("script loaded, resume pid=%s" % pid)
|
||||
device.resume(pid)
|
||||
|
||||
alive = 0
|
||||
for i in range(HOLD_SEC):
|
||||
time.sleep(1)
|
||||
# check process still alive
|
||||
out = adb("shell", "pidof", PKG)
|
||||
pids = out.stdout.strip()
|
||||
if not pids:
|
||||
print("DEAD after %ss" % (i + 1))
|
||||
break
|
||||
alive = i + 1
|
||||
if (i + 1) % 5 == 0:
|
||||
print("alive %ss pid=%s" % (alive, pids))
|
||||
else:
|
||||
print("STABLE %ss pid=%s" % (HOLD_SEC, adb("shell", "pidof", PKG).stdout.strip()))
|
||||
|
||||
# dump activity focus
|
||||
focus = adb("shell", "dumpsys", "activity", "activities")
|
||||
for line in focus.stdout.splitlines():
|
||||
if "tngdigital" in line.lower() and (
|
||||
"mResumedActivity" in line
|
||||
or "topResumedActivity" in line
|
||||
or "UserLogin" in line
|
||||
or "SecurityError" in line
|
||||
or "Splash" in line
|
||||
):
|
||||
print("ACT: " + line.strip())
|
||||
|
||||
try:
|
||||
session.detach()
|
||||
except Exception:
|
||||
pass
|
||||
print("done alive=%ss log=%s" % (alive, LOG))
|
||||
return 0 if alive >= 15 else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
178
reverse/frida/run_tng_compare.py
Normal file
178
reverse/frida/run_tng_compare.py
Normal file
@@ -0,0 +1,178 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Compare: Xposed-only vs Frida-stealth-spawn."""
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
import frida
|
||||
|
||||
ADB = r"C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
|
||||
PKG = "my.com.tngdigital.ewallet"
|
||||
|
||||
STEALTH = r"""
|
||||
"use strict";
|
||||
function log(m){ send("[stealth] "+m); }
|
||||
function findExport(mod, name) {
|
||||
try {
|
||||
var m = Process.getModuleByName(mod);
|
||||
var a = m.findExportByName(name);
|
||||
if (a) return a;
|
||||
} catch (e) {}
|
||||
try { return Module.getGlobalExportByName(name); } catch (e2) { return null; }
|
||||
}
|
||||
|
||||
// Rename frida threads
|
||||
try {
|
||||
var pthread_setname = findExport("libc.so", "pthread_setname_np");
|
||||
// also patch existing: best-effort via Java later
|
||||
} catch (e) {}
|
||||
|
||||
// Hide maps
|
||||
var markers = ["frida","gadget","linjector","gum-js","gmain","pool-frida","hluda"];
|
||||
var tracked = {};
|
||||
function hideLine(line) {
|
||||
var l = (line||"").toLowerCase();
|
||||
for (var i=0;i<markers.length;i++) if (l.indexOf(markers[i])>=0) return true;
|
||||
return false;
|
||||
}
|
||||
function filterBuf(buf, len) {
|
||||
try {
|
||||
var t = buf.readUtf8String(len);
|
||||
if (!t) return len;
|
||||
var out = t.split("\n").filter(function(x){return !hideLine(x);}).join("\n");
|
||||
var b = Memory.allocUtf8String(out);
|
||||
var n = Math.min(len, out.length);
|
||||
Memory.copy(buf, b, n);
|
||||
return n;
|
||||
} catch (e) { return len; }
|
||||
}
|
||||
var openat = findExport("libc.so","openat");
|
||||
var readFn = findExport("libc.so","read");
|
||||
if (openat) {
|
||||
Interceptor.attach(openat, {
|
||||
onEnter: function(args){ this.path = args[1].isNull()?null:args[1].readCString(); },
|
||||
onLeave: function(retval){
|
||||
var fd=retval.toInt32();
|
||||
if (fd>=0 && this.path && (this.path.indexOf("maps")>=0 || this.path.indexOf("status")>=0 || this.path.indexOf("task")>=0))
|
||||
tracked[fd]=this.path;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (readFn) {
|
||||
Interceptor.attach(readFn, {
|
||||
onEnter: function(args){ this.fd=args[0].toInt32(); this.buf=args[1]; },
|
||||
onLeave: function(retval){
|
||||
var n=retval.toInt32();
|
||||
if (n>0 && tracked[this.fd]) retval.replace(ptr(filterBuf(this.buf, n)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Patch libc exit_group SVC
|
||||
var libc = Process.getModuleByName("libc.so");
|
||||
libc.enumerateRanges("r-x").forEach(function(r){
|
||||
for (var off=0; off+8<r.size; off+=4) {
|
||||
var p=r.base.add(off);
|
||||
var w;
|
||||
try { w=p.readU32(); } catch(e){ return; }
|
||||
if (w!==0xd2800bc8 && w!==0xd2800ba8 && w!==0x52800bc8 && w!==0x52800ba8) continue;
|
||||
for (var j=4;j<=24;j+=4) {
|
||||
var s=p.add(j);
|
||||
try {
|
||||
if (s.readU32()===0xd4000001) {
|
||||
Memory.protect(s,4,"rwx");
|
||||
s.writeU32(0xd65f03c0);
|
||||
log("SVC->RET "+s);
|
||||
}
|
||||
} catch(e2){}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Replace exit APIs
|
||||
["_exit","exit","abort"].forEach(function(n){
|
||||
var a=findExport("libc.so",n);
|
||||
if(!a) return;
|
||||
try {
|
||||
Interceptor.replace(a, new NativeCallback(function(c){ log("block "+n+"("+c+")"); }, "void", ["int"]));
|
||||
} catch(e){}
|
||||
});
|
||||
|
||||
log("stealth ready");
|
||||
"""
|
||||
|
||||
|
||||
def adb(*a):
|
||||
return subprocess.run([ADB, *a], capture_output=True, text=True)
|
||||
|
||||
|
||||
def on_msg(m, _d):
|
||||
print(m.get("payload", m), flush=True)
|
||||
|
||||
|
||||
def wait_alive(sec, label):
|
||||
for i in range(sec):
|
||||
time.sleep(1)
|
||||
p = adb("shell", "pidof", PKG).stdout.strip()
|
||||
act = ""
|
||||
dump = adb("shell", "dumpsys", "activity", "activities").stdout
|
||||
for line in dump.splitlines():
|
||||
if "tngdigital" in line.lower() and any(
|
||||
x in line for x in ("UserLogin", "SecurityError", "Splash", "topResumedActivity")
|
||||
):
|
||||
act = line.strip()[:120]
|
||||
break
|
||||
print("%s t=%ds pid=%s act=%s" % (label, i + 1, p or "DEAD", act), flush=True)
|
||||
if not p:
|
||||
return i + 1
|
||||
return sec
|
||||
|
||||
|
||||
def test_xposed_only():
|
||||
print("=== Xposed-only (monkey) ===", flush=True)
|
||||
adb("shell", "am", "force-stop", PKG)
|
||||
adb("logcat", "-c")
|
||||
time.sleep(0.3)
|
||||
adb(
|
||||
"shell",
|
||||
"monkey",
|
||||
"-p",
|
||||
PKG,
|
||||
"-c",
|
||||
"android.intent.category.LAUNCHER",
|
||||
"1",
|
||||
)
|
||||
alive = wait_alive(15, "XPOSED")
|
||||
print("xposed_alive_sec", alive, flush=True)
|
||||
for line in adb("logcat", "-d").stdout.splitlines():
|
||||
if "TngRoot" in line and any(
|
||||
x in line for x in ("install", "UserLogin", "short-circuit", "finishing", "blocked intent")
|
||||
):
|
||||
print(line[line.find("TngRoot") :][:180], flush=True)
|
||||
if "Displayed" in line and "tngdigital" in line:
|
||||
print(line.strip()[:200], flush=True)
|
||||
if "exited cleanly" in line:
|
||||
print(line.strip()[:160], flush=True)
|
||||
|
||||
|
||||
def test_frida_stealth():
|
||||
print("=== Frida stealth spawn ===", flush=True)
|
||||
adb("shell", "am", "force-stop", PKG)
|
||||
time.sleep(0.3)
|
||||
d = frida.get_usb_device(10)
|
||||
pid = d.spawn([PKG])
|
||||
s = d.attach(pid)
|
||||
sc = s.create_script(STEALTH)
|
||||
sc.on("message", on_msg)
|
||||
sc.load()
|
||||
d.resume(pid)
|
||||
alive = wait_alive(15, "FRIDA")
|
||||
print("frida_alive_sec", alive, flush=True)
|
||||
|
||||
|
||||
def main():
|
||||
test_xposed_only()
|
||||
test_frida_stealth()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
48
reverse/frida/run_tng_diag.py
Normal file
48
reverse/frida/run_tng_diag.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import frida
|
||||
|
||||
ADB = r"C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
|
||||
PKG = "my.com.tngdigital.ewallet"
|
||||
SCRIPT = Path(__file__).with_name("trace_tng_diag_exit.js").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def adb(*a):
|
||||
return subprocess.run([ADB, *a], capture_output=True, text=True)
|
||||
|
||||
|
||||
def on_msg(m, d):
|
||||
if m.get("type") == "send":
|
||||
print(m["payload"], flush=True)
|
||||
else:
|
||||
print(m, flush=True)
|
||||
|
||||
|
||||
def main():
|
||||
adb("shell", "am", "force-stop", PKG)
|
||||
time.sleep(0.5)
|
||||
d = frida.get_usb_device(10)
|
||||
pid = d.spawn([PKG])
|
||||
s = d.attach(pid)
|
||||
sc = s.create_script(SCRIPT)
|
||||
sc.on("message", on_msg)
|
||||
sc.load()
|
||||
print("resume", pid, flush=True)
|
||||
d.resume(pid)
|
||||
for i in range(15):
|
||||
time.sleep(1)
|
||||
p = adb("shell", "pidof", PKG).stdout.strip()
|
||||
print("t=%ds pid=%s" % (i + 1, p or "DEAD"), flush=True)
|
||||
if not p:
|
||||
break
|
||||
try:
|
||||
s.detach()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
42
reverse/frida/run_tng_empty_spawn.py
Normal file
42
reverse/frida/run_tng_empty_spawn.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
import frida
|
||||
|
||||
ADB = r"C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
|
||||
PKG = "my.com.tngdigital.ewallet"
|
||||
|
||||
|
||||
def adb(*a):
|
||||
return subprocess.run([ADB, *a], capture_output=True, text=True)
|
||||
|
||||
|
||||
def main():
|
||||
adb("shell", "am", "force-stop", PKG)
|
||||
adb("logcat", "-c")
|
||||
time.sleep(0.3)
|
||||
d = frida.get_usb_device(10)
|
||||
pid = d.spawn([PKG])
|
||||
s = d.attach(pid)
|
||||
sc = s.create_script('send("empty ok " + String(Process.id));')
|
||||
sc.on("message", lambda m, _d: print(m, flush=True))
|
||||
sc.load()
|
||||
print("resume", pid, flush=True)
|
||||
d.resume(pid)
|
||||
for i in range(10):
|
||||
time.sleep(1)
|
||||
p = adb("shell", "pidof", PKG).stdout.strip()
|
||||
print("t=%d %s" % (i + 1, p or "DEAD"), flush=True)
|
||||
if not p:
|
||||
break
|
||||
print("--- death lines ---", flush=True)
|
||||
for line in adb("logcat", "-d").stdout.splitlines():
|
||||
if "exited cleanly" in line or "has died" in line and "tngdigital" in line:
|
||||
print(line[:240], flush=True)
|
||||
if "TngRoot" in line and ("install" in line or "short-circuit" in line or "blocked" in line):
|
||||
print(line[:240], flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
214
reverse/frida/run_tng_patch_libc_svc.py
Normal file
214
reverse/frida/run_tng_patch_libc_svc.py
Normal file
@@ -0,0 +1,214 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Resolve libc _exit real target and patch its SVC."""
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
import frida
|
||||
|
||||
ADB = r"C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
|
||||
PKG = "my.com.tngdigital.ewallet"
|
||||
|
||||
SCRIPT = r"""
|
||||
"use strict";
|
||||
function log(m){ send("[TNG-native] "+m); }
|
||||
|
||||
function findExport(mod, name) {
|
||||
try {
|
||||
var m = Process.getModuleByName(mod);
|
||||
var a = m.findExportByName(name);
|
||||
if (a) return a;
|
||||
} catch (e) {}
|
||||
try { return Module.getGlobalExportByName(name); } catch (e2) { return null; }
|
||||
}
|
||||
|
||||
function resolveTrampoline(addr, name) {
|
||||
// Follow simple ADRP+ADD+BR / LDR+BR patterns a few times
|
||||
var cur = addr;
|
||||
for (var depth = 0; depth < 5; depth++) {
|
||||
var w0 = cur.readU32();
|
||||
var w1 = cur.add(4).readU32();
|
||||
// BR Xn: 0xD61F0000 | (Rn<<5)
|
||||
if ((w1 & 0xfffffc1f) === 0xd61f0000) {
|
||||
var rn = (w1 >> 5) & 0x1f;
|
||||
// LDR Xn, [PC, #imm] : 0x58000000
|
||||
if ((w0 & 0xff000000) === 0x58000000) {
|
||||
var imm19 = (w0 >> 5) & 0x7ffff;
|
||||
if (imm19 & 0x40000) imm19 -= 0x80000;
|
||||
var targetPtr = cur.add(imm19 * 4);
|
||||
var target = targetPtr.readPointer();
|
||||
log(name + " trampoline LDR+BR -> " + target);
|
||||
cur = target;
|
||||
continue;
|
||||
}
|
||||
// ADRP Xn, page
|
||||
if ((w0 & 0x9f000000) === 0x90000000) {
|
||||
var rd = w0 & 0x1f;
|
||||
var immhi = (w0 >> 5) & 0x7ffff;
|
||||
var immlo = (w0 >> 29) & 0x3;
|
||||
var imm = ((immhi << 2) | immlo) << 12;
|
||||
if (imm & 0x100000000) imm = imm - 0x200000000;
|
||||
var page = cur.and(ptr("0xfffffffffffff000")).add(imm);
|
||||
// next might be LDR/ADD
|
||||
var w2 = cur.add(8).readU32();
|
||||
log(name + " ADRP page="+page+" rn="+rn+" rd="+rd+" w2="+w2.toString(16));
|
||||
}
|
||||
log(name + " BR X" + rn + " at " + cur + " (stop follow)");
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return cur;
|
||||
}
|
||||
|
||||
function patchSvcNear(addr, name, windowSize) {
|
||||
var nop = [0x1f, 0x20, 0x03, 0xd5];
|
||||
var n = 0;
|
||||
for (var i = 0; i < windowSize; i += 4) {
|
||||
try {
|
||||
var p = addr.add(i);
|
||||
if (p.readU32() !== 0xd4000001) continue;
|
||||
Memory.protect(p, 4, "rwx");
|
||||
// replace svc with: mov x0, x0; ret — or just nop and hope
|
||||
// Better: movz x0, #0; ret so "exit" becomes return 0
|
||||
// movz x0,#0 = 0xD2800000; ret = 0xD65F03C0
|
||||
p.writeU32(0xd2800000); // movz x0, #0
|
||||
if (i + 4 < windowSize) {
|
||||
var p2 = addr.add(i + 4);
|
||||
// only overwrite next if also svc/brk or nop pad — safer: write ret at svc place only via branch
|
||||
}
|
||||
// Just NOP the svc — caller may hang; use ret instead by overwriting svc with ret
|
||||
p.writeU32(0xd65f03c0); // RET
|
||||
n++;
|
||||
log("patched SVC->RET @ " + p + " (" + name + "+" + i + ")");
|
||||
} catch (e) {
|
||||
log("patch fail: " + e);
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
function hookByPatchingLibcExit() {
|
||||
var libc = Process.getModuleByName("libc.so");
|
||||
// Scan entire libc for the classic exit_group sequence:
|
||||
// mov x8, #94; mov x0, ...; svc #0 OR svc inside _exit impl
|
||||
var count = 0;
|
||||
var svcAddrs = [];
|
||||
// Focused: exports that must lead to exit
|
||||
["_exit", "exit"].forEach(function (n) {
|
||||
var a = libc.findExportByName(n);
|
||||
if (!a) return;
|
||||
log(n + " export " + a);
|
||||
// DebugSymbol / Instruction parse: find first BL/B to real impl
|
||||
});
|
||||
|
||||
// Brute: scan libc executable for movz x8,#94 followed within 16 bytes by svc
|
||||
var ranges = libc.enumerateRanges("r-x");
|
||||
ranges.forEach(function (r) {
|
||||
for (var off = 0; off + 8 < r.size; off += 4) {
|
||||
var p = r.base.add(off);
|
||||
var w;
|
||||
try { w = p.readU32(); } catch (e) { return; }
|
||||
// movz x8, #94 = 0xD2800BC8 ; movz w8,#94 = 0x52800BC8
|
||||
// movz x8, #93 = 0xD2800BA8
|
||||
if (w !== 0xd2800bc8 && w !== 0x52800bc8 && w !== 0xd2800ba8 && w !== 0x52800ba8) continue;
|
||||
// look ahead for svc
|
||||
for (var j = 4; j <= 24; j += 4) {
|
||||
try {
|
||||
if (p.add(j).readU32() === 0xd4000001) {
|
||||
svcAddrs.push(p.add(j));
|
||||
log("exit-seq movz@ " + p + " svc@ " + p.add(j));
|
||||
}
|
||||
} catch (e2) {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
svcAddrs.forEach(function (svc) {
|
||||
try {
|
||||
Memory.protect(svc, 4, "rwx");
|
||||
// Replace svc with ret — turns exit into function return
|
||||
svc.writeU32(0xd65f03c0);
|
||||
count++;
|
||||
log("SVC->RET " + svc);
|
||||
} catch (e) {
|
||||
log("SVC patch fail " + svc + ": " + e);
|
||||
}
|
||||
});
|
||||
log("libc exit SVC patches=" + count);
|
||||
}
|
||||
|
||||
function installEntryLog() {
|
||||
["_exit", "exit", "abort"].forEach(function (n) {
|
||||
var a = findExport("libc.so", n);
|
||||
if (!a) return;
|
||||
try {
|
||||
Interceptor.attach(a, {
|
||||
onEnter: function (args) {
|
||||
log("ENTER " + n + "(" + args[0] + ")");
|
||||
}
|
||||
});
|
||||
} catch (e) {}
|
||||
});
|
||||
}
|
||||
|
||||
// Also Interceptor.replace as backup
|
||||
function installReplace() {
|
||||
["_exit", "exit", "abort"].forEach(function (n) {
|
||||
var a = findExport("libc.so", n);
|
||||
if (!a) return;
|
||||
try {
|
||||
Interceptor.replace(a, new NativeCallback(function (code) {
|
||||
log("REPLACED-HIT " + n + "(" + (code|0) + ")");
|
||||
}, "void", ["int"]));
|
||||
log("replaced " + n);
|
||||
} catch (e) {
|
||||
log("replace " + n + " fail: " + e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
log("pid=" + Process.id);
|
||||
hookByPatchingLibcExit();
|
||||
installReplace();
|
||||
installEntryLog();
|
||||
log("ready");
|
||||
"""
|
||||
|
||||
|
||||
def adb(*a):
|
||||
return subprocess.run([ADB, *a], capture_output=True, text=True)
|
||||
|
||||
|
||||
def on_msg(m, d):
|
||||
print(m.get("payload", m), flush=True)
|
||||
|
||||
|
||||
def main():
|
||||
adb("shell", "am", "force-stop", PKG)
|
||||
time.sleep(0.4)
|
||||
device = frida.get_usb_device(10)
|
||||
pid = device.spawn([PKG])
|
||||
session = device.attach(pid)
|
||||
script = session.create_script(SCRIPT)
|
||||
script.on("message", on_msg)
|
||||
script.load()
|
||||
print("resume", pid, flush=True)
|
||||
device.resume(pid)
|
||||
for i in range(25):
|
||||
time.sleep(1)
|
||||
p = adb("shell", "pidof", PKG).stdout.strip()
|
||||
print("t=%ds %s" % (i + 1, p or "DEAD"), flush=True)
|
||||
if not p:
|
||||
break
|
||||
else:
|
||||
print("STABLE", flush=True)
|
||||
focus = adb("shell", "dumpsys", "activity", "activities").stdout
|
||||
for line in focus.splitlines():
|
||||
if "tngdigital" in line.lower() and any(
|
||||
x in line for x in ("UserLogin", "SecurityError", "Splash", "mResumed", "topResumed")
|
||||
):
|
||||
print("ACT", line.strip()[:200], flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
107
reverse/frida/run_tng_tiger_fread.py
Normal file
107
reverse/frida/run_tng_tiger_fread.py
Normal file
@@ -0,0 +1,107 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Spawn TNG, observe TigerTally fread blocking (observe-only, no behavior change)."""
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import frida
|
||||
|
||||
PKG = "my.com.tngdigital.ewallet"
|
||||
HERE = Path(__file__).resolve().parent
|
||||
LOGS_DIR = HERE.parent / "logs" / "frida"
|
||||
LOGS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
SCRIPT = HERE / "trace_tng_tiger_fread.js"
|
||||
LOG = LOGS_DIR / ("tng_tiger_fread_%s.log" % datetime.now().strftime("%Y%m%d_%H%M%S"))
|
||||
ADB = r"C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
|
||||
HOLD_SEC = 45
|
||||
|
||||
|
||||
def on_message(message, data):
|
||||
mtype = message.get("type")
|
||||
if mtype == "send":
|
||||
line = message.get("payload")
|
||||
elif mtype == "log":
|
||||
line = message.get("payload", "")
|
||||
else:
|
||||
line = str(message)
|
||||
text = line if isinstance(line, str) else repr(line)
|
||||
print(text, flush=True)
|
||||
with open(LOG, "a", encoding="utf-8") as f:
|
||||
f.write(text + "\n")
|
||||
|
||||
|
||||
def adb(*args):
|
||||
return subprocess.run([ADB, *args], capture_output=True, text=True)
|
||||
|
||||
|
||||
def ensure_frida_server():
|
||||
out = adb("shell", "su", "-c", "pgrep -x frida-server")
|
||||
if out.stdout.strip():
|
||||
print("frida-server running pid=%s" % out.stdout.strip())
|
||||
return
|
||||
adb("shell", "su", "-c", "pkill -9 frida-server; true")
|
||||
subprocess.Popen(
|
||||
[ADB, "shell", "su", "-c", "/data/local/tmp/frida-server -D"],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
time.sleep(2)
|
||||
out = adb("shell", "su", "-c", "pgrep -x frida-server")
|
||||
if not out.stdout.strip():
|
||||
raise RuntimeError("frida-server failed to start")
|
||||
print("frida-server pid=%s" % out.stdout.strip())
|
||||
|
||||
|
||||
def main():
|
||||
ensure_frida_server()
|
||||
adb("shell", "am", "force-stop", PKG)
|
||||
time.sleep(1)
|
||||
|
||||
device = frida.get_usb_device(10)
|
||||
source = SCRIPT.read_text(encoding="utf-8")
|
||||
print("spawn %s ..." % PKG)
|
||||
print("log=%s" % LOG)
|
||||
|
||||
pid = device.spawn([PKG])
|
||||
session = device.attach(pid)
|
||||
script = session.create_script(source)
|
||||
script.on("message", on_message)
|
||||
script.load()
|
||||
print("script loaded, resume pid=%s" % pid)
|
||||
device.resume(pid)
|
||||
|
||||
alive = 0
|
||||
for i in range(HOLD_SEC):
|
||||
time.sleep(1)
|
||||
out = adb("shell", "pidof", PKG)
|
||||
pids = out.stdout.strip()
|
||||
if not pids:
|
||||
print("DEAD after %ss" % (i + 1))
|
||||
break
|
||||
alive = i + 1
|
||||
if (i + 1) % 5 == 0:
|
||||
print("alive %ss pid=%s" % (alive, pids))
|
||||
else:
|
||||
print("STABLE %ss pid=%s" % (HOLD_SEC, adb("shell", "pidof", PKG).stdout.strip()))
|
||||
|
||||
# 焦点 Activity
|
||||
focus = adb("shell", "dumpsys", "activity", "activities")
|
||||
for line in focus.stdout.splitlines():
|
||||
if "tngdigital" in line.lower() and (
|
||||
"mResumedActivity" in line or "topResumedActivity" in line
|
||||
or "UserLogin" in line or "SecurityError" in line or "Splash" in line
|
||||
):
|
||||
print("ACT: " + line.strip())
|
||||
|
||||
try:
|
||||
session.detach()
|
||||
except Exception:
|
||||
pass
|
||||
print("done alive=%ss log=%s" % (alive, LOG))
|
||||
return 0 if alive >= 15 else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
60
reverse/frida/strace_tng_exit.py
Normal file
60
reverse/frida/strace_tng_exit.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Spawn TNG and strace for exit syscalls (needs root)."""
|
||||
import subprocess
|
||||
import time
|
||||
import sys
|
||||
|
||||
ADB = r"C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools\adb.exe"
|
||||
PKG = "my.com.tngdigital.ewallet"
|
||||
|
||||
|
||||
def adb(*args, timeout=30):
|
||||
return subprocess.run([ADB, *args], capture_output=True, text=True, timeout=timeout)
|
||||
|
||||
|
||||
def main():
|
||||
adb("shell", "am", "force-stop", PKG)
|
||||
time.sleep(0.5)
|
||||
adb("logcat", "-c")
|
||||
# start app
|
||||
adb(
|
||||
"shell",
|
||||
"monkey",
|
||||
"-p",
|
||||
PKG,
|
||||
"-c",
|
||||
"android.intent.category.LAUNCHER",
|
||||
"1",
|
||||
)
|
||||
time.sleep(0.4)
|
||||
out = adb("shell", "pidof", PKG)
|
||||
pid = out.stdout.strip().split()[0] if out.stdout.strip() else ""
|
||||
if not pid:
|
||||
print("no pid")
|
||||
return 1
|
||||
print("pid", pid)
|
||||
# strace briefly
|
||||
p = subprocess.Popen(
|
||||
[
|
||||
ADB,
|
||||
"shell",
|
||||
"su",
|
||||
"-c",
|
||||
f"timeout 8 strace -f -e trace=exit,exit_group,kill,tkill,tgkill,write -p {pid} 2>&1 | head -80",
|
||||
],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
)
|
||||
try:
|
||||
stdout, _ = p.communicate(timeout=15)
|
||||
print(stdout)
|
||||
except subprocess.TimeoutExpired:
|
||||
p.kill()
|
||||
print(p.stdout.read() if p.stdout else "timeout")
|
||||
print("alive?", adb("shell", "pidof", PKG).stdout.strip())
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
634
reverse/frida/trace_maribank_sg_native.js
Normal file
634
reverse/frida/trace_maribank_sg_native.js
Normal file
@@ -0,0 +1,634 @@
|
||||
'use strict';
|
||||
/**
|
||||
* MariBank SG 3.2.2 — Java + native attestation / encrypt trace
|
||||
* Package: sg.com.maribankmobile.digitalbank
|
||||
*
|
||||
* SG 差异: 无 utils.d / com.shopee.shpssdk.*,仅 shpssdkbank + uvwuvwuv
|
||||
*/
|
||||
const TAG = '[MB-NATIVE]';
|
||||
const MAX_STR = 4000;
|
||||
const MAX_BYTES_LOG = 8192;
|
||||
const HOOKED_NATIVE_PTRS = {};
|
||||
|
||||
const JAVA_TARGETS = [
|
||||
'com.shopee.shpssdkbank.wvvvuwwu',
|
||||
'com.shopee.shpssdkbank.uwuvuvvww.vvuuuuvvv',
|
||||
'com.shopee.shpssdkbank.uwuvuvvww.uvwuuuuuw.vvvvuwwvu',
|
||||
'com.shopee.shpssdkbank.uwuvuvvww.wvvuuwvwu',
|
||||
'com.shopee.shpssdkbank.uvuwwuvwv.uvwwuuvvw',
|
||||
'com.shopee.bke.lib.jni.utils.uvwuvwuv',
|
||||
'com.shopee.bke.lib.jni.utils.uvwwwwuv',
|
||||
'com.shopee.shpssdkbank.SHPSSDK',
|
||||
];
|
||||
|
||||
const SO_WATCH = [
|
||||
'libshpssdk_bank.so',
|
||||
'libshpssdk.so',
|
||||
'libsdkutils.so',
|
||||
'libbkutils.so',
|
||||
];
|
||||
|
||||
function log(msg) {
|
||||
send(TAG + ' ' + msg);
|
||||
}
|
||||
|
||||
function jniFn(envPtr, index, ret, args) {
|
||||
const funcs = envPtr.readPointer();
|
||||
const addr = funcs.add(index * Process.pointerSize).readPointer();
|
||||
if (!addr || addr.isNull()) return null;
|
||||
return new NativeFunction(addr, ret, args);
|
||||
}
|
||||
|
||||
function jniReadByteArray(envPtr, jarrayPtr) {
|
||||
if (!jarrayPtr || jarrayPtr.isNull()) return null;
|
||||
try {
|
||||
const GetArrayLength = jniFn(envPtr, 171, 'int', ['pointer', 'pointer']);
|
||||
const GetByteArrayElements = jniFn(envPtr, 184, 'pointer', ['pointer', 'pointer', 'pointer']);
|
||||
const ReleaseByteArrayElements = jniFn(envPtr, 187, 'void', ['pointer', 'pointer', 'pointer', 'int']);
|
||||
if (!GetArrayLength || !GetByteArrayElements || !ReleaseByteArrayElements) {
|
||||
return jniReadByteArrayArt(envPtr, jarrayPtr);
|
||||
}
|
||||
const len = GetArrayLength(envPtr, jarrayPtr);
|
||||
if (len <= 0 || len > MAX_BYTES_LOG) return { len: len, hex: '', text: '' };
|
||||
const elems = GetByteArrayElements(envPtr, jarrayPtr, ptr(0));
|
||||
if (!elems || elems.isNull()) return { len: len, hex: '', text: '' };
|
||||
const raw = elems.readByteArray(Math.min(len, MAX_BYTES_LOG));
|
||||
ReleaseByteArrayElements(envPtr, jarrayPtr, elems, 0);
|
||||
return bytesToPreview(raw, len);
|
||||
} catch (e) {
|
||||
return { len: -1, hex: 'err:' + e, text: '' };
|
||||
}
|
||||
}
|
||||
|
||||
function bytesToPreview(raw, len) {
|
||||
const arr = new Uint8Array(raw);
|
||||
let text = '';
|
||||
try {
|
||||
text = String.fromCharCode.apply(null, arr);
|
||||
if (text.indexOf('\u0000') >= 0 || !/^[\x20-\x7e\r\n\t\u4e00-\u9fff\u0100-\u024f]+$/.test(text.substring(0, Math.min(text.length, 200)))) {
|
||||
text = '';
|
||||
}
|
||||
} catch (e) {
|
||||
text = '';
|
||||
}
|
||||
if (text.length > MAX_STR) text = text.substring(0, MAX_STR) + '...';
|
||||
return { len: len, hex: hexPreview(arr, 64), text: text, arr: arr };
|
||||
}
|
||||
|
||||
function jniReadByteArrayArt(envPtr, jarrayPtr) {
|
||||
const art = moduleByName('libart.so');
|
||||
if (!art) return { len: -1, hex: 'err:no-art', text: '' };
|
||||
let sym = null;
|
||||
art.enumerateSymbols().forEach(function (s) {
|
||||
if (sym) return;
|
||||
if (s.name.indexOf('GetByteArrayRegion') >= 0 && s.name.indexOf('JNI') >= 0) {
|
||||
sym = s.address;
|
||||
}
|
||||
});
|
||||
if (!sym) return { len: -1, hex: 'err:no-GetByteArrayRegion', text: '' };
|
||||
const GetLen = jniFn(envPtr, 171, 'int', ['pointer', 'pointer']);
|
||||
const len = GetLen ? GetLen(envPtr, jarrayPtr) : 0;
|
||||
if (len <= 0 || len > MAX_BYTES_LOG) return { len: len, hex: '', text: '' };
|
||||
const buf = Memory.alloc(len);
|
||||
const GetRegion = new NativeFunction(sym, 'void', ['pointer', 'pointer', 'int', 'int', 'pointer']);
|
||||
GetRegion(envPtr, jarrayPtr, 0, len, buf);
|
||||
return bytesToPreview(buf.readByteArray(len), len);
|
||||
}
|
||||
|
||||
function jniReadJstring(envPtr, jstrPtr) {
|
||||
if (!jstrPtr || jstrPtr.isNull()) return '';
|
||||
try {
|
||||
const GetStringUTFChars = jniFn(envPtr, 169, 'pointer', ['pointer', 'pointer', 'pointer']);
|
||||
const ReleaseStringUTFChars = jniFn(envPtr, 170, 'void', ['pointer', 'pointer', 'pointer']);
|
||||
if (!GetStringUTFChars || !ReleaseStringUTFChars) return '';
|
||||
const chars = GetStringUTFChars(envPtr, jstrPtr, ptr(0));
|
||||
if (!chars || chars.isNull()) return '';
|
||||
const s = chars.readCString();
|
||||
ReleaseStringUTFChars(envPtr, jstrPtr, chars);
|
||||
return s || '';
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function dumpNativeArgs(methodName, sig, envPtr, args) {
|
||||
if (methodName === 'vuwuuwvw' && sig.indexOf('[B[B') >= 0) {
|
||||
const a0 = jniReadByteArray(envPtr, args[2]);
|
||||
const a1 = jniReadByteArray(envPtr, args[3]);
|
||||
if (a0) log(' nat in0 len=' + a0.len + ' hex=' + a0.hex);
|
||||
if (a1) log(' nat in1 len=' + a1.len + ' hex=' + a1.hex);
|
||||
return;
|
||||
}
|
||||
if (methodName === 'uvwuuww') {
|
||||
const plain = jniReadByteArray(envPtr, args[2]);
|
||||
const key = jniReadJstring(envPtr, args[3]);
|
||||
const flag = args[4] ? args[4].toInt32() : 0;
|
||||
if (plain) {
|
||||
log(' nat plain len=' + plain.len + ' hex=' + plain.hex);
|
||||
if (plain.text) log(' nat plain utf8=' + plain.text);
|
||||
}
|
||||
if (key) log(' nat key=' + key + ' flag=' + flag);
|
||||
return;
|
||||
}
|
||||
if (methodName === 'vuwuuuwv' && sig.indexOf('[B[B') >= 0) {
|
||||
const a0 = jniReadByteArray(envPtr, args[2]);
|
||||
const a1 = jniReadByteArray(envPtr, args[3]);
|
||||
if (a0) log(' nat defense in0 len=' + a0.len + ' hex=' + a0.hex);
|
||||
if (a1) log(' nat defense in1 len=' + a1.len + ' hex=' + a1.hex);
|
||||
}
|
||||
}
|
||||
|
||||
function hexPreview(arr, limit) {
|
||||
if (!arr) return '';
|
||||
const n = Math.min(arr.length, limit || 64);
|
||||
let hex = '';
|
||||
for (let i = 0; i < n; i++) {
|
||||
const b = (arr[i] & 0xff).toString(16);
|
||||
hex += (b.length === 1 ? '0' : '') + b;
|
||||
}
|
||||
if (arr.length > n) hex += '...(' + arr.length + ')';
|
||||
return hex;
|
||||
}
|
||||
|
||||
function dumpBytes(label, jobj) {
|
||||
if (jobj === null || jobj === undefined) {
|
||||
log(label + ' = null');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const arr = Java.cast(jobj, Java.use('[B'));
|
||||
let text = '';
|
||||
try {
|
||||
text = Java.use('java.lang.String').$new(arr, 'UTF-8').toString();
|
||||
} catch (e) {
|
||||
text = '';
|
||||
}
|
||||
const printable = text.length > 0 && text.indexOf('\u0000') < 0;
|
||||
if (printable && (text.indexOf('rdVerifyInfo') >= 0 || text.indexOf('REGISTRATION') >= 0
|
||||
|| text.indexOf('deviceFingerprint') >= 0 || text.length < MAX_STR)) {
|
||||
const show = text.length > MAX_STR ? text.substring(0, MAX_STR) + '...' : text;
|
||||
log(label + ' byte[' + arr.length + '] utf8=' + show);
|
||||
} else {
|
||||
log(label + ' byte[' + arr.length + '] hex=' + hexPreview(arr, 48));
|
||||
}
|
||||
} catch (e) {
|
||||
log(label + ' dump err=' + e);
|
||||
}
|
||||
}
|
||||
|
||||
function dumpJava(label, obj) {
|
||||
if (obj === null || obj === undefined) {
|
||||
log(label + ' = null');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const cls = obj.getClass().getName();
|
||||
if (cls === '[B') {
|
||||
dumpBytes(label, obj);
|
||||
return;
|
||||
}
|
||||
if (cls === 'java.lang.String') {
|
||||
const s = Java.cast(obj, Java.use('java.lang.String')).toString();
|
||||
const show = s.length > MAX_STR ? s.substring(0, MAX_STR) + '...' : s;
|
||||
log(label + ' String(' + s.length + ') ' + show);
|
||||
return;
|
||||
}
|
||||
if (cls === '[Ljava.lang.String;') {
|
||||
const arr = Java.cast(obj, Java.use('[Ljava.lang.String;'));
|
||||
log(label + ' String[' + arr.length + ']');
|
||||
for (let i = 0; i < arr.length; i++) dumpJava(label + '[' + i + ']', arr[i]);
|
||||
return;
|
||||
}
|
||||
if (cls === '[[B') {
|
||||
const outer = Java.cast(obj, Java.use('[[B'));
|
||||
log(label + ' byte[][] len=' + outer.length);
|
||||
for (let i = 0; i < outer.length; i++) dumpBytes(label + '[' + i + ']', outer[i]);
|
||||
return;
|
||||
}
|
||||
log(label + ' ' + cls + ' = ' + obj.toString());
|
||||
} catch (e) {
|
||||
log(label + ' err=' + e);
|
||||
}
|
||||
}
|
||||
|
||||
function shouldLogRegisterText(s) {
|
||||
if (!s) return false;
|
||||
const low = s.toLowerCase();
|
||||
return low.indexOf('register') >= 0 || low.indexOf('rdverifyinfo') >= 0
|
||||
|| low.indexOf('datakey') >= 0 || low.indexOf('fingerprint') >= 0
|
||||
|| low.indexOf('3100012') >= 0 || s.indexOf('|') >= 0;
|
||||
}
|
||||
|
||||
/* ---------- native: dlopen + RegisterNatives ---------- */
|
||||
|
||||
function moduleExport(moduleName, symbol) {
|
||||
if (typeof Module.getExportByName === 'function') {
|
||||
try {
|
||||
return Module.getExportByName(moduleName, symbol);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (typeof Module.findExportByName === 'function') {
|
||||
return Module.findExportByName(moduleName, symbol);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function moduleByName(name) {
|
||||
if (typeof Process.getModuleByName === 'function') {
|
||||
try {
|
||||
return Process.getModuleByName(name);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (typeof Process.findModuleByName === 'function') {
|
||||
return Process.findModuleByName(name);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function moduleByAddress(addr) {
|
||||
if (typeof Process.getModuleByAddress === 'function') {
|
||||
try {
|
||||
return Process.getModuleByAddress(addr);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (typeof Process.findModuleByAddress === 'function') {
|
||||
return Process.findModuleByAddress(addr);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function hookDlopen() {
|
||||
const names = ['android_dlopen_ext', '__loader_android_dlopen_ext', 'dlopen'];
|
||||
names.forEach(function (sym) {
|
||||
const addr = moduleExport(null, sym);
|
||||
if (!addr) return;
|
||||
Interceptor.attach(addr, {
|
||||
onEnter(args) {
|
||||
try {
|
||||
this.path = args[0].readCString();
|
||||
} catch (e) {
|
||||
this.path = '';
|
||||
}
|
||||
},
|
||||
onLeave() {
|
||||
if (!this.path) return;
|
||||
SO_WATCH.forEach(function (so) {
|
||||
if (this.path.indexOf(so) >= 0) log('dlopen ' + this.path);
|
||||
}, this);
|
||||
},
|
||||
});
|
||||
log('hooked ' + sym);
|
||||
});
|
||||
}
|
||||
|
||||
function findRegisterNatives() {
|
||||
const art = moduleByName('libart.so');
|
||||
if (!art) return null;
|
||||
let found = null;
|
||||
art.enumerateSymbols().forEach(function (sym) {
|
||||
if (found) return;
|
||||
const n = sym.name;
|
||||
if (n.indexOf('RegisterNatives') >= 0
|
||||
&& n.indexOf('CheckJNI') < 0
|
||||
&& n.indexOf('art') >= 0) {
|
||||
found = sym.address;
|
||||
}
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
function hookNativePtr(className, methodName, sig, fnPtr) {
|
||||
const key = fnPtr.toString();
|
||||
if (HOOKED_NATIVE_PTRS[key]) return;
|
||||
HOOKED_NATIVE_PTRS[key] = true;
|
||||
const mod = moduleByAddress(fnPtr);
|
||||
const modName = mod ? mod.name : '?';
|
||||
const off = mod ? fnPtr.sub(mod.base) : fnPtr;
|
||||
log('RegisterNatives HOOK ' + className + '.' + methodName + sig
|
||||
+ ' @ ' + modName + '+0x' + off.toString(16));
|
||||
|
||||
try {
|
||||
Interceptor.attach(fnPtr, {
|
||||
onEnter(args) {
|
||||
this.mname = methodName;
|
||||
this.msig = sig;
|
||||
this.env = args[0];
|
||||
log('native>> ' + className + '.' + methodName + sig);
|
||||
dumpNativeArgs(methodName, sig, this.env, args);
|
||||
},
|
||||
onLeave(retval) {
|
||||
log('native<< ' + className + '.' + methodName + ' ret=' + retval);
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
log('Interceptor.attach fail ' + methodName + ': ' + e);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveJClassName(jclassPtr) {
|
||||
if (typeof Java === 'undefined' || !Java.available) {
|
||||
return '';
|
||||
}
|
||||
let className = '';
|
||||
const run = (typeof Java.performNow === 'function') ? Java.performNow : Java.perform;
|
||||
try {
|
||||
run(function () {
|
||||
className = Java.cast(jclassPtr, Java.use('java.lang.Class')).getName();
|
||||
});
|
||||
} catch (e) {
|
||||
className = '';
|
||||
}
|
||||
return className;
|
||||
}
|
||||
|
||||
function isInterestingSo(modName) {
|
||||
if (!modName) return false;
|
||||
return modName.indexOf('shpssdk') >= 0
|
||||
|| modName.indexOf('sdkutils') >= 0
|
||||
|| modName.indexOf('bkutils') >= 0;
|
||||
}
|
||||
|
||||
function hookRegisterNatives() {
|
||||
const addr = findRegisterNatives();
|
||||
if (!addr) {
|
||||
log('RegisterNatives symbol not found');
|
||||
return;
|
||||
}
|
||||
Interceptor.attach(addr, {
|
||||
onEnter(args) {
|
||||
const count = args[3].toInt32();
|
||||
const methods = args[2];
|
||||
const clazz = args[1];
|
||||
const className = resolveJClassName(clazz) || '<unknown>';
|
||||
const classHit = className.indexOf('shpssdk') >= 0
|
||||
|| className.indexOf('jni.utils') >= 0
|
||||
|| className.indexOf('bke.lib.jni') >= 0;
|
||||
|
||||
const ptrSize = Process.pointerSize;
|
||||
let loggedClass = false;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const base = methods.add(i * ptrSize * 3);
|
||||
const name = base.readPointer().readCString();
|
||||
const sig = base.add(ptrSize).readPointer().readCString();
|
||||
const fnPtr = base.add(ptrSize * 2).readPointer();
|
||||
const mod = moduleByAddress(fnPtr);
|
||||
const modName = mod ? mod.name : '';
|
||||
if (!classHit && !isInterestingSo(modName)) continue;
|
||||
if (!loggedClass) {
|
||||
log('RegisterNatives class=' + className + ' count=' + count);
|
||||
loggedClass = true;
|
||||
}
|
||||
log(' JNI ' + name + sig + ' -> ' + fnPtr + ' (' + modName + ')');
|
||||
hookNativePtr(className, name, sig, fnPtr);
|
||||
}
|
||||
},
|
||||
});
|
||||
log('hooked RegisterNatives @ ' + addr);
|
||||
}
|
||||
|
||||
/* ---------- Java: hook static native + key methods ---------- */
|
||||
|
||||
function hookJavaMethod(className, clazz, methodName, isNative, retName, isStatic) {
|
||||
try {
|
||||
const overloads = clazz[methodName].overloads;
|
||||
overloads.forEach(function (ovl) {
|
||||
ovl.implementation = function () {
|
||||
const args = [].slice.call(arguments);
|
||||
log('Java>> ' + className + '.' + methodName
|
||||
+ (isStatic ? ' static' : '')
|
||||
+ (isNative ? ' native' : '') + ' args=' + args.length);
|
||||
args.forEach(function (a, idx) { dumpJava(' in' + idx, a); });
|
||||
|
||||
const ret = ovl.apply(this, args);
|
||||
|
||||
if (retName === 'void') {
|
||||
log('Java<< ' + methodName + ' void');
|
||||
} else if (retName === '[B') {
|
||||
dumpBytes(' out', ret);
|
||||
} else if (retName === 'java.lang.String') {
|
||||
dumpJava(' out', ret);
|
||||
} else if (retName === '[[B') {
|
||||
dumpJava(' out', ret);
|
||||
} else if (retName === 'boolean' || retName === 'int' || retName === 'long') {
|
||||
log(' out=' + ret);
|
||||
} else {
|
||||
dumpJava(' out', ret);
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
});
|
||||
log('hooked ' + className + '.' + methodName + ' overloads=' + overloads.length
|
||||
+ (isNative ? ' native' : '') + (isStatic ? ' static' : ''));
|
||||
return 1;
|
||||
} catch (e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function hookClassMethods(className, staticOnly, instanceOnly) {
|
||||
let clazz;
|
||||
try {
|
||||
clazz = Java.use(className);
|
||||
} catch (e) {
|
||||
log('skip Java class ' + className + ': ' + e);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Modifier = Java.use('java.lang.reflect.Modifier');
|
||||
const declared = clazz.class.getDeclaredMethods();
|
||||
let hooked = 0;
|
||||
|
||||
for (let i = 0; i < declared.length; i++) {
|
||||
const m = declared[i];
|
||||
const isStatic = Modifier.isStatic(m.getModifiers());
|
||||
if (staticOnly && !isStatic) continue;
|
||||
if (instanceOnly && isStatic) continue;
|
||||
|
||||
const methodName = m.getName();
|
||||
const isNative = Modifier.isNative(m.getModifiers());
|
||||
const retName = m.getReturnType().getName();
|
||||
hooked += hookJavaMethod(className, clazz, methodName, isNative, retName, isStatic);
|
||||
}
|
||||
return hooked;
|
||||
}
|
||||
|
||||
function hookShpsSdkFacade() {
|
||||
try {
|
||||
const SHPSSDK = Java.use('com.shopee.shpssdkbank.SHPSSDK');
|
||||
['getRiskToken', 'getRiskSync', 'requestDefense', 'assessRisk'].forEach(function (name) {
|
||||
if (!SHPSSDK[name]) return;
|
||||
SHPSSDK[name].overloads.forEach(function (ovl) {
|
||||
ovl.implementation = function () {
|
||||
const args = [].slice.call(arguments);
|
||||
log('Java>> SHPSSDK.' + name);
|
||||
args.forEach(function (a, idx) { dumpJava(' in' + idx, a); });
|
||||
const ret = ovl.apply(this, args);
|
||||
dumpJava(' out', ret);
|
||||
return ret;
|
||||
};
|
||||
});
|
||||
log('hooked SHPSSDK.' + name);
|
||||
});
|
||||
} catch (e) {
|
||||
log('SHPSSDK facade skip: ' + e);
|
||||
}
|
||||
}
|
||||
|
||||
function hookOkHttp() {
|
||||
try {
|
||||
const RealCall = Java.use('okhttp3.RealCall');
|
||||
RealCall.execute.implementation = function () {
|
||||
const req = this.request();
|
||||
const url = req.url().toString();
|
||||
if (url.indexOf('register') >= 0 || url.indexOf('dfp') >= 0 || url.indexOf('uapi') >= 0) {
|
||||
log('HTTP>> ' + req.method() + ' ' + url);
|
||||
}
|
||||
const resp = this.execute.call(this);
|
||||
if (url.indexOf('register') >= 0) {
|
||||
try {
|
||||
const peek = resp.peekBody(Java.use('java.lang.Long').parseLong('1048576'));
|
||||
log('HTTP<< register ' + peek.string());
|
||||
} catch (e) {
|
||||
log('HTTP<< register peek err=' + e);
|
||||
}
|
||||
}
|
||||
return resp;
|
||||
};
|
||||
log('hooked OkHttp RealCall.execute');
|
||||
} catch (e) {
|
||||
log('OkHttp skip: ' + e);
|
||||
}
|
||||
}
|
||||
|
||||
function hookGsonRegister() {
|
||||
try {
|
||||
const Gson = Java.use('com.google.gson.Gson');
|
||||
Gson.toJson.overload('java.lang.Object').implementation = function (obj) {
|
||||
const ret = this.toJson(obj);
|
||||
if (shouldLogRegisterText(ret)) {
|
||||
const show = ret.length > MAX_STR ? ret.substring(0, MAX_STR) + '...' : ret;
|
||||
log('Gson.toJson REGISTRATION len=' + ret.length + ' ' + show);
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
log('hooked Gson.toJson');
|
||||
} catch (e) {
|
||||
log('Gson skip: ' + e);
|
||||
}
|
||||
}
|
||||
|
||||
function hookRiskTokenEntry() {
|
||||
try {
|
||||
const V = Java.use('com.shopee.shpssdkbank.uwuvuvvww.vvuuuuvvv');
|
||||
hookJavaMethod(
|
||||
'com.shopee.shpssdkbank.uwuvuvvww.vvuuuuvvv',
|
||||
V, 'wwvuwuwvu', false, 'java.lang.String', true);
|
||||
} catch (e) {
|
||||
log('vvuuuuvvv skip: ' + e);
|
||||
}
|
||||
}
|
||||
|
||||
function hookEncryptHelper() {
|
||||
hookClassMethods('com.shopee.bke.lib.jni.utils.uvwwwwuv', false, true);
|
||||
}
|
||||
|
||||
function isAdbSettingKey(key) {
|
||||
if (!key) return false;
|
||||
const lower = key.toLowerCase();
|
||||
return lower.indexOf('adb') >= 0
|
||||
|| lower === 'development_settings_enabled'
|
||||
|| lower.indexOf('wireless_debug') >= 0;
|
||||
}
|
||||
|
||||
function hookAdbBypassJava() {
|
||||
try {
|
||||
const fakeInt = function (key) {
|
||||
if (isAdbSettingKey(key)) {
|
||||
log('fake Settings int ' + key + ' -> 0');
|
||||
return 0;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const fakeStr = function (key) {
|
||||
if (isAdbSettingKey(key)) {
|
||||
log('fake Settings str ' + key + ' -> 0');
|
||||
return '0';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
['Global', 'Secure', 'System'].forEach(function (bucket) {
|
||||
const Cls = Java.use('android.provider.Settings$' + bucket);
|
||||
Cls.getInt.overloads.forEach(function (ovl) {
|
||||
ovl.implementation = function () {
|
||||
const key = arguments[1];
|
||||
const f = fakeInt(String(key));
|
||||
if (f !== null) return f;
|
||||
return ovl.apply(this, arguments);
|
||||
};
|
||||
});
|
||||
if (Cls.getString) {
|
||||
Cls.getString.overloads.forEach(function (ovl) {
|
||||
ovl.implementation = function () {
|
||||
const key = arguments[1];
|
||||
const f = fakeStr(String(key));
|
||||
if (f !== null) return f;
|
||||
return ovl.apply(this, arguments);
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const SysProp = Java.use('android.os.SystemProperties');
|
||||
SysProp.get.overload('java.lang.String').implementation = function (key) {
|
||||
if (key === 'init.svc.adbd' || key === 'init.svc.adb_wifi') {
|
||||
log('fake SystemProperties ' + key + ' -> stopped');
|
||||
return 'stopped';
|
||||
}
|
||||
return this.get(key);
|
||||
};
|
||||
log('hooked ADB Settings/SystemProperties bypass');
|
||||
} catch (e) {
|
||||
log('ADB Java bypass skip: ' + e);
|
||||
}
|
||||
}
|
||||
|
||||
function installJavaHooks() {
|
||||
hookAdbBypassJava();
|
||||
let total = 0;
|
||||
JAVA_TARGETS.forEach(function (cn) {
|
||||
total += hookClassMethods(cn, true, false);
|
||||
});
|
||||
hookRiskTokenEntry();
|
||||
hookEncryptHelper();
|
||||
hookShpsSdkFacade();
|
||||
hookOkHttp();
|
||||
hookGsonRegister();
|
||||
log('Java hooks installed methods=' + total + ' pid=' + Process.id);
|
||||
log('READY SG — Sign up -> +65 -> Next (watch native>> / Gson / HTTP)');
|
||||
}
|
||||
|
||||
function waitForJava(n) {
|
||||
n = n || 0;
|
||||
if (typeof Java === 'undefined' || !Java.available) {
|
||||
if (n % 10 === 0) log('waiting Java.available attempt=' + n);
|
||||
setTimeout(function () { waitForJava(n + 1); }, 500);
|
||||
return;
|
||||
}
|
||||
Java.perform(function () {
|
||||
installJavaHooks();
|
||||
});
|
||||
}
|
||||
|
||||
setImmediate(function () {
|
||||
log('SG native trace loaded pid=' + Process.id);
|
||||
hookDlopen();
|
||||
hookRegisterNatives();
|
||||
waitForJava(0);
|
||||
});
|
||||
74
reverse/frida/trace_tng_diag_exit.js
Normal file
74
reverse/frida/trace_tng_diag_exit.js
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Diagnostic: only LOG exit-related calls, do not block.
|
||||
*/
|
||||
"use strict";
|
||||
function log(msg) { send("[TNG-diag] " + msg); }
|
||||
function findExport(mod, name) {
|
||||
try {
|
||||
var m = Process.findModuleByName(mod);
|
||||
if (m) { var a = m.findExportByName(name); if (a) return a; }
|
||||
} catch (e) {}
|
||||
try { return Module.getGlobalExportByName(name); } catch (e2) { return null; }
|
||||
}
|
||||
function bt(ctx) {
|
||||
try {
|
||||
return Thread.backtrace(ctx, Backtracer.FUZZY).map(DebugSymbol.fromAddress).slice(0, 8).join(" <- ");
|
||||
} catch (e) { return "?"; }
|
||||
}
|
||||
|
||||
["_exit", "exit", "abort", "quick_exit"].forEach(function (n) {
|
||||
var a = findExport("libc.so", n);
|
||||
if (!a) return;
|
||||
Interceptor.attach(a, {
|
||||
onEnter: function (args) {
|
||||
log("CALL " + n + "(" + args[0] + ") " + bt(this.context));
|
||||
}
|
||||
});
|
||||
log("watch " + n + " @ " + a);
|
||||
});
|
||||
|
||||
["kill", "tgkill", "raise"].forEach(function (n) {
|
||||
var a = findExport("libc.so", n);
|
||||
if (!a) return;
|
||||
Interceptor.attach(a, {
|
||||
onEnter: function (args) {
|
||||
log("CALL " + n + "(" + args[0] + "," + args[1] + ") " + bt(this.context));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var sys = findExport("libc.so", "syscall");
|
||||
if (sys) {
|
||||
Interceptor.attach(sys, {
|
||||
onEnter: function (args) {
|
||||
var nr = args[0].toInt32();
|
||||
if (nr === 93 || nr === 94 || nr === 129 || nr === 131) {
|
||||
log("CALL syscall(" + nr + ") " + bt(this.context));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// count mprotect EXEC
|
||||
var mp = findExport("libc.so", "mprotect");
|
||||
if (mp) {
|
||||
Interceptor.attach(mp, {
|
||||
onEnter: function (args) {
|
||||
if (args[2].toInt32() & 4) {
|
||||
log("mprotect EXEC " + args[0] + " len=" + args[1]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
log("diag ready pid=" + Process.id);
|
||||
setTimeout(function () {
|
||||
var n = 0;
|
||||
Process.enumerateRanges("r-x").forEach(function (r) {
|
||||
var file = r.file ? r.file.path : "anon";
|
||||
if (file.indexOf("/system") === 0 || file.indexOf("/apex") === 0) return;
|
||||
n++;
|
||||
log("RX " + file + " " + r.base + " +" + r.size);
|
||||
});
|
||||
log("app RX ranges=" + n);
|
||||
}, 800);
|
||||
282
reverse/frida/trace_tng_native_exit.js
Normal file
282
reverse/frida/trace_tng_native_exit.js
Normal file
@@ -0,0 +1,282 @@
|
||||
/**
|
||||
* TNG — catch Promon exit after runtime code decrypt (mmap/mprotect RX).
|
||||
* Frida 17 compatible.
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
function log(msg) {
|
||||
send("[TNG-native] " + msg);
|
||||
}
|
||||
|
||||
function findExport(moduleName, name) {
|
||||
try {
|
||||
if (moduleName) {
|
||||
var m = Process.findModuleByName(moduleName);
|
||||
if (m) {
|
||||
var a = m.findExportByName(name);
|
||||
if (a) return a;
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
try {
|
||||
return Module.getGlobalExportByName(name);
|
||||
} catch (e2) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
var patched = {};
|
||||
|
||||
function looksLikeExitSetup(addr) {
|
||||
for (var i = 1; i <= 12; i++) {
|
||||
try {
|
||||
var w = addr.sub(i * 4).readU32();
|
||||
var opc = w & 0xff800000;
|
||||
if (opc === 0x52800000 || opc === 0xd2800000) {
|
||||
var rd = w & 0x1f;
|
||||
var imm = (w >> 5) & 0xffff;
|
||||
if (rd === 8 && (imm === 93 || imm === 94)) return imm;
|
||||
}
|
||||
// mov x8, xN then earlier load — also catch svc after mov x0, #imm (exit code)
|
||||
if ((w & 0xffe0ffff) === 0xaa0003e8) return 8; // mov x8, x0.. pattern loose
|
||||
} catch (e) {}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function patchRegion(base, size, tag) {
|
||||
if (size <= 0 || size > 64 * 1024 * 1024) return;
|
||||
var key = base + ":" + size;
|
||||
if (patched[key]) return;
|
||||
patched[key] = true;
|
||||
var nop = [0x1f, 0x20, 0x03, 0xd5];
|
||||
var n = 0;
|
||||
var totalSvc = 0;
|
||||
try {
|
||||
// only scan 4-byte aligned by walking manually for reliability
|
||||
var end = base.add(size - 4);
|
||||
for (var p = base; p.compare(end) <= 0; p = p.add(4)) {
|
||||
var w;
|
||||
try {
|
||||
w = p.readU32();
|
||||
} catch (e) {
|
||||
break;
|
||||
}
|
||||
if (w !== 0xd4000001) continue; // svc #0
|
||||
totalSvc++;
|
||||
var kind = looksLikeExitSetup(p);
|
||||
if (!kind) continue;
|
||||
try {
|
||||
Memory.protect(p, 4, "rwx");
|
||||
p.writeByteArray(nop);
|
||||
n++;
|
||||
log("patched exit SVC#" + kind + " @ " + p + " [" + tag + "]");
|
||||
} catch (e2) {
|
||||
log("patch err " + p + ": " + e2);
|
||||
}
|
||||
}
|
||||
if (totalSvc > 0) {
|
||||
log("region " + tag + " svc#0=" + totalSvc + " patched=" + n + " size=" + size);
|
||||
}
|
||||
} catch (e) {
|
||||
log("scan err " + tag + ": " + e);
|
||||
}
|
||||
}
|
||||
|
||||
function scanAllExecutable(tag) {
|
||||
Process.enumerateRanges("r-x").forEach(function (r) {
|
||||
var file = r.file ? r.file.path : "anon";
|
||||
// skip system libs except if anonymous / app
|
||||
if (file.indexOf("/system/") === 0 || file.indexOf("/apex/") === 0) return;
|
||||
if (file.indexOf("frida") >= 0) return;
|
||||
patchRegion(r.base, r.size, tag + ":" + file);
|
||||
});
|
||||
}
|
||||
|
||||
function installLibcExitHooks() {
|
||||
function blockExit(name, address) {
|
||||
try {
|
||||
Interceptor.replace(
|
||||
address,
|
||||
new NativeCallback(
|
||||
function (code) {
|
||||
log("BLOCKED " + name + "(" + (code | 0) + ")");
|
||||
},
|
||||
"void",
|
||||
["int"]
|
||||
)
|
||||
);
|
||||
log("replaced " + name);
|
||||
} catch (e) {
|
||||
Interceptor.attach(address, {
|
||||
onEnter: function (args) {
|
||||
log("BLOCKED(attach) " + name + "(" + args[0].toInt32() + ")");
|
||||
while (true) Thread.sleep(60);
|
||||
},
|
||||
});
|
||||
log("attached " + name);
|
||||
}
|
||||
}
|
||||
|
||||
["_exit", "exit", "abort", "quick_exit"].forEach(function (n) {
|
||||
var a = findExport("libc.so", n);
|
||||
if (a) blockExit(n, a);
|
||||
});
|
||||
|
||||
["kill", "tgkill", "pthread_kill", "raise"].forEach(function (n) {
|
||||
var a = findExport("libc.so", n);
|
||||
if (!a) return;
|
||||
Interceptor.attach(a, {
|
||||
onEnter: function (args) {
|
||||
var pid = args[0].toInt32();
|
||||
var sig = args[1].toInt32();
|
||||
if ((pid === Process.id || pid === 0 || pid === -1) &&
|
||||
(sig === 9 || sig === 15 || sig === 6 || sig === 5)) {
|
||||
log("BLOCKED " + n + " sig=" + sig);
|
||||
args[1] = ptr(0);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
var sys = findExport("libc.so", "syscall");
|
||||
if (sys) {
|
||||
Interceptor.attach(sys, {
|
||||
onEnter: function (args) {
|
||||
var nr = args[0].toInt32();
|
||||
if (nr === 93 || nr === 94) {
|
||||
log("BLOCKED syscall exit " + nr);
|
||||
args[0] = ptr(-1);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
log("libc hooks OK");
|
||||
}
|
||||
|
||||
function installMprotectWatcher() {
|
||||
var mprotect = findExport("libc.so", "mprotect");
|
||||
var mmap = findExport("libc.so", "mmap");
|
||||
if (mprotect) {
|
||||
Interceptor.attach(mprotect, {
|
||||
onEnter: function (args) {
|
||||
this.addr = args[0];
|
||||
this.len = args[1].toInt32();
|
||||
this.prot = args[2].toInt32();
|
||||
},
|
||||
onLeave: function () {
|
||||
// PROT_EXEC = 4
|
||||
if (this.prot & 4) {
|
||||
log("mprotect+EXEC " + this.addr + " len=" + this.len);
|
||||
patchRegion(this.addr, this.len, "mprotect");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
if (mmap) {
|
||||
Interceptor.attach(mmap, {
|
||||
onEnter: function (args) {
|
||||
this.len = args[1].toInt32();
|
||||
this.prot = args[2].toInt32();
|
||||
},
|
||||
onLeave: function (retval) {
|
||||
if ((this.prot & 4) && !retval.isNull()) {
|
||||
log("mmap+EXEC " + retval + " len=" + this.len);
|
||||
patchRegion(retval, this.len, "mmap");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
log("mprotect/mmap watchers OK");
|
||||
}
|
||||
|
||||
function installMapsHide() {
|
||||
var markers = ["frida", "gadget", "xposed", "lsposed", "vector", "zygisk", "magisk", "liblspd"];
|
||||
var tracked = {};
|
||||
function hide(line) {
|
||||
var l = (line || "").toLowerCase();
|
||||
for (var i = 0; i < markers.length; i++) if (l.indexOf(markers[i]) >= 0) return true;
|
||||
return false;
|
||||
}
|
||||
function filter(buf, len) {
|
||||
try {
|
||||
var text = buf.readUtf8String(len);
|
||||
if (!text) return len;
|
||||
var out = text.split("\n").filter(function (x) { return !hide(x); }).join("\n");
|
||||
var bytes = Memory.allocUtf8String(out);
|
||||
var n = Math.min(len, out.length);
|
||||
Memory.copy(buf, bytes, n);
|
||||
return n;
|
||||
} catch (e) {
|
||||
return len;
|
||||
}
|
||||
}
|
||||
var openat = findExport("libc.so", "openat");
|
||||
var readFn = findExport("libc.so", "read");
|
||||
if (openat) {
|
||||
Interceptor.attach(openat, {
|
||||
onEnter: function (args) {
|
||||
this.path = args[1].isNull() ? null : args[1].readCString();
|
||||
},
|
||||
onLeave: function (retval) {
|
||||
var fd = retval.toInt32();
|
||||
if (fd >= 0 && this.path && this.path.indexOf("maps") >= 0) tracked[fd] = 1;
|
||||
},
|
||||
});
|
||||
}
|
||||
if (readFn) {
|
||||
Interceptor.attach(readFn, {
|
||||
onEnter: function (args) {
|
||||
this.fd = args[0].toInt32();
|
||||
this.buf = args[1];
|
||||
},
|
||||
onLeave: function (retval) {
|
||||
var n = retval.toInt32();
|
||||
if (n > 0 && tracked[this.fd]) retval.replace(ptr(filter(this.buf, n)));
|
||||
},
|
||||
});
|
||||
}
|
||||
log("maps hide OK");
|
||||
}
|
||||
|
||||
function installJavaGuards() {
|
||||
if (typeof Java === "undefined") {
|
||||
setTimeout(installJavaGuards, 500);
|
||||
return;
|
||||
}
|
||||
Java.perform(function () {
|
||||
try {
|
||||
Java.use("java.lang.System").exit.implementation = function (c) {
|
||||
log("Java System.exit(" + c + ") blocked");
|
||||
};
|
||||
} catch (e) {}
|
||||
try {
|
||||
var R = Java.use("java.lang.Runtime");
|
||||
R.exit.overload("int").implementation = function (c) {
|
||||
log("Java Runtime.exit(" + c + ") blocked");
|
||||
};
|
||||
} catch (e) {}
|
||||
try {
|
||||
var P = Java.use("android.os.Process");
|
||||
P.killProcess.implementation = function (pid) {
|
||||
if (pid === P.myPid()) {
|
||||
log("Java killProcess(self) blocked");
|
||||
return;
|
||||
}
|
||||
return this.killProcess(pid);
|
||||
};
|
||||
} catch (e) {}
|
||||
log("Java guards OK");
|
||||
});
|
||||
}
|
||||
|
||||
log("load pid=" + Process.id);
|
||||
installLibcExitHooks();
|
||||
installMapsHide();
|
||||
installMprotectWatcher();
|
||||
scanAllExecutable("boot");
|
||||
setInterval(function () {
|
||||
scanAllExecutable("tick");
|
||||
}, 1000);
|
||||
installJavaGuards();
|
||||
log("ready");
|
||||
147
reverse/frida/trace_tng_tiger_fread.js
Normal file
147
reverse/frida/trace_tng_tiger_fread.js
Normal file
@@ -0,0 +1,147 @@
|
||||
"use strict";
|
||||
/*
|
||||
* TNG eWallet — 定位 TigerTally(libtiger_tally.so) 启动期 fread 阻塞。观察不改行为。
|
||||
*
|
||||
* 背景(ANR 栈): CaptchaInitializer → TigerTallyAPI.init → t.B.genericNt1(native)
|
||||
* → libtiger_tally.so (mNYjyzyN23) → fread → __sread → read 永远读不到数据
|
||||
*/
|
||||
const TIGER_SO = "libtiger_tally.so";
|
||||
|
||||
const LIBC = Process.getModuleByName("libc.so");
|
||||
const readlink = new NativeFunction(
|
||||
LIBC.findExportByName("readlink"), "long", ["pointer", "pointer", "ulong"]);
|
||||
|
||||
/* ---- Tiger 模块范围(热路径缓存,每 2s 刷新一次) ---- */
|
||||
let tigerMod = null;
|
||||
function refreshTiger() {
|
||||
const m = Process.findModuleByName(TIGER_SO);
|
||||
if (m) tigerMod = m;
|
||||
return !!tigerMod;
|
||||
}
|
||||
function inTiger(addr) {
|
||||
if (!addr) return false;
|
||||
if (!tigerMod) return false;
|
||||
return addr.compare(tigerMod.base) >= 0 && addr.compare(tigerMod.base.add(tigerMod.size)) < 0;
|
||||
}
|
||||
setInterval(() => { refreshTiger(); }, 2000);
|
||||
|
||||
function resolveFd(fd) {
|
||||
try {
|
||||
const link = Memory.allocUtf8String(`/proc/self/fd/${fd}`);
|
||||
const out = Memory.alloc(256);
|
||||
const n = readlink(link, out, 256);
|
||||
if (n > 0) return out.readUtf8String(Math.min(n, 255));
|
||||
} catch (e) { /* ignore */ }
|
||||
return "?";
|
||||
}
|
||||
function fdKind(fd) {
|
||||
const p = resolveFd(fd);
|
||||
if (p.indexOf("socket:") === 0) return "SOCKET " + p;
|
||||
if (p.indexOf("pipe:") === 0) return "PIPE " + p;
|
||||
if (p.indexOf("anon_inode:") === 0) return "ANON " + p;
|
||||
return p;
|
||||
}
|
||||
function threadName() {
|
||||
try { return Process.getCurrentThreadName(); } catch (e) { return "?"; }
|
||||
}
|
||||
function fmtAddr(a) { return a ? a.toString(16) : "?"; }
|
||||
|
||||
const stats = {}; // tid -> info
|
||||
function bump(fd, kind, ret) {
|
||||
const tid = Process.getCurrentThreadId();
|
||||
let s = stats[tid];
|
||||
if (!s) { s = { name: threadName(), reads: 0, lastFd: fd, lastFdKind: kind, lastRet: ret }; stats[tid] = s; }
|
||||
s.name = threadName();
|
||||
s.reads++;
|
||||
s.lastFd = fd;
|
||||
s.lastFdKind = kind;
|
||||
s.lastRet = ret;
|
||||
}
|
||||
|
||||
/* ---- fread: FILE* 第4参数; bionic __sFILE._file 偏移约 18 ---- */
|
||||
Interceptor.attach(LIBC.findExportByName("fread"), {
|
||||
onEnter(args) {
|
||||
const caller = this.returnAddress;
|
||||
if (!inTiger(caller)) return;
|
||||
const fp = args[3];
|
||||
let fd = -1;
|
||||
for (const off of [18, 16, 24, 20]) {
|
||||
try { const v = fp.add(off).readU16(); if (v > 0 && v < 4096) { fd = v; break; } }
|
||||
catch (e) { /* try next */ }
|
||||
}
|
||||
const kind = fd > 0 ? fdKind(fd) : "?";
|
||||
bump(fd, kind, "pending");
|
||||
console.log(`[FREAD] tid=${Process.getCurrentThreadId()} "${threadName()}" caller=${fmtAddr(caller)} fd=${fd} kind=${kind}`);
|
||||
},
|
||||
onLeave(ret) {
|
||||
if (!inTiger(this.returnAddress)) return;
|
||||
bump(-1, "", ret.toInt32());
|
||||
console.log(`[FREAD-LEAVE] tid=${Process.getCurrentThreadId()} ret=${ret.toInt32()}`);
|
||||
}
|
||||
});
|
||||
|
||||
/* ---- read: 只观察调用者位于 libtiger_tally.so 的 ---- */
|
||||
Interceptor.attach(LIBC.findExportByName("read"), {
|
||||
onEnter(args) {
|
||||
const caller = this.returnAddress;
|
||||
if (!inTiger(caller)) return;
|
||||
const fd = args[0].toInt32();
|
||||
const kind = fdKind(fd);
|
||||
bump(fd, kind, "?");
|
||||
console.log(`[READ] tid=${Process.getCurrentThreadId()} "${threadName()}" caller=${fmtAddr(caller)} fd=${fd} kind=${kind}`);
|
||||
},
|
||||
onLeave(ret) {
|
||||
if (!inTiger(this.returnAddress)) return;
|
||||
const tid = Process.getCurrentThreadId();
|
||||
const s = stats[tid];
|
||||
const fd = s ? s.lastFd : -1;
|
||||
const r = ret.toInt32();
|
||||
if (s) s.lastRet = r;
|
||||
if (r < 0) console.log(`[READ-RET] tid=${tid} fd=${fd} ret=${r} (blocked/error)`);
|
||||
else if (r === 0) console.log(`[READ-EOF] tid=${tid} fd=${fd} ret=0 (EOF/closed)`);
|
||||
else console.log(`[READ-RET] tid=${tid} fd=${fd} ret=${r}`);
|
||||
}
|
||||
});
|
||||
|
||||
/* ---- 周期 dump ---- */
|
||||
function dumpThreads() {
|
||||
try {
|
||||
const dir = new File("/proc/self/task", "r");
|
||||
const entries = dir.list();
|
||||
dir.close();
|
||||
let relevant = [];
|
||||
for (const e of entries) {
|
||||
let nm = "?";
|
||||
try { const nf = new File(`/proc/self/task/${e}/comm`, "r"); nm = nf.readString().trim(); nf.close(); } catch (err) {}
|
||||
const s = stats[e] || null;
|
||||
const lower = nm.toLowerCase();
|
||||
if (lower.indexOf("location") >= 0 || lower.indexOf("tally") >= 0 || s) {
|
||||
let info = `tid=${e} "${nm}"`;
|
||||
if (s) info += ` tigerReads=${s.reads} lastFd=${s.lastFd} kind=${s.lastFdKind} lastRet=${s.lastRet}`;
|
||||
relevant.push(info);
|
||||
}
|
||||
}
|
||||
console.log(`[DUMP] tiger-fread threads: ${relevant.length ? relevant.join(" | ") : "(none)"}`);
|
||||
} catch (e) {
|
||||
console.log(`[DUMP] failed: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
dumpThreads();
|
||||
try {
|
||||
const dir = new File("/proc/self/fd", "r");
|
||||
const fds = dir.list();
|
||||
dir.close();
|
||||
let pipes = [], socks = [];
|
||||
for (const f of fds) {
|
||||
const kind = fdKind(parseInt(f, 10));
|
||||
if (kind.indexOf("PIPE") === 0) pipes.push(f + ":" + kind.split(" ").slice(1).join(" "));
|
||||
if (kind.indexOf("SOCKET") === 0) socks.push(f + ":" + kind.split(" ").slice(1).join(" "));
|
||||
}
|
||||
if (pipes.length) console.log(`[DUMP-FD] pipes: ${pipes.join(" | ")}`);
|
||||
if (socks.length) console.log(`[DUMP-FD] sockets: ${socks.join(" | ")}`);
|
||||
} catch (e) { /* ignore */ }
|
||||
}, 3000);
|
||||
|
||||
console.log("[TIGER-FREAD] armed (observe-only)");
|
||||
Reference in New Issue
Block a user