diff --git a/.gitignore b/.gitignore index f282510..a6d822d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,6 @@ data go_al/*.go onebot/onebot *.tar.gz -release \ No newline at end of file +release +onebot/audio +onebot/image \ No newline at end of file diff --git a/frida-wechat.md b/frida-wechat.md index 068a8e5..041f87e 100644 --- a/frida-wechat.md +++ b/frida-wechat.md @@ -1,5 +1,6 @@ 触发 STNManager__MMStartTask (ida搜 MMStartTask) 图片hook: startUploadMedia (ida搜), OnUploadCompleted (ce查) startUploadMedia 上传图片,OnUploadCompleted 上传图片完成后的回调 +加密后的图片在_OnRecvFileData WeChatExt中会有导致idapro出问题的代码 \ No newline at end of file diff --git a/frida/image.js b/frida/image.js index 30e3a3c..bae82e4 100644 --- a/frida/image.js +++ b/frida/image.js @@ -82,65 +82,6 @@ function generateAESKey() { return key; } -function getCleanString(uint8Array) { - var out = ""; - var i = 0; - var len = uint8Array.length; - - while (i < len) { - var c = uint8Array[i++]; - - // 1. 处理单字节 (ASCII: 0xxxxxxx) - if (c < 0x80) { - // 只保留可见字符 (Space 32 到 ~ 126) - if (c >= 32 && c <= 126) { - out += String.fromCharCode(c); - } - } - // 2. 处理双字节 (110xxxxx 10xxxxxx) - else if ((c & 0xE0) === 0xC0 && i < len) { - var c2 = uint8Array[i++]; - if ((c2 & 0xC0) === 0x80) { - // 这种通常是特殊拉丁字母等,按需保留 - var charCode = ((c & 0x1F) << 6) | (c2 & 0x3F); - out += String.fromCharCode(charCode); - } - } - // 3. 处理三字节 (1110xxxx 10xxxxxx 10xxxxxx) -> 绝大多数汉字在此 - else if ((c & 0xF0) === 0xE0 && i + 1 < len) { - var c2 = uint8Array[i++]; - var c3 = uint8Array[i++]; - if ((c2 & 0xC0) === 0x80 && (c3 & 0xC0) === 0x80) { - var charCode = ((c & 0x0F) << 12) | ((c2 & 0x3F) << 6) | (c3 & 0x3F); - if ( - (charCode >= 0x4E00 && charCode <= 0x9FA5) || // 基本汉字 - (charCode >= 0x3000 && charCode <= 0x303F) || // 常用中文标点 (。,、) - (charCode >= 0xFF00 && charCode <= 0xFFEF) || // 全角符号/标点 (!:?) - (charCode >= 0x2000 && charCode <= 0x206F) || // 常用标点扩展 (含 \u2005) - (charCode >= 0x3400 && charCode <= 0x4DBF) // 扩展 A 区汉字 - ) { - out += String.fromCharCode(charCode); - } - } - } else if ((c & 0xF8) === 0xF0 && i + 2 < len) { - var c2 = uint8Array[i++]; - var c3 = uint8Array[i++]; - var c4 = uint8Array[i++]; - if ((c2 & 0xC0) === 0x80 && (c3 & 0xC0) === 0x80 && (c4 & 0xC0) === 0x80) { - // 计算 Unicode 码点 - var codePoint = ((c & 0x07) << 18) | ((c2 & 0x3F) << 12) | ((c3 & 0x3F) << 6) | (c4 & 0x3F); - - // Emoji 范围通常在 U+1F000 到 U+1F9FF 之间 - if (codePoint >= 0x1F000 && codePoint <= 0x1FADF) { - // 使用 fromCodePoint 处理 4 字节字符 - out += String.fromCodePoint(codePoint); - } - } - } - } - return out; -} - function generateBytes(n) { // 生成随机字符串 const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; diff --git a/frida/receiver.js b/frida/receiver.js index 4f34bc6..a61d420 100644 --- a/frida/receiver.js +++ b/frida/receiver.js @@ -4,13 +4,11 @@ if (!baseAddr) { } var buf2RespAddr = baseAddr.add(0x37173B0) +var downloadImagAddr = baseAddr.add(0x49FB92C) // -------------------------接收消息分区------------------------- function setReceiver() { - - // 3. 开始拦截 - Interceptor.attach(buf2RespAddr, - { + Interceptor.attach(buf2RespAddr, { onEnter: function (args) { const currentPtr = this.context.x1; let start = 0x1e; @@ -40,7 +38,7 @@ function setReceiver() { const userContent = fields[5] const msgId = protobufVarintToNumberString(fields[6]) - if (sender === "" || receiver === "" || content === "") { + if (sender === "" || receiver === "" || content === "" || msgId === "") { console.log("字段缺失,无法解析 sender:" + sender + " receiver:" + receiver + hexdump(currentPtr, { length: x2, header: true, @@ -60,6 +58,7 @@ function setReceiver() { msgType = "group" groupId = sender + let splitIndex = content.indexOf(':') const sendUserStart = content.indexOf('wxid_') senderUser = content.substring(sendUserStart, splitIndex).trim(); @@ -77,13 +76,13 @@ function setReceiver() { // 处理用户的名称 splitIndex = userContent?.indexOf(':') if (splitIndex === -1) { - splitIndex = userContent?.indexOf('在群聊中@了你') + splitIndex = userContent?.indexOf('在群聊中@了你') !== -1 ? userContent?.indexOf('在群聊中@了你') : userContent?.indexOf('在群聊中发了一段语') senderNickname = userContent?.substring(0, splitIndex).trim(); } else { senderNickname = userContent?.substring(0, splitIndex).trim(); } if (!senderNickname) { - senderNickname = sender + senderNickname = senderUser } } else { @@ -91,7 +90,7 @@ function setReceiver() { const splitIndex = userContent?.indexOf(':') senderNickname = userContent?.substring(0, splitIndex).trim(); if (!senderNickname) { - senderNickname = sender + senderNickname = senderUser } } @@ -109,10 +108,31 @@ function setReceiver() { sender: {user_id: senderUser, nickname: senderNickname}, msgsource: xml, raw_message: content, - show_content:userContent + show_content: userContent }) }, }); + + Interceptor.attach(downloadImagAddr, { // 建议使用函数起始地址或你计算出的偏移地址 + 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, + }) + } + } + }); } @@ -139,11 +159,11 @@ function getMessages(content, sender, mediaContent) { if (content.startsWith(" 标签 +type VoiceMsg struct { + XMLName xml.Name `xml:"msg"` + VoiceMsg *VoiceMsgInfo `xml:"voicemsg"` +} + +// VoiceMsgInfo 对应内部的 标签及其属性 +type VoiceMsgInfo struct { + EndFlag int `xml:"endflag,attr"` + CancelFlag int `xml:"cancelflag,attr"` + ForwardFlag int `xml:"forwardflag,attr"` + VoiceFormat int `xml:"voiceformat,attr"` + VoiceLength int `xml:"voicelength,attr"` + Length int `xml:"length,attr"` + BufID int `xml:"bufid,attr"` + AESKey string `xml:"aeskey,attr"` + VoiceURL string `xml:"voiceurl,attr"` + VoiceMD5 string `xml:"voicemd5,attr"` + ClientMsgID string `xml:"clientmsgid,attr"` + FromUserName string `xml:"fromusername,attr"` +} + +// FileMsg 对应 标签 +type FileMsg struct { + XMLName xml.Name `xml:"msg"` + Image Image `xml:"img"` +} + +// Image 对应 标签及其属性和子节点 +type Image struct { + // 属性(Attributes) + AesKey string `xml:"aeskey,attr"` + EncryVer int `xml:"encryver,attr"` + ThumbAesKey string `xml:"cdnthumbaeskey,attr"` + ThumbURL string `xml:"cdnthumburl,attr"` + Length int `xml:"length,attr"` + Md5 string `xml:"md5,attr"` + HDHeight int `xml:"cdnhdheight,attr"` + HDWidth int `xml:"cdnhdwidth,attr"` + MidImgURL string `xml:"cdnmidimgurl,attr"` + + // 子节点 + SecHashInfo string `xml:"secHashInfoBase64"` + Live Live `xml:"live"` +} + +// Live 对应 标签 +type Live struct { + Duration int `xml:"duration"` + Size int `xml:"size"` + FileID string `xml:"fileid"` +} + +type DownloadRequest struct { + FileID string `json:"file_id"` + Media []byte `json:"media"` + CDNURL string `json:"cdn_url"` +} + +type ScriptMessage struct { + Type string `json:"type"` + Payload interface{} `json:"payload"` +} diff --git a/onebot/script.js b/onebot/script.js index c33cf06..57e192d 100644 --- a/onebot/script.js +++ b/onebot/script.js @@ -284,6 +284,7 @@ var uploadGetCallbackWrapperAddr = baseAddr.add({{.uploadGetCallbackWrapperAddr} var uploadGetCallbackWrapperFuncAddr = baseAddr.add({{.uploadGetCallbackWrapperFuncAddr}}); var uploadOnCompleteAddr = baseAddr.add({{.uploadOnCompleteAddr}}); var uploadOnCompleteFuncAddr = baseAddr.add({{.uploadOnCompleteFuncAddr}}); +var downloadImagAddr = baseAddr.add({{.downloadImagAddr}}); var uploadImageX1; var imgCgiAddr = ptr(0); @@ -1105,8 +1106,6 @@ rpc.exports = { // -------------------------接收消息分区------------------------- function setReceiver() { - - // 3. 开始拦截 Interceptor.attach(buf2RespAddr, { onEnter: function (args) { const currentPtr = this.context.x1; @@ -1137,7 +1136,7 @@ function setReceiver() { const userContent = fields[5] const msgId = protobufVarintToNumberString(fields[6]) - if (sender === "" || receiver === "" || content === "") { + if (sender === "" || receiver === "" || content === "" || msgId === "") { console.log("字段缺失,无法解析 sender:" + sender + " receiver:" + receiver + hexdump(currentPtr, { length: x2, header: true, @@ -1157,6 +1156,7 @@ function setReceiver() { msgType = "group" groupId = sender + let splitIndex = content.indexOf(':') const sendUserStart = content.indexOf('wxid_') senderUser = content.substring(sendUserStart, splitIndex).trim(); @@ -1174,13 +1174,13 @@ function setReceiver() { // 处理用户的名称 splitIndex = userContent?.indexOf(':') if (splitIndex === -1) { - splitIndex = userContent?.indexOf('在群聊中@了你') + splitIndex = userContent?.indexOf('在群聊中@了你') !== -1 ? userContent?.indexOf('在群聊中@了你') : userContent?.indexOf('在群聊中发了一段语') senderNickname = userContent?.substring(0, splitIndex).trim(); } else { senderNickname = userContent?.substring(0, splitIndex).trim(); } if (!senderNickname) { - senderNickname = sender + senderNickname = senderUser } } else { @@ -1188,7 +1188,7 @@ function setReceiver() { const splitIndex = userContent?.indexOf(':') senderNickname = userContent?.substring(0, splitIndex).trim(); if (!senderNickname) { - senderNickname = sender + senderNickname = senderUser } } @@ -1206,11 +1206,31 @@ function setReceiver() { sender: {user_id: senderUser, nickname: senderNickname}, msgsource: xml, raw_message: content, - // media: mediaContent, - show_content:userContent + show_content: userContent }) }, }); + + Interceptor.attach(downloadImagAddr, { // 建议使用函数起始地址或你计算出的偏移地址 + 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, + }) + } + } + }); } diff --git a/onebot/utils.go b/onebot/utils.go index ad57122..59e9132 100644 --- a/onebot/utils.go +++ b/onebot/utils.go @@ -2,10 +2,12 @@ package main import ( "bytes" + "crypto/aes" "crypto/md5" "encoding/base64" "encoding/hex" "encoding/json" + "encoding/xml" "fmt" "io" log "log" @@ -155,7 +157,8 @@ func SilkToMp3(silkBytes []byte) ([]byte, error) { return out.Bytes(), nil } -func HandleMsg(jsonData []byte, m *WechatMessage) ([]byte, error) { +func HandleMsg(jsonData []byte) ([]byte, error) { + m := new(WechatMessage) err := json.Unmarshal(jsonData, m) if err != nil { log.Printf("解析消息失败: %v\n", err) @@ -173,9 +176,71 @@ func HandleMsg(jsonData []byte, m *WechatMessage) ([]byte, error) { log.Printf("保存音频失败: %v\n", err) return nil, err } - msg.Data.URL = path + 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 { + log.Printf("XML解析失败: %v\n", err) + return nil, err + } + + // 如果有图片等待图片下载完成再处理 + if len(msg.Data.Media) == 0 { + if downloadMsgInter, ok := userID2FileMsgMap.Load(fileMsg.Image.ThumbURL); ok { + downloadReq := downloadMsgInter.(*DownloadRequest) + aesKey, _ := hex.DecodeString(fileMsg.Image.ThumbAesKey) + path, err := GetImagePath(downloadReq.Media, aesKey) + if err != nil { + log.Printf("获取图片路径失败: %v\n", 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) { + block, _ := aes.NewCipher(key) + decrypted := make([]byte, len(data)) + bs := block.BlockSize() + for i := 0; i < len(data); i += bs { + 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) + } + + return "", fmt.Errorf("无法解析的图像数据") +} + +func SaveImageToFile(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() + fileName := fmt.Sprintf("%d_%d.%s", randomNumber, timestamp, ext) + targetPath := filepath.Dir(exePath) + "/image/" + fileName + err = os.WriteFile(targetPath, data, 0644) + if err != nil { + return "", err + } + + return targetPath, nil +} diff --git a/onebot/websocket.go b/onebot/websocket.go index 5a9918b..785c06c 100644 --- a/onebot/websocket.go +++ b/onebot/websocket.go @@ -81,7 +81,7 @@ func handleWebSocket(w http.ResponseWriter, r *http.Request) { } -func SendWebSocketMsg(msg map[string]interface{}) { +func SendWebSocketMsg(jsonData []byte) { defer func() { if r := recover(); r != nil { log.Printf("panic: %v, %v\n", r, string(debug.Stack())) @@ -89,17 +89,13 @@ func SendWebSocketMsg(msg map[string]interface{}) { }() time.Sleep(time.Duration(config.SendInterval) * time.Millisecond) - // 这里处理你的 X1 数据 - jsonData, err := json.Marshal(msg["payload"]) + + jsonReq, err := HandleMsg(jsonData) if err != nil { log.Printf("JSON 序列化失败: %v\n", err) return } - - m := new(WechatMessage) - jsonReq, err := HandleMsg(jsonData, m) - if err != nil { - log.Printf("JSON 序列化失败: %v\n", err) + if jsonReq == nil { return } diff --git a/wechat_version/4_1_7_55_mac.json b/wechat_version/4_1_7_55_mac.json index fa37678..6113ff6 100644 --- a/wechat_version/4_1_7_55_mac.json +++ b/wechat_version/4_1_7_55_mac.json @@ -17,5 +17,6 @@ "uploadGetCallbackWrapperAddr": "0x49068CC", "uploadGetCallbackWrapperFuncAddr": "0x36AFBEC", "uploadOnCompleteAddr": "0x4906EC8", - "uploadOnCompleteFuncAddr": "0x36B0DD0" + "uploadOnCompleteFuncAddr": "0x36B0DD0", + "downloadImagAddr": "0x49FB92C" }