fix(tng): 修复区号页 HWUI 闪退并放行 Compose HW 绘制

拦截 HardwareRenderer.setName,校验/缓存 libandroid.so,避免软件绘制撞 hardware bitmap;附带 Money Packet hook 与 mitm 脚本。
This commit is contained in:
mars
2026-08-04 12:26:27 +08:00
parent 609635aba1
commit 5378a34f58
8 changed files with 1394 additions and 76 deletions

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

View File

@@ -6,6 +6,7 @@
* pc==lr 循环 SEGV 也 skiplibc++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 时跳回 lrLooper 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=129Promon 自杀是 kill(pid,SIGABRT) → si_code=SI_USER。
* 不拦 tgkill/tkillART 的 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 mappersphal 失败时尝试绝对路径兜底
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 调用方 PLTlibhwui/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 1abort 仍拦截。
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();

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

View File

@@ -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() {
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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 一律 noopA16+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 一律 noopA16 Zygisk 下 native setName 会 abort
hookHardwareRendererSetNameNoop();
try {
Class<?> vri = XposedHelpers.findClass("android.view.ViewRootImpl", null);
@@ -1976,7 +2123,9 @@ public final class TngRootBypassHook {
}
/**
* Promon lifecycle1.9.10 用 vhvlnqgy.u旧版用 w。只吞异常,不全拦。
* Promon lifecycle1.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-circuita/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 aborta.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 {
}
/**
* TigerTallyinit/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) {