mirror of
https://github.com/yincongcyincong/wechat_chatter.git
synced 2026-07-15 10:26:52 +08:00
+8
-1
@@ -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/
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+99
-44
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+8
-1
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Executable
+51
@@ -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"
|
||||
Reference in New Issue
Block a user