diff --git a/.gitignore b/.gitignore index e0a2cc4..73dbddb 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ release onebot/audio onebot/image onebot/log -.comate \ No newline at end of file +.comate +onebot/file \ No newline at end of file diff --git a/frida/auto.js b/frida/auto.js index c5007e5..af27c66 100644 --- a/frida/auto.js +++ b/frida/auto.js @@ -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" } ]; diff --git a/frida/receiver.js b/frida/receiver.js index 3c5041a..b1ddc4a 100644 --- a/frida/receiver.js +++ b/frida/receiver.js @@ -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("(.*?)<\/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("(.*?)<\/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(" 标签 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 对应 标签及其属性和子节点 @@ -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"` +} diff --git a/onebot/script.js b/onebot/script.js index 103dd7a..ab19f4e 100644 --- a/onebot/script.js +++ b/onebot/script.js @@ -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("(.*?)<\/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("(.*?)<\/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(" 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) +} diff --git a/onebot/worker.go b/onebot/worker.go index da66f4b..2acbf08 100644 --- a/onebot/worker.go +++ b/onebot/worker.go @@ -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("文件下载超时或数据为空") +} diff --git a/wechat_version/4_1_7_57_mac.json b/wechat_version/4_1_7_57_mac.json index 8257191..64b6b5a 100644 --- a/wechat_version/4_1_7_57_mac.json +++ b/wechat_version/4_1_7_57_mac.json @@ -14,5 +14,6 @@ "uploadOnCompleteAddr": "0x4913A88", "uploadOnCompleteFuncAddr": "0x36BB9C0", "downloadImagAddr": "0x4A6975C", - "hdPicDownloadAddr": "0x494652C" + "startDownloadMedia": "0x494663C", + "downloadFileAddr": "0x4A084EC" } \ No newline at end of file