WeChatSendHook.java
5.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
package com.example.wechathook;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Handler;
import android.os.Looper;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;
/**
* WeChatSendHook: Route A — 通过 WeChat 内部 API 发送文本消息。
*
* 调用链(逆向 WeChat 8.0.74):
* 外部 am broadcast → BroadcastReceiver (WeChat 进程内)
* → w11.s1.a(talker) → w11.r1 (SendMsgCgiFactory, p1.TEXT pipeline)
* → r1.e(content) → 设置文本
* → r1.b() → executeByPPC
* → w11.r0(talker, content, type=1, ...)
* → message 表写入 (status=SENDING)
* → /cgi-bin/micromsg-bin/newsendmsg over MMTLS
*
* 触发:
* adb shell am broadcast -a com.hermes.wechat.SEND_MSG_INTERNAL \
* --es to "wxid_xxx" --es text "Hello from hook"
*
* Talker 格式:
* 1 对 1: wxid_xxx (对方 WeChat ID)
* 群聊: 12345678901@chatroom
*
* 注意:发送在 WeChat 主线程执行,与用户正常发送路径完全一致。
* 不触发 A11y,不走 UI 自动化,消息经 MMTLS 加密发出。
*
* 类位置(WeChat 8.0.74 多 DEX):
* w11.s1 — classes9.dex
* w11.r1 — classes11.dex
* w11.r0 — classes12.dex
* p1.TEXT — classes9.dex (enum value 'd')
*/
public class WeChatSendHook implements IXposedHookLoadPackage {
private static final String ACTION = "com.hermes.wechat.SEND_MSG_INTERNAL";
private static ClassLoader sWeChat;
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
if (!"com.tencent.mm".equals(lpparam.packageName)) return;
// Only hook the main process (not :push/:tools sub-processes)
if (!lpparam.processName.equals("com.tencent.mm")) return;
sWeChat = lpparam.classLoader;
XposedBridge.log("[WeChatSendHook] Injected, registering receiver");
// Hook Application.onCreate to get Context for receiver registration.
// android.app.Application.onCreate() is a framework call — never AOT-inlined.
XposedHelpers.findAndHookMethod(
"android.app.Application", null,
"onCreate",
new de.robv.android.xposed.XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
Context ctx = (Context) param.thisObject;
if (ctx.getPackageName().equals("com.tencent.mm")) {
registerSendReceiver(ctx);
}
}
});
}
private static void registerSendReceiver(Context ctx) {
BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String talker = intent.getStringExtra("to");
String text = intent.getStringExtra("text");
if (talker == null || talker.isEmpty() || text == null || text.isEmpty()) {
XposedBridge.log("[WeChatSendHook] Received intent missing 'to' or 'text'");
return;
}
XposedBridge.log("[WeChatSendHook] Queuing send to=" + talker + " len=" + text.length());
sendOnMainThread(talker, text);
}
};
IntentFilter filter = new IntentFilter(ACTION);
// NOT_EXPORTED (0x4) — only accept from root/same-uid callers via am broadcast
ctx.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED);
XposedBridge.log("[WeChatSendHook] Receiver registered for " + ACTION);
}
private static void sendOnMainThread(String talker, String text) {
// WeChat's send path touches SQLite and the network dispatcher from the main thread.
new Handler(Looper.getMainLooper()).post(() -> {
try {
sendTextInternal(talker, text);
} catch (Throwable t) {
XposedBridge.log("[WeChatSendHook] Send failed: " + t);
}
});
}
/**
* Core send via w11.s1 factory (SendMsgCgiFactory):
* s1.a(talker) → r1 with pipeline=p1.TEXT, talker pre-set
* r1.e(content) → set field d (content)
* r1.b() → executeByPPC (no callback)
*
* r1 field mapping (WeChat 8.0.74):
* field b = talker (set by g())
* field d = content (set by e())
* field e = type (default 1=text, set by h())
* field l = pipeline (set to p1.TEXT by s1.a())
*/
private static void sendTextInternal(String talker, String text) throws Throwable {
Class<?> s1 = XposedHelpers.findClass("w11.s1", sWeChat);
// s1.a(talker) → r1 with g(talker) pre-called and l=p1.TEXT
Object r1 = XposedHelpers.callStaticMethod(s1, "a", talker);
// r1.e(content) → sets field d
XposedHelpers.callMethod(r1, "e", text);
// r1.b() → d(this,null,1,null) → c(q1.d) → executeByPPC
XposedHelpers.callMethod(r1, "b");
XposedBridge.log("[WeChatSendHook] Sent: to=" + talker);
}
}