finish 4.1.9.52

This commit is contained in:
yincong
2026-05-11 12:12:08 +08:00
parent 62a77d34c1
commit 62767e3358
11 changed files with 916 additions and 17 deletions
+13 -6
View File
@@ -23,7 +23,7 @@ function scanPatterns() {
console.log(`"${name}": "${offset}", 绝对地址: ${address}`);
break;
case "downloadFileAddr":
console.log(`"${name}": "${offset}", 绝对地址: ${address}`);
console.log(`"${name}": "${offset.sub(0x08)}", 绝对地址: ${address.sub(0x08)}`);
break;
case "uploadImageAddr":
console.log(`"${name}": "${offset.sub(0x30)}", 绝对地址: ${address.sub(0x30)}`);
@@ -32,13 +32,16 @@ function scanPatterns() {
console.log(`"${name}": "${offset.sub(0x44)}", 绝对地址: ${address.sub(0x44)}`);
break;
case "buf2RespAddr":
console.log(`"${name}": "${offset.sub(0x10)}", 绝对地址: ${address.sub(0x10)}`);
console.log(`"${name}": "${offset.sub(0x14)}", 绝对地址: ${address.sub(0x14)}`);
break;
case "startDownloadMedia":
console.log(`"${name}": "${offset.sub(0x28)}", 绝对地址: ${address.sub(0x28)}`);
break;
case "downloadVideoAddr":
console.log(`"${name}": "${offset.sub(0xc)}", 绝对地址: ${address.sub(0xc)}`);
console.log(`"${name}": "${offset.sub(0x2c)}", 绝对地址: ${address.sub(0x2c)}`);
break;
case "downloadImageAddr":
console.log(`"${name}": "${offset.sub(0x18)}", 绝对地址: ${address.sub(0x18)}`);
break;
}
},
@@ -64,9 +67,13 @@ const myPatterns = [
name: "req2bufEnterAddr",
pattern: "09 0F 46 F8 C9 01 00 B4 E8 03 18 AA 2A 21 40 B9"
},
{
name: "downloadImageAddr",
pattern: "68 52 41 F9 19 75 41 F9 1A 00 14 8B 5F 03 19 EB"
},
{
name: "downloadFileAddr",
pattern: "08 01 40 F9 A8 83 1B F8 08 50 41 F9 08 29 41 F9"
pattern: "FF 07 00 B9 68 52 41 F9 15 71 41 F9 E0 03 14 AA"
},
{
name: "uploadImageAddr",
@@ -78,7 +85,7 @@ const myPatterns = [
},
{
name: "buf2RespAddr",
pattern: "3C 00 80 52 E0 C3 00 91"
pattern: "E8 DF C1 39 28 E6 FF 36 E0 33 40 F9 E8 3B 40 F9"
},
{
name: "startDownloadMedia",
@@ -86,7 +93,7 @@ const myPatterns = [
},
{
name: "downloadVideoAddr",
pattern: "F5 03 00 AA 76 43 09 91 E0 03 16 AA"
pattern: "E0 03 01 91 01 00 80 52 A5 5B 80 52 06 00 80 52"
}
];
+3 -2
View File
@@ -18,7 +18,7 @@ var triggerX1Payload;
var insertMsgAddr;
// 回复消息回调函数地址 (基于用户提供: 0x24BDE40)
var replyCallbackFuncAddr = baseAddr.add(0x24BDE40);
var replyCallbackFuncAddr = baseAddr.add(0x260C444);
var replyProtobufAddr = replyCallbackFuncAddr.add(0x50);
var patchReplyProtobufFunc1 = replyCallbackFuncAddr.add(0x10);
var patchReplyProtobufFunc1Byte;
@@ -26,13 +26,14 @@ var patchReplyProtobufFunc2 = replyCallbackFuncAddr.add(0x30);
var patchReplyProtobufFunc2Byte;
var replyProtobufDeleteAddr = replyCallbackFuncAddr.add(0x6c);
var replyProtobufDeleteAddrByte;
var replyMessageCallbackFunc1 = baseAddr.add(0x8C29A08); // 复用图片的callback函数
// Reply消息相关地址 - 需要导出为全局变量供file.js使用
var sendReplyMessageAddr = ptr(0);
var replyMessageAddr = ptr(0);
var replyCgiAddr = ptr(0);
var replyProtobufBufAddr = ptr(0); // 用于存储protobuf数据的缓冲区
var replyMessageCallbackFunc1 = baseAddr.add(0x89170D0); // 复用图片的callback函数
// Reply消息内容相关
var replyContent = ""; // 回复的文本内容
+59
View File
@@ -55,6 +55,52 @@ func sendHandler(w http.ResponseWriter, r *http.Request) {
Content: v.Data.File,
Type: v.Type,
}
} else if v.Type == "reply" {
if v.Data.ReplyMessage == nil {
Error("reply_message为空")
continue
}
rm := v.Data.ReplyMessage
// 提取被回复消息的内容
referContent := ""
referMsgType := 1 // 默认text
if len(rm.Message) > 0 {
switch rm.Message[0].Type {
case "text":
referContent = rm.Message[0].Data.Text
referMsgType = 1
case "image":
referMsgType = 3
case "video":
referMsgType = 43
case "file":
referMsgType = 49
}
}
// 提取发送者昵称
displayName := ""
if rm.Sender != nil {
displayName = rm.Sender.Nickname
}
// msgsource需要JSON unescape(双重编码: \\u003c → \u003c → <
msgsource := jsonUnescapeString(rm.MsgResource)
msgChan <- &SendMsg{
UserId: req.UserID,
GroupID: req.GroupID,
Content: v.Data.Text,
Type: "reply",
ReferMsgId: rm.MessageId,
ReferMsgSender: rm.UserID,
ReferMsgType: referMsgType,
ReferCreateTime: rm.Time,
ReferMsgsource: msgsource,
ReferDisplayName: displayName,
ReferContent: referContent,
}
}
}
@@ -123,3 +169,16 @@ func SendHttpReq(jsonData []byte) {
Info("返回内容", "status", resp.StatusCode, "body", string(body))
}
// jsonUnescapeString 对双重JSON编码的字符串做unescape
// 例如: \\u003c → \u003c (第一次json.Unmarshal) → < (本函数)
func jsonUnescapeString(s string) string {
if s == "" {
return s
}
var result string
if err := json.Unmarshal([]byte(`"`+s+`"`), &result); err != nil {
return s
}
return result
}
+15 -6
View File
@@ -63,6 +63,14 @@ type SendMsg struct {
VideoId string
Duration int32
VideoSize int32
ReferMsgId string
ReferMsgSender string
ReferMsgType int
ReferCreateTime int64
ReferMsgsource string
ReferDisplayName string
ReferContent string
}
type VideoInfo struct {
@@ -83,12 +91,13 @@ type Message struct {
}
type SendRequestData struct {
Id string `json:"id,omitempty"`
Text string `json:"text,omitempty"`
File string `json:"file,omitempty"`
URL string `json:"url,omitempty"`
QQ string `json:"qq,omitempty"`
Media []byte `json:"media,omitempty"`
Id string `json:"id,omitempty"`
Text string `json:"text,omitempty"`
File string `json:"file,omitempty"`
URL string `json:"url,omitempty"`
QQ string `json:"qq,omitempty"`
Media []byte `json:"media,omitempty"`
ReplyMessage *WechatMessage `json:"reply_message,omitempty"`
}
type Config struct {
+404
View File
@@ -0,0 +1,404 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v3.5.1
// source: proto/wxproto/reply_msg.proto
package wxproto
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// 发送回复(引用)消息的protobuf结构
// CG路径: /cgi-bin/micromsg-bin/sendappmsg
// 消息类型: appmsg type=57
type WxSendReplyMsg struct {
state protoimpl.MessageState `protogen:"open.v1"`
Header *ReplyMsgHeader `protobuf:"bytes,1,opt,name=header" json:"header,omitempty"`
Body *ReplyMsgBody `protobuf:"bytes,2,opt,name=body" json:"body,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *WxSendReplyMsg) Reset() {
*x = WxSendReplyMsg{}
mi := &file_proto_wxproto_reply_msg_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *WxSendReplyMsg) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*WxSendReplyMsg) ProtoMessage() {}
func (x *WxSendReplyMsg) ProtoReflect() protoreflect.Message {
mi := &file_proto_wxproto_reply_msg_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use WxSendReplyMsg.ProtoReflect.Descriptor instead.
func (*WxSendReplyMsg) Descriptor() ([]byte, []int) {
return file_proto_wxproto_reply_msg_proto_rawDescGZIP(), []int{0}
}
func (x *WxSendReplyMsg) GetHeader() *ReplyMsgHeader {
if x != nil {
return x.Header
}
return nil
}
func (x *WxSendReplyMsg) GetBody() *ReplyMsgBody {
if x != nil {
return x.Body
}
return nil
}
// Header: 固定64字节
type ReplyMsgHeader struct {
state protoimpl.MessageState `protogen:"open.v1"`
Flag []byte `protobuf:"bytes,1,opt,name=flag" json:"flag,omitempty"` // 固定 [0x00]
Timestamp *uint32 `protobuf:"varint,2,opt,name=timestamp" json:"timestamp,omitempty"` // 请求时间戳(秒)
ClientProof []byte `protobuf:"bytes,3,opt,name=client_proof,json=clientProof" json:"client_proof,omitempty"` // 16字节随机数据/md5
DeviceId *uint64 `protobuf:"varint,4,opt,name=device_id,json=deviceId" json:"device_id,omitempty"` // 设备标识(大整数)
Platform *string `protobuf:"bytes,5,opt,name=platform" json:"platform,omitempty"` // 平台信息 "UnifiedPCMac 26 arm64"
Version *uint32 `protobuf:"varint,6,opt,name=version" json:"version,omitempty"` // 版本/标记 = 163
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ReplyMsgHeader) Reset() {
*x = ReplyMsgHeader{}
mi := &file_proto_wxproto_reply_msg_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ReplyMsgHeader) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ReplyMsgHeader) ProtoMessage() {}
func (x *ReplyMsgHeader) ProtoReflect() protoreflect.Message {
mi := &file_proto_wxproto_reply_msg_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ReplyMsgHeader.ProtoReflect.Descriptor instead.
func (*ReplyMsgHeader) Descriptor() ([]byte, []int) {
return file_proto_wxproto_reply_msg_proto_rawDescGZIP(), []int{1}
}
func (x *ReplyMsgHeader) GetFlag() []byte {
if x != nil {
return x.Flag
}
return nil
}
func (x *ReplyMsgHeader) GetTimestamp() uint32 {
if x != nil && x.Timestamp != nil {
return *x.Timestamp
}
return 0
}
func (x *ReplyMsgHeader) GetClientProof() []byte {
if x != nil {
return x.ClientProof
}
return nil
}
func (x *ReplyMsgHeader) GetDeviceId() uint64 {
if x != nil && x.DeviceId != nil {
return *x.DeviceId
}
return 0
}
func (x *ReplyMsgHeader) GetPlatform() string {
if x != nil && x.Platform != nil {
return *x.Platform
}
return ""
}
func (x *ReplyMsgHeader) GetVersion() uint32 {
if x != nil && x.Version != nil {
return *x.Version
}
return 0
}
// Body: 消息体
type ReplyMsgBody struct {
state protoimpl.MessageState `protogen:"open.v1"`
Sender *string `protobuf:"bytes,1,opt,name=sender" json:"sender,omitempty"` // 接收方wxid (field1是target)
Unknown2 []byte `protobuf:"bytes,2,opt,name=unknown2" json:"unknown2,omitempty"` // 空 (必须显式存在)
Unknown3 *int32 `protobuf:"varint,3,opt,name=unknown3" json:"unknown3,omitempty"` // 0 (必须显式存在)
Receiver *string `protobuf:"bytes,4,opt,name=receiver" json:"receiver,omitempty"` // 发送方wxid
MsgType *int32 `protobuf:"varint,5,opt,name=msg_type,json=msgType" json:"msg_type,omitempty"` // 消息类型 = 57
Content []byte `protobuf:"bytes,6,opt,name=content" json:"content,omitempty"` // XML内容 (appmsg XML)
SendTimestamp *int64 `protobuf:"varint,7,opt,name=send_timestamp,json=sendTimestamp" json:"send_timestamp,omitempty"` // 发送时间戳
ClientMsgId *string `protobuf:"bytes,8,opt,name=client_msg_id,json=clientMsgId" json:"client_msg_id,omitempty"` // 消息ID
Unknown9 *int32 `protobuf:"varint,9,opt,name=unknown9" json:"unknown9,omitempty"` // 0
Flag *int32 `protobuf:"varint,10,opt,name=flag" json:"flag,omitempty"` // 1
Unknown11 *int32 `protobuf:"varint,11,opt,name=unknown11" json:"unknown11,omitempty"` // 0
Msgsource []byte `protobuf:"bytes,12,opt,name=msgsource" json:"msgsource,omitempty"` // msgsource XML
Unknown13 []byte `protobuf:"bytes,13,opt,name=unknown13" json:"unknown13,omitempty"` // 空 (必须显式存在)
Unknown14 []byte `protobuf:"bytes,14,opt,name=unknown14" json:"unknown14,omitempty"` // 空 (必须显式存在)
Unknown15 []byte `protobuf:"bytes,15,opt,name=unknown15" json:"unknown15,omitempty"` // 空 (必须显式存在)
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ReplyMsgBody) Reset() {
*x = ReplyMsgBody{}
mi := &file_proto_wxproto_reply_msg_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ReplyMsgBody) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ReplyMsgBody) ProtoMessage() {}
func (x *ReplyMsgBody) ProtoReflect() protoreflect.Message {
mi := &file_proto_wxproto_reply_msg_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ReplyMsgBody.ProtoReflect.Descriptor instead.
func (*ReplyMsgBody) Descriptor() ([]byte, []int) {
return file_proto_wxproto_reply_msg_proto_rawDescGZIP(), []int{2}
}
func (x *ReplyMsgBody) GetSender() string {
if x != nil && x.Sender != nil {
return *x.Sender
}
return ""
}
func (x *ReplyMsgBody) GetUnknown2() []byte {
if x != nil {
return x.Unknown2
}
return nil
}
func (x *ReplyMsgBody) GetUnknown3() int32 {
if x != nil && x.Unknown3 != nil {
return *x.Unknown3
}
return 0
}
func (x *ReplyMsgBody) GetReceiver() string {
if x != nil && x.Receiver != nil {
return *x.Receiver
}
return ""
}
func (x *ReplyMsgBody) GetMsgType() int32 {
if x != nil && x.MsgType != nil {
return *x.MsgType
}
return 0
}
func (x *ReplyMsgBody) GetContent() []byte {
if x != nil {
return x.Content
}
return nil
}
func (x *ReplyMsgBody) GetSendTimestamp() int64 {
if x != nil && x.SendTimestamp != nil {
return *x.SendTimestamp
}
return 0
}
func (x *ReplyMsgBody) GetClientMsgId() string {
if x != nil && x.ClientMsgId != nil {
return *x.ClientMsgId
}
return ""
}
func (x *ReplyMsgBody) GetUnknown9() int32 {
if x != nil && x.Unknown9 != nil {
return *x.Unknown9
}
return 0
}
func (x *ReplyMsgBody) GetFlag() int32 {
if x != nil && x.Flag != nil {
return *x.Flag
}
return 0
}
func (x *ReplyMsgBody) GetUnknown11() int32 {
if x != nil && x.Unknown11 != nil {
return *x.Unknown11
}
return 0
}
func (x *ReplyMsgBody) GetMsgsource() []byte {
if x != nil {
return x.Msgsource
}
return nil
}
func (x *ReplyMsgBody) GetUnknown13() []byte {
if x != nil {
return x.Unknown13
}
return nil
}
func (x *ReplyMsgBody) GetUnknown14() []byte {
if x != nil {
return x.Unknown14
}
return nil
}
func (x *ReplyMsgBody) GetUnknown15() []byte {
if x != nil {
return x.Unknown15
}
return nil
}
var File_proto_wxproto_reply_msg_proto protoreflect.FileDescriptor
const file_proto_wxproto_reply_msg_proto_rawDesc = "" +
"\n" +
"\x1dproto/wxproto/reply_msg.proto\x12\awxproto\"l\n" +
"\x0eWxSendReplyMsg\x12/\n" +
"\x06header\x18\x01 \x01(\v2\x17.wxproto.ReplyMsgHeaderR\x06header\x12)\n" +
"\x04body\x18\x02 \x01(\v2\x15.wxproto.ReplyMsgBodyR\x04body\"\xb8\x01\n" +
"\x0eReplyMsgHeader\x12\x12\n" +
"\x04flag\x18\x01 \x01(\fR\x04flag\x12\x1c\n" +
"\ttimestamp\x18\x02 \x01(\rR\ttimestamp\x12!\n" +
"\fclient_proof\x18\x03 \x01(\fR\vclientProof\x12\x1b\n" +
"\tdevice_id\x18\x04 \x01(\x04R\bdeviceId\x12\x1a\n" +
"\bplatform\x18\x05 \x01(\tR\bplatform\x12\x18\n" +
"\aversion\x18\x06 \x01(\rR\aversion\"\xc0\x03\n" +
"\fReplyMsgBody\x12\x16\n" +
"\x06sender\x18\x01 \x01(\tR\x06sender\x12\x1a\n" +
"\bunknown2\x18\x02 \x01(\fR\bunknown2\x12\x1a\n" +
"\bunknown3\x18\x03 \x01(\x05R\bunknown3\x12\x1a\n" +
"\breceiver\x18\x04 \x01(\tR\breceiver\x12\x19\n" +
"\bmsg_type\x18\x05 \x01(\x05R\amsgType\x12\x18\n" +
"\acontent\x18\x06 \x01(\fR\acontent\x12%\n" +
"\x0esend_timestamp\x18\a \x01(\x03R\rsendTimestamp\x12\"\n" +
"\rclient_msg_id\x18\b \x01(\tR\vclientMsgId\x12\x1a\n" +
"\bunknown9\x18\t \x01(\x05R\bunknown9\x12\x12\n" +
"\x04flag\x18\n" +
" \x01(\x05R\x04flag\x12\x1c\n" +
"\tunknown11\x18\v \x01(\x05R\tunknown11\x12\x1c\n" +
"\tmsgsource\x18\f \x01(\fR\tmsgsource\x12\x1c\n" +
"\tunknown13\x18\r \x01(\fR\tunknown13\x12\x1c\n" +
"\tunknown14\x18\x0e \x01(\fR\tunknown14\x12\x1c\n" +
"\tunknown15\x18\x0f \x01(\fR\tunknown15B>Z<github.com/yincongcyincong/weixin-macos/onebot/proto/wxproto"
var (
file_proto_wxproto_reply_msg_proto_rawDescOnce sync.Once
file_proto_wxproto_reply_msg_proto_rawDescData []byte
)
func file_proto_wxproto_reply_msg_proto_rawDescGZIP() []byte {
file_proto_wxproto_reply_msg_proto_rawDescOnce.Do(func() {
file_proto_wxproto_reply_msg_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_wxproto_reply_msg_proto_rawDesc), len(file_proto_wxproto_reply_msg_proto_rawDesc)))
})
return file_proto_wxproto_reply_msg_proto_rawDescData
}
var file_proto_wxproto_reply_msg_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_proto_wxproto_reply_msg_proto_goTypes = []any{
(*WxSendReplyMsg)(nil), // 0: wxproto.WxSendReplyMsg
(*ReplyMsgHeader)(nil), // 1: wxproto.ReplyMsgHeader
(*ReplyMsgBody)(nil), // 2: wxproto.ReplyMsgBody
}
var file_proto_wxproto_reply_msg_proto_depIdxs = []int32{
1, // 0: wxproto.WxSendReplyMsg.header:type_name -> wxproto.ReplyMsgHeader
2, // 1: wxproto.WxSendReplyMsg.body:type_name -> wxproto.ReplyMsgBody
2, // [2:2] is the sub-list for method output_type
2, // [2:2] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
}
func init() { file_proto_wxproto_reply_msg_proto_init() }
func file_proto_wxproto_reply_msg_proto_init() {
if File_proto_wxproto_reply_msg_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_wxproto_reply_msg_proto_rawDesc), len(file_proto_wxproto_reply_msg_proto_rawDesc)),
NumEnums: 0,
NumMessages: 3,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_proto_wxproto_reply_msg_proto_goTypes,
DependencyIndexes: file_proto_wxproto_reply_msg_proto_depIdxs,
MessageInfos: file_proto_wxproto_reply_msg_proto_msgTypes,
}.Build()
File_proto_wxproto_reply_msg_proto = out.File
file_proto_wxproto_reply_msg_proto_goTypes = nil
file_proto_wxproto_reply_msg_proto_depIdxs = nil
}
+42
View File
@@ -0,0 +1,42 @@
syntax = "proto2";
package wxproto;
option go_package = "github.com/yincongcyincong/weixin-macos/onebot/proto/wxproto";
// 发送回复(引用)消息的protobuf结构
// CG路径: /cgi-bin/micromsg-bin/sendappmsg
// 消息类型: appmsg type=57
message WxSendReplyMsg {
optional ReplyMsgHeader header = 1;
optional ReplyMsgBody body = 2;
}
// Header: 固定64字节
message ReplyMsgHeader {
optional bytes flag = 1; // 固定 [0x00]
optional uint32 timestamp = 2; // 请求时间戳(秒)
optional bytes client_proof = 3; // 16字节随机数据/md5
optional uint64 device_id = 4; // 设备标识(大整数)
optional string platform = 5; // 平台信息 "UnifiedPCMac 26 arm64"
optional uint32 version = 6; // 版本/标记 = 163
}
// Body: 消息体
message ReplyMsgBody {
optional string sender = 1; // 接收方wxid (field1是target)
optional bytes unknown2 = 2; // 空 (必须显式存在)
optional int32 unknown3 = 3; // 0 (必须显式存在)
optional string receiver = 4; // 发送方wxid
optional int32 msg_type = 5; // 消息类型 = 57
optional bytes content = 6; // XML内容 (appmsg XML)
optional int64 send_timestamp = 7; // 发送时间戳
optional string client_msg_id = 8; // 消息ID
optional int32 unknown9 = 9; // 0
optional int32 flag = 10; // 1
optional int32 unknown11 = 11; // 0
optional bytes msgsource = 12; // msgsource XML
optional bytes unknown13 = 13; // 空 (必须显式存在)
optional bytes unknown14 = 14; // 空 (必须显式存在)
optional bytes unknown15 = 15; // 空 (必须显式存在)
}
+177
View File
@@ -0,0 +1,177 @@
package main
import (
"encoding/hex"
"fmt"
"math/rand"
"time"
"google.golang.org/protobuf/proto"
"github.com/yincongcyincong/weixin-macos/onebot/proto/wxproto"
)
// BuildReplyMsgProto 构建发送回复消息的protobuf并返回hex编码的字符串
func BuildReplyMsgProto(sender, receiver string, replyInfo *ReplyInfo) (string, error) {
now := time.Now().Unix()
// 构建appmsg XML
appmsgXml := buildReplyAppmsgXml(replyInfo)
// 构建客户端消息ID
clientMsgId := fmt.Sprintf("%s_%d_%d_xwechat_1", sender, now, rand.Intn(100))
// msgsource
msgsource := "<msgsource><alnode><fr>1</fr></alnode></msgsource>"
// proto2 需要使用指针
var (
unknown2 = []byte{}
unknown3 = int32(0)
msgType = int32(57)
unknown9 = int32(0)
flag = int32(1)
unknown11 = int32(0)
unknown13 = []byte{}
unknown14 = []byte{}
unknown15 = []byte{}
timestamp = uint32(now)
deviceId = generateDeviceId()
version = uint32(163)
)
msg := &wxproto.WxSendReplyMsg{
Header: &wxproto.ReplyMsgHeader{
Flag: []byte{0x00},
Timestamp: &timestamp,
ClientProof: generateRandomBytes(16),
DeviceId: &deviceId,
Platform: proto.String("UnifiedPCMac 26 arm64"),
Version: &version,
},
Body: &wxproto.ReplyMsgBody{
Sender: &receiver,
Unknown2: unknown2,
Unknown3: &unknown3,
Receiver: &sender,
MsgType: &msgType,
Content: []byte(appmsgXml),
SendTimestamp: proto.Int64(now),
ClientMsgId: &clientMsgId,
Unknown9: &unknown9,
Flag: &flag,
Unknown11: &unknown11,
Msgsource: []byte(msgsource),
Unknown13: unknown13,
Unknown14: unknown14,
Unknown15: unknown15,
},
}
data, err := proto.Marshal(msg)
if err != nil {
return "", fmt.Errorf("marshal reply proto failed: %w", err)
}
fmt.Println(fmt.Printf("0x% x\n", data))
return hex.EncodeToString(data), nil
}
// ReplyInfo 回复消息的全部信息
type ReplyInfo struct {
Content string // 回复的文本内容
MsgId string // 被回复消息的svrid
MsgSender string // 被回复消息的发送者wxid
MsgType int // 被回复消息的类型 (1=text, 3=image, 43=video, 49=appmsg)
CreateTime int64 // 被回复消息的时间戳(毫秒)
Msgsource string // 被回复消息的msgsource
DisplayName string // 被回复消息发送者的昵称
MsgContent string // 被回复消息的内容
}
// buildReplyAppmsgXml 构建回复消息的appmsg XML,字段顺序匹配微信真实protobuf
func buildReplyAppmsgXml(info *ReplyInfo) string {
// 时间戳:毫秒转秒
createTime := info.CreateTime / 1000
xml := `<appmsg appid="" sdkver="0">`
xml += `<title>` + escapeXmlStr(info.Content) + `</title>`
xml += `<des></des>`
xml += `<action></action>`
xml += `<type>57</type>`
xml += `<showtype>0</showtype>`
xml += `<soundtype>0</soundtype>`
xml += `<mediatagname></mediatagname>`
xml += `<messageext></messageext>`
xml += `<messageaction></messageaction>`
xml += `<content></content>`
xml += `<contentattr>0</contentattr>`
xml += `<url></url>`
xml += `<lowurl></lowurl>`
xml += `<dataurl></dataurl>`
xml += `<lowdataurl></lowdataurl>`
xml += `<songalbumurl></songalbumurl>`
xml += `<songlyric></songlyric>`
xml += `<template_id></template_id>`
xml += `<appattach><totallen>0</totallen><attachid></attachid><emoticonmd5></emoticonmd5><fileext></fileext><aeskey></aeskey></appattach>`
xml += `<extinfo></extinfo>`
xml += `<sourceusername></sourceusername>`
xml += `<sourcedisplayname></sourcedisplayname>`
xml += `<thumburl></thumburl>`
xml += `<md5></md5>`
xml += `<statextstr></statextstr>`
// refermsg - 字段顺序与微信一致: chatusr → type → createtime → msgsource → displayname → svrid → fromusr → content
xml += `<refermsg>`
xml += `<chatusr>` + escapeXmlStr(info.MsgSender) + `</chatusr>`
xml += `<type>` + fmt.Sprintf("%d", info.MsgType) + `</type>`
xml += `<createtime>` + fmt.Sprintf("%d", createTime) + `</createtime>`
xml += `<msgsource>` + escapeXmlStr(info.Msgsource) + `</msgsource>`
xml += `<displayname>` + escapeXmlStr(info.DisplayName) + `</displayname>`
xml += `<svrid>` + escapeXmlStr(info.MsgId) + `</svrid>`
xml += `<fromusr>` + escapeXmlStr(info.MsgSender) + `</fromusr>`
xml += `<content>` + escapeXmlStr(info.MsgContent) + `</content>`
xml += `</refermsg>`
xml += `</appmsg>`
xml += `<fromusername>` + escapeXmlStr(info.MsgSender) + `</fromusername>`
return xml
}
// escapeXmlStr 简单的XML转义
func escapeXmlStr(s string) string {
result := ""
for _, c := range s {
switch c {
case '&':
result += "&amp;"
case '<':
result += "&lt;"
case '>':
result += "&gt;"
case '"':
result += "&quot;"
case '\'':
result += "&apos;"
default:
result += string(c)
}
}
return result
}
// generateRandomBytes 生成随机字节
func generateRandomBytes(n int) []byte {
b := make([]byte, n)
for i := range b {
b[i] = byte(rand.Intn(256))
}
return b
}
// generateDeviceId 动态生成设备ID (大整数,高位置1模拟真实设备)
func generateDeviceId() uint64 {
return rand.Uint64() | (0xFFFFFFFF << 32)
}
+164
View File
@@ -137,6 +137,23 @@ var textProtoHexGlobal = "";
var imgProtoHexGlobal = "";
// 视频消息protobuf全局变量 (从Go直接传入hex编码)
var videoProtoHexGlobal = "";
// 回复消息protobuf全局变量 (从Go直接传入hex编码)
var replyProtoHexGlobal = "";
// 回复消息全局变量
var replyCallbackFuncAddr = baseAddr.add({{.replyCallbackFuncAddr}});
var replyProtobufAddr = replyCallbackFuncAddr.add(0x50);
var patchReplyProtobufFunc1 = replyCallbackFuncAddr.add(0x10);
var patchReplyProtobufFunc1Byte;
var patchReplyProtobufFunc2 = replyCallbackFuncAddr.add(0x30);
var patchReplyProtobufFunc2Byte;
var replyProtobufDeleteAddr = replyCallbackFuncAddr.add(0x6c);
var replyProtobufDeleteAddrByte;
var replyMessageCallbackFunc = baseAddr.add({{.replyMessageCallbackFunc}});
var replyCgiAddr = ptr(0);
var sendReplyMessageAddr = ptr(0);
var replyMessageAddr = ptr(0);
var replyProtoX1PayloadAddr = ptr(0);
// -------------------------全局变量分区-------------------------
@@ -344,6 +361,10 @@ function attachReq2buf() {
insertMsgAddr.writePointer(sendVideoMessageAddr);
console.log("[+] 发送视频消息成功! Req2Buf 已将 X24+0x60 指向新地址: " + sendVideoMessageAddr +
"[+] Req2Buf 写入后内存预览: " + insertMsgAddr);
} else if (sendMsgType === "reply") {
insertMsgAddr.writePointer(sendReplyMessageAddr);
console.log("[+] 发送回复消息成功! Req2Buf 已将 X24+0x60 指向新地址: " + sendReplyMessageAddr +
"[+] Req2Buf 写入后内存预览: " + insertMsgAddr);
}
}
});
@@ -853,6 +874,148 @@ function attachGetCallbackFromWrapper() {
setImmediate(attachGetCallbackFromWrapper);
// -------------------------发送回复消息分区-------------------------
function setupSendReplyMessageDynamic() {
replyCgiAddr = Memory.alloc(128);
sendReplyMessageAddr = Memory.alloc(256);
replyMessageAddr = Memory.alloc(256);
replyProtoX1PayloadAddr = Memory.alloc(4096);
patchString(replyCgiAddr, "/cgi-bin/micromsg-bin/sendappmsg");
sendReplyMessageAddr.add(0x00).writeU64(0);
sendReplyMessageAddr.add(0x08).writeU64(0);
sendReplyMessageAddr.add(0x10).writeU64(0);
sendReplyMessageAddr.add(0x18).writeU64(1);
sendReplyMessageAddr.add(0x20).writeU32(taskIdGlobal);
sendReplyMessageAddr.add(0x28).writePointer(replyMessageAddr);
replyMessageAddr.add(0x00).writePointer(replyMessageCallbackFunc);
replyMessageAddr.add(0x08).writeU32(taskIdGlobal);
replyMessageAddr.add(0x0c).writeU32(0x6e);
replyMessageAddr.add(0x10).writeU64(0x3);
replyMessageAddr.add(0x18).writePointer(replyCgiAddr);
replyMessageAddr.add(0x20).writeU64(0x22);
replyMessageAddr.add(0x28).writeU64(uint64("0x8000000000000030"));
replyMessageAddr.add(0x30).writeU64(uint64("0x0000000001010100"));
patchReplyProtobufFunc1Byte = patchReplyProtobufFunc1.readByteArray(4);
patchReplyProtobufFunc2Byte = patchReplyProtobufFunc2.readByteArray(4);
replyProtobufDeleteAddrByte = replyProtobufDeleteAddr.readByteArray(4);
console.log("[+] Reply message setup complete. CgiAddr: " + replyCgiAddr + " SendAddr: " + sendReplyMessageAddr);
}
setImmediate(setupSendReplyMessageDynamic);
function patchReplyProtoBuf() {
Interceptor.attach(replyCallbackFuncAddr, {
onEnter: function (args) {
var firstValue = this.context.sp.add(0x10).readU32();
if (firstValue === taskIdGlobal) {
if (patchReplyProtobufFunc1.readU32() !== 3573751839) {
Memory.patchCode(patchReplyProtobufFunc1, 4, code => {
const cw = new Arm64Writer(code, {pc: patchReplyProtobufFunc1});
cw.putNop();
cw.flush();
});
Memory.patchCode(patchReplyProtobufFunc2, 4, code => {
const cw = new Arm64Writer(code, {pc: patchReplyProtobufFunc2});
cw.putNop();
cw.flush();
});
Memory.patchCode(replyProtobufDeleteAddr, 4, code => {
const cw = new Arm64Writer(code, {pc: replyProtobufDeleteAddr});
cw.putNop();
cw.flush();
});
}
} else {
if (patchReplyProtobufFunc1.readU32() === 3573751839) {
Memory.patchCode(patchReplyProtobufFunc1, 4, code => {
const cw = new Arm64Writer(code, {pc: patchReplyProtobufFunc1});
cw.putBytes(new Uint8Array(patchReplyProtobufFunc1Byte));
cw.flush();
});
Memory.patchCode(patchReplyProtobufFunc2, 4, code => {
const cw = new Arm64Writer(code, {pc: patchReplyProtobufFunc2});
cw.putBytes(new Uint8Array(patchReplyProtobufFunc2Byte));
cw.flush();
});
Memory.patchCode(replyProtobufDeleteAddr, 4, code => {
const cw = new Arm64Writer(code, {pc: replyProtobufDeleteAddr});
cw.putBytes(new Uint8Array(replyProtobufDeleteAddrByte));
cw.flush();
});
}
}
}
})
}
setImmediate(patchReplyProtoBuf);
function attachReplyProto() {
Interceptor.attach(replyProtobufAddr, {
onEnter: function (args) {
var currTaskId = this.context.sp.add(0x30).readU32();
if (currTaskId !== taskIdGlobal) {
return;
}
if (!replyProtoHexGlobal || replyProtoHexGlobal.length === 0) {
console.error("[!] replyProtoHexGlobal 为空");
return;
}
const finalPayload = hexToByteArray(replyProtoHexGlobal);
replyProtoX1PayloadAddr.writeByteArray(finalPayload);
this.context.x1 = replyProtoX1PayloadAddr;
this.context.x2 = ptr(finalPayload.length);
console.log("[+] Reply protobuf注入完成, length=" + finalPayload.length);
},
});
}
setImmediate(attachReplyProto);
function triggerSendReplyMessage(taskId, sender, receiver, protoHex, payloadHex) {
if (!taskId || !receiver || !sender) {
console.error("[!] reply: taskId or receiver or sender is empty!");
return "fail";
}
if (!triggerX0 || !triggerX1Payload) {
console.error("[!] triggerX0 或 triggerX1Payload 尚未初始化,请等待 hook 捕获");
return "fail";
}
replyProtoHexGlobal = protoHex;
taskIdGlobal = taskId;
replyMessageAddr.add(0x08).writeU32(taskIdGlobal);
sendReplyMessageAddr.add(0x20).writeU32(taskIdGlobal);
const payloadData = hexToByteArray(payloadHex);
triggerX1Payload.writeByteArray(payloadData);
triggerX1Payload.add(0x18).writePointer(replyCgiAddr);
triggerX1Payload.add(0xb8).writePointer(triggerX1Payload.add(0xc0));
triggerX1Payload.add(0x190).writePointer(triggerX1Payload.add(0x198));
sendMsgType = "reply"
const MMStartTask = new NativeFunction(sendFuncAddr, 'int64', ['pointer', 'pointer']);
try {
MMStartTask(triggerX0, triggerX1Payload);
return "1";
} catch (e) {
console.error("[!] Error trigger reply MMStartTask: " + e);
return "fail";
}
}
// -------------------------发送回复消息分区-------------------------
rpc.exports = {
triggerSendImgMessage: triggerSendImgMessage,
triggerUploadImg: triggerUploadImg,
@@ -860,6 +1023,7 @@ rpc.exports = {
triggerDownload: triggerDownload,
triggerUploadVideo: triggerUploadVideo,
triggerSendVideoMessage: triggerSendVideoMessage,
triggerSendReplyMessage: triggerSendReplyMessage,
};
// -------------------------发送图片消息分区-------------------------
+5
View File
@@ -124,6 +124,11 @@ func BuildSendPayload(taskId int64, msgType string) string {
payloadData[16] = 0x10
payloadData[28] = 0x21
payloadData[92] = 0x6E
case "reply":
payloadData[0] = 0x6E
payloadData[16] = 0x10
payloadData[28] = 0x22
payloadData[92] = 0x6E
}
// 构建完整buffer: taskId(4字节) + payloadData(412字节) = 0x1A0字节
+31 -2
View File
@@ -142,6 +142,29 @@ func SendWechatMsg(m *SendMsg) {
case "download":
result := fridaScript.ExportsCall("triggerDownload", targetId, m.FIleCdnUrl, m.AesKey, m.FilePath, m.FileType)
Info("📩 下载任务执行结果", "result", result, "task_id", currTaskId, "wechat_id", myWechatId, "target_id", targetId)
case "reply":
replyInfo := &ReplyInfo{
Content: m.Content,
MsgId: m.ReferMsgId,
MsgSender: m.ReferMsgSender,
MsgType: m.ReferMsgType,
CreateTime: m.ReferCreateTime,
Msgsource: m.ReferMsgsource,
DisplayName: m.ReferDisplayName,
MsgContent: m.ReferContent,
}
protoHex, err := BuildReplyMsgProto(myWechatId, targetId, replyInfo)
if err != nil {
Error("构建回复protobuf失败", "err", err)
return
}
payloadHex := BuildSendPayload(currTaskId, "reply")
result := fridaScript.ExportsCall("triggerSendReplyMessage", currTaskId, myWechatId, targetId, protoHex, payloadHex)
Info("📩 发送回复任务执行结果", "result", result, "task_id", currTaskId, "wechat_id", myWechatId, "target_id", targetId)
if result != "1" {
Error("发送回复失败", "task_id", currTaskId, "target_id", targetId, "result", result)
return
}
}
select {
@@ -225,8 +248,14 @@ func HandleMsg(jsonData []byte) ([]byte, error) {
Error("XML解析失败", "err", err)
return nil, err
}
data, err := DownloadFile(fileMsg.Emoji.ThumbUrl)
// 优先thumburl,为空则用externurl
emojiUrl := fileMsg.Emoji.ThumbUrl
if emojiUrl == "" {
emojiUrl = fileMsg.Emoji.ExternUrl
}
data, err := DownloadFile(emojiUrl)
if err != nil {
Error("下载表情失败", "err", err)
return nil, err
+3 -1
View File
@@ -18,5 +18,7 @@
"downloadFileAddr": "0x4E0E264",
"downloadVideoAddr": "0x4E28044",
"videoCallbackFuncAddr": "0x26DD8C8",
"videoMessageCallbackFunc": "0x8C2F0F8"
"videoMessageCallbackFunc": "0x8C2F0F8",
"replyCallbackFuncAddr": "0x260C444",
"replyMessageCallbackFunc": "0x8C29A08"
}