fix(tng): 修复区号页 HWUI 闪退并放行 Compose HW 绘制
拦截 HardwareRenderer.setName,校验/缓存 libandroid.so,避免软件绘制撞 hardware bitmap;附带 Money Packet hook 与 mitm 脚本。
This commit is contained in:
137
debug-server/tng_mmp_mitm_addon.py
Normal file
137
debug-server/tng_mmp_mitm_addon.py
Normal file
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
mitmproxy 插件:自动保存 TNG Money Packet 含 receiverList 的 API 响应。
|
||||
|
||||
用法:
|
||||
mitmdump -s debug-server/tng_mmp_mitm_addon.py -p 8888
|
||||
或
|
||||
mitmweb -s debug-server/tng_mmp_mitm_addon.py -p 8888
|
||||
|
||||
手机 WiFi 代理 -> PC_IP:8888,安装 mitmproxy CA 后打开 TNG 红包 Leaderboard。
|
||||
命中响应会打印到终端,并写入 reverse/dumps/mitm_mmp/
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
from mitmproxy import ctx, http
|
||||
|
||||
OUTPUT_DIR = os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||
"reverse",
|
||||
"dumps",
|
||||
"mitm_mmp",
|
||||
)
|
||||
|
||||
MMP_HINTS = (
|
||||
"receiverlist",
|
||||
"claimedamount",
|
||||
"mmpreceiver",
|
||||
"moneypacket",
|
||||
"merchantmoneypacket",
|
||||
)
|
||||
|
||||
HOST_HINTS = (
|
||||
"ebuckler.com",
|
||||
"tngdigital.com",
|
||||
"alipaydev.com",
|
||||
)
|
||||
|
||||
|
||||
def _ensure_dir() -> None:
|
||||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||
|
||||
|
||||
def _looks_like_mmp(body: str) -> bool:
|
||||
lower = body.lower()
|
||||
if any(h in lower for h in MMP_HINTS):
|
||||
return True
|
||||
return "mmp" in lower and "amount" in lower
|
||||
|
||||
|
||||
def _extract_receiver_list(obj):
|
||||
"""递归找 receiverList 并格式化为 [(nickname, amount), ...]"""
|
||||
rows = []
|
||||
|
||||
def walk(node):
|
||||
if isinstance(node, dict):
|
||||
if "receiverList" in node and isinstance(node["receiverList"], list):
|
||||
for item in node["receiverList"]:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
name = (
|
||||
item.get("nickName")
|
||||
or item.get("displayName")
|
||||
or item.get("userName")
|
||||
or item.get("receiverName")
|
||||
or item.get("name")
|
||||
)
|
||||
amount = (
|
||||
item.get("claimedAmount")
|
||||
or item.get("receiveAmount")
|
||||
or item.get("amount")
|
||||
)
|
||||
if name and amount is not None:
|
||||
rows.append((str(name), str(amount)))
|
||||
for v in node.values():
|
||||
walk(v)
|
||||
elif isinstance(node, list):
|
||||
for v in node:
|
||||
walk(v)
|
||||
|
||||
walk(obj)
|
||||
return rows
|
||||
|
||||
|
||||
class TngMmpCapture:
|
||||
def __init__(self) -> None:
|
||||
_ensure_dir()
|
||||
self.count = 0
|
||||
ctx.log.info(f"TNG MMP capture -> {OUTPUT_DIR}")
|
||||
|
||||
def response(self, flow: http.HTTPFlow) -> None:
|
||||
if flow.response is None or not flow.response.content:
|
||||
return
|
||||
host = (flow.request.host or "").lower()
|
||||
if not any(h in host for h in HOST_HINTS):
|
||||
return
|
||||
try:
|
||||
text = flow.response.get_text(strict=False)
|
||||
except Exception:
|
||||
return
|
||||
if not text or not _looks_like_mmp(text):
|
||||
return
|
||||
|
||||
self.count += 1
|
||||
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
safe_host = re.sub(r"[^\w.-]", "_", host)[:40]
|
||||
path = os.path.join(OUTPUT_DIR, f"mmp_{ts}_{self.count}_{safe_host}.json")
|
||||
|
||||
summary_lines = [
|
||||
f"[TNG-MMP #{self.count}] {flow.request.method} {flow.request.url}",
|
||||
]
|
||||
try:
|
||||
data = json.loads(text)
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
rows = _extract_receiver_list(data)
|
||||
if rows:
|
||||
summary_lines.append(f" receiverList ({len(rows)} 条):")
|
||||
for name, amount in rows:
|
||||
summary_lines.append(f" {name} -> {amount}")
|
||||
else:
|
||||
summary_lines.append(" (JSON 已保存,未解析到 receiverList)")
|
||||
except json.JSONDecodeError:
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
summary_lines.append(" (非 JSON,已保存原文)")
|
||||
|
||||
summary_lines.append(f" saved: {path}")
|
||||
ctx.log.info("\n".join(summary_lines))
|
||||
|
||||
|
||||
addons = [TngMmpCapture()]
|
||||
@@ -6,6 +6,7 @@
|
||||
* pc==lr 循环 SEGV 也 skip;libc++abi __cxa_guard_acquire → SIGABRT 吞掉。
|
||||
*/
|
||||
#include <android/log.h>
|
||||
#include <dlfcn.h>
|
||||
#include <errno.h>
|
||||
#include <linux/audit.h>
|
||||
#include <linux/filter.h>
|
||||
@@ -174,11 +175,20 @@ static void fatal_skip_handler(int sig, siginfo_t *info, void *ctx) {
|
||||
freeze_forever();
|
||||
}
|
||||
int n = ++g_abrt_swallow;
|
||||
if (n <= 5 || n % 50 == 0) {
|
||||
LOGI("ABRT pc+4 tid=%d pc=%lx lr=%lx streak=%d", (int)tid,
|
||||
(unsigned long)pc, (unsigned long)lr, streak);
|
||||
/* 主线程:abort 后 _exit 被 seccomp 拦 → abort 内部死循环(还复位 handler)→
|
||||
* 主线程永久卡死 → 黑屏/ANR。主线程 ABRT 时跳回 lr(Looper pollOnce)恢复;
|
||||
* 仅当 lr 距 pc 远(不在 abort 内部)才跳,否则仍在 abort epilogue 内跳 LR 会再 abort。 */
|
||||
uintptr_t target = pc + 4;
|
||||
if (tid == g_main_tid.load() && lr != 0 && pc != 0
|
||||
&& (lr < pc - 0x1000 || lr > pc + 0x1000)) {
|
||||
target = lr;
|
||||
}
|
||||
uc->uc_mcontext.pc = pc + 4;
|
||||
if (n <= 5 || n % 50 == 0) {
|
||||
LOGI("ABRT tid=%d pc=%lx lr=%lx streak=%d -> %lx", (int)tid,
|
||||
(unsigned long)pc, (unsigned long)lr, streak,
|
||||
(unsigned long)target);
|
||||
}
|
||||
uc->uc_mcontext.pc = target;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -236,14 +246,23 @@ static void install_soft_signals() {
|
||||
((struct sock_filter){(unsigned short)(code), (jt), (jf), (unsigned int)(k)})
|
||||
|
||||
static int install_seccomp_exit_group_only() {
|
||||
/* exit_group 拦 + 精准拦 Promon 自杀 kill(SIGABRT)(SI_USER)。
|
||||
* 只拦 kill()(nr=129):Promon 自杀是 kill(pid,SIGABRT) → si_code=SI_USER。
|
||||
* 不拦 tgkill/tkill:ART 的 abort() 用 tgkill(self) → si_code=SI_TKILL,
|
||||
* 拦它会让 libart 状态错乱 → 进程崩(之前验证)。 */
|
||||
struct sock_filter filter[] = {
|
||||
// 0: arch
|
||||
_BPFI(BPF_LD | BPF_W | BPF_ABS, 0, 0, offsetof(struct seccomp_data, arch)),
|
||||
_BPFI(BPF_JMP | BPF_JEQ | BPF_K, 1, 0, AUDIT_ARCH_AARCH64),
|
||||
_BPFI(BPF_RET | BPF_K, 0, 0, SC_RET_ALLOW),
|
||||
_BPFI(BPF_RET | BPF_K, 0, 0, SC_RET_ALLOW), // 2: not aarch64
|
||||
_BPFI(BPF_LD | BPF_W | BPF_ABS, 0, 0, offsetof(struct seccomp_data, nr)),
|
||||
_BPFI(BPF_JMP | BPF_JEQ | BPF_K, 0, 1, 94),
|
||||
_BPFI(BPF_RET | BPF_K, 0, 0, SC_RET_ERRNO_EPERM),
|
||||
_BPFI(BPF_RET | BPF_K, 0, 0, SC_RET_ALLOW),
|
||||
_BPFI(BPF_JMP | BPF_JEQ | BPF_K, 0, 1, __NR_exit_group), // 4: exit_group? true→5
|
||||
_BPFI(BPF_RET | BPF_K, 0, 0, SC_RET_ERRNO_EPERM), // 5: EPERM exit_group
|
||||
_BPFI(BPF_JMP | BPF_JEQ | BPF_K, 0, 3, __NR_kill), // 6: kill? true→7
|
||||
_BPFI(BPF_LD | BPF_W | BPF_ABS, 0, 0, offsetof(struct seccomp_data, args) + 8), // 7: args[1]=sig
|
||||
_BPFI(BPF_JMP | BPF_JEQ | BPF_K, 0, 1, SIGABRT), // 8: sig==SIGABRT? true→9
|
||||
_BPFI(BPF_RET | BPF_K, 0, 0, SC_RET_ERRNO_EPERM), // 9: EPERM kill ABRT
|
||||
_BPFI(BPF_RET | BPF_K, 0, 0, SC_RET_ALLOW), // 10: allow
|
||||
};
|
||||
struct sock_fprog prog = {
|
||||
.len = (unsigned short)(sizeof(filter) / sizeof(filter[0])),
|
||||
@@ -258,9 +277,9 @@ static int install_seccomp_exit_group_only() {
|
||||
LOGE("seccomp failed errno=%d", errno);
|
||||
return -1;
|
||||
}
|
||||
LOGI("seccomp exit_group via prctl");
|
||||
LOGI("seccomp exit_group+kill-ABRT via prctl");
|
||||
} else {
|
||||
LOGI("seccomp exit_group via TSYNC");
|
||||
LOGI("seccomp exit_group+kill-ABRT via TSYNC");
|
||||
}
|
||||
g_seccomp_ok.store(1);
|
||||
return 0;
|
||||
@@ -277,10 +296,31 @@ using pthread_kill_fn = int (*)(pthread_t, int);
|
||||
using cxa_guard_acquire_fn = int (*)(void *);
|
||||
using cxa_guard_abort_fn = void (*)();
|
||||
using dlopen_fn = void *(*)(const char *, int);
|
||||
using android_dlopen_ext_fn = void *(*)(const char *, int, const void *);
|
||||
using sphal_load_fn = void *(*)(const char *, int);
|
||||
using open_passthrough_hal_fn = void *(*)(const char *, const char *, int);
|
||||
|
||||
static cxa_guard_acquire_fn orig_cxa_guard_acquire = nullptr;
|
||||
static cxa_guard_abort_fn orig_cxa_guard_abort = nullptr;
|
||||
static dlopen_fn orig_dlopen = nullptr;
|
||||
static android_dlopen_ext_fn orig_android_dlopen_ext = nullptr;
|
||||
static sphal_load_fn orig_sphal_load = nullptr;
|
||||
static open_passthrough_hal_fn orig_open_passthrough_hal = nullptr;
|
||||
static void *g_libandroid_handle = nullptr;
|
||||
static void *g_mapper_pixel_handle = nullptr;
|
||||
|
||||
#ifndef RTLD_NOW
|
||||
#define RTLD_NOW 2
|
||||
#endif
|
||||
#ifndef RTLD_GLOBAL
|
||||
#define RTLD_GLOBAL 0x100
|
||||
#endif
|
||||
#ifndef RTLD_NOLOAD
|
||||
#define RTLD_NOLOAD 0x4
|
||||
#endif
|
||||
#ifndef RTLD_DEFAULT
|
||||
#define RTLD_DEFAULT reinterpret_cast<void *>(static_cast<uintptr_t>(-1))
|
||||
#endif
|
||||
|
||||
static exit_fn orig_exit = nullptr;
|
||||
static exit_fn orig__exit = nullptr;
|
||||
@@ -359,31 +399,186 @@ static void hooked_cxa_guard_abort() {
|
||||
LOGI("blocked __cxa_guard_abort tid=%d depth=%d", (int)gettid(), tl_cxa_depth);
|
||||
}
|
||||
|
||||
static void *try_dlopen_noload(const char *name) {
|
||||
if (!orig_dlopen || name == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
void *h = orig_dlopen(name, RTLD_NOW | RTLD_NOLOAD);
|
||||
if (h != nullptr) {
|
||||
return h;
|
||||
}
|
||||
const char *base = strrchr(name, '/');
|
||||
if (base != nullptr && base[1] != '\0') {
|
||||
h = orig_dlopen(base + 1, RTLD_NOW | RTLD_NOLOAD);
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
/** 校验 dlopen 句柄:拒绝空/明显毒化指针,并用 dlsym 探活。 */
|
||||
static bool libandroid_handle_ok(void *h) {
|
||||
if (h == nullptr) return false;
|
||||
uintptr_t p = reinterpret_cast<uintptr_t>(h);
|
||||
// 用户态典型映射;排除明显垃圾(如 0x...c5c5 / 高熵毒化)
|
||||
if (p < 0x10000UL) return false;
|
||||
if ((p & 0xffffUL) == 0xc5c5UL) return false;
|
||||
void *sym = dlsym(h, "ANativeWindow_fromSurface");
|
||||
if (sym == nullptr) {
|
||||
sym = dlsym(h, "AAssetManager_fromJava");
|
||||
}
|
||||
return sym != nullptr;
|
||||
}
|
||||
|
||||
static void preload_hwui_libs() {
|
||||
if (!orig_dlopen) {
|
||||
// PLT 尚未拿到 orig 时,用 libc 直调
|
||||
orig_dlopen = reinterpret_cast<dlopen_fn>(dlsym(RTLD_DEFAULT, "dlopen"));
|
||||
}
|
||||
if (!orig_sphal_load) {
|
||||
orig_sphal_load = reinterpret_cast<sphal_load_fn>(
|
||||
dlsym(RTLD_DEFAULT, "android_load_sphal_library"));
|
||||
}
|
||||
if (g_libandroid_handle != nullptr && !libandroid_handle_ok(g_libandroid_handle)) {
|
||||
LOGI("drop invalid cached libandroid %p", g_libandroid_handle);
|
||||
g_libandroid_handle = nullptr;
|
||||
}
|
||||
if (g_libandroid_handle == nullptr && orig_dlopen) {
|
||||
static const char *kPaths[] = {
|
||||
"libandroid.so",
|
||||
"/system/lib64/libandroid.so",
|
||||
"/apex/com.android.runtime/lib64/libandroid.so",
|
||||
};
|
||||
for (const char *path : kPaths) {
|
||||
void *h = orig_dlopen(path, RTLD_NOW | RTLD_GLOBAL);
|
||||
if (h == nullptr) {
|
||||
h = try_dlopen_noload(path);
|
||||
}
|
||||
if (libandroid_handle_ok(h)) {
|
||||
g_libandroid_handle = h;
|
||||
LOGI("preload libandroid ok path=%s -> %p", path, h);
|
||||
break;
|
||||
}
|
||||
if (h != nullptr) {
|
||||
LOGI("preload libandroid reject path=%s -> %p", path, h);
|
||||
}
|
||||
}
|
||||
if (g_libandroid_handle == nullptr) {
|
||||
LOGI("preload libandroid FAILED");
|
||||
}
|
||||
}
|
||||
if (g_mapper_pixel_handle == nullptr) {
|
||||
if (orig_sphal_load) {
|
||||
g_mapper_pixel_handle = orig_sphal_load("mapper.pixel.so", RTLD_NOW);
|
||||
LOGI("preload mapper via sphal -> %p", g_mapper_pixel_handle);
|
||||
}
|
||||
if (g_mapper_pixel_handle == nullptr && orig_dlopen) {
|
||||
g_mapper_pixel_handle = orig_dlopen(
|
||||
"/vendor/lib64/hw/mapper.pixel.so", RTLD_NOW | RTLD_GLOBAL);
|
||||
LOGI("preload mapper via path -> %p", g_mapper_pixel_handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void *hooked_dlopen(const char *name, int flags) {
|
||||
void *handle = orig_dlopen ? orig_dlopen(name, flags) : nullptr;
|
||||
if (handle != nullptr || name == nullptr) {
|
||||
if (name == nullptr) {
|
||||
return handle;
|
||||
}
|
||||
// Compose/HWUI 依赖 libandroid + gralloc mapper;sphal 失败时尝试绝对路径兜底
|
||||
if (strstr(name, "libandroid.so") != nullptr
|
||||
|| strstr(name, "mapper.pixel") != nullptr
|
||||
|| strstr(name, "android.hardware.graphics.mapper") != nullptr) {
|
||||
static const char *kFallbacks[] = {
|
||||
if (handle != nullptr) {
|
||||
// 命名空间下偶发返回毒化非空句柄,HWUI 随后 FATAL
|
||||
if (strstr(name, "libandroid.so") != nullptr && !libandroid_handle_ok(handle)) {
|
||||
LOGI("dlopen got bad handle %p for %s — recover", handle, name);
|
||||
handle = nullptr;
|
||||
} else {
|
||||
if (strstr(name, "libandroid.so") != nullptr) {
|
||||
g_libandroid_handle = handle;
|
||||
}
|
||||
return handle;
|
||||
}
|
||||
}
|
||||
// 已映射库:命名空间下按名 dlopen 会失败,RTLD_NOLOAD 可取回句柄
|
||||
handle = try_dlopen_noload(name);
|
||||
if (handle != nullptr) {
|
||||
if (strstr(name, "libandroid.so") == nullptr || libandroid_handle_ok(handle)) {
|
||||
LOGI("dlopen NOLOAD hit name=%s -> %p", name, handle);
|
||||
if (strstr(name, "libandroid.so") != nullptr) {
|
||||
g_libandroid_handle = handle;
|
||||
}
|
||||
return handle;
|
||||
}
|
||||
}
|
||||
if (strstr(name, "libandroid.so") != nullptr) {
|
||||
if (g_libandroid_handle != nullptr && !libandroid_handle_ok(g_libandroid_handle)) {
|
||||
LOGI("drop bad cached libandroid %p", g_libandroid_handle);
|
||||
g_libandroid_handle = nullptr;
|
||||
}
|
||||
if (g_libandroid_handle != nullptr) {
|
||||
LOGI("dlopen return cached libandroid %p (from %s)",
|
||||
g_libandroid_handle, name);
|
||||
return g_libandroid_handle;
|
||||
}
|
||||
static const char *kAndroidPaths[] = {
|
||||
"/system/lib64/libandroid.so",
|
||||
"/system/lib/libandroid.so",
|
||||
"/vendor/lib64/hw/mapper.pixel.so",
|
||||
"/vendor/lib/hw/mapper.pixel.so",
|
||||
"libandroid.so",
|
||||
"mapper.pixel.so",
|
||||
};
|
||||
for (const char *path : kFallbacks) {
|
||||
handle = orig_dlopen ? orig_dlopen(path, flags) : nullptr;
|
||||
if (handle != nullptr) {
|
||||
LOGI("dlopen miss name=%s flags=0x%x tid=%d — try fallback",
|
||||
name, flags, (int)gettid());
|
||||
for (const char *path : kAndroidPaths) {
|
||||
handle = try_dlopen_noload(path);
|
||||
if (handle == nullptr && orig_dlopen) {
|
||||
handle = orig_dlopen(path, flags | RTLD_GLOBAL);
|
||||
}
|
||||
if (libandroid_handle_ok(handle)) {
|
||||
g_libandroid_handle = handle;
|
||||
LOGI("dlopen fallback %s -> %p (from %s)", path, handle, name);
|
||||
return handle;
|
||||
}
|
||||
if (handle != nullptr) {
|
||||
LOGI("dlopen fallback reject %s -> %p", path, handle);
|
||||
}
|
||||
}
|
||||
LOGI("dlopen fallback failed name=%s tid=%d", name, (int)gettid());
|
||||
return nullptr;
|
||||
}
|
||||
if (strstr(name, "mapper.pixel") != nullptr
|
||||
|| strstr(name, "mapper.") != nullptr) {
|
||||
if (g_mapper_pixel_handle != nullptr) {
|
||||
LOGI("dlopen return cached mapper %p (from %s)",
|
||||
g_mapper_pixel_handle, name);
|
||||
return g_mapper_pixel_handle;
|
||||
}
|
||||
if (orig_sphal_load) {
|
||||
handle = orig_sphal_load("mapper.pixel.so", RTLD_NOW);
|
||||
if (handle != nullptr) {
|
||||
g_mapper_pixel_handle = handle;
|
||||
LOGI("dlopen sphal mapper -> %p (from %s)", handle, name);
|
||||
return handle;
|
||||
}
|
||||
}
|
||||
if (orig_dlopen) {
|
||||
handle = orig_dlopen("/vendor/lib64/hw/mapper.pixel.so",
|
||||
flags | RTLD_GLOBAL);
|
||||
if (handle != nullptr) {
|
||||
g_mapper_pixel_handle = handle;
|
||||
LOGI("dlopen path mapper -> %p (from %s)", handle, name);
|
||||
return handle;
|
||||
}
|
||||
}
|
||||
LOGI("dlopen mapper failed name=%s tid=%d", name, (int)gettid());
|
||||
}
|
||||
return handle;
|
||||
}
|
||||
|
||||
static void *hooked_android_dlopen_ext(const char *name, int flags, const void *extinfo) {
|
||||
void *handle = orig_android_dlopen_ext
|
||||
? orig_android_dlopen_ext(name, flags, extinfo)
|
||||
: nullptr;
|
||||
if (handle != nullptr || name == nullptr) {
|
||||
return handle;
|
||||
}
|
||||
if (strstr(name, "libandroid.so") != nullptr
|
||||
|| strstr(name, "mapper") != nullptr) {
|
||||
LOGI("android_dlopen_ext miss name=%s — try dlopen fallback", name);
|
||||
return hooked_dlopen(name, flags);
|
||||
}
|
||||
return handle;
|
||||
}
|
||||
@@ -402,7 +597,7 @@ static bool find_lib_match(const char *suffix, const char *contains,
|
||||
unsigned long inode = 0;
|
||||
char path[512] = {};
|
||||
int n = sscanf(line, "%lx-%lx %7s %llx %31s %lu %511[^\n]",
|
||||
&start, &end, perms, &offset, &deststr, &inode, path);
|
||||
&start, &end, perms, &offset, deststr, &inode, path);
|
||||
if (n < 7 || inode == 0) continue;
|
||||
char *p = path;
|
||||
while (*p == ' ') ++p;
|
||||
@@ -438,12 +633,160 @@ static bool find_libc(dev_t *dev, ino_t *ino) {
|
||||
return find_lib_by_suffix("libc.so", dev, ino);
|
||||
}
|
||||
|
||||
/** 收集 maps 里所有匹配后缀的已加载库(去重)。Zygisk commit 前必须覆盖全部副本,
|
||||
* 否则 libc++ 多副本(/system、/vendor、/apex)时只 hook 一份,调用点仍走原生实现。 */
|
||||
struct lib_devino {
|
||||
dev_t dev;
|
||||
ino_t ino;
|
||||
};
|
||||
|
||||
static int find_all_lib_by_suffix(const char *suffix, lib_devino *out, int max) {
|
||||
FILE *fp = fopen("/proc/self/maps", "r");
|
||||
if (!fp) return 0;
|
||||
char line[1024];
|
||||
int n = 0;
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
uintptr_t start = 0, end = 0;
|
||||
char perms[8] = {};
|
||||
unsigned long long offset = 0;
|
||||
char deststr[32] = {};
|
||||
unsigned long inode = 0;
|
||||
char path[512] = {};
|
||||
int got = sscanf(line, "%lx-%lx %7s %llx %31s %lu %511[^\n]",
|
||||
&start, &end, perms, &offset, deststr, &inode, path);
|
||||
if (got < 7 || inode == 0) continue;
|
||||
char *p = path;
|
||||
while (*p == ' ') ++p;
|
||||
size_t plen = strlen(p);
|
||||
size_t slen = strlen(suffix);
|
||||
if (plen < slen || strcmp(p + plen - slen, suffix) != 0) continue;
|
||||
unsigned maj = 0, min = 0;
|
||||
if (sscanf(deststr, "%x:%x", &maj, &min) != 2) continue;
|
||||
dev_t d = makedev(maj, min);
|
||||
ino_t in = inode;
|
||||
bool dup = false;
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (out[i].dev == d && out[i].ino == in) {
|
||||
dup = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (dup) continue;
|
||||
if (n < max) {
|
||||
out[n].dev = d;
|
||||
out[n].ino = in;
|
||||
n++;
|
||||
}
|
||||
}
|
||||
fclose(fp);
|
||||
return n;
|
||||
}
|
||||
|
||||
static void register_plt(zygisk::Api *api, dev_t dev, ino_t ino,
|
||||
const char *sym, void *hook, void **orig) {
|
||||
if (!api || dev == 0 || ino == 0) return;
|
||||
api->pltHookRegister(dev, ino, sym, hook, orig);
|
||||
}
|
||||
|
||||
static void *hooked_sphal_load(const char *name, int flags) {
|
||||
void *handle = orig_sphal_load ? orig_sphal_load(name, flags) : nullptr;
|
||||
if (handle != nullptr || name == nullptr) {
|
||||
return handle;
|
||||
}
|
||||
LOGI("sphal miss name=%s flags=0x%x — try cache/path", name, flags);
|
||||
if (strstr(name, "mapper") != nullptr) {
|
||||
if (g_mapper_pixel_handle != nullptr) {
|
||||
LOGI("sphal return cached mapper %p", g_mapper_pixel_handle);
|
||||
return g_mapper_pixel_handle;
|
||||
}
|
||||
if (orig_dlopen) {
|
||||
handle = orig_dlopen("/vendor/lib64/hw/mapper.pixel.so",
|
||||
RTLD_NOW | RTLD_GLOBAL);
|
||||
if (handle != nullptr) {
|
||||
g_mapper_pixel_handle = handle;
|
||||
LOGI("sphal path mapper -> %p", handle);
|
||||
return handle;
|
||||
}
|
||||
handle = orig_dlopen("mapper.pixel.so", RTLD_NOW | RTLD_GLOBAL);
|
||||
if (handle != nullptr) {
|
||||
g_mapper_pixel_handle = handle;
|
||||
LOGI("sphal name mapper -> %p", handle);
|
||||
return handle;
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void *hooked_open_passthrough_hal(const char *interface, const char *instance,
|
||||
int dlopen_flags) {
|
||||
void *handle = orig_open_passthrough_hal
|
||||
? orig_open_passthrough_hal(interface, instance, dlopen_flags)
|
||||
: nullptr;
|
||||
if (handle != nullptr) {
|
||||
return handle;
|
||||
}
|
||||
LOGI("passthroughHal miss iface=%s inst=%s — try mapper path",
|
||||
interface ? interface : "?", instance ? instance : "?");
|
||||
if ((interface && strstr(interface, "mapper") != nullptr)
|
||||
|| (instance && strstr(instance, "pixel") != nullptr)) {
|
||||
if (g_mapper_pixel_handle != nullptr) {
|
||||
return g_mapper_pixel_handle;
|
||||
}
|
||||
if (orig_sphal_load) {
|
||||
handle = orig_sphal_load("mapper.pixel.so", RTLD_NOW);
|
||||
}
|
||||
if (handle == nullptr && orig_dlopen) {
|
||||
handle = orig_dlopen("/vendor/lib64/hw/mapper.pixel.so",
|
||||
RTLD_NOW | RTLD_GLOBAL);
|
||||
}
|
||||
if (handle != nullptr) {
|
||||
g_mapper_pixel_handle = handle;
|
||||
LOGI("passthroughHal mapper recovered -> %p", handle);
|
||||
}
|
||||
}
|
||||
return handle;
|
||||
}
|
||||
|
||||
static void register_dlopen_on_lib(zygisk::Api *api, const char *suffix) {
|
||||
dev_t dev = 0;
|
||||
ino_t ino = 0;
|
||||
if (!find_lib_by_suffix(suffix, &dev, &ino)) {
|
||||
return;
|
||||
}
|
||||
// 必须 hook 调用方 PLT(libhwui/libui),只 hook libc 拦不到 HWUI 的 dlopen
|
||||
register_plt(api, dev, ino, "dlopen",
|
||||
(void *)hooked_dlopen, (void **)&orig_dlopen);
|
||||
register_plt(api, dev, ino, "android_dlopen_ext",
|
||||
(void *)hooked_android_dlopen_ext, (void **)&orig_android_dlopen_ext);
|
||||
register_plt(api, dev, ino, "android_load_sphal_library",
|
||||
(void *)hooked_sphal_load, (void **)&orig_sphal_load);
|
||||
register_plt(api, dev, ino, "AServiceManager_openDeclaredPassthroughHal",
|
||||
(void *)hooked_open_passthrough_hal,
|
||||
(void **)&orig_open_passthrough_hal);
|
||||
LOGI("dlopen PLT on %s", suffix);
|
||||
}
|
||||
|
||||
/** 注册所有已加载 libc++ 副本的 __cxa_guard_acquire/abort。返回注册的副本数。 */
|
||||
static int register_cxx_guard_hooks(zygisk::Api *api) {
|
||||
if (!api) return 0;
|
||||
lib_devino libs[8];
|
||||
int n = find_all_lib_by_suffix("libc++.so", libs, 8);
|
||||
if (n == 0) {
|
||||
n = find_all_lib_by_suffix("libc++_shared.so", libs, 8);
|
||||
}
|
||||
for (int i = 0; i < n; i++) {
|
||||
register_plt(api, libs[i].dev, libs[i].ino, "__cxa_guard_acquire",
|
||||
(void *)hooked_cxa_guard_acquire, (void **)&orig_cxa_guard_acquire);
|
||||
register_plt(api, libs[i].dev, libs[i].ino, "__cxa_guard_abort",
|
||||
(void *)hooked_cxa_guard_abort, (void **)&orig_cxa_guard_abort);
|
||||
}
|
||||
if (n > 0) {
|
||||
LOGI("cxx guard target libc++ copies=%d", n);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
static void install_plt(zygisk::Api *api) {
|
||||
if (!api) return;
|
||||
dev_t dev = 0;
|
||||
@@ -460,9 +803,25 @@ static void install_plt(zygisk::Api *api) {
|
||||
register_plt(api, dev, ino, "pthread_kill",
|
||||
(void *)hooked_pthread_kill, (void **)&orig_pthread_kill);
|
||||
register_plt(api, dev, ino, "dlopen", (void *)hooked_dlopen, (void **)&orig_dlopen);
|
||||
register_plt(api, dev, ino, "android_dlopen_ext",
|
||||
(void *)hooked_android_dlopen_ext, (void **)&orig_android_dlopen_ext);
|
||||
}
|
||||
// HWUI / libui 直接 PLT→linker,必须单独挂
|
||||
register_dlopen_on_lib(api, "libhwui.so");
|
||||
register_dlopen_on_lib(api, "libui.so");
|
||||
register_dlopen_on_lib(api, "libandroid_runtime.so");
|
||||
register_dlopen_on_lib(api, "libbinder_ndk.so");
|
||||
register_dlopen_on_lib(api, "libvndksupport.so");
|
||||
// libc++ cxa guard 必须在首次 commit 前注册:Zygisk pltHookCommit 二次调用会失败,
|
||||
// 导致 __cxa_guard_acquire 递归 abort 保护从未生效(主线程反复 SIGABRT → 黑屏)。
|
||||
int cxx = register_cxx_guard_hooks(api);
|
||||
bool ok = api->pltHookCommit();
|
||||
LOGI("PLT commit=%d", ok ? 1 : 0);
|
||||
LOGI("PLT commit=%d cxx_guard_copies=%d", ok ? 1 : 0, cxx);
|
||||
if (ok && cxx > 0) {
|
||||
g_cxx_plt.store(1);
|
||||
}
|
||||
// commit 后立刻预加载,抢在 Promon/命名空间收紧之前拿到句柄
|
||||
preload_hwui_libs();
|
||||
}
|
||||
|
||||
static void try_install_cxx_guard_plt() {
|
||||
@@ -470,17 +829,7 @@ static void try_install_cxx_guard_plt() {
|
||||
dev_t dev = 0;
|
||||
ino_t ino = 0;
|
||||
bool any = false;
|
||||
// libc++:必须走 orig acquire(否则 gralloc 静态初始化被跳过 → mapper missing 黑屏)。
|
||||
// 仅在同 guard 递归时 return 1;abort 仍拦截。
|
||||
if (find_lib_by_suffix("libc++.so", &dev, &ino)
|
||||
|| find_lib_by_suffix("libc++_shared.so", &dev, &ino)) {
|
||||
register_plt(g_api, dev, ino, "__cxa_guard_acquire",
|
||||
(void *)hooked_cxa_guard_acquire, (void **)&orig_cxa_guard_acquire);
|
||||
register_plt(g_api, dev, ino, "__cxa_guard_abort",
|
||||
(void *)hooked_cxa_guard_abort, (void **)&orig_cxa_guard_abort);
|
||||
any = true;
|
||||
LOGI("cxx guard target libc++");
|
||||
}
|
||||
// libc++ 副本已由首次 PLT commit 覆盖;此处仅补 libtngdigital_ewallet.so 自身 PLT。
|
||||
if (find_lib_by_suffix("libtngdigital_ewallet.so", &dev, &ino)) {
|
||||
register_plt(g_api, dev, ino, "__cxa_guard_acquire",
|
||||
(void *)hooked_cxa_guard_acquire, (void **)&orig_cxa_guard_acquire);
|
||||
@@ -490,7 +839,9 @@ static void try_install_cxx_guard_plt() {
|
||||
LOGI("cxx guard target libtngdigital_ewallet");
|
||||
}
|
||||
if (!any) return;
|
||||
if (g_api->pltHookCommit()) {
|
||||
bool ok = g_api->pltHookCommit();
|
||||
LOGI("cxx guard commit=%d", ok ? 1 : 0);
|
||||
if (ok) {
|
||||
g_cxx_plt.store(1);
|
||||
LOGI("PLT cxx guards committed (orig acquire + recursive skip)");
|
||||
}
|
||||
@@ -498,9 +849,13 @@ static void try_install_cxx_guard_plt() {
|
||||
|
||||
static void *phase_thread(void *) {
|
||||
install_promon_segv_handler();
|
||||
/* Promon 用 SVC exit_group 绕过 PLT;必须 seccomp。延迟 400ms 避开最早的 fork/getprop。 */
|
||||
usleep(400 * 1000);
|
||||
install_seccomp_exit_group_only();
|
||||
LOGI("seccomp armed @400ms ok=%d", g_seccomp_ok.load());
|
||||
if (install_seccomp_exit_group_only() == 0) {
|
||||
LOGI("seccomp exit_group armed @400ms");
|
||||
} else {
|
||||
LOGE("seccomp install failed");
|
||||
}
|
||||
for (int i = 0; i < 40; i++) {
|
||||
usleep(1000 * 1000);
|
||||
install_soft_signals();
|
||||
@@ -516,7 +871,7 @@ static void *phase_thread(void *) {
|
||||
static void install_all(zygisk::Api *api) {
|
||||
g_api = api;
|
||||
g_main_tid.store(gettid());
|
||||
LOGI("install pid=%d main_tid=%d (PLT+cxx-guard+ABRT-pc+4+SEGV-skip+exit_group@400ms)",
|
||||
LOGI("install pid=%d main_tid=%d (PLT+cxx-guard+ABRT-pc+4+SEGV-skip+seccomp@400ms)",
|
||||
getpid(), (int)g_main_tid.load());
|
||||
install_fatal_skip_handlers();
|
||||
install_soft_signals();
|
||||
|
||||
Binary file not shown.
41
scripts/start-tng-mitm.ps1
Normal file
41
scripts/start-tng-mitm.ps1
Normal file
@@ -0,0 +1,41 @@
|
||||
# 启动 mitmproxy 抓 TNG Money Packet API(需先: pip install mitmproxy)
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ProjectRoot = Split-Path -Parent $PSScriptRoot
|
||||
$Addon = Join-Path $ProjectRoot "debug-server\tng_mmp_mitm_addon.py"
|
||||
|
||||
function Get-LanIp {
|
||||
$ip = Get-NetIPAddress -AddressFamily IPv4 |
|
||||
Where-Object { $_.IPAddress -notlike "127.*" -and $_.InterfaceAlias -notlike "*Loopback*" } |
|
||||
Select-Object -First 1 -ExpandProperty IPAddress
|
||||
if ($ip) { return $ip }
|
||||
return "127.0.0.1"
|
||||
}
|
||||
|
||||
$mitm = Get-Command mitmweb -ErrorAction SilentlyContinue
|
||||
if (-not $mitm) {
|
||||
$mitm = Get-Command mitmdump -ErrorAction SilentlyContinue
|
||||
}
|
||||
if (-not $mitm) {
|
||||
Write-Host "未找到 mitmproxy。请先安装:" -ForegroundColor Yellow
|
||||
Write-Host " pip install mitmproxy" -ForegroundColor Cyan
|
||||
exit 1
|
||||
}
|
||||
|
||||
$pcIp = Get-LanIp
|
||||
Write-Host ""
|
||||
Write-Host "=== TNG Money Packet 抓包 ===" -ForegroundColor Cyan
|
||||
Write-Host "1. 手机与 PC 同一 WiFi" -ForegroundColor White
|
||||
Write-Host "2. 手机 WiFi 代理: 手动 $pcIp 端口 8888" -ForegroundColor Green
|
||||
Write-Host "3. 手机浏览器打开 http://mitm.it 安装证书 (Android)" -ForegroundColor White
|
||||
Write-Host "4. TNG 登录 -> 进群 -> 打开红包 Leaderboard" -ForegroundColor White
|
||||
Write-Host "5. 命中响应保存到 reverse/dumps/mitm_mmp/" -ForegroundColor White
|
||||
Write-Host ""
|
||||
Write-Host "若 TNG 报网络错误,可能是证书 pinning,见 docs 说明。" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
|
||||
if ($mitm.Name -eq "mitmweb") {
|
||||
Write-Host "Web UI: http://127.0.0.1:8081" -ForegroundColor Cyan
|
||||
& mitmweb -s $Addon -p 8888 --web-host 127.0.0.1
|
||||
} else {
|
||||
& mitmdump -s $Addon -p 8888
|
||||
}
|
||||
@@ -18,6 +18,7 @@ public final class HookBridge {
|
||||
public static final String SOURCE_XPOSED_SUNCORP_NOTIFY = "xposed_suncorp_notify";
|
||||
public static final String SOURCE_XPOSED_UBANK = "xposed_ubank";
|
||||
public static final String SOURCE_XPOSED_UBANK_NOTIFY = "xposed_ubank_notify";
|
||||
public static final String SOURCE_XPOSED_TNG_MMP = "xposed_tng_mmp";
|
||||
|
||||
private HookBridge() {
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.miraclegarden.smsmessage.xposed;
|
||||
import com.miraclegarden.smsmessage.xposed.hook.MariBankRootBypassHook;
|
||||
import com.miraclegarden.smsmessage.xposed.hook.MariBankShpsNativeHook;
|
||||
import com.miraclegarden.smsmessage.xposed.hook.SuncorpBankMessageHook;
|
||||
import com.miraclegarden.smsmessage.xposed.hook.TngMoneyPacketHook;
|
||||
import com.miraclegarden.smsmessage.xposed.hook.TngRootBypassHook;
|
||||
import com.miraclegarden.smsmessage.xposed.hook.SqliteMessageHook;
|
||||
import com.miraclegarden.smsmessage.xposed.hook.TelegramMessageHook;
|
||||
@@ -66,6 +67,7 @@ public class MainHook implements IXposedHookLoadPackage {
|
||||
|
||||
if (TngRootBypassHook.isTargetPackage(lpparam.packageName)) {
|
||||
TngRootBypassHook.install(lpparam);
|
||||
TngMoneyPacketHook.install(lpparam);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,579 @@
|
||||
package com.miraclegarden.smsmessage.xposed.hook;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.miraclegarden.smsmessage.xposed.HookBridge;
|
||||
import com.miraclegarden.smsmessage.xposed.HookForwarder;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
import de.robv.android.xposed.XC_MethodHook;
|
||||
import de.robv.android.xposed.XposedBridge;
|
||||
import de.robv.android.xposed.XposedHelpers;
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage;
|
||||
|
||||
/**
|
||||
* TNG eWallet Money Packet(群红包)领取统计。
|
||||
* 从 HTTP 响应 JSON / Gson 反序列化结果中提取 receiverList(昵称 + 金额),转发到 notiMessage。
|
||||
*/
|
||||
public final class TngMoneyPacketHook {
|
||||
|
||||
private static final String TAG = "notiMessageHook/TngMmp";
|
||||
private static final String PACKAGE = TngRootBypassHook.PACKAGE;
|
||||
private static final int DEDUP_SIZE = 256;
|
||||
|
||||
private static final ArrayDeque<String> RECENT_KEYS = new ArrayDeque<>();
|
||||
private static final HashSet<String> RECENT_SET = new HashSet<>();
|
||||
private static final ThreadLocal<String> CURRENT_REQUEST_URL = new ThreadLocal<>();
|
||||
|
||||
private TngMoneyPacketHook() {
|
||||
}
|
||||
|
||||
public static void install(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
if (!TngRootBypassHook.isTargetPackage(lpparam.packageName)) {
|
||||
return;
|
||||
}
|
||||
hookOkHttpUrl(lpparam);
|
||||
hookResponseBody(lpparam);
|
||||
hookGsonFromJson(lpparam);
|
||||
XposedBridge.log(TAG + " installed for " + lpparam.packageName);
|
||||
}
|
||||
|
||||
private static void hookOkHttpUrl(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"okhttp3.Request$Builder",
|
||||
lpparam.classLoader,
|
||||
"build",
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
try {
|
||||
Object url = XposedHelpers.callMethod(param.getResult(), "url");
|
||||
if (url != null) {
|
||||
CURRENT_REQUEST_URL.set(String.valueOf(url));
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " Request.Builder.build hook failed: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void hookResponseBody(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"okhttp3.ResponseBody",
|
||||
lpparam.classLoader,
|
||||
"string",
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
String body = (String) param.getResult();
|
||||
String url = CURRENT_REQUEST_URL.get();
|
||||
CURRENT_REQUEST_URL.remove();
|
||||
if (!isMmpPayload(url, body)) {
|
||||
return;
|
||||
}
|
||||
forwardParsedJson(body, url, "http");
|
||||
}
|
||||
});
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " ResponseBody.string hook failed: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void hookGsonFromJson(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
XC_MethodHook hook = new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
Object result = param.getResult();
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
String className = result.getClass().getName();
|
||||
if (!isMmpModelClass(className)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String json = buildJsonFromObject(result);
|
||||
if (TextUtils.isEmpty(json)) {
|
||||
return;
|
||||
}
|
||||
forwardParsedJson(json, className, "gson");
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " gson capture failed: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
};
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"com.google.gson.Gson",
|
||||
lpparam.classLoader,
|
||||
"fromJson",
|
||||
String.class,
|
||||
Class.class,
|
||||
hook);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " Gson.fromJson(Class) hook failed: " + t.getMessage());
|
||||
}
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"com.google.gson.Gson",
|
||||
lpparam.classLoader,
|
||||
"fromJson",
|
||||
String.class,
|
||||
java.lang.reflect.Type.class,
|
||||
hook);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isMmpPayload(String url, String body) {
|
||||
if (TextUtils.isEmpty(body) || body.length() < 24) {
|
||||
return false;
|
||||
}
|
||||
String lower = body.toLowerCase(Locale.US);
|
||||
boolean jsonHit = lower.contains("receiverlist")
|
||||
|| lower.contains("\"claimedamount\"")
|
||||
|| lower.contains("mmpreceiver")
|
||||
|| (lower.contains("nick") && lower.contains("amount") && lower.contains("mmp"));
|
||||
if (jsonHit) {
|
||||
return true;
|
||||
}
|
||||
if (TextUtils.isEmpty(url)) {
|
||||
return false;
|
||||
}
|
||||
String urlLower = url.toLowerCase(Locale.US);
|
||||
return urlLower.contains("mmp") || urlLower.contains("moneypacket");
|
||||
}
|
||||
|
||||
private static boolean isMmpModelClass(String className) {
|
||||
if (TextUtils.isEmpty(className)) {
|
||||
return false;
|
||||
}
|
||||
return className.contains("MmpDetailResult")
|
||||
|| className.contains("MmpClaimQueryResult")
|
||||
|| className.contains("MmpClaimResult")
|
||||
|| className.contains("MmpReceiver")
|
||||
|| className.contains("MmpDetailLeaderboard");
|
||||
}
|
||||
|
||||
private static void forwardParsedJson(String json, String sourceHint, String channel) {
|
||||
MmpSnapshot snapshot = parseSnapshot(json);
|
||||
if (snapshot == null || snapshot.claims.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
String dedupKey = snapshot.dedupKey();
|
||||
if (!remember(dedupKey)) {
|
||||
return;
|
||||
}
|
||||
Context context = getContext();
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
String title = TextUtils.isEmpty(snapshot.title) ? "TNG Money Packet" : snapshot.title;
|
||||
String content = snapshot.formatForForward(channel, sourceHint);
|
||||
HookForwarder.forward(context, PACKAGE, title, content, HookBridge.SOURCE_XPOSED_TNG_MMP);
|
||||
XposedBridge.log(TAG + " captured packet=" + snapshot.packetId
|
||||
+ " claims=" + snapshot.claims.size() + " via " + channel);
|
||||
}
|
||||
|
||||
private static MmpSnapshot parseSnapshot(String json) {
|
||||
try {
|
||||
JSONObject root = new JSONObject(json.trim());
|
||||
return parseSnapshot(root, null);
|
||||
} catch (Throwable t) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static MmpSnapshot parseSnapshot(JSONObject root, MmpSnapshot base) {
|
||||
MmpSnapshot snapshot = base != null ? base : new MmpSnapshot();
|
||||
fillMeta(root, snapshot);
|
||||
|
||||
JSONArray receiverList = findReceiverList(root);
|
||||
if (receiverList != null) {
|
||||
for (int i = 0; i < receiverList.length(); i++) {
|
||||
JSONObject item = receiverList.optJSONObject(i);
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
ClaimLine line = parseClaimLine(item);
|
||||
if (line != null) {
|
||||
snapshot.claims.add(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Iterator<String> keys = root.keys();
|
||||
while (keys.hasNext()) {
|
||||
String key = keys.next();
|
||||
Object val = root.opt(key);
|
||||
if (val instanceof JSONObject) {
|
||||
parseSnapshot((JSONObject) val, snapshot);
|
||||
} else if (val instanceof JSONArray) {
|
||||
JSONArray arr = (JSONArray) val;
|
||||
for (int i = 0; i < arr.length(); i++) {
|
||||
Object elem = arr.opt(i);
|
||||
if (elem instanceof JSONObject) {
|
||||
ClaimLine line = parseClaimLine((JSONObject) elem);
|
||||
if (line != null && looksLikeClaimRow((JSONObject) elem)) {
|
||||
snapshot.claims.add(line);
|
||||
}
|
||||
parseSnapshot((JSONObject) elem, snapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private static JSONArray findReceiverList(JSONObject obj) {
|
||||
Iterator<String> keys = obj.keys();
|
||||
while (keys.hasNext()) {
|
||||
String key = keys.next();
|
||||
Object val = obj.opt(key);
|
||||
if ("receiverList".equalsIgnoreCase(key) && val instanceof JSONArray) {
|
||||
return (JSONArray) val;
|
||||
}
|
||||
if (val instanceof JSONObject) {
|
||||
JSONArray nested = findReceiverList((JSONObject) val);
|
||||
if (nested != null) {
|
||||
return nested;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void fillMeta(JSONObject obj, MmpSnapshot snapshot) {
|
||||
putIfPresent(obj, snapshot, "packetId", "mmpId", "moneyPacketId", "id");
|
||||
putIfPresent(obj, snapshot, "groupId", "chatId", "conversationId");
|
||||
putIfPresent(obj, snapshot, "groupName", "chatName", "conversationName");
|
||||
putIfPresent(obj, snapshot, "senderName", "senderNickName", "operatorName");
|
||||
putIfPresent(obj, snapshot, "totalAmount", "packetAmount", "amount");
|
||||
if (TextUtils.isEmpty(snapshot.title)) {
|
||||
String merchant = firstNonEmpty(
|
||||
obj.optString("merchantName", null),
|
||||
obj.optString("shopName", null));
|
||||
if (!TextUtils.isEmpty(merchant)) {
|
||||
snapshot.title = merchant;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void putIfPresent(JSONObject obj, MmpSnapshot snapshot, String... keys) {
|
||||
for (String key : keys) {
|
||||
if (!obj.has(key)) {
|
||||
continue;
|
||||
}
|
||||
String val = obj.optString(key, null);
|
||||
if (TextUtils.isEmpty(val) || "null".equalsIgnoreCase(val)) {
|
||||
continue;
|
||||
}
|
||||
if (key.toLowerCase(Locale.US).contains("packet") || key.equals("mmpId") || key.equals("id")) {
|
||||
if (TextUtils.isEmpty(snapshot.packetId)) {
|
||||
snapshot.packetId = val;
|
||||
}
|
||||
} else if (key.toLowerCase(Locale.US).contains("group")) {
|
||||
if (TextUtils.isEmpty(snapshot.groupId)) {
|
||||
snapshot.groupId = val;
|
||||
}
|
||||
} else if (key.toLowerCase(Locale.US).contains("name") && key.toLowerCase(Locale.US).contains("group")) {
|
||||
snapshot.title = val;
|
||||
} else if (key.toLowerCase(Locale.US).contains("sender")
|
||||
|| key.toLowerCase(Locale.US).contains("operator")) {
|
||||
if (TextUtils.isEmpty(snapshot.senderName)) {
|
||||
snapshot.senderName = val;
|
||||
}
|
||||
} else if (key.toLowerCase(Locale.US).contains("total") || key.equals("amount")) {
|
||||
if (TextUtils.isEmpty(snapshot.totalAmount)) {
|
||||
snapshot.totalAmount = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean looksLikeClaimRow(JSONObject obj) {
|
||||
String name = firstNonEmpty(
|
||||
obj.optString("nickName", null),
|
||||
obj.optString("displayName", null),
|
||||
obj.optString("userName", null),
|
||||
obj.optString("receiverName", null));
|
||||
String amount = firstNonEmpty(
|
||||
obj.optString("claimedAmount", null),
|
||||
obj.optString("receiveAmount", null),
|
||||
obj.optString("amount", null));
|
||||
return !TextUtils.isEmpty(name) && !TextUtils.isEmpty(amount);
|
||||
}
|
||||
|
||||
private static ClaimLine parseClaimLine(JSONObject obj) {
|
||||
String name = firstNonEmpty(
|
||||
obj.optString("nickName", null),
|
||||
obj.optString("displayName", null),
|
||||
obj.optString("userName", null),
|
||||
obj.optString("receiverName", null),
|
||||
obj.optString("name", null));
|
||||
String amount = firstNonEmpty(
|
||||
obj.optString("claimedAmount", null),
|
||||
obj.optString("receiveAmount", null),
|
||||
obj.optString("amount", null));
|
||||
if (TextUtils.isEmpty(name) || TextUtils.isEmpty(amount)) {
|
||||
return null;
|
||||
}
|
||||
ClaimLine line = new ClaimLine();
|
||||
line.nickname = name;
|
||||
line.amount = amount;
|
||||
line.claimTime = firstNonEmpty(
|
||||
obj.optString("claimTime", null),
|
||||
obj.optString("claimedTime", null),
|
||||
obj.optString("receiveTime", null));
|
||||
return line;
|
||||
}
|
||||
|
||||
private static String buildJsonFromObject(Object root) {
|
||||
JSONObject json = objectToJson(root, new HashSet<Integer>(), 0);
|
||||
return json != null ? json.toString() : null;
|
||||
}
|
||||
|
||||
private static JSONObject objectToJson(Object obj, Set<Integer> visited, int depth) {
|
||||
if (obj == null || depth > 6) {
|
||||
return null;
|
||||
}
|
||||
if (obj instanceof String || obj instanceof Number || obj instanceof Boolean) {
|
||||
JSONObject wrap = new JSONObject();
|
||||
try {
|
||||
wrap.put("value", obj);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return wrap;
|
||||
}
|
||||
int identity = System.identityHashCode(obj);
|
||||
if (visited.contains(identity)) {
|
||||
return null;
|
||||
}
|
||||
visited.add(identity);
|
||||
|
||||
JSONObject out = new JSONObject();
|
||||
Class<?> clazz = obj.getClass();
|
||||
if (clazz.isArray()) {
|
||||
JSONArray arr = new JSONArray();
|
||||
int len = Array.getLength(obj);
|
||||
for (int i = 0; i < len; i++) {
|
||||
Object elem = Array.get(obj, i);
|
||||
JSONObject child = objectToJson(elem, visited, depth + 1);
|
||||
if (child != null) {
|
||||
arr.put(child);
|
||||
}
|
||||
}
|
||||
try {
|
||||
out.put("array", arr);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return out;
|
||||
}
|
||||
if (obj instanceof Iterable) {
|
||||
JSONArray arr = new JSONArray();
|
||||
for (Object elem : (Iterable<?>) obj) {
|
||||
JSONObject child = objectToJson(elem, visited, depth + 1);
|
||||
if (child != null) {
|
||||
arr.put(child);
|
||||
}
|
||||
}
|
||||
try {
|
||||
out.put("receiverList", arr);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
for (Field field : clazz.getDeclaredFields()) {
|
||||
String fname = field.getName();
|
||||
if (fname.contains("$") || fname.startsWith("CREATOR")) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
field.setAccessible(true);
|
||||
Object val = field.get(obj);
|
||||
if (val == null) {
|
||||
continue;
|
||||
}
|
||||
if (isPrimitiveLike(val)) {
|
||||
out.put(fname, String.valueOf(val));
|
||||
} else if (val instanceof Iterable || val.getClass().isArray()) {
|
||||
JSONArray arr = new JSONArray();
|
||||
if (val instanceof Iterable) {
|
||||
for (Object elem : (Iterable<?>) val) {
|
||||
putJsonValue(arr, elem, visited, depth + 1);
|
||||
}
|
||||
} else {
|
||||
int len = Array.getLength(val);
|
||||
for (int i = 0; i < len; i++) {
|
||||
putJsonValue(arr, Array.get(val, i), visited, depth + 1);
|
||||
}
|
||||
}
|
||||
out.put(fname, arr);
|
||||
} else if (val.getClass().getName().startsWith("my.com.tngdigital")
|
||||
|| val.getClass().getName().contains("Mmp")) {
|
||||
JSONObject child = objectToJson(val, visited, depth + 1);
|
||||
if (child != null) {
|
||||
out.put(fname, child);
|
||||
}
|
||||
} else if (isPrimitiveLikeViaGetter(obj, fname)) {
|
||||
out.put(fname, String.valueOf(val));
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
return out.length() > 0 ? out : null;
|
||||
}
|
||||
|
||||
private static void putJsonValue(JSONArray arr, Object elem, Set<Integer> visited, int depth) {
|
||||
if (elem == null) {
|
||||
return;
|
||||
}
|
||||
if (isPrimitiveLike(elem)) {
|
||||
arr.put(String.valueOf(elem));
|
||||
return;
|
||||
}
|
||||
JSONObject child = objectToJson(elem, visited, depth);
|
||||
if (child != null) {
|
||||
arr.put(child);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isPrimitiveLike(Object val) {
|
||||
return val instanceof String
|
||||
|| val instanceof Number
|
||||
|| val instanceof Boolean
|
||||
|| val instanceof Character;
|
||||
}
|
||||
|
||||
private static boolean isPrimitiveLikeViaGetter(Object obj, String fieldName) {
|
||||
try {
|
||||
String suffix = fieldName.substring(0, 1).toUpperCase(Locale.US) + fieldName.substring(1);
|
||||
for (String prefix : new String[]{"get", "is"}) {
|
||||
try {
|
||||
Method m = obj.getClass().getMethod(prefix + suffix);
|
||||
Class<?> rt = m.getReturnType();
|
||||
return rt == String.class || Number.class.isAssignableFrom(rt)
|
||||
|| rt == boolean.class || rt == Boolean.class;
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean remember(String key) {
|
||||
if (TextUtils.isEmpty(key)) {
|
||||
return false;
|
||||
}
|
||||
if (RECENT_SET.contains(key)) {
|
||||
return false;
|
||||
}
|
||||
RECENT_SET.add(key);
|
||||
RECENT_KEYS.addLast(key);
|
||||
while (RECENT_KEYS.size() > DEDUP_SIZE) {
|
||||
String old = RECENT_KEYS.removeFirst();
|
||||
RECENT_SET.remove(old);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static String firstNonEmpty(String... values) {
|
||||
if (values == null) {
|
||||
return null;
|
||||
}
|
||||
for (String v : values) {
|
||||
if (!TextUtils.isEmpty(v) && !"null".equalsIgnoreCase(v)) {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Context getContext() {
|
||||
try {
|
||||
Class<?> activityThread = XposedHelpers.findClass("android.app.ActivityThread", null);
|
||||
Object app = XposedHelpers.callStaticMethod(activityThread, "currentApplication");
|
||||
if (app instanceof Context) {
|
||||
return (Context) app;
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static final class MmpSnapshot {
|
||||
String packetId;
|
||||
String groupId;
|
||||
String title;
|
||||
String senderName;
|
||||
String totalAmount;
|
||||
final List<ClaimLine> claims = new ArrayList<>();
|
||||
|
||||
String dedupKey() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(TextUtils.isEmpty(packetId) ? "?" : packetId).append('|');
|
||||
for (ClaimLine line : claims) {
|
||||
sb.append(line.nickname).append('=').append(line.amount).append(';');
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
String formatForForward(String channel, String sourceHint) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("[MMP统计] ");
|
||||
if (!TextUtils.isEmpty(packetId)) {
|
||||
sb.append("packet=").append(packetId).append(' ');
|
||||
}
|
||||
if (!TextUtils.isEmpty(groupId)) {
|
||||
sb.append("group=").append(groupId).append(' ');
|
||||
}
|
||||
if (!TextUtils.isEmpty(senderName)) {
|
||||
sb.append("sender=").append(senderName).append(' ');
|
||||
}
|
||||
if (!TextUtils.isEmpty(totalAmount)) {
|
||||
sb.append("total=").append(totalAmount).append(' ');
|
||||
}
|
||||
sb.append("via=").append(channel);
|
||||
if (!TextUtils.isEmpty(sourceHint)) {
|
||||
sb.append(" src=").append(sourceHint.length() > 120
|
||||
? sourceHint.substring(0, 120) + "..." : sourceHint);
|
||||
}
|
||||
sb.append("\n");
|
||||
for (ClaimLine line : claims) {
|
||||
sb.append(line.nickname).append(" -> ").append(line.amount);
|
||||
if (!TextUtils.isEmpty(line.claimTime)) {
|
||||
sb.append(" (").append(line.claimTime).append(')');
|
||||
}
|
||||
sb.append('\n');
|
||||
}
|
||||
return sb.toString().trim();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ClaimLine {
|
||||
String nickname;
|
||||
String amount;
|
||||
String claimTime;
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,12 @@ public final class TngRootBypassHook {
|
||||
/** Promon 混淆包名:1.9.10 为 vhvlnqgy,旧版为 xwwqazamx。 */
|
||||
private static final String[] PROMON_PKG_PREFIXES = {"vhvlnqgy", "xwwqazamx"};
|
||||
|
||||
/**
|
||||
* 登录优先:少 Hook、少短路 native-bridge,避免 JNI DeleteLocalRef 损坏 → Runtime abort。
|
||||
* 能进 UserLogin 后再逐步打开诊断 Hook。
|
||||
*/
|
||||
private static final boolean LOGIN_FIRST_MINIMAL = true;
|
||||
|
||||
private static final String SECURITY_ERROR_ACTIVITY =
|
||||
"my.com.tngdigital.common.security.ui.SecurityErrorActivity";
|
||||
|
||||
@@ -230,11 +236,15 @@ public final class TngRootBypassHook {
|
||||
}
|
||||
XposedBridge.log(TAG + " install for " + lpparam.packageName
|
||||
+ " pid=" + Process.myPid()
|
||||
+ " proc=" + getProcessName());
|
||||
+ " proc=" + getProcessName()
|
||||
+ " minimal=" + LOGIN_FIRST_MINIMAL);
|
||||
|
||||
// Splash 常卡死;Instrumentation 强拉即可。双通道会抢跑导致 Login 重载/卡死。
|
||||
hookEarlyAttachLog(lpparam);
|
||||
hookConscryptStatsLogGuard();
|
||||
if (LOGIN_FIRST_MINIMAL) {
|
||||
installLoginFirstMinimal(lpparam);
|
||||
return;
|
||||
}
|
||||
hookRegistrationFlowGuard(lpparam);
|
||||
hookSplashForceLogin(lpparam);
|
||||
hookLoginDismissSplash(lpparam);
|
||||
@@ -277,6 +287,43 @@ public final class TngRootBypassHook {
|
||||
hookWebViewErrorDiag(lpparam);
|
||||
}
|
||||
|
||||
/** 最小集:藏 root + 拦自杀 Java 链 + bl#a/b,不碰 native-bridge 探测短路。 */
|
||||
private static void installLoginFirstMinimal(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
hookSplashForceLogin(lpparam);
|
||||
hookLoginDismissSplash(lpparam);
|
||||
// 点区号会走 onCountryClick → i7.l loading;真 show → HWUI setName/gralloc abort
|
||||
hookLoadingDialogSkip();
|
||||
// setName 一律 noop(A16+Zygisk 下 native setName→dlopen libandroid 易 ART abort)。
|
||||
// 区号 Compose 必须 HW:软件绘制会 IllegalArgumentException(hardware bitmaps)。
|
||||
hookHardwareRendererSetNameNoop();
|
||||
hookCallingCodeAllowHwSurface(lpparam);
|
||||
|
||||
RootBypassHelper.hookFileExists(lpparam);
|
||||
RootBypassHelper.hookRuntimeExec(lpparam);
|
||||
RootBypassHelper.hookSystemGetProperty(lpparam);
|
||||
ProcMapsFilterHook.install(lpparam);
|
||||
|
||||
hookAntiSuicide();
|
||||
hookPromonSuicideUpstream(lpparam);
|
||||
hookUncaughtPromonException(lpparam);
|
||||
hookKillApplicationHandler(lpparam);
|
||||
hookBlockSecurityErrorLaunch(lpparam);
|
||||
hookPromonNativeGuard(lpparam);
|
||||
hookPromonLifecycle(lpparam);
|
||||
hookActivityThreadExit(lpparam);
|
||||
hookFinishAllActivityAndKillApp(lpparam);
|
||||
hookSecurityUrlOpeners(lpparam);
|
||||
hookJailBroken(lpparam);
|
||||
hookJailBrokenRpc(lpparam);
|
||||
hookAppSecurityManager(lpparam);
|
||||
hookAppSecurityCallbacks(lpparam);
|
||||
hookSecurityErrorActivity(lpparam);
|
||||
// seccomp 开着时必须 stub TigerTally init,否则 fork getprop 永不退出 → App.onCreate ANR
|
||||
hookTigerTally(lpparam);
|
||||
hookTigerTallyAppWrappers(lpparam);
|
||||
XposedBridge.log(TAG + " login-first minimal hooks armed");
|
||||
}
|
||||
|
||||
/**
|
||||
* Zygisk/命名空间下 android.util.StatsLog native 常 UnsatisfiedLinkError,
|
||||
* Conscrypt TLS 指标线程一写就炸 → ART fatal。直接 noop 指标写入。
|
||||
@@ -1066,30 +1113,130 @@ public final class TngRootBypassHook {
|
||||
}
|
||||
|
||||
/**
|
||||
* 非区号:清 HW flag + 拦 enableHW(成对,防半开黑屏);
|
||||
* 区号 Compose / BottomSelect:完整 HW(软件绘制 Compose 必黑)。
|
||||
* Dialog.show 窗口期可拦 setName。
|
||||
* 一律跳过 HardwareRenderer.setName。
|
||||
* Pixel/A16 + Zygisk 命名空间下 native setName → dlopen("libandroid.so") 失败会 ART abort。
|
||||
*/
|
||||
private static void hookHwuiBySurfacePolicy(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
private static void hookHardwareRendererSetNameNoop() {
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
"android.graphics.HardwareRenderer", null, "setName", String.class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
if (Looper.myLooper() != Looper.getMainLooper()) {
|
||||
return;
|
||||
}
|
||||
if (!shouldBlockSetNameOnMain()) {
|
||||
return;
|
||||
}
|
||||
param.setResult(null);
|
||||
}
|
||||
});
|
||||
XposedBridge.log(TAG + " hooked HardwareRenderer.setName (dialog-only block)");
|
||||
XposedBridge.log(TAG + " hooked HardwareRenderer.setName (always noop)");
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " HardwareRenderer.setName hook failed: " + t.getMessage());
|
||||
XposedBridge.log(TAG + " HardwareRenderer.setName noop failed: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 区号 Compose:标记 surface + 保证 HW flag(勿清、勿拦 enableHW)。
|
||||
* 软件绘制会崩:Software rendering doesn't support hardware bitmaps。
|
||||
*/
|
||||
private static void hookCallingCodeAllowHwSurface(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
XC_MethodHook markOn = new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
Intent intent = extractIntent(param.args);
|
||||
if (isCallingCodeIntent(intent)) {
|
||||
callingCodeSurfaceActive = true;
|
||||
XposedBridge.log(TAG + " callingCode surface ON (HW path)");
|
||||
}
|
||||
}
|
||||
};
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
Activity.class, "startActivity", Intent.class, markOn);
|
||||
XposedHelpers.findAndHookMethod(
|
||||
Activity.class, "startActivity", Intent.class, Bundle.class, markOn);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " startActivity callingCode mark failed: " + t.getMessage());
|
||||
}
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
Instrumentation.class,
|
||||
"execStartActivity",
|
||||
Context.class,
|
||||
android.os.IBinder.class,
|
||||
android.os.IBinder.class,
|
||||
Activity.class,
|
||||
Intent.class,
|
||||
int.class,
|
||||
Bundle.class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
Intent intent = (Intent) param.args[4];
|
||||
if (isCallingCodeIntent(intent)) {
|
||||
callingCodeSurfaceActive = true;
|
||||
XposedBridge.log(TAG
|
||||
+ " callingCode surface ON (execStart HW)");
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " execStart callingCode mark failed: " + t.getMessage());
|
||||
}
|
||||
try {
|
||||
XposedHelpers.findAndHookMethod(
|
||||
Instrumentation.class,
|
||||
"callActivityOnCreate",
|
||||
Activity.class,
|
||||
Bundle.class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
Activity activity = (Activity) param.args[0];
|
||||
if (activity == null) {
|
||||
return;
|
||||
}
|
||||
if (isCallingCodeActivity(activity.getClass().getName())) {
|
||||
callingCodeSurfaceActive = true;
|
||||
try {
|
||||
Window window = activity.getWindow();
|
||||
if (window != null) {
|
||||
window.addFlags(
|
||||
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
XposedBridge.log(TAG
|
||||
+ " callingCode onCreate — keep HW for Compose");
|
||||
}
|
||||
}
|
||||
});
|
||||
XposedHelpers.findAndHookMethod(
|
||||
Instrumentation.class,
|
||||
"callActivityOnDestroy",
|
||||
Activity.class,
|
||||
new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
Activity activity = (Activity) param.args[0];
|
||||
if (activity != null
|
||||
&& isCallingCodeActivity(activity.getClass().getName())) {
|
||||
callingCodeSurfaceActive = false;
|
||||
XposedBridge.log(TAG + " callingCode surface OFF");
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " callingCode lifecycle mark failed: " + t.getMessage());
|
||||
}
|
||||
XposedBridge.log(TAG + " callingCode allow-HW surface armed");
|
||||
}
|
||||
|
||||
/**
|
||||
* 非区号:清 HW flag + 拦 enableHW(成对,防半开黑屏);
|
||||
* 区号 Compose / BottomSelect:完整 HW(软件绘制 Compose 必黑)。
|
||||
* Dialog.show 窗口期可拦 setName。
|
||||
*/
|
||||
private static void hookHwuiBySurfacePolicy(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
// setName 一律 noop(A16 Zygisk 下 native setName 会 abort)
|
||||
hookHardwareRendererSetNameNoop();
|
||||
|
||||
try {
|
||||
Class<?> vri = XposedHelpers.findClass("android.view.ViewRootImpl", null);
|
||||
@@ -1976,7 +2123,9 @@ public final class TngRootBypassHook {
|
||||
}
|
||||
|
||||
/**
|
||||
* Promon lifecycle:1.9.10 用 vhvlnqgy.u,旧版用 w。只吞异常,不全拦。
|
||||
* Promon lifecycle:1.9.10 用 vhvlnqgy.u,旧版用 w。
|
||||
* 实测 afterHook 仍跑 native → DeleteLocalRef 损坏 → ART Runtime abort (signal 6)。
|
||||
* 登录优先:onActivity* / onApplication* 全部 noop,不调原 native。
|
||||
*/
|
||||
private static void hookPromonLifecycle(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
Class<?> promonExc = findPromonClass(lpparam.classLoader, "W");
|
||||
@@ -2005,25 +2154,37 @@ public final class TngRootBypassHook {
|
||||
if (!name.startsWith("onActivity") && !name.startsWith("onApplication")) {
|
||||
continue;
|
||||
}
|
||||
XposedBridge.hookMethod(method, new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
if (!param.hasThrowable()) {
|
||||
return;
|
||||
if (LOGIN_FIRST_MINIMAL) {
|
||||
// 不调原 native:避免 JNI DeleteLocalRef → ART abort
|
||||
XposedBridge.hookMethod(method, new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
setSafeHookResult(param, method);
|
||||
}
|
||||
Throwable t = param.getThrowable();
|
||||
if (isPromonThrowable(t, promonExc)) {
|
||||
XposedBridge.log(TAG + " swallowed " + t.getClass().getSimpleName()
|
||||
+ " in " + lifecycleName + "#" + method.getName());
|
||||
param.setThrowable(null);
|
||||
});
|
||||
} else {
|
||||
XposedBridge.hookMethod(method, new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
if (!param.hasThrowable()) {
|
||||
return;
|
||||
}
|
||||
Throwable t = param.getThrowable();
|
||||
if (isPromonThrowable(t, promonExc)) {
|
||||
XposedBridge.log(TAG + " swallowed " + t.getClass().getSimpleName()
|
||||
+ " in " + lifecycleName + "#" + method.getName());
|
||||
param.setThrowable(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
hooked++;
|
||||
}
|
||||
if (hooked > 0) {
|
||||
XposedBridge.log(TAG + " hooked " + hooked
|
||||
+ " " + lifecycleName + " lifecycle method(s) (afterHook only)");
|
||||
XposedBridge.log(TAG + " hooked " + hooked + " " + lifecycleName
|
||||
+ " lifecycle ("
|
||||
+ (LOGIN_FIRST_MINIMAL ? "noop (login-first)" : "afterHook only")
|
||||
+ ")");
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " " + lifecycleName + " lifecycle hook failed: "
|
||||
@@ -2061,8 +2222,8 @@ public final class TngRootBypassHook {
|
||||
}
|
||||
|
||||
/**
|
||||
* bl/R 全方法 short-circuit:a/b 之外的方法仍会跑 native,~40s 后 stack_chk/SEGV。
|
||||
* a.run 是 bl#b 后台 Runnable,必须 beforeHook 直接 return。
|
||||
* 登录优先:只 short-circuit bl#a / bl#b(文档有效路径)。全拦 bl/R 六个方法会破坏
|
||||
* u.onActivityCreated JNI → ART abort;a.run 仍单独 noop。
|
||||
*/
|
||||
private static void hookPromonBlSwallowExceptions(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
for (String simple : new String[]{"bl", "R"}) {
|
||||
@@ -2079,6 +2240,10 @@ public final class TngRootBypassHook {
|
||||
try {
|
||||
int hooked = 0;
|
||||
for (Method method : clazz.getDeclaredMethods()) {
|
||||
String methodName = method.getName();
|
||||
if (!"a".equals(methodName) && !"b".equals(methodName)) {
|
||||
continue;
|
||||
}
|
||||
XposedBridge.hookMethod(method, new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
@@ -2089,7 +2254,7 @@ public final class TngRootBypassHook {
|
||||
}
|
||||
if (hooked > 0) {
|
||||
XposedBridge.log(TAG + " hooked " + hooked + " " + clazz.getName()
|
||||
+ " method(s), all short-circuit");
|
||||
+ " method(s), a/b short-circuit only");
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " " + clazz.getName() + " short-circuit failed: "
|
||||
@@ -2423,18 +2588,56 @@ public final class TngRootBypassHook {
|
||||
}
|
||||
|
||||
/**
|
||||
* TigerTally:init/initCommon + genericNt1 必须短路。
|
||||
* 只短路 genericNt1 而放行 init 会导致 encodeUid 缺失(cloudauth 403)、JNI 损坏、主线程 ABRT 无响应。
|
||||
* seccomp 开着时:必须 stub init/initCommon/genericNt1,否则 fork getprop 卡死。
|
||||
* seccomp 关且非登录优先:只 stub genericNt1 探测。
|
||||
*/
|
||||
private static boolean isTigerShortCircuit(String className, String methodName) {
|
||||
if ("com.aliyun.TigerTally.TigerTallyAPI".equals(className)
|
||||
&& ("init".equals(methodName) || "initCommon".equals(methodName))) {
|
||||
return true;
|
||||
|| "com.aliyun.TigerTally.t.B".equals(className)
|
||||
|| "com.aliyun.TigerTally.t.C".equals(className)) {
|
||||
if ("init".equals(methodName) || "initCommon".equals(methodName)
|
||||
|| "genericNt1".equals(methodName) || "initialize".equals(methodName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return "com.aliyun.TigerTally.t.B".equals(className)
|
||||
&& "genericNt1".equals(methodName);
|
||||
}
|
||||
|
||||
/** TNG 封装层:CaptchaInitializer / TigerTallyApiWrapper 直接 noop,避免 Startup latch 卡住。 */
|
||||
private static void hookTigerTallyAppWrappers(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
String[] classes = {
|
||||
"my.com.tngdigital.captcha.TigerTallyApiWrapper",
|
||||
"my.com.tngdigital.app.launcher.initializer.CaptchaInitializer",
|
||||
};
|
||||
for (String className : classes) {
|
||||
try {
|
||||
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
|
||||
int n = 0;
|
||||
for (Method method : clazz.getDeclaredMethods()) {
|
||||
String name = method.getName();
|
||||
if (!"initialize".equals(name) && !"init".equals(name)
|
||||
&& !"create".equals(name)) {
|
||||
continue;
|
||||
}
|
||||
XposedBridge.hookMethod(method, new XC_MethodHook() {
|
||||
@Override
|
||||
protected void beforeHookedMethod(MethodHookParam param) {
|
||||
XposedBridge.log(TAG + " TigerTally wrapper SC "
|
||||
+ className + "#" + method.getName());
|
||||
setSafeHookResult(param, method);
|
||||
}
|
||||
});
|
||||
n++;
|
||||
}
|
||||
XposedBridge.log(TAG + " hooked TigerTally wrapper " + className + " n=" + n);
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " TigerTally wrapper " + className
|
||||
+ " skipped: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Object safeJnicReturn(int cmd, Object[] args) {
|
||||
if (args != null) {
|
||||
for (Object arg : args) {
|
||||
|
||||
Reference in New Issue
Block a user