Files
wechat_chatter/onebot/utils.go
T

491 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"bytes"
"crypto/aes"
"crypto/md5"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"io"
"math/rand"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
"github.com/wdvxdr1123/go-silk"
)
func SaveBase64Image(base64Data string) (string, string, error) {
rawContents := base64Data
if strings.HasPrefix(base64Data, "base64://") {
rawContents = strings.TrimPrefix(base64Data, "base64://")
} else if idx := strings.Index(base64Data, ","); idx != -1 {
rawContents = base64Data[idx+1:]
}
data, err := base64.StdEncoding.DecodeString(rawContents)
if err != nil {
return "", "", fmt.Errorf("base64 decode failed: %v", err)
}
salt := []byte(fmt.Sprintf("\n#md5_salt_%d_%d#", time.Now().UnixNano(), rand.Intn(10000)))
data = append(data, salt...)
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, DetectFileFormat(data))
targetPath := config.ImagePath + fileName
dir := filepath.Dir(targetPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return "", "", fmt.Errorf("create directory failed: %v", err)
}
err = os.WriteFile(targetPath, data, 0644)
if err != nil {
return "", "", fmt.Errorf("write file failed: %v", err)
}
md5Str, err := GetFileMD5(targetPath)
if err != nil {
return "", "", fmt.Errorf("get file md5 failed: %v", err)
}
return targetPath, md5Str, nil
}
// GetVideoDuration 使用ffprobe获取视频时长(秒)
func GetVideoDuration(filePath string) (int32, error) {
cmd := exec.Command("ffprobe",
"-v", "error",
"-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1",
filePath,
)
var out bytes.Buffer
cmd.Stdout = &out
if err := cmd.Run(); err != nil {
return 0, fmt.Errorf("ffprobe error: %v", err)
}
durationStr := strings.TrimSpace(out.String())
durationFloat, err := strconv.ParseFloat(durationStr, 64)
if err != nil {
return 0, fmt.Errorf("parse duration failed: %v", err)
}
return int32(durationFloat), nil
}
func GetFileMD5(filePath string) (string, error) {
file, err := os.Open(filePath)
if err != nil {
return "", err
}
defer file.Close()
hash := md5.New()
if _, err := io.Copy(hash, file); err != nil {
return "", err
}
return hex.EncodeToString(hash.Sum(nil)), nil
}
// FileToBase64 读取文件并返回 base64 编码字符串
func FileToBase64(filePath string) (string, error) {
data, err := os.ReadFile(filePath)
if err != nil {
return "", fmt.Errorf("read file failed: %v", err)
}
return base64.StdEncoding.EncodeToString(data), nil
}
func SaveAudioFile(silkBytes []byte) (path string, err error) {
mp3Bytes, err := SilkToMp3(silkBytes)
if err != nil {
return "", err
}
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.mp3", randomNumber, timestamp)
targetPath := filepath.Dir(exePath) + "/audio/" + fileName
err = os.WriteFile(targetPath, mp3Bytes, 0644)
if err != nil {
return "", err
}
return targetPath, nil
}
func SilkToMp3(silkBytes []byte) ([]byte, error) {
var pcm, err = silk.DecodeSilkBuffToPcm(silkBytes, 16000)
if err != nil {
return nil, err
}
cmd := exec.Command("ffmpeg",
"-f", "s16le",
"-ar", "16000",
"-ac", "1",
"-i", "pipe:0",
"-codec:a", "libmp3lame",
"-b:a", "192k",
"-f", "mp3",
"pipe:1",
)
cmd.Stdin = bytes.NewReader(pcm)
var out bytes.Buffer
cmd.Stdout = &out
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("ffmpeg error: %v, details: %s", err, stderr.String())
}
return out.Bytes(), nil
}
func GetFilePath(data []byte, key []byte) (string, error) {
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
if len(data) == 0 || len(data)%block.BlockSize() != 0 {
return "", fmt.Errorf("invalid encrypted data length: %d, block_size: %d", len(data), block.BlockSize())
}
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])
}
ext := DetectFileFormat(decrypted)
if ext == "unknown" {
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{0x00, 0x00, 0x00}): // MP4/MOV 通常以 ftyp 开头,后面是具体类型
if len(data) > 4 {
switch string(data[4:8]) {
case "ftyp", "moov", "mdat", "wide", "free":
return "mp4"
case "isom", "mp41", "mp42", "M4V ", "M4A ", "M4P ":
return "mp4"
}
}
case bytes.HasPrefix(data, []byte("FLV\x01")): // FLV
return "flv"
case bytes.HasPrefix(data, []byte{0x30, 0x26, 0xB2, 0x75, 0x8E, 0x66, 0xCF, 0x11}): // ASF/WMV/WMA
if len(data) > 8 && bytes.HasPrefix(data[8:], []byte{0xA6, 0xD9, 0x00, 0xAA, 0x00, 0x62, 0xCE, 0x6C}) {
return "wmv"
}
// 图片格式
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"
// 音频格式
case data[0] == 0x02 && bytes.HasPrefix(data[1:], []byte("#!SILK")):
return "silk"
case bytes.HasPrefix(data, []byte("#!SILK")):
return "silk"
case bytes.HasPrefix(data, []byte("RIFF")) && len(data) > 8 && bytes.HasPrefix(data[8:], []byte("WAVE")):
return "wav"
case bytes.HasPrefix(data, []byte{0xFF, 0xFB}) || bytes.HasPrefix(data, []byte{0xFF, 0xF3}) || bytes.HasPrefix(data, []byte{0xFF, 0xF2}):
return "mp3"
case bytes.HasPrefix(data, []byte("ID3")):
return "mp3"
case bytes.HasPrefix(data, []byte("OggS")):
return "ogg"
case bytes.HasPrefix(data, []byte("fLaC")):
return "flac"
default:
return "unknown"
}
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) {
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
}
func GetWeChatPID() (int, error) {
cmd := exec.Command("pgrep", "-x", "WeChat")
output, err := cmd.Output()
if err != nil {
return 0, fmt.Errorf("未发现正在运行的微信进程")
}
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)
}
// MonitorProcess 监控指定 PID 的进程是否退出
// 如果进程退出,清理 Frida 资源并等待微信重新启动后重新 attach
func MonitorProcess(pid int) {
Info("开始监控微信进程", "PID", pid)
go func() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for range ticker.C {
proc, err := os.FindProcess(pid)
if err != nil {
Info("微信进程已退出,清理 Frida 资源,等待微信重新启动...")
cleanAndReattach()
return
}
// 检查进程是否存活
err = proc.Signal(syscall.Signal(0))
if err != nil {
Info("微信进程已退出,清理 Frida 资源,等待微信重新启动...")
cleanAndReattach()
return
}
}
}()
}
func cleanAndReattach() {
if fridaScript != nil {
fridaScript.Clean()
Info("Frida 脚本资源已清理")
}
if session != nil {
session.Clean()
Info("Frida 会话资源已清理")
}
Info("等待微信重新启动...")
// 重新等待微信进程并 attach
attachWechat()
}
// HexDump formats data like:
// 0000000C12334C00 0A 40 0A 01 00 10 C6 BC 90 B9 08 1A 10 6D 36 34 .@....Ƽ.....m64
func HexDump(data []byte, baseAddr uint64) string {
var sb strings.Builder
for i := 0; i < len(data); i += 16 {
// Address
sb.WriteString(fmt.Sprintf("%016X ", baseAddr+uint64(i)))
// Hex bytes
for j := 0; j < 16; j++ {
if j == 8 {
sb.WriteByte(' ')
}
if i+j < len(data) {
sb.WriteString(fmt.Sprintf("%02X ", data[i+j]))
} else {
sb.WriteString(" ")
}
}
// ASCII
sb.WriteByte(' ')
for j := 0; j < 16; j++ {
if i+j < len(data) {
b := data[i+j]
if b >= 0x20 && b <= 0x7E {
sb.WriteByte(b)
} else {
sb.WriteByte('.')
}
}
}
sb.WriteByte('\n')
}
return sb.String()
}