support send file

This commit is contained in:
yincong
2026-03-12 18:06:30 +08:00
parent bcac8ae75d
commit eb4c1c2e0e
13 changed files with 717 additions and 133 deletions
+2 -1
View File
@@ -12,4 +12,5 @@ release
onebot/audio
onebot/image
onebot/log
.comate
.comate
onebot/file
+7 -3
View File
@@ -37,7 +37,11 @@ const myPatterns = [
pattern: "09 0F 46 F8 C9 01 00 B4 E8 03 18 AA 2A 21 40 B9"
},
{
name: "downloadImagAddr",
name: "downloadFileAddr",
pattern: "68 52 41 F9 19 8D 41 F9 1A 00 14 8B 5F 03 19 EB"
},
{
name: "downloadImageAddr",
pattern: "68 52 41 F9 19 8D 41 F9 1A 00 14 8B 5F 03 19 EB"
},
{
@@ -53,8 +57,8 @@ const myPatterns = [
pattern: "3C 00 80 52 E0 C3 00 91"
},
{
name: "hdPicDownloadAddr",
pattern: "E0 63 00 91 41 00 80 52 25 5C 80 52 06 00 80 52"
name: "startDownloadMedia",
pattern: "B6 E3 02 D1 68 E1 01 D0 08 71 44 F9 08 01 40 F9 A8 83 1C F8 28 7C 42 39 09 1D 00 13 2A 48 40 F9 3F 01 00 71 48 B1 88 9A"
}
];
+62 -3
View File
@@ -6,6 +6,7 @@ if (!baseAddr) {
var buf2RespAddr = baseAddr.add(0x3721FA0)
var downloadImagAddr = baseAddr.add(0x4A6975C)
var startDownloadMedia = baseAddr.add(0x494663C)
var downloadFileAddr = baseAddr.add(0x4A084EC)
var downloadGlobalX0;
var downloadFileX1 = ptr(0)
@@ -140,14 +141,42 @@ function setReceiver() {
Interceptor.attach(startDownloadMedia, {
onEnter: function (args) {
if (downloadGlobalX0) {
downloadGlobalX0 = this.context.x0;
var fileIDAddr = this.context.x1.add(0x40).readPointer();
var fileId = fileIDAddr?.readUtf8String();
if (!fileId.endsWith("_1")) {
return
}
downloadGlobalX0 = this.context.x0;
console.log("downloadGlobalX0: " + downloadGlobalX0)
console.log(" [+] download file: ", fileId);
const t = this.context.x1.add(0xA0).readU32()
if (t === 3) {
this.context.x1.add(0xA0).writeU32(0x02);
}
}
})
Interceptor.attach(downloadFileAddr, {
onEnter: function (args) {
var dataPtr = this.context.x1;
var dataLen = this.context.x2.toInt32();
var fileId = this.context.sp.add(0x30).readPointer().readUtf8String();
var cdnUrl = this.context.x19.add(0x2F8).readPointer().readUtf8String();
if (dataLen > 0) {
var buffer = dataPtr.readByteArray(dataLen);
var uint8Array = new Uint8Array(buffer);
send({
type: "download",
media: Array.from(uint8Array),
file_id: fileId,
cdn_url: cdnUrl,
})
}
}
});
Interceptor.attach(downloadImagAddr, {
onEnter: function (args) {
var dataPtr = this.context.x1;
@@ -186,6 +215,21 @@ function getMessages(content, sender, mediaContent) {
messages.push({type: "image", data: {text: part}});
} else if (part.startsWith("<msg><voicemsg")) {
messages.push({type: "record", data: {text: part}});
} else if (part.startsWith("<?xml version=\"1.0\"?><msg><appmsg")) {
const regex = /<type>(.*?)<\/type>/s;
const match = part.match(regex);
if (match.length > 1) {
switch (match[1]) {
case "5":
messages.push({type: "share", data: {text: part}});
break
case "6":
messages.push({type: "file", data: {text: part}});
break
}
}
} else if (content.startsWith("<msg><emoji")) {
messages.push({type: "face", data: {text: part}});
} else {
messages.push({type: "text", data: {text: part}});
}
@@ -199,6 +243,21 @@ function getMessages(content, sender, mediaContent) {
mediaContent = mediaContent.subarray(audioStart);
}
messages.push({type: "record", data: {text: content, media: Array.from(mediaContent)}});
} else if (content.startsWith("<?xml version=\"1.0\"?><msg><appmsg")) {
const regex = /<type>(.*?)<\/type>/s;
const match = content.match(regex);
if (match.length > 1) {
switch (match[1]) {
case "5":
messages.push({type: "share", data: {text: content}});
break
case "6":
messages.push({type: "file", data: {text: content}});
break
}
}
} else if (content.startsWith("<msg><emoji")) {
messages.push({type: "face", data: {text: content}});
} else {
messages.push({type: "text", data: {text: content}});
}
+1 -1
View File
@@ -454,7 +454,7 @@ setImmediate(AttachSendTextProto);
// 拦截 SendTextProto 编码逻辑,注入自定义 Payload
function attachSendTextProto() {
console.log("[+] proto注入拦截目标地址: " + protobufAddr);
textProtoX1PayloadAddr = Memory.alloc(1024);
textProtoX1PayloadAddr = Memory.alloc(3096);
console.log("[+] Frida 分配的 Payload 地址: " + textProtoX1PayloadAddr);
Interceptor.attach(protobufAddr, {
+1 -1
View File
@@ -441,7 +441,7 @@ function triggerSendTextMessage(taskId, receiver, content, atUser) {
// 拦截 SendTextProto 编码逻辑,注入自定义 Payload
function attachSendTextProto() {
console.log("[+] proto注入拦截目标地址: " + protobufAddr);
textProtoX1PayloadAddr = Memory.alloc(1024);
textProtoX1PayloadAddr = Memory.alloc(3096);
console.log("[+] Frida 分配的 Payload 地址: " + textProtoX1PayloadAddr);
Interceptor.attach(protobufAddr, {
+1 -1
View File
@@ -321,7 +321,7 @@ setImmediate(AttachSendTextProto);
// 拦截 SendTextProto 编码逻辑,注入自定义 Payload
function attachSendTextProto() {
console.log("[+] proto注入拦截目标地址: " + protobufAddr);
textProtoX1PayloadAddr = Memory.alloc(1024);
textProtoX1PayloadAddr = Memory.alloc(3096);
console.log("[+] Frida 分配的 Payload 地址: " + textProtoX1PayloadAddr);
Interceptor.attach(protobufAddr, {
-9
View File
@@ -72,15 +72,6 @@ func initFlag() {
fmt.Println("ConnType", config.ConnType)
fmt.Println("SendInterval", config.SendInterval)
fmt.Println("LogLevel", logLevel)
err := EnsureDir("./audio")
if err != nil {
Fatal("❌ 无法创建音频目录", err)
}
err = EnsureDir("./image")
if err != nil {
Fatal("❌ 无法创建图片目录", err)
}
}
func initFridaGadget() {
+4 -1
View File
@@ -13,9 +13,12 @@ func Download(rawMsg []byte) error {
return err
}
Info("下载文件", "file_id", downloadReq.FileID, "media_len", len(downloadReq.Media), "cdn_url", downloadReq.CDNURL[:30])
Info("下载文件", "file_id", downloadReq.FileID, "media_len", len(downloadReq.Media), "cdn_url", downloadReq.CDNURL)
if downloadReqInter, ok := userID2FileMsgMap.Load(downloadReq.CDNURL); ok {
beforeDownloadReq := downloadReqInter.(*DownloadRequest)
if beforeDownloadReq.FilePath != "" {
return nil
}
beforeDownloadReq.Media = append(beforeDownloadReq.Media, downloadReq.Media...)
beforeDownloadReq.LastAppendTime = time.Now().UnixMilli()
} else {
+93 -2
View File
@@ -49,6 +49,12 @@ type SendMsg struct {
Content string
Type string
AtUser string
FIleCdnUrl string
Md5 string
AesKey string
FilePath string
FileType int
}
// SendRequest 请求结构体
@@ -109,8 +115,16 @@ type VoiceMsgInfo struct {
// FileMsg 对应 <msg> 标签
type FileMsg struct {
XMLName xml.Name `xml:"msg"`
Image Image `xml:"img"`
XMLName xml.Name `xml:"msg"`
Image Image `xml:"img"`
Emoji Emoji `xml:"emoji"`
GameExt GameExt `xml:"gameext"`
AppMsg AppMsg `xml:"appmsg"`
ExtCommonInfo ExtCommonInfo `xml:"extcommoninfo"`
FromUsername string `xml:"fromusername"`
Scene string `xml:"scene"`
AppInfo AppInfo `xml:"appinfo"`
CommentURL string `xml:"commenturl"`
}
// Image 对应 <img> 标签及其属性和子节点
@@ -143,9 +157,86 @@ type DownloadRequest struct {
Media []byte `json:"media"`
CDNURL string `json:"cdn_url"`
LastAppendTime int64 `json:"last_append_time"`
FilePath string `json:"file_path"`
}
type ScriptMessage struct {
Type string `json:"type"`
Payload interface{} `json:"payload"`
}
type Emoji struct {
FromUsername string `xml:"fromusername,attr"`
ToUserName string `xml:"tousername,attr"`
Type string `xml:"type,attr"`
IdBuffer string `xml:"idbuffer,attr"`
Md5 string `xml:"md5,attr"`
Len string `xml:"len,attr"`
ProductId string `xml:"productid,attr"`
AndroidMd5 string `xml:"androidmd5,attr"`
AndroidLen string `xml:"androidlen,attr"`
S60v3Md5 string `xml:"s60v3md5,attr"`
S60v3Len string `xml:"s60v3len,attr"`
S60v5Md5 string `xml:"s60v5md5,attr"`
S60v5Len string `xml:"s60v5len,attr"`
CdnUrl string `xml:"cdnurl,attr"`
DesignerId string `xml:"designerid,attr"`
ThumbUrl string `xml:"thumburl,attr"`
EncryptUrl string `xml:"encrypturl,attr"`
AesKey string `xml:"aeskey,attr"`
ExternUrl string `xml:"externurl,attr"`
ExternMd5 string `xml:"externmd5,attr"`
Width string `xml:"width,attr"`
Height string `xml:"height,attr"`
TpUrl string `xml:"tpurl,attr"`
TpAuthKey string `xml:"tpauthkey,attr"`
AttachedText string `xml:"attachedtext,attr"`
AttachedTextColor string `xml:"attachedtextcolor,attr"`
LensId string `xml:"lensid,attr"`
EmojiAttr string `xml:"emojiattr,attr"`
LinkId string `xml:"linkid,attr"`
Desc string `xml:"desc,attr"`
}
type GameExt struct {
Type string `xml:"type,attr"`
Content string `xml:"content,attr"`
}
type AppMsg struct {
AppID string `xml:"appid,attr"`
SDKVer string `xml:"sdkver,attr"`
Title string `xml:"title"`
Type string `xml:"type"`
Action string `xml:"action"`
AppAttach AppAttach `xml:"appattach"`
MD5 string `xml:"md5"`
WebViewShared WebViewShared `xml:"webviewshared"`
}
type AppAttach struct {
TotalLen string `xml:"totallen"`
FileExt string `xml:"fileext"`
AttachID string `xml:"attachid"`
CdnAttachURL string `xml:"cdnattachurl"`
CdnThumbAesKey string `xml:"cdnthumbaeskey"`
AesKey string `xml:"aeskey"`
EncryVer string `xml:"encryver"`
FileKey string `xml:"filekey"`
OverwriteNewMsgID string `xml:"overwrite_newmsgid"`
FileUploadToken string `xml:"fileuploadtoken"`
}
type WebViewShared struct {
JsAppID string `xml:"jsAppId"`
PublisherReqID string `xml:"publisherReqId"`
}
type ExtCommonInfo struct {
MediaExpireAt string `xml:"media_expire_at"`
}
type AppInfo struct {
Version string `xml:"version"`
AppName string `xml:"appname"`
}
+237 -9
View File
@@ -299,8 +299,16 @@ var uploadGetCallbackWrapperFuncAddr = baseAddr.add({{.uploadGetCallbackWrapperF
var uploadOnCompleteAddr = baseAddr.add({{.uploadOnCompleteAddr}});
var uploadOnCompleteFuncAddr = baseAddr.add({{.uploadOnCompleteFuncAddr}});
var downloadImagAddr = baseAddr.add({{.downloadImagAddr}});
var hdPicDownloadAddr = baseAddr.add({{.hdPicDownloadAddr}})
var startDownloadMedia = baseAddr.add({{.startDownloadMedia}})
var downloadFileAddr = baseAddr.add({{.downloadFileAddr}})
var downloadGlobalX0;
var downloadFileX1 = ptr(0)
var fileIdAddr = ptr(0)
var fileMd5Addr = ptr(0)
var downloadAesKeyAddr = ptr(0)
var filePathAddr = ptr(0)
var fileCdnUrlAddr = ptr(0)
var uploadImageX1 = ptr(0);
var imgCgiAddr = ptr(0);
var sendImgMessageAddr = ptr(0);
@@ -538,7 +546,7 @@ setImmediate(AttachSendTextProto);
// 拦截 SendTextProto 编码逻辑,注入自定义 Payload
function attachSendTextProto() {
textProtoX1PayloadAddr = Memory.alloc(2048);
textProtoX1PayloadAddr = Memory.alloc(3096);
console.log("[+] Frida 分配的 Payload 地址: " + textProtoX1PayloadAddr);
Interceptor.attach(protobufAddr, {
@@ -1171,12 +1179,25 @@ setImmediate(attachGetCallbackFromWrapper);
rpc.exports = {
triggerSendImgMessage: triggerSendImgMessage,
triggerUploadImg: triggerUploadImg,
triggerSendTextMessage: triggerSendTextMessage
triggerSendTextMessage: triggerSendTextMessage,
triggerDownload: triggerDownload,
};
// -------------------------发送图片消息分区-------------------------
// -------------------------接收消息分区-------------------------
function setupDownloadFileDynamic() {
downloadFileX1 = Memory.alloc(1624)
fileIdAddr = Memory.alloc(128)
fileMd5Addr = Memory.alloc(128)
downloadAesKeyAddr = Memory.alloc(128)
filePathAddr = Memory.alloc(256)
fileCdnUrlAddr = Memory.alloc(256)
}
setImmediate(setupDownloadFileDynamic)
function setReceiver() {
Interceptor.attach(buf2RespAddr, {
onEnter: function (args) {
@@ -1287,8 +1308,9 @@ function setReceiver() {
},
});
Interceptor.attach(hdPicDownloadAddr, {
Interceptor.attach(startDownloadMedia, {
onEnter: function (args) {
downloadGlobalX0 = this.context.x0;
var fileIDAddr = this.context.x1.add(0x40).readPointer();
var fileId = fileIDAddr?.readUtf8String();
if (!fileId.endsWith("_1")) {
@@ -1296,12 +1318,35 @@ function setReceiver() {
}
console.log(" [+] download file: ", fileId);
this.context.x1.add(0x148).writeByteArray([0xa0, 0x86, 0x01, 0x00]);
this.context.x1.add(0xA0).writeU32(0x02);
const t = this.context.x1.add(0xA0).readU32()
if (t === 3) {
this.context.x1.add(0xA0).writeU32(0x02);
}
}
})
Interceptor.attach(downloadFileAddr, {
onEnter: function (args) {
var dataPtr = this.context.x1;
var dataLen = this.context.x2.toInt32();
var fileId = this.context.sp.add(0x30).readPointer().readUtf8String();
var cdnUrl = this.context.x19.add(0x2F8).readPointer().readUtf8String();
if (dataLen > 0) {
var buffer = dataPtr.readByteArray(dataLen);
var uint8Array = new Uint8Array(buffer);
send({
type: "download",
media: Array.from(uint8Array),
file_id: fileId,
cdn_url: cdnUrl,
})
}
}
});
Interceptor.attach(downloadImagAddr, { // 建议使用函数起始地址或你计算出的偏移地址
Interceptor.attach(downloadImagAddr, {
onEnter: function (args) {
var dataPtr = this.context.x1;
var dataLen = this.context.x2.toInt32();
@@ -1323,10 +1368,163 @@ function setReceiver() {
});
}
// 使用 setImmediate 确保在模块加载后执行
setImmediate(setReceiver)
// fileType: HdImage => 1,Image => 2, thumbImage => 3, Video => 4, File => 5,
function triggerDownload(receiver, cdnUrl, aesKey, filePath, fileType) {
const downloadMediaPayload = [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x10
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x20
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x30
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xF0, 0xB6, 0x4C, 0xFC, 0x0A, 0x00, 0x00, 0x00, // 0x40
0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80,
0x80, 0x10, 0x4B, 0xFA, 0x0A, 0x00, 0x00, 0x00, // 0x58
0xB2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80,
0xF0, 0xB3, 0x4C, 0xFC, 0x0A, 0x00, 0x00, 0x00, // 0x70
0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80,
0x60, 0xC4, 0x2D, 0xFE, 0x0A, 0x00, 0x00, 0x00, // 0x88
0xC8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x90
0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, // 0x98
0x03, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, // 0xa0
0x00, 0x00, 0x00, 0x00, 0x01, 0xAA, 0xAA, 0xAA, // 0xa8
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0xb0
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0xc0
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0xd0
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0xd8
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0xe0
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0xf0
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x100
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x110
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x00, // 0x128
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x11, 0x28, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x148
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x02, 0x00, 0x00, 0xAA, 0xAA, 0xAA, // 0x170
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x00, // 0x180
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x1E, 0x00, 0x00, 0x00, 0xAA, 0xAA, 0xAA, 0xAA, // 0x1a0
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xAA, 0xAA, 0xAA, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x22, 0x1A, 0xFE, 0x0A, 0x00, 0x00, 0x00, // 0x1d0
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x1f0
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x200
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x288
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x298
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x2a0
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x00, 0x4F, 0x56, 0xFC, 0x0A, 0x00, 0x00, 0x00, // 0x2c0
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x300
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x00, // 0x318
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x340
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x00, // 0x378
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x03, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0x00, // 0x3e0
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
patchString(fileIdAddr, receiver + "_" + String(Math.floor(Date.now() / 1000)) + "_" + Math.floor(Math.random() * 1001) + "_1");
patchString(fileCdnUrlAddr, cdnUrl)
patchString(downloadAesKeyAddr, aesKey)
patchString(filePathAddr, filePath);
downloadFileX1.writeByteArray(downloadMediaPayload);
downloadFileX1.add(0x40).writePointer(fileIdAddr);
downloadFileX1.add(0x58).writePointer(fileCdnUrlAddr);
downloadFileX1.add(0x70).writePointer(downloadAesKeyAddr);
downloadFileX1.add(0x88).writePointer(filePathAddr);
downloadFileX1.add(0xa0).writeU32(fileType);
const startDwMedia = new NativeFunction(startDownloadMedia, 'int64', ['pointer', 'pointer']);
const result = startDwMedia(downloadGlobalX0, downloadFileX1);
console.log("下载调用结果: " + result);
return result;
}
function getMessages(content, sender, mediaContent) {
var messages = [];
if (sender.includes("@chatroom")) {
@@ -1339,6 +1537,21 @@ function getMessages(content, sender, mediaContent) {
messages.push({type: "image", data: {text: part}});
} else if (part.startsWith("<msg><voicemsg")) {
messages.push({type: "record", data: {text: part}});
} else if (part.startsWith("<?xml version=\"1.0\"?><msg><appmsg")) {
const regex = /<type>(.*?)<\/type>/s;
const match = part.match(regex);
if (match.length > 1) {
switch (match[1]) {
case "5":
messages.push({type: "share", data: {text: part}});
break
case "6":
messages.push({type: "file", data: {text: part}});
break
}
}
} else if (content.startsWith("<msg><emoji")) {
messages.push({type: "face", data: {text: part}});
} else {
messages.push({type: "text", data: {text: part}});
}
@@ -1352,6 +1565,21 @@ function getMessages(content, sender, mediaContent) {
mediaContent = mediaContent.subarray(audioStart);
}
messages.push({type: "record", data: {text: content, media: Array.from(mediaContent)}});
} else if (content.startsWith("<?xml version=\"1.0\"?><msg><appmsg")) {
const regex = /<type>(.*?)<\/type>/s;
const match = content.match(regex);
if (match.length > 1) {
switch (match[1]) {
case "5":
messages.push({type: "share", data: {text: content}});
break
case "6":
messages.push({type: "file", data: {text: content}});
break
}
}
} else if (content.startsWith("<msg><emoji")) {
messages.push({type: "face", data: {text: content}});
} else {
messages.push({type: "text", data: {text: content}});
}
+171 -99
View File
@@ -6,11 +6,12 @@ import (
"crypto/md5"
"encoding/base64"
"encoding/hex"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"io"
"math/rand"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
@@ -39,7 +40,7 @@ func SaveBase64Image(base64Data string) (string, string, error) {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
randomNumber := r.Intn(1000) // 生成 0-999 的随机数
timestamp := time.Now().Unix()
fileName := fmt.Sprintf("%d_%d.%s", randomNumber, timestamp, DetectImageFormat(data))
fileName := fmt.Sprintf("%d_%d.%s", randomNumber, timestamp, DetectFileFormat(data))
targetPath := config.ImagePath + fileName
dir := filepath.Dir(targetPath)
if err := os.MkdirAll(dir, 0755); err != nil {
@@ -73,36 +74,6 @@ func GetFileMD5(filePath string) (string, error) {
return hex.EncodeToString(hash.Sum(nil)), nil
}
func DetectImageFormat(data []byte) string {
if len(data) < 12 {
return "unknown"
}
switch {
case bytes.HasPrefix(data, []byte{0xFF, 0xD8, 0xFF}):
return "jpg"
case bytes.HasPrefix(data, []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}):
return "png"
case bytes.HasPrefix(data, []byte("GIF87a")) || bytes.HasPrefix(data, []byte("GIF89a")):
return "gif"
case bytes.HasPrefix(data, []byte{0x42, 0x4D}):
return "bmp"
case bytes.HasPrefix(data, []byte("RIFF")) && bytes.HasPrefix(data[8:], []byte("WEBP")):
return "webp"
default:
return "unknown"
}
}
func EnsureDir(path string) error {
_, err := os.Stat(path)
if os.IsNotExist(err) {
return os.MkdirAll(path, 0755)
}
return err
}
func SaveAudioFile(silkBytes []byte) (path string, err error) {
mp3Bytes, err := SilkToMp3(silkBytes)
if err != nil {
@@ -157,63 +128,7 @@ func SilkToMp3(silkBytes []byte) ([]byte, error) {
return out.Bytes(), nil
}
func HandleMsg(jsonData []byte) ([]byte, error) {
m := new(WechatMessage)
err := json.Unmarshal(jsonData, m)
if err != nil {
Error("解析消息失败", "err", err)
return nil, err
}
myWechatId = m.SelfID
if m.GroupId != "" {
userID2NicknameMap.Store(m.GroupId+"_"+m.UserID, m.Sender.Nickname)
}
for _, msg := range m.Message {
if msg.Type == "record" {
path, err := SaveAudioFile(msg.Data.Media)
if err != nil {
Error("保存音频失败", "err", err)
return nil, err
}
msg.Data.URL = "file://" + path
msg.Data.Media = nil
} else if msg.Type == "image" {
var fileMsg FileMsg
err = xml.Unmarshal([]byte(msg.Data.Text), &fileMsg)
if err != nil {
Error("XML解析失败", "err", err)
return nil, err
}
// 如果有图片等待图片下载完成再处理
if len(msg.Data.Media) == 0 {
for i := 0; i < 3; i++ {
if downloadMsgInter, ok := userID2FileMsgMap.Load(fileMsg.Image.ThumbURL); ok {
downloadReq := downloadMsgInter.(*DownloadRequest)
// 进行重试,最后一次进行兜底
if time.Now().UnixMilli()-downloadReq.LastAppendTime < 500 && i < 2 {
Info("等待图片下载完成", "times", i, "url", fileMsg.Image.ThumbURL)
time.Sleep(2 * time.Second)
continue
}
aesKey, _ := hex.DecodeString(fileMsg.Image.AesKey)
path, err := GetImagePath(downloadReq.Media, aesKey)
if err != nil {
Error("获取图片路径失败", "err", err)
return nil, err
}
msg.Data.URL = "file://" + path
userID2FileMsgMap.Delete(fileMsg.Image.ThumbURL)
}
}
}
}
}
return json.Marshal(m)
}
func GetImagePath(data []byte, key []byte) (string, error) {
func GetFilePath(data []byte, key []byte) (string, error) {
block, _ := aes.NewCipher(key)
decrypted := make([]byte, len(data))
bs := block.BlockSize()
@@ -221,17 +136,120 @@ func GetImagePath(data []byte, key []byte) (string, error) {
block.Decrypt(decrypted[i:i+bs], data[i:i+bs])
}
if bytes.HasPrefix(decrypted, []byte{0xFF, 0xD8, 0xFF}) {
return SaveImageToFile("jpg", decrypted)
} else if bytes.HasPrefix(decrypted, []byte{0x89, 0x50, 0x4E, 0x47}) {
return SaveImageToFile("png", decrypted)
} else if bytes.HasPrefix(decrypted, []byte("GIF89a")) || bytes.HasPrefix(decrypted, []byte("GIF87a")) {
return SaveImageToFile("gif", decrypted)
} else if bytes.HasPrefix(decrypted, []byte{0x42, 0x4D}) {
return SaveImageToFile("bmp", decrypted)
ext := DetectFileFormat(decrypted)
if ext == "unknown" {
return "", fmt.Errorf("无法解析的文件数据")
}
return "", fmt.Errorf("无法解析的图像数据")
return SaveFileToFile(ext, decrypted)
}
// DetectFileFormat 检测文件格式,返回扩展名
func DetectFileFormat(data []byte) string {
if len(data) < 8 {
return "unknown"
}
switch {
// 图片格式
case bytes.HasPrefix(data, []byte{0xFF, 0xD8, 0xFF}):
return "jpg"
case bytes.HasPrefix(data, []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}):
return "png"
case bytes.HasPrefix(data, []byte("GIF87a")) || bytes.HasPrefix(data, []byte("GIF89a")):
return "gif"
case bytes.HasPrefix(data, []byte{0x42, 0x4D}):
return "bmp"
case bytes.HasPrefix(data, []byte("RIFF")) && len(data) > 8 && bytes.HasPrefix(data[8:], []byte("WEBP")):
return "webp"
// 文档格式
case bytes.HasPrefix(data, []byte("%PDF")):
return "pdf"
// Office 2007+ 格式 (docx, xlsx, pptx 都是 ZIP 格式)
case bytes.HasPrefix(data, []byte{0x50, 0x4B, 0x03, 0x04}):
return detectOfficeFormat(data)
// Office 97-2003 格式 (OLE2 格式)
case bytes.HasPrefix(data, []byte{0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1}):
return detectLegacyOfficeFormat(data)
// 压缩文件
case bytes.HasPrefix(data, []byte("Rar!\x1a\x07")):
return "rar"
case bytes.HasPrefix(data, []byte("7z\xBC\xAF\x27\x1C")):
return "7z"
default:
return "unknown"
}
}
// detectOfficeFormat 检测 Office 2007+ 文件具体类型
func detectOfficeFormat(data []byte) string {
// 查找 ZIP 内的特定文件来区分类型
if bytes.Contains(data, []byte("[Content_Types].xml")) {
if bytes.Contains(data, []byte("word/")) {
return "docx"
}
if bytes.Contains(data, []byte("xl/")) {
return "xlsx"
}
if bytes.Contains(data, []byte("ppt/")) {
return "pptx"
}
}
// 普通 ZIP 文件
return "zip"
}
// detectLegacyOfficeFormat 检测 Office 97-2003 文件具体类型
func detectLegacyOfficeFormat(data []byte) string {
// 通过文件内容特征判断
if bytes.Contains(data, []byte("Word.Document")) {
return "doc"
}
if bytes.Contains(data, []byte("Excel.Sheet")) {
return "xls"
}
if bytes.Contains(data, []byte("PowerPoint.Show")) {
return "ppt"
}
return "ole"
}
// SaveFileToFile 通用文件保存函数
func SaveFileToFile(ext string, data []byte) (string, error) {
exePath, err := os.Executable()
if err != nil {
return "", err
}
r := rand.New(rand.NewSource(time.Now().UnixNano()))
randomNumber := r.Intn(1000)
timestamp := time.Now().Unix()
// 根据文件类型选择保存目录
dir := "file"
if ext == "jpg" || ext == "png" || ext == "gif" || ext == "bmp" || ext == "webp" {
dir = "image"
}
fileName := fmt.Sprintf("%d_%d.%s", randomNumber, timestamp, ext)
targetPath := filepath.Dir(exePath) + "/" + dir + "/" + fileName
// 确保目录存在
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
return "", err
}
err = os.WriteFile(targetPath, data, 0644)
if err != nil {
return "", err
}
return targetPath, nil
}
func SaveImageToFile(ext string, data []byte) (string, error) {
@@ -262,3 +280,57 @@ func GetWeChatPID() (int, error) {
return strconv.Atoi(strings.TrimSpace(string(output)))
}
func DownloadFile(urlStr string) ([]byte, error) {
if urlStr == "" {
return nil, errors.New("url is empty")
}
// 解析 URL 以判断协议
parsedURL, err := url.Parse(urlStr)
if err != nil {
return nil, errors.New("invalid URL format: " + err.Error())
}
// 处理 file:// 协议
if parsedURL.Scheme == "file" {
// 去除 "file://" 前缀,得到本地文件路径
filePath := strings.TrimPrefix(urlStr, "file://")
// 对于 Windows 路径可能需要额外处理,但你的路径是 macOS/Linux 格式
data, err := os.ReadFile(filePath)
if err != nil {
return nil, errors.New("failed to read local file: " + err.Error())
}
return data, nil
}
client := &http.Client{}
resp, err := client.Get(urlStr)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, errors.New("failed to download file: " + resp.Status)
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return data, nil
}
// DetectAndSaveImage 自动检测图片格式并保存到本地
func DetectAndSaveImage(data []byte) (string, error) {
// 先检测图片格式
ext := DetectFileFormat(data)
if ext == "unknown" {
return "", fmt.Errorf("无法识别的图片格式")
}
// 调用保存函数
return SaveImageToFile(ext, data)
}
+136 -2
View File
@@ -2,6 +2,10 @@ package main
import (
"context"
"encoding/hex"
"encoding/json"
"encoding/xml"
"errors"
"runtime/debug"
"sync/atomic"
"time"
@@ -41,10 +45,15 @@ func SendWechatMsg(m *SendMsg) {
targetId = m.GroupID
}
if targetId == "" {
Error("目标为空", "task_id", currTaskId, "target_id", targetId)
return
}
switch m.Type {
case "text":
result := fridaScript.ExportsCall("triggerSendTextMessage", currTaskId, targetId, m.Content, m.AtUser)
Info("📩 发送文本任务执行结果", "result", result, "task_id", currTaskId, "target_id", targetId, "content", m.Content, "at_user", m.AtUser)
Info("📩 发送文本任务执行结果", "result", result, "task_id", currTaskId, "target_id", targetId, "at_user", m.AtUser)
case "image":
targetPath, md5Str, err := SaveBase64Image(m.Content)
if err != nil {
@@ -57,12 +66,137 @@ func SendWechatMsg(m *SendMsg) {
case "send_image":
result := fridaScript.ExportsCall("triggerSendImgMessage", currTaskId, myWechatId, targetId)
Info("📩 发送图片任务执行结果", "result", result, "task_id", currTaskId, "wechat_id", myWechatId, "target_id", targetId)
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)
}
select {
case <-ctx.Done():
Info("任务执行超时!", "taskId", currTaskId)
Error("任务执行超时!", "taskId", currTaskId)
case <-finishChan:
Info("收到完成信号,任务完成", "taskId", currTaskId)
}
}
func HandleMsg(jsonData []byte) ([]byte, error) {
m := new(WechatMessage)
err := json.Unmarshal(jsonData, m)
if err != nil {
Error("解析消息失败", "err", err)
return nil, err
}
myWechatId = m.SelfID
if m.GroupId != "" {
userID2NicknameMap.Store(m.GroupId+"_"+m.UserID, m.Sender.Nickname)
}
for _, msg := range m.Message {
switch msg.Type {
case "record":
path, err := SaveAudioFile(msg.Data.Media)
if err != nil {
Error("保存音频失败", "err", err)
return nil, err
}
msg.Data.URL = "file://" + path
msg.Data.Media = nil
case "image":
var fileMsg FileMsg
err = xml.Unmarshal([]byte(msg.Data.Text), &fileMsg)
if err != nil {
Error("XML解析失败", "err", err)
return nil, err
}
path, err := GetDownloadPath(fileMsg.Image.MidImgURL, fileMsg.Image.AesKey)
if err != nil {
Error("获取文件路径失败", "err", err)
return nil, err
}
msg.Data.URL = "file://" + path
case "file":
var fileMsg FileMsg
err = xml.Unmarshal([]byte(msg.Data.Text), &fileMsg)
if err != nil {
Error("XML解析失败", "err", err)
return nil, err
}
path, err := GetDownloadPath(fileMsg.AppMsg.AppAttach.CdnAttachURL, fileMsg.AppMsg.AppAttach.AesKey)
if err != nil {
Error("获取文件路径失败", "err", err)
return nil, err
}
msg.Data.URL = "file://" + path
case "face":
var fileMsg FileMsg
err = xml.Unmarshal([]byte(msg.Data.Text), &fileMsg)
if err != nil {
Error("XML解析失败", "err", err)
return nil, err
}
data, err := DownloadFile(fileMsg.Emoji.ThumbUrl)
if err != nil {
Error("下载表情失败", "err", err)
return nil, err
}
path, err := DetectAndSaveImage(data)
if err != nil {
Error("保存表情失败", "err", err)
return nil, err
}
msg.Data.URL = "file://" + path
}
}
return json.Marshal(m)
}
func GetDownloadPath(cdnUrl, aesKeyStr string) (string, error) {
for i := 0; i < 5; i++ {
if downloadMsgInter, ok := userID2FileMsgMap.Load(cdnUrl); ok {
downloadReq := downloadMsgInter.(*DownloadRequest)
if downloadReq.FilePath != "" {
return downloadReq.FilePath, nil
}
// 检查数据是否还在接收中
timeSinceLastAppend := time.Now().UnixMilli() - downloadReq.LastAppendTime
Info("文件等待下载", "url", cdnUrl, "times", i, "last_append_time", timeSinceLastAppend)
// 如果数据仍在接收中(1秒内有新数据),继续等待
if timeSinceLastAppend < 1000 && i < 4 {
time.Sleep(2 * time.Second)
continue
}
// 数据接收完成,尝试解密
if len(downloadReq.Media) > 0 {
aesKey, err := hex.DecodeString(aesKeyStr)
if err != nil {
Error("AES key 解码失败", "err", err)
return "", err
}
filePath, err := GetFilePath(downloadReq.Media, aesKey)
if err != nil {
Error("获取文件路径失败", "err", err, "media_len", len(downloadReq.Media))
userID2FileMsgMap.Delete(cdnUrl)
return "", err
}
downloadReq.FilePath = filePath
downloadReq.Media = nil
return filePath, nil
}
}
time.Sleep(2 * time.Second)
}
return "", errors.New("文件下载超时或数据为空")
}
+2 -1
View File
@@ -14,5 +14,6 @@
"uploadOnCompleteAddr": "0x4913A88",
"uploadOnCompleteFuncAddr": "0x36BB9C0",
"downloadImagAddr": "0x4A6975C",
"hdPicDownloadAddr": "0x494652C"
"startDownloadMedia": "0x494663C",
"downloadFileAddr": "0x4A084EC"
}