diff --git a/frida/auto.js b/frida/auto.js index eff0fc6..73e4cf9 100644 --- a/frida/auto.js +++ b/frida/auto.js @@ -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" } ]; diff --git a/frida/reply.js b/frida/reply.js index 418cf49..e3d1db3 100644 --- a/frida/reply.js +++ b/frida/reply.js @@ -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 = ""; // 回复的文本内容 diff --git a/onebot/http.go b/onebot/http.go index e805a0f..f4af0ac 100644 --- a/onebot/http.go +++ b/onebot/http.go @@ -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 +} diff --git a/onebot/param.go b/onebot/param.go index f231644..bcf422a 100644 --- a/onebot/param.go +++ b/onebot/param.go @@ -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 { diff --git a/onebot/proto/wxproto/reply_msg.pb.go b/onebot/proto/wxproto/reply_msg.pb.go new file mode 100644 index 0000000..c19afdb --- /dev/null +++ b/onebot/proto/wxproto/reply_msg.pb.go @@ -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 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 +} diff --git a/onebot/proto/wxproto/reply_msg.proto b/onebot/proto/wxproto/reply_msg.proto new file mode 100644 index 0000000..957c1d1 --- /dev/null +++ b/onebot/proto/wxproto/reply_msg.proto @@ -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; // 空 (必须显式存在) +} diff --git a/onebot/reply_builder.go b/onebot/reply_builder.go new file mode 100644 index 0000000..9f935b2 --- /dev/null +++ b/onebot/reply_builder.go @@ -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 := "1" + + // 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: ×tamp, + 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 := `` + xml += `` + escapeXmlStr(info.Content) + `` + xml += `` + xml += `` + xml += `57` + xml += `0` + xml += `0` + xml += `` + xml += `` + xml += `` + xml += `` + xml += `0` + xml += `` + xml += `` + xml += `` + xml += `` + xml += `` + xml += `` + xml += `` + xml += `0` + xml += `` + xml += `` + xml += `` + xml += `` + xml += `` + xml += `` + + // refermsg - 字段顺序与微信一致: chatusr → type → createtime → msgsource → displayname → svrid → fromusr → content + xml += `` + xml += `` + escapeXmlStr(info.MsgSender) + `` + xml += `` + fmt.Sprintf("%d", info.MsgType) + `` + xml += `` + fmt.Sprintf("%d", createTime) + `` + xml += `` + escapeXmlStr(info.Msgsource) + `` + xml += `` + escapeXmlStr(info.DisplayName) + `` + xml += `` + escapeXmlStr(info.MsgId) + `` + xml += `` + escapeXmlStr(info.MsgSender) + `` + xml += `` + escapeXmlStr(info.MsgContent) + `` + xml += `` + xml += `` + + xml += `` + escapeXmlStr(info.MsgSender) + `` + + return xml +} + +// escapeXmlStr 简单的XML转义 +func escapeXmlStr(s string) string { + result := "" + for _, c := range s { + switch c { + case '&': + result += "&" + case '<': + result += "<" + case '>': + result += ">" + case '"': + result += """ + case '\'': + result += "'" + 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) +} diff --git a/onebot/script.js b/onebot/script.js index 2e641ea..e2e0d4d 100644 --- a/onebot/script.js +++ b/onebot/script.js @@ -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, }; // -------------------------发送图片消息分区------------------------- diff --git a/onebot/text_builder.go b/onebot/text_builder.go index c1e2946..c82d5f8 100644 --- a/onebot/text_builder.go +++ b/onebot/text_builder.go @@ -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字节 diff --git a/onebot/worker.go b/onebot/worker.go index b182914..c4792be 100644 --- a/onebot/worker.go +++ b/onebot/worker.go @@ -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 diff --git a/wechat_version/4_1_9_52_mac.json b/wechat_version/4_1_9_52_mac.json index 4ec1b93..36c455b 100644 --- a/wechat_version/4_1_9_52_mac.json +++ b/wechat_version/4_1_9_52_mac.json @@ -18,5 +18,7 @@ "downloadFileAddr": "0x4E0E264", "downloadVideoAddr": "0x4E28044", "videoCallbackFuncAddr": "0x26DD8C8", - "videoMessageCallbackFunc": "0x8C2F0F8" + "videoMessageCallbackFunc": "0x8C2F0F8", + "replyCallbackFuncAddr": "0x260C444", + "replyMessageCallbackFunc": "0x8C29A08" }