diff --git a/.gitignore b/.gitignore index 7c9260c..911cc9a 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,11 @@ onebot/log onebot/file onebot/videos .claude - +ai_session.md +codex.md +install_frida_gadget_automated.sh +note.md +requirements.md +.gocache/ +.gomodcache/ +frida-devkit/ \ No newline at end of file diff --git a/onebot/protobuf_handler.go b/onebot/protobuf_handler.go index c5c7459..ac9420b 100644 --- a/onebot/protobuf_handler.go +++ b/onebot/protobuf_handler.go @@ -52,6 +52,9 @@ func HandleProtobufMsg(payload map[string]interface{}) ([]byte, error) { msg := &wxproto.WxRecvMsg{} err := proto.Unmarshal(rawBytes, msg) if err != nil { + if strings.Contains(err.Error(), "cannot parse invalid wire-format data") { + return nil, nil + } return nil, fmt.Errorf("protobuf unmarshal failed: %w", err) } diff --git a/onebot/script.js b/onebot/script.js index 327bc8b..7f0fdc4 100644 --- a/onebot/script.js +++ b/onebot/script.js @@ -133,6 +133,81 @@ function generateAESKey() { return key; } +const MAX_FRIDA_MESSAGE_BYTES = 4 * 1024 * 1024; +const DOWNLOAD_CHUNK_BYTES = 256 * 1024; + +function isReadablePointer(addr) { + try { + if (!addr || addr.isNull()) { + return false; + } + const range = Process.findRangeByAddress(addr); + return range !== null && range.protection.indexOf('r') !== -1; + } catch (e) { + return false; + } +} + +function readPointerIfReadable(addr) { + try { + if (!isReadablePointer(addr)) { + return ptr(0); + } + const value = addr.readPointer(); + if (!isReadablePointer(value)) { + return ptr(0); + } + return value; + } catch (e) { + return ptr(0); + } +} + +function readUtf8StringIfReadable(addr) { + try { + if (!isReadablePointer(addr)) { + return ""; + } + return addr.readUtf8String(4096); + } catch (e) { + return ""; + } +} + +function readByteArrayIfReadable(addr, len) { + try { + if (len <= 0 || !isReadablePointer(addr)) { + return null; + } + return addr.readByteArray(len); + } catch (e) { + return null; + } +} + +function sendDownloadChunks(dataPtr, dataLen, fileId, cdnUrl) { + if (!cdnUrl || dataLen <= 0) { + return; + } + const safeFileId = fileId || ""; + + for (let offset = 0; offset < dataLen; offset += DOWNLOAD_CHUNK_BYTES) { + const chunkLen = Math.min(DOWNLOAD_CHUNK_BYTES, dataLen - offset); + const buffer = readByteArrayIfReadable(dataPtr.add(offset), chunkLen); + if (!buffer) { + return; + } + const uint8Array = new Uint8Array(buffer); + + send({ + type: "download", + media: Array.from(uint8Array), + file_id: safeFileId, + cdn_url: cdnUrl, + }); + } +} + function fillUploadX1AndStart(idAddr, pathAddr, x1Buffer, receiver, md5, filePath, payloadHex) { if (uploadGlobalX0.equals(ptr(0))) { console.error("[!] uploadGlobalX0 尚未初始化,请等待 hook 捕获"); @@ -945,6 +1020,9 @@ function setReceiver() { var respTaskId = this.context.sp.add(0x140).readS32(); const currentPtr = this.context.x20; const x2 = this.context.x0.toInt32(); + if (!isReadablePointer(currentPtr)) { + return; + } // 判断是否是我们发送的消息的 ack if (pendingBuf2RespTaskId !== 0 && respTaskId === pendingBuf2RespTaskId) { @@ -956,7 +1034,7 @@ function setReceiver() { } // 读取响应数据 - var respData = currentPtr.readByteArray(x2); + var respData = x2 >= 4 && x2 <= MAX_FRIDA_MESSAGE_BYTES ? readByteArrayIfReadable(currentPtr, x2) : null; if (respData) { var bytes = new Uint8Array(respData); console.log("[+] buf2resp: 收到响应, msgType=" + pendingSendMsgType + " taskId=" + respTaskId + " len=" + x2); @@ -972,11 +1050,15 @@ function setReceiver() { return; } + if (x2 < 4 || x2 > MAX_FRIDA_MESSAGE_BYTES) { + return; + } + // 2. field 2 wrapper长度 < 128 (单字节varint) → wrapper内容过小,非聊天消息 if (currentPtr.add(2).readU8() === 0x10 || currentPtr.add(2).readU8() === 0x16 || (currentPtr.add(3).readU8() & 0x80) === 0) { return; } - const mem = currentPtr.readByteArray(x2); + const mem = readByteArrayIfReadable(currentPtr, x2); if (!mem) return; const uint8Array = new Uint8Array(mem); @@ -990,8 +1072,11 @@ function setReceiver() { Interceptor.attach(startDownloadMedia, { onEnter: function (args) { downloadGlobalX0 = this.context.x0; - var fileIDAddr = this.context.x1.add(0x40).readPointer(); - var fileId = fileIDAddr?.readUtf8String(); + var fileIDAddr = readPointerIfReadable(this.context.x1.add(0x40)); + var fileId = readUtf8StringIfReadable(fileIDAddr); + if (!fileId || !isReadablePointer(this.context.x1.add(0xA0))) { + return; + } const t = this.context.x1.add(0xA0).readU32() if (t === 3) { if (fileId.endsWith("_1")) { @@ -1008,20 +1093,10 @@ function setReceiver() { onEnter: function (args) { var dataPtr = this.context.x22; var dataLen = this.context.x2.toInt32(); - var fileId = this.context.x19.add(0x2E0).readPointer().readUtf8String(); - var cdnUrl = this.context.x19.add(0x2F8).readPointer().readUtf8String(); + var fileId = readUtf8StringIfReadable(readPointerIfReadable(this.context.x19.add(0x2E0))); + var cdnUrl = readUtf8StringIfReadable(readPointerIfReadable(this.context.x19.add(0x2F8))); - 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, - }) - } + sendDownloadChunks(dataPtr, dataLen, fileId, cdnUrl); } }); @@ -1029,41 +1104,21 @@ function setReceiver() { onEnter: function (args) { var dataPtr = this.context.x22; var dataLen = this.context.x2.toInt32(); - var fileId = this.context.x19.add(0x2E0).readPointer().readUtf8String(); - var cdnUrl = this.context.x19.add(0x2F8).readPointer().readUtf8String(); + var fileId = readUtf8StringIfReadable(readPointerIfReadable(this.context.x19.add(0x2E0))); + var cdnUrl = readUtf8StringIfReadable(readPointerIfReadable(this.context.x19.add(0x2F8))); - 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, - }) - } + sendDownloadChunks(dataPtr, dataLen, fileId, cdnUrl); } }); Interceptor.attach(downloadVideoAddr, { onEnter: function (args) { - var dataPtr = this.context.x20.add(0x178).readPointer(); + var dataPtr = readPointerIfReadable(this.context.x20.add(0x178)); var dataLen = this.context.x23.toInt32(); - var fileId = this.context.x19.add(0x2E0).readPointer().readUtf8String(); - var cdnUrl = this.context.x19.add(0x2F8).readPointer().readUtf8String(); + var fileId = readUtf8StringIfReadable(readPointerIfReadable(this.context.x19.add(0x2E0))); + var cdnUrl = readUtf8StringIfReadable(readPointerIfReadable(this.context.x19.add(0x2F8))); - 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, - }) - } + sendDownloadChunks(dataPtr, dataLen, fileId, cdnUrl); } }); } diff --git a/onebot/utils.go b/onebot/utils.go index 8c47af9..c30b129 100644 --- a/onebot/utils.go +++ b/onebot/utils.go @@ -162,7 +162,14 @@ func SilkToMp3(silkBytes []byte) ([]byte, error) { } func GetFilePath(data []byte, key []byte) (string, error) { - block, _ := aes.NewCipher(key) + 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 { diff --git a/onebot/worker.go b/onebot/worker.go index 08b3cd8..65b6b95 100644 --- a/onebot/worker.go +++ b/onebot/worker.go @@ -2,6 +2,7 @@ package main import ( "context" + "crypto/aes" "encoding/hex" "encoding/json" "encoding/xml" @@ -334,6 +335,16 @@ func GetDownloadPath(cdnUrl, aesKeyStr string) (string, error) { // 数据接收完成,尝试解密 if len(downloadReq.Media) > 0 { + if len(downloadReq.Media)%aes.BlockSize != 0 { + Info("文件数据仍未对齐 AES 块,继续等待", "url", cdnUrl, "times", i, "media_len", len(downloadReq.Media), "block_size", aes.BlockSize) + if i < 9 { + time.Sleep(2 * time.Second) + continue + } + userID2FileMsgMap.Delete(cdnUrl) + return "", fmt.Errorf("文件数据长度不是 AES 块大小倍数: media_len=%d block_size=%d", len(downloadReq.Media), aes.BlockSize) + } + aesKey, err := hex.DecodeString(aesKeyStr) if err != nil { Error("AES key 解码失败", "err", err) diff --git a/setup_frida_and_build.sh b/setup_frida_and_build.sh new file mode 100755 index 0000000..786802a --- /dev/null +++ b/setup_frida_and_build.sh @@ -0,0 +1,51 @@ +#!/bin/bash +set -e + +# Frida devkit must match the frida-go API used by go.mod. +FRIDA_VERSION="${FRIDA_VERSION:-17.8.0}" +OS="macos" +ARCH="arm64" +DEVKIT_NAME="frida-core-devkit-${FRIDA_VERSION}-${OS}-${ARCH}" +DOWNLOAD_URL="https://github.com/frida/frida/releases/download/${FRIDA_VERSION}/${DEVKIT_NAME}.tar.xz" +DEVKIT_DIR="$(pwd)/frida-devkit" + +echo "🔧 配置 Frida 开发环境 (Version: $FRIDA_VERSION, Arch: $ARCH)..." + +# 检查是否已存在且版本/API匹配 +if [ -f "$DEVKIT_DIR/libfrida-core.a" ] && [ -f "$DEVKIT_DIR/frida-core.h" ] && grep -q "FridaPackageManager" "$DEVKIT_DIR/frida-core.h"; then + echo "✅ 发现已存在且兼容的 Frida Devkit,跳过下载。" +else + # 如果版本不匹配,清理旧的 + if [ -d "$DEVKIT_DIR" ]; then + echo "⚠️ 清理旧的或不兼容的 Devkit 目录..." + rm -rf "$DEVKIT_DIR" + fi + + mkdir -p "$DEVKIT_DIR" + echo "⬇️ 正在下载 ${DEVKIT_NAME}..." + curl -L -o "$DEVKIT_DIR/devkit.tar.xz" "$DOWNLOAD_URL" + + echo "📦 正在解压..." + tar -xf "$DEVKIT_DIR/devkit.tar.xz" -C "$DEVKIT_DIR" + rm "$DEVKIT_DIR/devkit.tar.xz" + echo "✅ 解压完成" +fi + + +# 2. 编译 +echo "🚀 开始编译 OneBot..." +cd onebot + +# 强制设置 GOARCH 为 arm64,因为我们下载的是 arm64 的库 +export GOARCH=arm64 +export CGO_ENABLED=1 +export CGO_CFLAGS="-I$DEVKIT_DIR" +export CGO_LDFLAGS="-L$DEVKIT_DIR -lfrida-core" + +echo "⚙️ GOARCH=$GOARCH" +echo "⚙️ CGO_CFLAGS=$CGO_CFLAGS" +echo "⚙️ CGO_LDFLAGS=$CGO_LDFLAGS" + +go build -v -o onebot . + +echo "🎉 编译成功!二进制文件位置: $(pwd)/onebot"