fix(tng): 修复区号页 HWUI 闪退并放行 Compose HW 绘制
拦截 HardwareRenderer.setName,校验/缓存 libandroid.so,避免软件绘制撞 hardware bitmap;附带 Money Packet hook 与 mitm 脚本。
This commit is contained in:
@@ -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