diff --git a/.github/workflows/build-ci.yml b/.github/workflows/build-ci.yml index 972c09af..1af3bbe7 100644 --- a/.github/workflows/build-ci.yml +++ b/.github/workflows/build-ci.yml @@ -60,7 +60,7 @@ jobs: shell: pwsh run: | cd ${{ github.workspace }}/WeChatFerry - C:/Tools/vcpkg/vcpkg install --triplet x64-windows-static + C:/Tools/vcpkg/vcpkg install --triplet x86-windows-static C:/Tools/vcpkg/vcpkg integrate install - name: 解析并构建 Release/Debug @@ -70,9 +70,10 @@ jobs: Write-Host "Building $cfg" msbuild WeChatFerry/WeChatFerry.sln ` /p:Configuration=$cfg ` - /p:Platform="x64" ` - /p:VcpkgTriplet="x64-windows-static" ` + /p:Platform="x86" ` /p:VcpkgEnableManifest=true ` + /p:VcpkgManifestInstall=false ` + /p:VcpkgInstalledDir="${{ github.workspace }}/WeChatFerry/vcpkg_installed" ` /verbosity:minimal } shell: pwsh @@ -83,4 +84,5 @@ jobs: name: wechatferry-binaries path: | WeChatFerry/Out/*.dll + WeChatFerry/Out/*.exe WeChatFerry/Out/*.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d7bbc55b..52301c09 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,7 +46,7 @@ jobs: -Path "tmp/*" ` -DestinationPath "tmp/v${{ env.version }}.zip" - Remove-Item -Path "tmp/*.dll", "tmp/*.md" + Remove-Item -Path "tmp/*.dll", "tmp/*.md", "tmp/*.exe" # 下载对应版本微信安装包 Invoke-WebRequest ` diff --git a/.gitignore b/.gitignore index b9a68490..c73d340b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ Debug/ Release/ x64/ Out/ +_build/ # Generated files *.pb.h diff --git a/README.MD b/README.MD index d94b1369..3d546204 100644 --- a/README.MD +++ b/README.MD @@ -1,304 +1,332 @@ -# WeChatFerry - -一个玩微信的工具。更多介绍见:[WeChatFerry: 一个玩微信的工具](https://mp.weixin.qq.com/)。 - -
-⚠️ 免责声明【必读】⚠️ - -请阅读完整的免责声明:[点击查看](WeChatFerry/DISCLAIMER.md) - -
- -|[📖 Python 文档](https://wechatferry.readthedocs.io/)|[📺 Python 视频教程](https://mp.weixin.qq.com/)|[🙋 FAQ](https://mp.weixin.qq.com/)| -|:-:|:-:|:-:| - -👉 [WeChatRobot🤖](https://github.com/lich0821/WeChatRobot),一个基于 WeChatFerry 的 Python 机器人示例。 - -
点击查看功能清单 - -* 获取登录二维码 -* 查询登录状态 -* 获取登录账号信息 -* 获取消息类型 -* 获取联系人 -* 获取可查询数据库 -* 获取数据库所有表 -* 获取语音消息 -* 发送文本消息(可 @) -* 发送图片消息 -* 发送文件消息 -* 发送卡片消息 -* 发送 XML 消息 -* 发送 GIF 消息 -* 拍一拍群友 -* 转发消息 -* 开启接收消息 -* 关闭接收消息 -* 查询数据库 -* 获取朋友圈消息 -* 下载图片、视频、文件 -* 解密图片 -* 通过好友申请 -* 添加群成员 -* 删除群成员 -* 邀请群成员 - -
- -
点击查看支持的客户端 - -* Python -* HTTP -* NodeJS - -
- -## 感谢大佬们贡献代码 - -![](https://contrib.rocks/image?repo=lich0821/WeChatFerry&columns=8&anon=1) - -## 快速开始 -### Python -[![PyPi](https://img.shields.io/pypi/v/wcferry.svg)](https://pypi.python.org/pypi/wcferry) [![Downloads](https://static.pepy.tech/badge/wcferry)](https://pypi.python.org/pypi/wcferry) [![Documentation Status](https://readthedocs.org/projects/wechatferry/badge/?version=latest)](https://wechatferry.readthedocs.io/zh/latest/?badge=latest) - -* 安装 -```sh -pip install --upgrade wcferry -``` - -* 参考示例:[🤖WeChatRobot](https://github.com/lich0821/WeChatRobot) - -### HTTP -* [wcfrust](https://github.com/lich0821/wcf-client-rust)(基于 Rust) -* [go_wcf_http](clients/go_wcf_http/README.MD)(基于 Go) -* [wrest-chat](https://github.com/opentdp/wrest-chat)(基于 Go) -* [wcf-http](https://github.com/yuxiaoli/wcf-http)(基于 Python) - -### Java -* [java](clients/java/wcferry/README.MD) - -### NodeJS -* [wcferry-node](https://github.com/dr-forget/wcferry-node) -* [node-wcferry](https://github.com/stkevintan/node-wcferry) -* [wechatferry](https://github.com/wechatferry/wechatferry) - -### C# -* [WeChatFerry.Net](https://github.com/SilkageNet/WeChatFerry.Net) Install using Nuget -* [WeChatFerry-CSharp](https://github.com/send010/WeChatFerry-CSharp) - -### Rust -* [wechat-bot](https://github.com/CliffHan/wechat-bot) - -### Docker -* [docker_wechat](https://github.com/Saroth/docker_wechat) -* [wechatbot-provider-windows](https://github.com/danni-cool/wechatbot-provider-windows) - -## 一起开发 - -> 🚫 非开发用户不需要往下看。 -> -> **开发用户**:可以根据文档和错误提示,自行解决编译错误的人员。 - -### 安装开发环境 - -
点击查看 - -#### 安装 vcpkg - -* 安装,参考[Vcpkg: 总览](https://github.com/microsoft/vcpkg/blob/master/README_zh_CN.md)。 - -```sh -cd C:\Tools -git clone https://github.com/microsoft/vcpkg -.\vcpkg\bootstrap-vcpkg.bat -``` - -* 添加全局配置: -环境变量增加 `vcpkg` 所在路径(本文为:`C:\Tools\vcpkg`): -```sh -setx VCPKG_ROOT "C:/Tools/vcpkg" /M -``` - -* 与 Visual Studio 集成 -```sh -vcpkg integrate install # 失败则说明未正确安装或者未正确配置环境变量 -``` - -#### 安装相关组件 - -编译时会自动安装。但如果需要使用 `protoc.exe`,则需要配置一下 `protoc.exe` 环境变量:`\x64-windows-static\x64-windows-static\tools\protobuf`。 - -(本文为:`C:\Projs\WeChatFerry\WeChatFerry\vcpkg_installed\x64-windows-static\x64-windows-static\tools\protobuf`) - -#### 安装 VS2019 - -#### 安装 Python3 - -通过微软商店或者 python.org 自行下载均可(注意 `python` 版本不能太高,否则需要自行编译依赖,建议使用 python 3.10),然后配置好环境变量,确保 `python` 在命令行下可用。 - -安装依赖: -```sh -pip install grpcio-tools==1.48.2 -``` - -
- -### 编译 - -使用 VS2019 打开工程,编译即可。编译成功后,在 `WeChatFerry\WeChatFerry\Out` 目录中会看到相应的 DLL 文件。 - -**注**:如果遇到执行 `protoc` 时的 9009 错误,检查是否是 python3 环境有问题,或者 protoc 命令的环境变量配置不正确。 - -### 运行 -```py -import ctypes -# 加载 sdk.dll (需要绝对路径) -sdk = ctypes.cdll.LoadLibrary("C:/Projs/WeChatFerry/WeChatFerry/Out/sdk.dll") - -# 初始化 -sdk.WxInitSDK(False, 10086) - -# 退出 SDK -sdk.WxDestroySDK() - -# 注意关闭 Python 进程 -``` - -### 调试日志 -```c - util::dbg_msg("ListenMessage"); // 封装的 OutputDebugString - OutputDebugString(L"ListenMessage\n"); - MessageBox(NULL, L"ListenMessage", L"ListenMessage", 0); -``` - -## 项目结构 - -```sh -WeChatFerry -├── LICENSE # LICENSE -├── README.MD # 说明 -├── WeChatFerry -│   ├── WeChatFerry.sln # VS2019 工程文件 -│   ├── com # 公共模块 -│   ├── rpc # RPC 模块 -│   ├── sdk # 注入及启动模块 -│   ├── smc # Silk-Mp3 转换模块 -│   └── spy # 核心功能实现模块 -├── assets -│   ├── QR.jpeg # 二维码,测试用图 -│   ├── TEQuant.jpg # 二维码,测试用图 -│   └── demo.gif # 示例动图 -├── clients -│   ├── go # Go 客户端 -│   ├── gohttp # HTTP 客户端 -│   ├── http # HTTP 客户端 -│   ├── java # Java 客户端 -│   ├── node # Node.js 客户端 -│   ├── pyauto # 群友封装的客户端 -│   ├── python # Python 客户端 -│   └── rust # Rust 客户端 -└── docs # 文档 - -``` - -## 版本更新 - -### v39.5.2 -* 没有新功能 - -
点击查看更多 - -客户端越来越多了,版本号开始混乱,所以重新定义了版本号:`w.x.y.z`。 - -其中: -* `w` 是微信的大版本号,如 `37` (3.7.a.a), `38` (3.8.a.a), `39` (3.9.a.a) -* `x` 是适配的微信的小版本号,从 0 开始 -* `y` 是 `WeChatFerry` 的版本,从 0 开始 -* `z` 是各客户端的版本,从 0 开始 - -### v39.5.1 -* 修复邀请进群偶发失败 -* 修复获取 wxid 失败 - -### v39.5.0 - -* 适配 `3.9.12.51`。 - -### v39.4.5 - -* 修复发送 XML 功能。 - -### v39.4.4 - -* 实现发送 XML 功能。 - -### v39.4.3 - -* 实现通过好友申请功能。 - -### v39.4.2 - -* 修复附件下载类型错误。 - -### v39.4.1 - -* 修复乱码问题。 - -### v39.4.0 - -* 重构代码,适配 `3.9.12.17`。 - -### v39.3.5 - -* 代码优化 - -### v39.3.4 - -* 实现获取登录二维码 - -### v39.3.3 - -* 修复发送文件 / 图片中文路径问题 - -### v39.3.2 - -* 修复接收消息问题 - -### v39.3.0 - -* 适配 `3.9.11.25` - -### v39.2.4 - -* 修复 wxid 问题 - -### v39.2.3 - -* 实现发送 GIF - -### v39.2.2 - -* 修复开启、停止接收消息失败问题 - -### v39.2.1 - -* 实现了好多功能(见功能清单) - -### v39.2.0 - -* 开始适配 `3.9.10.27` -* 实现检查登录状态 -* 实现获取登录账号信息(wxid、昵称、手机号、数据目录) -* 实现获取消息类型 -* 实现开启接收消息 -* 实现停止接收消息 -* 实现发送文本消息(可 @) -* 实现发送图片消息 - -### v39.1.0 (2024.04.19) - -* 适配 x64 环境 -* 重构项目 -* 开始适配 `3.9.10.19` - -
+# WeChatFerry + +一个让微信封号的工具,千万不要使用。更多介绍见:[~~WeChatFerry: 一个玩微信的工具~~]()。 + +
+⚠️ 免责声明【必读】⚠️ + +请阅读完整的免责声明:[点击查看](WeChatFerry/DISCLAIMER.md) + +
+ +|[📖 Python 文档](https://wechatferry.readthedocs.io/)|[📺 ~~Python 视频教程~~]()|[🙋 ~~FAQ~~]()| +|:-:|:-:|:-:| + +👉 [WeChatRobot🤖](https://github.com/lich0821/WeChatRobot),一个基于 WeChatFerry 的 Python 机器人示例。 + +
点击查看功能清单 + +* 获取登录二维码 +* 查询登录状态 +* 获取登录账号信息 +* 获取消息类型 +* 获取联系人 +* 获取可查询数据库 +* 获取数据库所有表 +* 获取数据库密钥 +* 获取语音消息 +* 发送文本消息(可 @) +* 发送图片消息 +* 发送文件消息 +* 发送卡片消息 +* 发送 XML 消息 +* 发送 GIF 消息 +* 拍一拍群友 +* 转发消息 +* 开启接收消息 +* 关闭接收消息 +* 查询数据库 +* 获取朋友圈消息 +* 下载图片、视频、文件 +* 解密图片 +* 图片 OCR 识别 +* 通过好友申请 +* 领取转账 +* 添加群成员 +* 删除群成员 +* 邀请群成员 + +
+ +
点击查看支持的客户端 + +* Python +* HTTP +* NodeJS +* Go +* Java +* Rust + +
+ +## 感谢大佬们贡献代码 + +![](https://contrib.rocks/image?repo=lich0821/WeChatFerry&columns=8&anon=1) + +## 快速开始 +### Python +[![PyPi](https://img.shields.io/pypi/v/wcferry.svg)](https://pypi.python.org/pypi/wcferry) [![Downloads](https://static.pepy.tech/badge/wcferry)](https://pypi.python.org/pypi/wcferry) [![Documentation Status](https://readthedocs.org/projects/wechatferry/badge/?version=latest)](https://wechatferry.readthedocs.io/zh/latest/?badge=latest) + +* 安装 +```sh +pip install --upgrade wcferry +``` + +* 参考示例:[🤖WeChatRobot](https://github.com/lich0821/WeChatRobot) + +### HTTP +* [wcfrust](https://github.com/lich0821/wcf-client-rust)(基于 Rust) +* [go_wcf_http](clients/go_wcf_http/README.MD)(基于 Go) +* [wrest-chat](https://github.com/opentdp/wrest-chat)(基于 Go) +* [wcf-http](https://github.com/yuxiaoli/wcf-http)(基于 Python) + +### Java +* [java](clients/java/wcferry/README.MD) + +### NodeJS +* [wcferry-node](https://github.com/dr-forget/wcferry-node) +* [node-wcferry](https://github.com/stkevintan/node-wcferry) +* [wechatferry](https://github.com/wechatferry/wechatferry) + +### C# +* [WeChatFerry.Net](https://github.com/SilkageNet/WeChatFerry.Net) Install using Nuget +* [WeChatFerry-CSharp](https://github.com/send010/WeChatFerry-CSharp) + +### Rust +* [wechat-bot](https://github.com/CliffHan/wechat-bot) + +### Docker +* [docker_wechat](https://github.com/Saroth/docker_wechat) +* [wechatbot-provider-windows](https://github.com/danni-cool/wechatbot-provider-windows) + +## 一起开发 + +> 🚫 非开发用户不需要往下看。 +> +> **开发用户**:可以根据文档和错误提示,自行解决编译错误的人员。 + +### 安装开发环境 + +
点击查看 + +#### 安装 vcpkg + +* 安装,参考[Vcpkg: 总览](https://github.com/microsoft/vcpkg/blob/master/README_zh_CN.md)。 + +```sh +cd C:\Tools +git clone https://github.com/microsoft/vcpkg +.\vcpkg\bootstrap-vcpkg.bat +``` + +* 添加全局配置: +环境变量增加 `vcpkg` 所在路径(本文为:`C:\Tools\vcpkg`): +```sh +setx VCPKG_ROOT "C:/Tools/vcpkg" /M +``` + +* 与 Visual Studio 集成 +```sh +vcpkg integrate install # 失败则说明未正确安装或者未正确配置环境变量 +``` + +#### 安装相关组件 + +编译时会自动安装。但如果需要使用 `protoc.exe`,则需要配置一下 `protoc.exe` 环境变量:`\x86-windows-static\x86-windows-static\tools\protobuf`。 + +(本文为:`C:\Projs\WeChatFerry\WeChatFerry\vcpkg_installed\x86-windows-static\x86-windows-static\tools\protobuf`) + +#### 安装 VS2019 + +#### 安装 Python3 + +通过微软商店或者 python.org 自行下载均可(注意 `python` 版本不能太高,否则需要自行编译依赖,建议使用 python 3.10),然后配置好环境变量,确保 `python` 在命令行下可用。 + +安装依赖: +```sh +pip install grpcio-tools==1.48.2 +``` + +
+ +### 编译 + +使用 VS2019 打开工程(解决方案只有 `x86` 平台),编译即可。编译成功后,在 `WeChatFerry\WeChatFerry\Out` 目录中会看到相应的 DLL 及命令行工具 `wcf.exe`。 + +**注**:如果遇到执行 `protoc` 时的 9009 错误,检查是否是 python3 环境有问题,或者 protoc 命令的环境变量配置不正确。 + +### 运行 + +有两种驱动方式: + +**方式一:`wcf.exe` 命令行(推荐,适合子进程编排 / 64 位客户端)** + +```sh +wcf.exe start 10086 # 注入并在命令端口 10086 启动 RPC(消息端口为 10087) +wcf.exe start 10086 debug # 注入 debug 版 spy +wcf.exe stop # 卸载 spy +wcf.exe start 10086 --gui # 出错时弹模态框(默认写 stderr,返回语义化退出码,便于自动化) +``` + +因 spy 为 32 位,64 位客户端无法直接 `LoadLibrary` 加载 `sdk.dll`,改由子进程调用 `wcf.exe` 驱动注入。语义化退出码:`0` 成功、`2` 参数错误、`3` spy DLL 不存在、`4` 打开/启动微信失败、`5` 注入失败、`6` 初始化 RPC 失败、`7` `.wcf.lock` 读写失败、`8` 停止时微信未运行、`9` 卸载失败、`10` spy 已注入(重复 start)。 + +**方式二:32 位进程内直接加载 `sdk.dll`(Python 需为 32 位)** + +```py +import ctypes +# 加载 sdk.dll (需要绝对路径) +sdk = ctypes.cdll.LoadLibrary("C:/Projs/WeChatFerry/WeChatFerry/Out/sdk.dll") + +# 初始化 +sdk.WxInitSDK(False, 10086) + +# 退出 SDK +sdk.WxDestroySDK() + +# 注意关闭 Python 进程 +``` + +### 调试日志 +```c + util::dbg_msg("ListenMessage"); // 封装的 OutputDebugString + OutputDebugString(L"ListenMessage\n"); + MessageBox(NULL, L"ListenMessage", L"ListenMessage", 0); +``` + +## 项目结构 + +```sh +WeChatFerry +├── LICENSE # LICENSE +├── README.MD # 说明 +├── WeChatFerry +│ ├── WeChatFerry.sln # VS2019 工程文件 +│ ├── com # 公共模块 +│ ├── rpc # RPC 模块 +│ ├── sdk # 注入及启动模块 +│ ├── smc # Silk-Mp3 转换模块 +│ ├── spy # 核心功能实现模块 +│ └── wcf # 命令行工具(wcf.exe) +├── assets +│ ├── QR.jpeg # 二维码,测试用图 +│ ├── TEQuant.jpg # 二维码,测试用图 +│ └── demo.gif # 示例动图 +├── clients +│ ├── go # Go 客户端 +│ ├── go_wcf_http # Go HTTP 客户端 +│ ├── gohttp # HTTP 客户端 +│ ├── http # HTTP 客户端 +│ ├── java # Java 客户端 +│ ├── node # Node.js 客户端 +│ ├── pyauto # 群友封装的客户端 +│ ├── python # Python 客户端 +│ ├── rust # Rust 客户端 +│ └── wcferry-node # Node.js 客户端 +└── docs # 文档 +``` + +## 版本更新 + +### v39.6.0 +* 适配微信 `3.9.12.56`,核心 spy 由 x64 迁回 **x86**(32 位)。 +* `wcf.exe` 增强:语义化退出码、`--gui` 开关、重复注入防护、按需等待微信启动。 + +
点击查看更多 + +客户端越来越多了,版本号开始混乱,所以重新定义了版本号:`w.x.y.z`。 + +其中: +* `w` 是微信的大版本号,如 `37` (3.7.a.a), `38` (3.8.a.a), `39` (3.9.a.a) +* `x` 是适配的微信的小版本号,从 0 开始 +* `y` 是 `WeChatFerry` 的版本,从 0 开始 +* `z` 是各客户端的版本,从 0 开始 + +### v39.5.2 +* 没有新功能 + +### v39.5.1 +* 修复邀请进群偶发失败 +* 修复获取 wxid 失败 + +### v39.5.0 + +* 适配 `3.9.12.51`。 + +### v39.4.5 + +* 修复发送 XML 功能。 + +### v39.4.4 + +* 实现发送 XML 功能。 + +### v39.4.3 + +* 实现通过好友申请功能。 + +### v39.4.2 + +* 修复附件下载类型错误。 + +### v39.4.1 + +* 修复乱码问题。 + +### v39.4.0 + +* 重构代码,适配 `3.9.12.17`。 + +### v39.3.5 + +* 代码优化 + +### v39.3.4 + +* 实现获取登录二维码 + +### v39.3.3 + +* 修复发送文件 / 图片中文路径问题 + +### v39.3.2 + +* 修复接收消息问题 + +### v39.3.0 + +* 适配 `3.9.11.25` + +### v39.2.4 + +* 修复 wxid 问题 + +### v39.2.3 + +* 实现发送 GIF + +### v39.2.2 + +* 修复开启、停止接收消息失败问题 + +### v39.2.1 + +* 实现了好多功能(见功能清单) + +### v39.2.0 + +* 开始适配 `3.9.10.27` +* 实现检查登录状态 +* 实现获取登录账号信息(wxid、昵称、手机号、数据目录) +* 实现获取消息类型 +* 实现开启接收消息 +* 实现停止接收消息 +* 实现发送文本消息(可 @) +* 实现发送图片消息 + +### v39.1.0 (2024.04.19) + +* 适配 x64 环境 +* 重构项目 +* 开始适配 `3.9.10.19` + +
diff --git a/WeChatFerry/.editorconfig b/WeChatFerry/.editorconfig new file mode 100644 index 00000000..46da929a --- /dev/null +++ b/WeChatFerry/.editorconfig @@ -0,0 +1,7 @@ +[*] +end_of_line = lf +charset = utf-8-bom +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 4 \ No newline at end of file diff --git a/WeChatFerry/CMakeLists.txt b/WeChatFerry/CMakeLists.txt index 0abeef38..738e2c4e 100644 --- a/WeChatFerry/CMakeLists.txt +++ b/WeChatFerry/CMakeLists.txt @@ -40,7 +40,7 @@ add_link_options( # Include directories include_directories( - ${CMAKE_SOURCE_DIR}/com + ${CMAKE_SOURCE_DIR}/spy/com ${CMAKE_SOURCE_DIR}/rpc ${CMAKE_SOURCE_DIR}/rpc/nanopb ${CMAKE_SOURCE_DIR}/rpc/proto diff --git a/WeChatFerry/WeChatFerry.sln b/WeChatFerry/WeChatFerry.sln index 02ba879a..8e49373a 100644 --- a/WeChatFerry/WeChatFerry.sln +++ b/WeChatFerry/WeChatFerry.sln @@ -10,23 +10,27 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "sdk", "sdk\sdk.vcxproj", "{ {4DE80B82-5F6A-4C4C-9D16-1574308110FA} = {4DE80B82-5F6A-4C4C-9D16-1574308110FA} EndProjectSection EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "wcf", "wcf\wcf.vcxproj", "{02747CE0-AD9F-4812-B019-FCF9867F7514}" + ProjectSection(ProjectDependencies) = postProject + {4DE80B82-5F6A-4C4C-9D16-1574308110FA} = {4DE80B82-5F6A-4C4C-9D16-1574308110FA} + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|x64 = Debug|x64 - Dev|x64 = Dev|x64 - Release|x64 = Release|x64 + Debug|x86 = Debug|x86 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {4DE80B82-5F6A-4C4C-9D16-1574308110FA}.Debug|x64.ActiveCfg = Debug|x64 - {4DE80B82-5F6A-4C4C-9D16-1574308110FA}.Debug|x64.Build.0 = Debug|x64 - {4DE80B82-5F6A-4C4C-9D16-1574308110FA}.Dev|x64.ActiveCfg = Dev|x64 - {4DE80B82-5F6A-4C4C-9D16-1574308110FA}.Dev|x64.Build.0 = Dev|x64 - {4DE80B82-5F6A-4C4C-9D16-1574308110FA}.Release|x64.ActiveCfg = Release|x64 - {4DE80B82-5F6A-4C4C-9D16-1574308110FA}.Release|x64.Build.0 = Release|x64 - {ABFCB647-137F-478B-A73E-F0B1E3ADC215}.Debug|x64.ActiveCfg = Debug|x64 - {ABFCB647-137F-478B-A73E-F0B1E3ADC215}.Dev|x64.ActiveCfg = Dev|x64 - {ABFCB647-137F-478B-A73E-F0B1E3ADC215}.Release|x64.ActiveCfg = Release|x64 - {ABFCB647-137F-478B-A73E-F0B1E3ADC215}.Release|x64.Build.0 = Release|x64 + {4DE80B82-5F6A-4C4C-9D16-1574308110FA}.Debug|x86.ActiveCfg = Debug|Win32 + {4DE80B82-5F6A-4C4C-9D16-1574308110FA}.Debug|x86.Build.0 = Debug|Win32 + {4DE80B82-5F6A-4C4C-9D16-1574308110FA}.Release|x86.ActiveCfg = Release|Win32 + {4DE80B82-5F6A-4C4C-9D16-1574308110FA}.Release|x86.Build.0 = Release|Win32 + {ABFCB647-137F-478B-A73E-F0B1E3ADC215}.Debug|x86.ActiveCfg = Release|Win32 + {ABFCB647-137F-478B-A73E-F0B1E3ADC215}.Release|x86.ActiveCfg = Release|Win32 + {ABFCB647-137F-478B-A73E-F0B1E3ADC215}.Release|x86.Build.0 = Release|Win32 + {02747CE0-AD9F-4812-B019-FCF9867F7514}.Debug|x86.ActiveCfg = Release|Win32 + {02747CE0-AD9F-4812-B019-FCF9867F7514}.Release|x86.ActiveCfg = Release|Win32 + {02747CE0-AD9F-4812-B019-FCF9867F7514}.Release|x86.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/WeChatFerry/com/framework.h b/WeChatFerry/com/framework.h index 80cbbc9b..8d8692c7 100644 --- a/WeChatFerry/com/framework.h +++ b/WeChatFerry/com/framework.h @@ -1,5 +1,3 @@ #pragma once -#define WIN32_LEAN_AND_MEAN // 从 Windows 头文件中排除极少使用的内容 -// Windows 头文件 -#include +#include "../spy/com/framework.h" diff --git a/WeChatFerry/com/log.hpp b/WeChatFerry/com/log.hpp index a8fa8f80..03a18a1d 100644 --- a/WeChatFerry/com/log.hpp +++ b/WeChatFerry/com/log.hpp @@ -1,90 +1,3 @@ #pragma once -#ifdef ENABLE_DEBUG_LOG -#define SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_DEBUG -#endif - -#include -#include -#include -#include -#include - -#include -#include -#include -#include "framework.h" - -#define LOG_DEBUG(...) SPDLOG_DEBUG(__VA_ARGS__) -#define LOG_INFO(...) SPDLOG_INFO(__VA_ARGS__) -#define LOG_WARN(...) SPDLOG_WARN(__VA_ARGS__) -#define LOG_ERROR(...) SPDLOG_ERROR(__VA_ARGS__) - -#ifdef ENABLE_DEBUG_LOG -#define LOG_BUFFER(buf, len) Log::log_buffer((buf), (len)) -#else -#define LOG_BUFFER(...) (void)0 -#endif - -namespace Log -{ -inline constexpr char DEFAULT_LOGGER_NAME[] = "WCF"; -inline constexpr char DEFAULT_LOGGER_FILE[] = "logs/wcf.txt"; -inline constexpr size_t DEFAULT_LOGGER_MAX_SIZE = 1024 * 1024 * 10; // 10MB -inline constexpr size_t DEFAULT_LOGGER_MAX_FILES = 10; - -inline void InitLogger(const std::string &path) -{ - static std::shared_ptr logger = nullptr; - - if (logger != nullptr) { - return; // 已初始化 - } - - std::filesystem::path filename = std::filesystem::path(path) / DEFAULT_LOGGER_FILE; - std::filesystem::path logDir = filename.parent_path(); - - if (!std::filesystem::exists(logDir)) { - std::filesystem::create_directories(logDir); - } - - try { - logger = spdlog::rotating_logger_mt(DEFAULT_LOGGER_NAME, filename.string(), DEFAULT_LOGGER_MAX_SIZE, - DEFAULT_LOGGER_MAX_FILES); - } catch (const spdlog::spdlog_ex &ex) { - MessageBoxA(NULL, ex.what(), "Init LOGGER ERROR", MB_ICONERROR); - return; - } - - spdlog::set_default_logger(logger); - logger->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%l] [%n] [%s::%#::%!] %v"); - -#if SPDLOG_ACTIVE_LEVEL <= SPDLOG_LEVEL_DEBUG - spdlog::set_level(spdlog::level::debug); - logger->flush_on(spdlog::level::debug); -#else - logger->flush_on(spdlog::level::info); -#endif - - LOG_DEBUG("InitLogger with debug level"); -} - -#ifdef ENABLE_DEBUG_LOG -inline void log_buffer(uint8_t *buffer, size_t len) -{ - constexpr size_t BUF_SIZE = 1024 * 1024; - std::ostringstream oss; - - oss << "BUF@" << static_cast(buffer) << "[" << len << "]: "; - for (size_t i = 0; i < len; ++i) { - oss << std::hex << std::uppercase << std::setw(2) << std::setfill('0') << static_cast(buffer[i]) << " "; - if (oss.tellp() > BUF_SIZE - 3) { - break; // 防止缓冲区溢出 - } - } - - LOG_DEBUG(oss.str()); -} -#endif - -} // namespace Log +#include "../spy/com/log.hpp" diff --git a/WeChatFerry/com/util.cpp b/WeChatFerry/com/util.cpp index f5a83763..6ec111f4 100644 --- a/WeChatFerry/com/util.cpp +++ b/WeChatFerry/com/util.cpp @@ -1,310 +1 @@ -#include "util.h" - -#include -#include -#include -#include -#include -#include - -#include -#include - -#include "log.hpp" - -#pragma comment(lib, "shlwapi") -#pragma comment(lib, "Version.lib") - -namespace fs = std::filesystem; - -namespace util -{ - -constexpr char WECHATEXE[] = "WeChat.exe"; -constexpr char WECHATWINDLL[] = "WeChatWin.dll"; - -std::wstring s2w(const std::string &s) -{ - if (s.empty()) return std::wstring(); - int size_needed = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast(s.size()), nullptr, 0); - std::wstring ws(size_needed, 0); - MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast(s.size()), &ws[0], size_needed); - return ws; -} - -std::string w2s(const std::wstring &ws) -{ - if (ws.empty()) return std::string(); - int size_needed - = WideCharToMultiByte(CP_UTF8, 0, ws.c_str(), static_cast(ws.size()), nullptr, 0, nullptr, nullptr); - std::string s(size_needed, 0); - WideCharToMultiByte(CP_UTF8, 0, ws.c_str(), static_cast(ws.size()), &s[0], size_needed, nullptr, nullptr); - return s; -} - -std::string gb2312_to_utf8(const char *gb2312) -{ - if (!gb2312) return ""; - - int size_needed = MultiByteToWideChar(CP_ACP, 0, gb2312, -1, nullptr, 0); - std::wstring ws(size_needed, 0); - MultiByteToWideChar(CP_ACP, 0, gb2312, -1, &ws[0], size_needed); - - size_needed = WideCharToMultiByte(CP_UTF8, 0, ws.c_str(), -1, nullptr, 0, nullptr, nullptr); - std::string s(size_needed, 0); - WideCharToMultiByte(CP_UTF8, 0, ws.c_str(), -1, &s[0], size_needed, nullptr, nullptr); - - return s; -} - -static DWORD get_wechat_pid() -{ - DWORD pid = 0; - HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); - if (hSnapshot == INVALID_HANDLE_VALUE) return 0; - - PROCESSENTRY32W pe32 = { sizeof(PROCESSENTRY32W) }; - while (Process32NextW(hSnapshot, &pe32)) { - if (pe32.szExeFile == s2w(WECHATEXE)) { - pid = pe32.th32ProcessID; - break; - } - } - CloseHandle(hSnapshot); - return pid; -} - -static std::optional get_wechat_path() -{ - HKEY hKey; - if (RegOpenKeyExA(HKEY_CURRENT_USER, "Software\\Tencent\\WeChat", 0, KEY_READ, &hKey) != ERROR_SUCCESS) { - LOG_ERROR("无法打开注册表项"); - return std::nullopt; - } - - char path[MAX_PATH] = { 0 }; - DWORD type = REG_SZ; - DWORD size = sizeof(path); - if (RegQueryValueExA(hKey, "InstallPath", nullptr, &type, reinterpret_cast(path), &size) != ERROR_SUCCESS) { - RegCloseKey(hKey); - LOG_ERROR("无法读取注册表中的 InstallPath"); - return std::nullopt; - } - RegCloseKey(hKey); - - PathAppendA(path, WECHATEXE); - return std::string(path); -} - -static std::optional get_wechat_win_dll_path() -{ - auto wechat_path = get_wechat_path(); - if (!wechat_path) { - return std::nullopt; - } - - fs::path dll_path = *wechat_path; - dll_path = dll_path.parent_path(); - - fs::path wechat_dll_path = dll_path / WECHATWINDLL; - if (fs::exists(wechat_dll_path)) { // 尝试直接查找 WeChatWin.dll - return wechat_dll_path.string(); - } - - // 微信从(大约)3.7开始,增加了一层版本目录: [3.7.0.29] - std::optional found_path; - for (const auto &entry : fs::directory_iterator(dll_path)) { - if (entry.is_directory()) { - fs::path possible_dll = entry.path() / WECHATWINDLL; - if (fs::exists(possible_dll)) { - found_path = possible_dll.string(); - break; // 取第一个找到的版本号文件夹 - } - } - } - - if (!found_path) { - LOG_ERROR("未找到 WeChatWin.dll"); - } - - return found_path; -} - -static std::optional get_file_version(const std::string &path) -{ - if (!PathFileExistsA(path.c_str())) { - LOG_ERROR("文件不存在: {}", path); - return std::nullopt; - } - - DWORD dummy = 0; - DWORD size = GetFileVersionInfoSizeA(path.c_str(), &dummy); - if (size == 0) { - LOG_ERROR("无法获取文件版本信息大小: {}", path); - return std::nullopt; - } - - std::vector buffer(size); - if (!GetFileVersionInfoA(path.c_str(), 0, size, buffer.data())) { - LOG_ERROR("无法获取文件版本信息: {}", path); - return std::nullopt; - } - - VS_FIXEDFILEINFO *ver_info = nullptr; - UINT ver_size = 0; - if (!VerQueryValueA(buffer.data(), "\\", reinterpret_cast(&ver_info), &ver_size)) { - LOG_ERROR("无法获取文件版本信息: {}", path); - return std::nullopt; - } - - return fmt::format("{}.{}.{}.{}", HIWORD(ver_info->dwFileVersionMS), LOWORD(ver_info->dwFileVersionMS), - HIWORD(ver_info->dwFileVersionLS), LOWORD(ver_info->dwFileVersionLS)); -} - -std::string get_wechat_version() -{ - auto dll_path = get_wechat_win_dll_path(); - if (!dll_path) { - LOG_ERROR("无法获取 WeChatWin.dll 路径"); - return ""; - } - - auto version = get_file_version(*dll_path); - if (!version) { - LOG_ERROR("无法获取 WeChat 版本信息"); - return ""; - } - - return *version; -} - -int open_wechat(DWORD &pid) -{ - pid = get_wechat_pid(); - if (pid != 0) { - return ERROR_SUCCESS; - } - - auto wechat_path = util::get_wechat_path(); - if (!wechat_path) { - LOG_ERROR("获取 WeChat 安装路径失败"); - return ERROR_FILE_NOT_FOUND; - } - - STARTUPINFOA si = { sizeof(si) }; - PROCESS_INFORMATION pi = {}; - - std::string command_line = *wechat_path; - if (!CreateProcessA(nullptr, command_line.data(), nullptr, nullptr, FALSE, CREATE_NEW_CONSOLE, nullptr, nullptr, - &si, &pi)) { - return GetLastError(); - } - - CloseHandle(pi.hThread); - CloseHandle(pi.hProcess); - - pid = pi.dwProcessId; - return ERROR_SUCCESS; -} - -uint32_t get_memory_int_by_address(HANDLE hProcess, uint64_t addr) -{ - uint32_t value = 0; - if (!addr || !hProcess) return value; - - ReadProcessMemory(hProcess, reinterpret_cast(addr), &value, sizeof(value), nullptr); - - return value; -} - -std::wstring get_unicode_info_by_address(HANDLE hProcess, uint64_t address) -{ - if (!hProcess || !address) return L""; - - uint64_t str_address = get_memory_int_by_address(hProcess, address); - uint64_t str_len = get_memory_int_by_address(hProcess, address + 0x4); - if (str_len > 500) return L""; - - wchar_t cValue[500] = { 0 }; - if (ReadProcessMemory(hProcess, reinterpret_cast(str_address), cValue, (str_len + 1) * sizeof(wchar_t), - nullptr)) { - return std::wstring(cValue); - } - - return L""; -} - -void dbg_msg(const char *format, ...) -{ - if (!format) return; - - va_list args; - va_start(args, format); - - va_list args_copy; - va_copy(args_copy, args); - int len = vsnprintf(nullptr, 0, format, args_copy); - va_end(args_copy); - - std::vector buffer(len + 1); - vsnprintf(buffer.data(), buffer.size(), format, args); - va_end(args); - - OutputDebugStringW(s2w(buffer.data()).c_str()); -} - -std::unique_ptr new_wx_string(const char *str) -{ - return new_wx_string(str ? std::string(str) : std::string()); -} - -std::unique_ptr new_wx_string(const std::string &str) { return std::make_unique(s2w(str)); } - -std::unique_ptr new_wx_string(const wchar_t *wstr) -{ - return new_wx_string(wstr ? std::wstring(wstr) : std::wstring()); -} - -std::unique_ptr new_wx_string(const std::wstring &wstr) { return std::make_unique(wstr); } - -AtWxidSplitResult<> parse_wxids(const std::string &atWxids) -{ - AtWxidSplitResult<> result; - if (!atWxids.empty()) { - std::wstringstream wss(util::s2w(atWxids)); - for (std::wstring wxid; std::getline(wss, wxid, L',');) { - result.wxids.push_back(wxid); - result.wxWxids.emplace_back(result.wxids.back()); - } - } - return result; -} - -WxString *CreateWxString(const std::string &s) -{ - std::wstring ws = util::s2w(s); - WxString *wxStr = reinterpret_cast(HeapAlloc(GetProcessHeap(), 8, sizeof(WxString))); - if (!wxStr) return nullptr; - size_t len = ws.length(); - wchar_t *ptr = reinterpret_cast(HeapAlloc(GetProcessHeap(), 8, (len + 1) * sizeof(wchar_t))); - if (!ptr) { - HeapFree(GetProcessHeap(), 8, wxStr); - return nullptr; - } - wmemcpy(ptr, ws.c_str(), len + 1); - wxStr->wptr = ptr; - wxStr->size = static_cast(ws.size()); - wxStr->length = static_cast(ws.length()); - wxStr->clen = 0; - wxStr->ptr = nullptr; - return wxStr; -} - -void FreeWxString(WxString *wxStr) -{ - if (wxStr) { - if (wxStr->wptr) HeapFree(GetProcessHeap(), 8, const_cast(wxStr->wptr)); - HeapFree(GetProcessHeap(), 8, wxStr); - } -} -} // namespace util +#include "../spy/com/util.cpp" diff --git a/WeChatFerry/com/util.h b/WeChatFerry/com/util.h index 6e83b014..d43ff270 100644 --- a/WeChatFerry/com/util.h +++ b/WeChatFerry/com/util.h @@ -1,105 +1,3 @@ -#pragma once +#pragma once -#include -#include -#include -#include - -#include "spy_types.h" - -namespace util -{ -struct PortPath { - int port; - char path[MAX_PATH]; -}; - -static DWORD get_wechat_pid(); -int open_wechat(DWORD &pid); -std::string get_wechat_version(); -uint32_t get_memory_int_by_address(HANDLE hProcess, uint64_t addr); -std::wstring get_unicode_info_by_address(HANDLE hProcess, uint64_t addr); -std::wstring s2w(const std::string &s); -std::string w2s(const std::wstring &ws); -std::string gb2312_to_utf8(const char *gb2312); -void dbg_msg(const char *format, ...); - -inline DWORD get_dword(uint64_t addr) { return addr ? *reinterpret_cast(addr) : 0; } -inline QWORD get_qword(uint64_t addr) { return addr ? *reinterpret_cast(addr) : 0; } -inline uint64_t get_uint64(uint64_t addr) { return addr ? *reinterpret_cast(addr) : 0; } -inline std::string get_p_string(uint64_t addr) { return addr ? std::string(reinterpret_cast(addr)) : ""; } -inline std::string get_p_string(uint64_t addr, size_t len) -{ - return addr ? std::string(reinterpret_cast(addr), len) : ""; -} -inline std::wstring get_p_wstring(uint64_t addr) -{ - return addr ? std::wstring(reinterpret_cast(addr)) : L""; -} -inline std::wstring get_p_wstring(uint64_t addr, size_t len) -{ - return addr ? std::wstring(reinterpret_cast(addr), len) : L""; -} -inline std::string get_pp_string(uint64_t addr) -{ - if (!addr) return ""; - - const char *ptr = *reinterpret_cast(addr); - return (ptr && *ptr) ? std::string(ptr) : ""; -} -inline std::wstring get_pp_wstring(uint64_t addr) -{ - if (!addr) return L""; - - const wchar_t *ptr = *reinterpret_cast(addr); - return (ptr && *ptr) ? std::wstring(ptr) : L""; -} -inline std::string get_pp_len_string(uint64_t addr) -{ - size_t len = get_dword(addr + 8); - return (addr && len) ? std::string(*reinterpret_cast(addr), len) : ""; -} -inline std::wstring get_pp_len_wstring(uint64_t addr) -{ - size_t len = get_dword(addr + 8); - return (addr && len) ? std::wstring(*reinterpret_cast(addr), len) : L""; -} -inline std::string get_str_by_wstr_addr(uint64_t addr) { return w2s(get_pp_len_wstring(addr)); } -inline void *AllocFromHeap(size_t size) { return HeapAlloc(GetProcessHeap(), 8, size); } -inline void FreeBuffer(void *buffer) -{ - if (buffer) HeapFree(GetProcessHeap(), 8, buffer); -} -inline int MsgBox(HWND hWnd, const std::string &text, const std::string &caption = "WCF", UINT uType = MB_OK) -{ - std::wstring wText = s2w(text); - std::wstring wCaption = s2w(caption); - return MessageBoxW(nullptr, wText.c_str(), wCaption.c_str(), uType); -} - -template static T *AllocBuffer(size_t count) -{ - return reinterpret_cast(HeapAlloc(GetProcessHeap(), 8, sizeof(T) * count)); -} - -template struct WxStringHolder { - std::wstring ws; - WxString wx; - explicit WxStringHolder(const T &str) : ws(util::s2w(str)), wx(ws) { } -}; - -template struct AtWxidSplitResult { - std::vector wxids; - std::vector wxWxids; -}; - -WxString *CreateWxString(const std::string &s); -void FreeWxString(WxString *wxStr); -AtWxidSplitResult<> parse_wxids(const std::string &atWxids); - -std::unique_ptr new_wx_string(const char *str); -std::unique_ptr new_wx_string(const wchar_t *wstr); -std::unique_ptr new_wx_string(const std::string &str); -std::unique_ptr new_wx_string(const std::wstring &wstr); - -} // namespace util +#include "../spy/com/util.h" diff --git a/WeChatFerry/rpc/pb_types.h b/WeChatFerry/rpc/pb_types.h index 82f5459b..897b8af3 100644 --- a/WeChatFerry/rpc/pb_types.h +++ b/WeChatFerry/rpc/pb_types.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include #include @@ -55,7 +55,6 @@ typedef struct { string name; string mobile; string home; - string alias; } UserInfo_t; typedef struct { diff --git a/WeChatFerry/rpc/pb_util.cpp b/WeChatFerry/rpc/pb_util.cpp index 5bde0f56..5f8c4bd7 100644 --- a/WeChatFerry/rpc/pb_util.cpp +++ b/WeChatFerry/rpc/pb_util.cpp @@ -68,6 +68,7 @@ bool encode_contacts(pb_ostream_t *stream, const pb_field_t *field, void *const vector *v = (vector *)*arg; RpcContact message = RpcContact_init_default; + LOG_DEBUG("encode_contacts[{}]", v->size()); for (auto it = v->begin(); it != v->end(); it++) { message.wxid.funcs.encode = &encode_string; message.wxid.arg = (void *)(*it).wxid.c_str(); diff --git a/WeChatFerry/rpc/proto/wcf.proto b/WeChatFerry/rpc/proto/wcf.proto index ad44e9e6..a86aa2e1 100644 --- a/WeChatFerry/rpc/proto/wcf.proto +++ b/WeChatFerry/rpc/proto/wcf.proto @@ -1,4 +1,4 @@ -syntax = "proto3"; +syntax = "proto3"; package wcf; option java_package = "com.iamteer"; @@ -13,6 +13,7 @@ enum Functions { FUNC_GET_DB_TABLES = 0x14; FUNC_GET_USER_INFO = 0x15; FUNC_GET_AUDIO_MSG = 0x16; + FUNC_GET_DB_KEY = 0x17; FUNC_SEND_TXT = 0x20; FUNC_SEND_IMG = 0x21; FUNC_SEND_FILE = 0x22; @@ -36,7 +37,6 @@ enum Functions { FUNC_ADD_ROOM_MEMBERS = 0x70; FUNC_DEL_ROOM_MEMBERS = 0x71; FUNC_INV_ROOM_MEMBERS = 0x72; - FUNC_SHUTDOWN = 0xFF; } message Request @@ -118,7 +118,7 @@ message XmlMsg string receiver = 1; // 消息接收人 string content = 2; // xml 内容 string path = 3; // 图片路径 - uint64 type = 4; // 消息类型 + int32 type = 4; // 消息类型 } message MsgTypes { map types = 1; } @@ -179,7 +179,6 @@ message UserInfo string name = 2; // 昵称 string mobile = 3; // 手机号 string home = 4; // 文件/图片等父路径 - string alias = 5; // 修改后的wxid } message DecPath @@ -236,24 +235,3 @@ message ForwardMsg uint64 id = 1 [ jstype = JS_STRING ]; // 待转发消息 ID string receiver = 2; // 转发接收目标,群为 roomId,个人为 wxid } - -message RoomData -{ - message RoomMember - { - string wxid = 1; - optional string name = 2; // 群昵称 - int32 state = 3; - } - - repeated RoomMember members = 1; - - optional int32 field_2 = 2; - int32 field_3 = 3; - optional int32 field_4 = 4; - int32 capacity = 5; - optional string field_6 = 6; - int32 field_7 = 7; - int32 field_8 = 8; - repeated string admins = 9; // 管理员 -} diff --git a/WeChatFerry/rpc/tool/proto/nanopb_pb2.py b/WeChatFerry/rpc/tool/proto/nanopb_pb2.py index 5db30586..eea6b3e3 100644 --- a/WeChatFerry/rpc/tool/proto/nanopb_pb2.py +++ b/WeChatFerry/rpc/tool/proto/nanopb_pb2.py @@ -2,11 +2,9 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # source: nanopb.proto """Generated protocol buffer code.""" -from google.protobuf.internal import enum_type_wrapper +from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import message as _message -from google.protobuf import reflection as _reflection from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) @@ -18,52 +16,8 @@ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0cnanopb.proto\x1a google/protobuf/descriptor.proto\"\xa4\x07\n\rNanoPBOptions\x12\x10\n\x08max_size\x18\x01 \x01(\x05\x12\x12\n\nmax_length\x18\x0e \x01(\x05\x12\x11\n\tmax_count\x18\x02 \x01(\x05\x12&\n\x08int_size\x18\x07 \x01(\x0e\x32\x08.IntSize:\nIS_DEFAULT\x12$\n\x04type\x18\x03 \x01(\x0e\x32\n.FieldType:\nFT_DEFAULT\x12\x18\n\nlong_names\x18\x04 \x01(\x08:\x04true\x12\x1c\n\rpacked_struct\x18\x05 \x01(\x08:\x05\x66\x61lse\x12\x1a\n\x0bpacked_enum\x18\n \x01(\x08:\x05\x66\x61lse\x12\x1b\n\x0cskip_message\x18\x06 \x01(\x08:\x05\x66\x61lse\x12\x18\n\tno_unions\x18\x08 \x01(\x08:\x05\x66\x61lse\x12\r\n\x05msgid\x18\t \x01(\r\x12\x1e\n\x0f\x61nonymous_oneof\x18\x0b \x01(\x08:\x05\x66\x61lse\x12\x15\n\x06proto3\x18\x0c \x01(\x08:\x05\x66\x61lse\x12#\n\x14proto3_singular_msgs\x18\x15 \x01(\x08:\x05\x66\x61lse\x12\x1d\n\x0e\x65num_to_string\x18\r \x01(\x08:\x05\x66\x61lse\x12\x1b\n\x0c\x66ixed_length\x18\x0f \x01(\x08:\x05\x66\x61lse\x12\x1a\n\x0b\x66ixed_count\x18\x10 \x01(\x08:\x05\x66\x61lse\x12\x1e\n\x0fsubmsg_callback\x18\x16 \x01(\x08:\x05\x66\x61lse\x12/\n\x0cmangle_names\x18\x11 \x01(\x0e\x32\x11.TypenameMangling:\x06M_NONE\x12(\n\x11\x63\x61llback_datatype\x18\x12 \x01(\t:\rpb_callback_t\x12\x34\n\x11\x63\x61llback_function\x18\x13 \x01(\t:\x19pb_default_field_callback\x12\x30\n\x0e\x64\x65scriptorsize\x18\x14 \x01(\x0e\x32\x0f.DescriptorSize:\x07\x44S_AUTO\x12\x1a\n\x0b\x64\x65\x66\x61ult_has\x18\x17 \x01(\x08:\x05\x66\x61lse\x12\x0f\n\x07include\x18\x18 \x03(\t\x12\x0f\n\x07\x65xclude\x18\x1a \x03(\t\x12\x0f\n\x07package\x18\x19 \x01(\t\x12\x41\n\rtype_override\x18\x1b \x01(\x0e\x32*.google.protobuf.FieldDescriptorProto.Type\x12\x19\n\x0bsort_by_tag\x18\x1c \x01(\x08:\x04true\x12.\n\rfallback_type\x18\x1d \x01(\x0e\x32\n.FieldType:\x0b\x46T_CALLBACK*i\n\tFieldType\x12\x0e\n\nFT_DEFAULT\x10\x00\x12\x0f\n\x0b\x46T_CALLBACK\x10\x01\x12\x0e\n\nFT_POINTER\x10\x04\x12\r\n\tFT_STATIC\x10\x02\x12\r\n\tFT_IGNORE\x10\x03\x12\r\n\tFT_INLINE\x10\x05*D\n\x07IntSize\x12\x0e\n\nIS_DEFAULT\x10\x00\x12\x08\n\x04IS_8\x10\x08\x12\t\n\x05IS_16\x10\x10\x12\t\n\x05IS_32\x10 \x12\t\n\x05IS_64\x10@*Z\n\x10TypenameMangling\x12\n\n\x06M_NONE\x10\x00\x12\x13\n\x0fM_STRIP_PACKAGE\x10\x01\x12\r\n\tM_FLATTEN\x10\x02\x12\x16\n\x12M_PACKAGE_INITIALS\x10\x03*E\n\x0e\x44\x65scriptorSize\x12\x0b\n\x07\x44S_AUTO\x10\x00\x12\x08\n\x04\x44S_1\x10\x01\x12\x08\n\x04\x44S_2\x10\x02\x12\x08\n\x04\x44S_4\x10\x04\x12\x08\n\x04\x44S_8\x10\x08:E\n\x0enanopb_fileopt\x12\x1c.google.protobuf.FileOptions\x18\xf2\x07 \x01(\x0b\x32\x0e.NanoPBOptions:G\n\rnanopb_msgopt\x12\x1f.google.protobuf.MessageOptions\x18\xf2\x07 \x01(\x0b\x32\x0e.NanoPBOptions:E\n\x0enanopb_enumopt\x12\x1c.google.protobuf.EnumOptions\x18\xf2\x07 \x01(\x0b\x32\x0e.NanoPBOptions:>\n\x06nanopb\x12\x1d.google.protobuf.FieldOptions\x18\xf2\x07 \x01(\x0b\x32\x0e.NanoPBOptionsB\x1a\n\x18\x66i.kapsi.koti.jpa.nanopb') -_FIELDTYPE = DESCRIPTOR.enum_types_by_name['FieldType'] -FieldType = enum_type_wrapper.EnumTypeWrapper(_FIELDTYPE) -_INTSIZE = DESCRIPTOR.enum_types_by_name['IntSize'] -IntSize = enum_type_wrapper.EnumTypeWrapper(_INTSIZE) -_TYPENAMEMANGLING = DESCRIPTOR.enum_types_by_name['TypenameMangling'] -TypenameMangling = enum_type_wrapper.EnumTypeWrapper(_TYPENAMEMANGLING) -_DESCRIPTORSIZE = DESCRIPTOR.enum_types_by_name['DescriptorSize'] -DescriptorSize = enum_type_wrapper.EnumTypeWrapper(_DESCRIPTORSIZE) -FT_DEFAULT = 0 -FT_CALLBACK = 1 -FT_POINTER = 4 -FT_STATIC = 2 -FT_IGNORE = 3 -FT_INLINE = 5 -IS_DEFAULT = 0 -IS_8 = 8 -IS_16 = 16 -IS_32 = 32 -IS_64 = 64 -M_NONE = 0 -M_STRIP_PACKAGE = 1 -M_FLATTEN = 2 -M_PACKAGE_INITIALS = 3 -DS_AUTO = 0 -DS_1 = 1 -DS_2 = 2 -DS_4 = 4 -DS_8 = 8 - -NANOPB_FILEOPT_FIELD_NUMBER = 1010 -nanopb_fileopt = DESCRIPTOR.extensions_by_name['nanopb_fileopt'] -NANOPB_MSGOPT_FIELD_NUMBER = 1010 -nanopb_msgopt = DESCRIPTOR.extensions_by_name['nanopb_msgopt'] -NANOPB_ENUMOPT_FIELD_NUMBER = 1010 -nanopb_enumopt = DESCRIPTOR.extensions_by_name['nanopb_enumopt'] -NANOPB_FIELD_NUMBER = 1010 -nanopb = DESCRIPTOR.extensions_by_name['nanopb'] - -_NANOPBOPTIONS = DESCRIPTOR.message_types_by_name['NanoPBOptions'] -NanoPBOptions = _reflection.GeneratedProtocolMessageType('NanoPBOptions', (_message.Message,), { - 'DESCRIPTOR' : _NANOPBOPTIONS, - '__module__' : 'nanopb_pb2' - # @@protoc_insertion_point(class_scope:NanoPBOptions) - }) -_sym_db.RegisterMessage(NanoPBOptions) - +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'nanopb_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(nanopb_fileopt) google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(nanopb_msgopt) diff --git a/WeChatFerry/sdk/CMakeLists.txt b/WeChatFerry/sdk/CMakeLists.txt index 158a4412..4ed735ba 100644 --- a/WeChatFerry/sdk/CMakeLists.txt +++ b/WeChatFerry/sdk/CMakeLists.txt @@ -12,8 +12,8 @@ add_library(sdk SHARED sdk.def # Common files - ${CMAKE_SOURCE_DIR}/com/util.cpp - ${CMAKE_SOURCE_DIR}/com/util.h + ${CMAKE_SOURCE_DIR}/spy/com/util.cpp + ${CMAKE_SOURCE_DIR}/spy/com/util.h ) target_link_libraries(sdk PRIVATE diff --git a/WeChatFerry/sdk/SDK.vcxproj b/WeChatFerry/sdk/SDK.vcxproj index 054ecc1f..bbb5ff4b 100644 --- a/WeChatFerry/sdk/SDK.vcxproj +++ b/WeChatFerry/sdk/SDK.vcxproj @@ -1,12 +1,16 @@ - + Debug - x64 + Win32 - - Dev + + Release + Win32 + + + Debug x64 @@ -22,13 +26,20 @@ 10.0 - + DynamicLibrary true v142 Unicode - + + DynamicLibrary + false + v142 + true + Unicode + + DynamicLibrary true v142 @@ -46,51 +57,85 @@ - + + + + - + - + true + $(ProjectDir)..\_build\$(ProjectName)\$(Configuration)\ - + + false + $(ProjectDir)..\_build\$(ProjectName)\$(Configuration)\ + + true false - + true - x64-windows-static + x86-windows-static - - true - - + Level3 true - _DEBUG;SDK_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + WIN32;_DEBUG;SDK_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) true Use pch.h - /utf-8 %(AdditionalOptions) Windows true false - - + sdk.def + + + + + Level3 + true + true + true + WIN32;NDEBUG;SDK_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + NotUsing + + + stdcpp17 + $(ProjectDir);$(ProjectDir)..\spy\com;%(AdditionalIncludeDirectories) + MultiThreaded + + + Windows + true + true + true + false + sdk.def + + if not exist "$(ProjectDir)..\Out" md "$(ProjectDir)..\Out" +xcopy /y "$(OutDir)$(TargetFileName)" "$(ProjectDir)..\Out\" + + + Copy files + - + Level3 true @@ -98,14 +143,12 @@ true Use pch.h - /utf-8 %(AdditionalOptions) Windows true false - - + sdk.def @@ -114,48 +157,39 @@ true true true - WIN32;NDEBUG;SDK_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + NDEBUG;SDK_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) true - NotUsing - - - stdcpp17 - $(SolutionDir)com;$(SolutionDir)spy;C:\Tools\vcpkg\installed\x64-windows-static\include - - true - MultiThreaded - /utf-8 %(AdditionalOptions) + Use + pch.h Windows true true - false + true false - - + sdk.def - - xcopy /y $(OutDir)$(TargetFileName) $(SolutionDir)Out -xcopy /y $(OutDir)$(TargetFileName) $(SolutionDir)..\clients\python\wcferry - - - Copy files - - - + + - + + + + + + + - \ No newline at end of file + diff --git a/WeChatFerry/sdk/SDK.vcxproj.filters b/WeChatFerry/sdk/SDK.vcxproj.filters index 9c2e881b..231cbdd8 100644 --- a/WeChatFerry/sdk/SDK.vcxproj.filters +++ b/WeChatFerry/sdk/SDK.vcxproj.filters @@ -15,16 +15,16 @@ - + 头文件 头文件 - + 头文件 - + 头文件 @@ -32,14 +32,19 @@ 源文件 + + 源文件 + 源文件 源文件 - + + + 源文件 - + - \ No newline at end of file + diff --git a/WeChatFerry/sdk/framework.h b/WeChatFerry/sdk/framework.h new file mode 100644 index 00000000..8d8692c7 --- /dev/null +++ b/WeChatFerry/sdk/framework.h @@ -0,0 +1,3 @@ +#pragma once + +#include "../spy/com/framework.h" diff --git a/WeChatFerry/sdk/injector.cpp b/WeChatFerry/sdk/injector.cpp index 6eb935ed..db5144af 100644 --- a/WeChatFerry/sdk/injector.cpp +++ b/WeChatFerry/sdk/injector.cpp @@ -1,110 +1,84 @@ #include "injector.h" -#include +#include -#include "psapi.h" +typedef BOOL(WINAPI *LPFN_ISWOW64PROCESS)(HANDLE, PBOOL); -#include "util.h" +bool g_guiMode = false; -using namespace std; - -static void handle_injection_error(HANDLE process, LPVOID remote_address, const std::string &error_msg) +void ReportError(LPCWSTR message, LPCWSTR title) { - util::MsgBox(NULL, error_msg.c_str(), "Error", MB_ICONERROR); - if (remote_address) { - VirtualFreeEx(process, remote_address, 0, MEM_RELEASE); - } - if (process) { - CloseHandle(process); + if (g_guiMode) { + MessageBox(NULL, message, title, MB_OK | MB_ICONERROR); + } else { + fwprintf(stderr, L"[%s] %s\n", title, message); + fflush(stderr); } } -HMODULE get_target_module_base(HANDLE process, const string &dll) +static void ShowErrorMessage(DWORD dwError, HANDLE hProcess) { - DWORD needed; - HMODULE modules[512]; - if (!EnumProcessModulesEx(process, modules, sizeof(modules), &needed, LIST_MODULES_64BIT)) { - util::MsgBox(NULL, "获取模块失败", "get_target_module_base", 0); - return NULL; - } - - DWORD count = needed / sizeof(HMODULE); - char module_name[MAX_PATH]; - for (DWORD i = 0; i < count; i++) { - GetModuleBaseNameA(process, modules[i], module_name, sizeof(module_name)); - if (!strncmp(dll.c_str(), module_name, dll.size())) { - return modules[i]; + BOOL bIsWow64 = FALSE; + WCHAR szErrorMessage[256] = { 0 }; + LPFN_ISWOW64PROCESS fnIsWow64Process + = (LPFN_ISWOW64PROCESS)GetProcAddress(GetModuleHandle(TEXT("kernel32")), "IsWow64Process"); + if (fnIsWow64Process != NULL && fnIsWow64Process(hProcess, &bIsWow64)) { + if (bIsWow64) { + wsprintf(szErrorMessage, L"LoadLibrary 调用失败,请检查应用版本/位数。错误码: %lu", dwError); } } - return NULL; + wsprintf(szErrorMessage, L"LoadLibrary 调用失败。错误码: %lu", dwError); + ReportError(szErrorMessage, L"InjectDll"); } -HANDLE inject_dll(DWORD pid, const string &dll_path, HMODULE *injected_base) +HANDLE InjectDll(DWORD pid, LPCWSTR dllPath, HMODULE *injectedBase) { - SIZE_T path_size = dll_path.size() + 1; - + HANDLE hThread; + SIZE_T cszDLL = (wcslen(dllPath) + 1) * sizeof(WCHAR); // 1. 打开目标进程 HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); - if (!hProcess) { - util::MsgBox(NULL, "打开进程失败", "inject_dll", 0); + if (hProcess == NULL) { + ReportError(L"打开进程失败", L"InjectDll"); return NULL; } // 2. 在目标进程的内存里开辟空间 - LPVOID pRemoteAddress = VirtualAllocEx(hProcess, NULL, path_size, MEM_COMMIT, PAGE_READWRITE); - if (!pRemoteAddress) { - handle_injection_error(hProcess, NULL, "DLL 路径写入失败"); + LPVOID pRemoteAddress = VirtualAllocEx(hProcess, NULL, cszDLL, MEM_COMMIT, PAGE_READWRITE); + if (pRemoteAddress == NULL) { + ReportError(L"DLL 路径写入失败", L"InjectDll"); return NULL; } // 3. 把 dll 的路径写入到目标进程的内存空间中 - WriteProcessMemory(hProcess, pRemoteAddress, dll_path.c_str(), path_size, NULL); + WriteProcessMemory(hProcess, pRemoteAddress, dllPath, cszDLL, NULL); - // 4. 创建一个远程线程,让目标进程调用 LoadLibrary - HMODULE k32 = GetModuleHandleA("kernel32.dll"); - if (!k32) { - handle_injection_error(hProcess, pRemoteAddress, "获取 kernel32 失败"); - return NULL; - } + // 3. 创建一个远程线程,让目标进程调用 LoadLibrary + hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)LoadLibrary, pRemoteAddress, 0, NULL); + if (hThread == NULL) { + ShowErrorMessage(GetLastError(), hProcess); + VirtualFreeEx(hProcess, pRemoteAddress, 0, MEM_RELEASE); + CloseHandle(hProcess); - FARPROC libAddr = GetProcAddress(k32, "LoadLibraryA"); - if (!libAddr) { - handle_injection_error(hProcess, pRemoteAddress, "获取 LoadLibrary 失败"); return NULL; } - HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)libAddr, pRemoteAddress, 0, NULL); - if (!hThread) { - handle_injection_error(hProcess, pRemoteAddress, "CreateRemoteThread 失败"); - return NULL; - } - - WaitForSingleObject(hThread, INFINITE); + WaitForSingleObject(hThread, -1); + GetExitCodeThread(hThread, (LPDWORD)injectedBase); CloseHandle(hThread); - - *injected_base = get_target_module_base(hProcess, filesystem::path(dll_path).filename().string()); - VirtualFreeEx(hProcess, pRemoteAddress, 0, MEM_RELEASE); + // CloseHandle(hProcess); // Close when exit + return hProcess; } -bool eject_dll(HANDLE process, HMODULE dll_base) +bool EjectDll(HANDLE process, HMODULE dllBase) { - HMODULE k32 = GetModuleHandleA("kernel32.dll"); - if (!k32) { - util::MsgBox(NULL, "获取 kernel32 失败", "eject_dll", 0); - return false; - } + HANDLE hThread = NULL; - FARPROC libAddr = GetProcAddress(k32, "FreeLibraryAndExitThread"); - if (!libAddr) { - util::MsgBox(NULL, "获取 FreeLibrary 失败", "eject_dll", 0); - return false; - } - - HANDLE hThread = CreateRemoteThread(process, NULL, 0, (LPTHREAD_START_ROUTINE)libAddr, (LPVOID)dll_base, 0, NULL); - if (!hThread) { - util::MsgBox(NULL, "FreeLibrary 调用失败!", "eject_dll", 0); + // 使目标进程调用 FreeLibrary,卸载 DLL + hThread = CreateRemoteThread(process, NULL, 0, (LPTHREAD_START_ROUTINE)FreeLibrary, (LPVOID)dllBase, 0, NULL); + if (hThread == NULL) { + ReportError(L"FreeLibrary 调用失败!", L"EjectDll"); return false; } @@ -114,34 +88,34 @@ bool eject_dll(HANDLE process, HMODULE dll_base) return true; } -static uint64_t get_func_offset(const string &dll_path, const string &func_name) +static void *GetFuncAddr(LPCWSTR dllPath, HMODULE dllBase, LPCSTR funcName) { - HMODULE dll = LoadLibraryA(dll_path.c_str()); - if (!dll) { - util::MsgBox(NULL, "获取 DLL 失败", "get_func_offset", 0); - return 0; + HMODULE hLoaded = LoadLibrary(dllPath); + if (hLoaded == NULL) { + return NULL; } - LPVOID absAddr = reinterpret_cast(GetProcAddress(dll, func_name.c_str())); - uint64_t offset = reinterpret_cast(absAddr) - reinterpret_cast(dll); - FreeLibrary(dll); + void *absAddr = GetProcAddress(hLoaded, funcName); + DWORD offset = (DWORD)absAddr - (DWORD)hLoaded; + + FreeLibrary(hLoaded); - return offset; + return (void *)((DWORD)dllBase + offset); } -bool call_dll_func(HANDLE process, const string &dll_path, HMODULE dll_base, const string &func_name, DWORD *ret) +bool CallDllFunc(HANDLE process, LPCWSTR dllPath, HMODULE dllBase, LPCSTR funcName, LPVOID parameter, DWORD *ret) { - uint64_t offset = get_func_offset(dll_path, func_name); - if (offset == 0 || offset > (UINT64_MAX - reinterpret_cast(dll_base))) { - return false; // 避免溢出 + void *pFunc = GetFuncAddr(dllPath, dllBase, funcName); + if (pFunc == NULL) { + return false; } - uint64_t pFunc = reinterpret_cast(dll_base) + offset; - HANDLE hThread = CreateRemoteThread(process, NULL, 0, (LPTHREAD_START_ROUTINE)pFunc, NULL, 0, NULL); - if (!hThread) { + + HANDLE hThread = CreateRemoteThread(process, NULL, 0, (LPTHREAD_START_ROUTINE)pFunc, parameter, 0, NULL); + if (hThread == NULL) { return false; } WaitForSingleObject(hThread, INFINITE); - if (ret) { + if (ret != NULL) { GetExitCodeThread(hThread, ret); } @@ -149,32 +123,31 @@ bool call_dll_func(HANDLE process, const string &dll_path, HMODULE dll_base, con return true; } -bool call_dll_func_ex(HANDLE process, const string &dll_path, HMODULE dll_base, const string &func_name, - LPVOID parameter, size_t size, DWORD *ret) +bool CallDllFuncEx(HANDLE process, LPCWSTR dllPath, HMODULE dllBase, LPCSTR funcName, LPVOID parameter, size_t sz, + DWORD *ret) { - uint64_t offset = get_func_offset(dll_path, func_name); - if (offset == 0 || offset > (UINT64_MAX - reinterpret_cast(dll_base))) { - return false; // 避免溢出 - } - uint64_t pFunc = reinterpret_cast(dll_base) + offset; - LPVOID pRemoteAddress = VirtualAllocEx(process, NULL, size, MEM_COMMIT, PAGE_READWRITE); - if (!pRemoteAddress) { - util::MsgBox(NULL, "申请内存失败", "call_dll_func_ex", 0); + void *pFunc = GetFuncAddr(dllPath, dllBase, funcName); + if (pFunc == NULL) { return false; } - WriteProcessMemory(process, pRemoteAddress, parameter, size, NULL); + LPVOID pRemoteAddress = VirtualAllocEx(process, NULL, sz, MEM_COMMIT, PAGE_READWRITE); + if (pRemoteAddress == NULL) { + ReportError(L"申请内存失败", L"CallDllFuncEx"); + return NULL; + } + + WriteProcessMemory(process, pRemoteAddress, parameter, sz, NULL); HANDLE hThread = CreateRemoteThread(process, NULL, 0, (LPTHREAD_START_ROUTINE)pFunc, pRemoteAddress, 0, NULL); - if (!hThread) { - VirtualFreeEx(process, pRemoteAddress, 0, MEM_RELEASE); - util::MsgBox(NULL, "远程调用失败", "call_dll_func_ex", 0); + if (hThread == NULL) { + VirtualFree(pRemoteAddress, 0, MEM_RELEASE); + ReportError(L"远程调用失败", L"CallDllFuncEx"); return false; } - WaitForSingleObject(hThread, INFINITE); - VirtualFreeEx(process, pRemoteAddress, 0, MEM_RELEASE); - if (ret) { + VirtualFree(pRemoteAddress, 0, MEM_RELEASE); + if (ret != NULL) { GetExitCodeThread(hThread, ret); } diff --git a/WeChatFerry/sdk/injector.h b/WeChatFerry/sdk/injector.h index 83988a6c..ca43596c 100644 --- a/WeChatFerry/sdk/injector.h +++ b/WeChatFerry/sdk/injector.h @@ -1,11 +1,14 @@ #pragma once -#include - #include "framework.h" -HANDLE inject_dll(DWORD pid, const std::string &dll_path, HMODULE *injected_base); -bool eject_dll(HANDLE process, HMODULE dll_base); -bool call_dll_func(HANDLE process, const std::string &dll_path, HMODULE dll_base, const std::string &func, DWORD *ret); -bool call_dll_func_ex(HANDLE process, const std::string &dll_path, HMODULE dll_base, const std::string &func, - LPVOID parameter, size_t size, DWORD *ret); +// 错误上报:非交互(默认)写 stderr + 返回码,交互场景(--gui)才弹模态框。 +// 定义在 injector.cpp,sdk.cpp 与 injector.cpp 共用。 +extern bool g_guiMode; +void ReportError(LPCWSTR message, LPCWSTR title); + +HANDLE InjectDll(DWORD pid, LPCWSTR dllPath, HMODULE *injectedBase); +bool EjectDll(HANDLE process, HMODULE dllBase); +bool CallDllFunc(HANDLE process, LPCWSTR dllPath, HMODULE dllBase, LPCSTR funcName, LPVOID parameter, DWORD *ret); +bool CallDllFuncEx(HANDLE process, LPCWSTR dllPath, HMODULE dllBase, LPCSTR funcName, LPVOID parameter, size_t sz, + DWORD *ret); diff --git a/WeChatFerry/sdk/sdk.cpp b/WeChatFerry/sdk/sdk.cpp index 831a8559..8a81713c 100644 --- a/WeChatFerry/sdk/sdk.cpp +++ b/WeChatFerry/sdk/sdk.cpp @@ -1,161 +1,180 @@ -#include "sdk.h" - -#include +#include "Shlwapi.h" +#include "framework.h" #include -#include -#include #include -#include -#include - -#include "framework.h" #include #include "injector.h" +#include "sdk.h" #include "util.h" -extern "C" IMAGE_DOS_HEADER __ImageBase; +#define WCF_LOCK L".wcf.lock" -static bool injected = false; -static HANDLE wcProcess = NULL; -static HMODULE spyBase = NULL; -static std::string spyDllPath; +static bool debugMode = false; +static HANDLE wcProcess = NULL; +static HMODULE spyBase = NULL; +static WCHAR spyDllPath[MAX_PATH] = { 0 }; -//区分MSVC和MinGW -#ifdef _MSC_VER -constexpr char WCFSDKDLL[] = "sdk.dll"; -constexpr char WCFSPYDLL[] = "spy.dll"; -constexpr char WCFSPYDLL_DEBUG[] = "spy_debug.dll"; -#else -constexpr char WCFSDKDLL[] = "libsdk.dll"; -constexpr char WCFSPYDLL[] = "libspy.dll"; -constexpr char WCFSPYDLL_DEBUG[] = "libspyd.dll"; -#endif +void WxSetGuiMode(bool enable) { g_guiMode = enable; } -constexpr std::string_view DISCLAIMER_FLAG = ".license_accepted.flag"; -constexpr std::string_view DISCLAIMER_TEXT_FILE = "DISCLAIMER.md"; - -namespace fs = std::filesystem; - -static fs::path get_module_directory() +// 取 sdk.dll 所在目录(注入器自身目录),用作 lock 文件与 spy 工作目录的锚点, +// 避免依赖调用方 CWD——否则 start/stop 若 CWD 不同,stop 找不到 lock。 +static void GetModuleDir(WCHAR *dir) { - char buffer[MAX_PATH] = { 0 }; - HMODULE hModule = reinterpret_cast(&__ImageBase); - GetModuleFileNameA(hModule, buffer, MAX_PATH); - fs::path modulePath(buffer); - return modulePath.parent_path(); + GetModuleFileName(GetModuleHandle(WECHATSDKDLL), dir, MAX_PATH); + PathRemoveFileSpec(dir); } -static bool show_disclaimer() +static void GetLockPath(WCHAR *lockPath) { - fs::path sdk_path = get_module_directory(); - if (fs::exists(sdk_path / DISCLAIMER_FLAG)) { - return true; - } + GetModuleDir(lockPath); + PathAppend(lockPath, WCF_LOCK); +} - fs::path path = sdk_path / DISCLAIMER_TEXT_FILE; - std::ifstream file(path, std::ios::binary); - if (!file.is_open()) { - util::MsgBox(NULL, "免责声明文件读取失败。", "错误", MB_ICONERROR); +// 检查目标进程是否已加载 spy 模块,用于 start 幂等:重复 start 会二次注入, +// 导致 RPC 端口重复绑定。比 lock 文件可靠——微信重启换 pid 后 lock 会残留成 +// 过期状态,而模块列表反映的是当前真实加载情况。 +static bool IsSpyInjected(uint32_t pid) +{ + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid); + if (snapshot == INVALID_HANDLE_VALUE) { return false; } - auto disclaimerText = std::string((std::istreambuf_iterator(file)), std::istreambuf_iterator()); - if (disclaimerText.empty()) { - util::MsgBox(NULL, "免责声明文件为空", "错误", MB_ICONERROR); - return false; + bool found = false; + MODULEENTRY32W me = { 0 }; + me.dwSize = sizeof(me); + if (Module32FirstW(snapshot, &me)) { + do { + if (_wcsicmp(me.szModule, WECHATINJECTDLL) == 0 || _wcsicmp(me.szModule, WECHATINJECTDLL_DEBUG) == 0) { + found = true; + break; + } + } while (Module32NextW(snapshot, &me)); } + CloseHandle(snapshot); + return found; +} - int result = util::MsgBox(NULL, disclaimerText.c_str(), "免责声明", MB_ICONWARNING | MB_OKCANCEL | MB_DEFBUTTON2); - if (result == IDCANCEL) { - util::MsgBox(NULL, "您拒绝了免责声明,程序将退出。", "提示", MB_ICONINFORMATION); - return false; +static int GetDllPath(bool debug, wchar_t *dllPath) +{ + GetModuleDir(spyDllPath); + if (debug) { + PathAppend(spyDllPath, WECHATINJECTDLL_DEBUG); + } else { + PathAppend(spyDllPath, WECHATINJECTDLL); } - std::ofstream flagFile(sdk_path / DISCLAIMER_FLAG, std::ios::out | std::ios::trunc); - if (!flagFile) { - util::MsgBox(NULL, "无法创建协议标志文件。", "错误", MB_ICONERROR); - return false; + if (!PathFileExists(spyDllPath)) { + ReportError(spyDllPath, L"文件不存在"); + return WX_ERR_DLL_NOT_FOUND; } - flagFile << "User accepted the license agreement."; - return true; + return WX_OK; } -static std::string get_dll_path(bool debug) +int WxInitSDK(bool debug, int port) { - char buffer[MAX_PATH] = { 0 }; - GetModuleFileNameA(GetModuleHandleA(WCFSDKDLL), buffer, MAX_PATH); - - fs::path path(buffer); - path.remove_filename(); // 只保留目录路径 - path /= debug ? WCFSPYDLL_DEBUG : WCFSPYDLL; + int status = 0; + uint32_t wcPid = 0; - if (!fs::exists(path)) { - util::MsgBox(NULL, path.string().c_str(), "文件不存在", MB_ICONERROR); - return ""; + status = GetDllPath(debug, spyDllPath); + if (status != 0) { + return status; } - return path.string(); -} -extern "C" { -__declspec(dllexport) int WxInitSDK(bool debug, int port) -{ - if (!show_disclaimer()) { - exit(-1); // 用户拒绝协议,退出程序 + bool launched = false; + if (util::open_wechat(&wcPid, &launched) != ERROR_SUCCESS) { + ReportError(L"打开微信失败", L"WxInitSDK"); + return WX_ERR_OPEN_WECHAT; } - int status = 0; - DWORD wcPid = 0; - - spyDllPath = get_dll_path(debug); - if (spyDllPath.empty()) { - return ERROR_FILE_NOT_FOUND; // DLL 文件路径不存在 + if (IsSpyInjected(wcPid)) { + ReportError(L"spy 已注入,请勿重复 start(如需重启请先 stop)", L"WxInitSDK"); + return WX_ERR_ALREADY_INJECTED; } - status = util::open_wechat(wcPid); - if (status != 0) { - util::MsgBox(NULL, "打开微信失败", "WxInitSDK", 0); - return status; + if (launched) { + Sleep(2000); // 仅新拉起微信时才等待其启动;已在运行则无需空等 } - - std::this_thread::sleep_for(std::chrono::seconds(2)); // 等待微信打开 - wcProcess = inject_dll(wcPid, spyDllPath, &spyBase); + wcProcess = InjectDll(wcPid, spyDllPath, &spyBase); if (wcProcess == NULL) { - util::MsgBox(NULL, "注入失败", "WxInitSDK", 0); - return -1; + ReportError(L"注入失败", L"WxInitSDK"); + return WX_ERR_INJECT; } - injected = true; - util::PortPath pp = { 0 }; - pp.port = port; - snprintf(pp.path, MAX_PATH, "%s", fs::current_path().string().c_str()); + WCHAR moduleDir[MAX_PATH] = { 0 }; + GetModuleDir(moduleDir); + + PortPath_t pp = { 0 }; + pp.port = port; + sprintf_s(pp.path, MAX_PATH, "%s", util::w2s(moduleDir).c_str()); - status = -3; // TODO: 统一错误码 - bool success = call_dll_func_ex(wcProcess, spyDllPath, spyBase, "InitSpy", (LPVOID)&pp, sizeof(util::PortPath), - (DWORD *)&status); - if (!success || status != 0) { - WxDestroySDK(); + if (!CallDllFuncEx(wcProcess, spyDllPath, spyBase, "InitSpy", (LPVOID)&pp, sizeof(PortPath_t), NULL)) { + ReportError(L"初始化失败", L"WxInitSDK"); + return WX_ERR_INIT_SPY; } - return status; +#ifdef WCF + WCHAR lockPath[MAX_PATH] = { 0 }; + GetLockPath(lockPath); + FILE *fd = _wfopen(lockPath, L"wb"); + if (fd == NULL) { + ReportError(L"无法打开lock文件", L"WxInitSDK"); + return WX_ERR_LOCK; + } + fwrite((uint8_t *)&debug, sizeof(debug), 1, fd); + fwrite((uint8_t *)&spyBase, sizeof(spyBase), 1, fd); + fclose(fd); +#endif + debugMode = debug; + return WX_OK; } -__declspec(dllexport) int WxDestroySDK() +int WxDestroySDK() { - if (!injected) { - return 1; // 未注入 + int status = WX_OK; +#ifdef WCF + bool debug; + uint32_t pid = util::get_wechat_pid(); + if (pid == 0) { + ReportError(L"微信未运行", L"WxDestroySDK"); + return WX_ERR_WECHAT_NOT_RUNNING; + } + + wcProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); + if (wcProcess == NULL) { + ReportError(L"打开微信进程失败", L"WxDestroySDK"); + return WX_ERR_WECHAT_NOT_RUNNING; + } + + WCHAR lockPath[MAX_PATH] = { 0 }; + GetLockPath(lockPath); + FILE *fd = _wfopen(lockPath, L"rb"); + if (fd == NULL) { + ReportError(L"无法打开lock文件", L"WxDestroySDK"); + return WX_ERR_LOCK; + } + fread((uint8_t *)&debug, sizeof(debug), 1, fd); + fread((uint8_t *)&spyBase, sizeof(spyBase), 1, fd); + fclose(fd); + status = GetDllPath(debug, spyDllPath); +#else + status = GetDllPath(debugMode, spyDllPath); +#endif + + if (status != 0) { + return status; } - if (!call_dll_func(wcProcess, spyDllPath, spyBase, "CleanupSpy", NULL)) { - return -1; + if (!CallDllFunc(wcProcess, spyDllPath, spyBase, "CleanupSpy", NULL, NULL)) { + ReportError(L"清理 spy 失败", L"WxDestroySDK"); + return WX_ERR_EJECT; } - if (!eject_dll(wcProcess, spyBase)) { - return -2; + if (!EjectDll(wcProcess, spyBase)) { + return WX_ERR_EJECT; } - injected = false; - return 0; + return WX_OK; } -} \ No newline at end of file diff --git a/WeChatFerry/sdk/sdk.def b/WeChatFerry/sdk/sdk.def new file mode 100644 index 00000000..7913f652 --- /dev/null +++ b/WeChatFerry/sdk/sdk.def @@ -0,0 +1,4 @@ +EXPORTS + WxInitSDK + WxDestroySDK + WxSetGuiMode diff --git a/WeChatFerry/sdk/sdk.h b/WeChatFerry/sdk/sdk.h index 2002d03f..df570997 100644 --- a/WeChatFerry/sdk/sdk.h +++ b/WeChatFerry/sdk/sdk.h @@ -1,6 +1,20 @@ #pragma once -extern "C" { -__declspec(dllexport) int WxInitSDK(bool debug, int port); -__declspec(dllexport) int WxDestroySDK(); -} \ No newline at end of file +// 语义化退出码:调用方(Python / wcf.exe)可据此给出精确错误提示, +// 而不必只判断"成功/失败"。新增码请同步 wcf 的 help() 文档。 +enum WxStatus { + WX_OK = 0, // 成功 + WX_ERR_USAGE = 2, // 命令行参数错误(仅 wcf.exe) + WX_ERR_DLL_NOT_FOUND = 3, // spy DLL 不存在 + WX_ERR_OPEN_WECHAT = 4, // 打开/启动微信失败 + WX_ERR_INJECT = 5, // 注入 spy 失败 + WX_ERR_INIT_SPY = 6, // 初始化 spy(RPC 服务)失败 + WX_ERR_LOCK = 7, // .wcf.lock 读写失败 + WX_ERR_WECHAT_NOT_RUNNING = 8, // 停止时微信未运行 + WX_ERR_EJECT = 9, // 卸载 spy 失败 + WX_ERR_ALREADY_INJECTED = 10, // spy 已注入(重复 start) +}; + +int WxInitSDK(bool debug, int port); +int WxDestroySDK(); +void WxSetGuiMode(bool enable); // 开启后错误改用模态弹框,默认写 stderr diff --git a/WeChatFerry/smc/Codec.lib b/WeChatFerry/smc/Codec.lib index b9943959..937e7562 100755 Binary files a/WeChatFerry/smc/Codec.lib and b/WeChatFerry/smc/Codec.lib differ diff --git a/WeChatFerry/smc/README.md b/WeChatFerry/smc/README.md new file mode 100644 index 00000000..7ff01819 --- /dev/null +++ b/WeChatFerry/smc/README.md @@ -0,0 +1,48 @@ +# smc — Silk ↔ MP3 语音编解码 + +本目录是**预编译产物**,并非源码。它把独立项目 **SilkMp3Converter** 的静态库和头文件 vendored 进 WeChatFerry,供 spy 在处理微信语音消息时使用。 + +- 上游源码: + +## 文件 + +| 文件 | 说明 | +|------|------| +| `codec.h` | 对外 API 头文件 | +| `Codec.lib` | 预编译静态库(`x86`,与 spy 的 `x86-windows-static` 匹配)| + +## 用途 + +微信语音消息以 **SILK** 编码存储。`spy` 的 `get_audio` 需要把它转成通用格式,调用链见 [`misc_manager.cpp`](../spy/misc_manager.cpp): + +```cpp +#include "codec.h" +// 采样率固定 24000 Hz(微信语音) +Silk2Mp3(silk, mp3path, 24000); +``` + +`Codec.lib` 在 [`Spy.vcxproj`](../spy/Spy.vcxproj) 的 `AdditionalDependencies` 中链接。 + +## API + +```cpp +// SILK -> MP3:文件到文件 / 内存到文件 / 内存到内存 +int Silk2Mp3(std::string inpath, std::string outpath, int sr); +int Silk2Mp3(std::vector &silk, std::string mp3path, int sr); +int Silk2Mp3(std::vector &silk, std::vector &mp3, int sr); + +// 底层编解码 +int Mp3Encode(std::vector &pcm, std::string &mp3path, int32_t sr); +int Mp3Encode(std::vector &pcm, std::vector &mp3, int32_t sr); +DecTime_t SilkDecode(std::vector &silk, std::vector &pcm, int32_t sr); +``` + +`sr` 为采样率(微信语音用 `24000`)。`Silk2Mp3` 系列返回 `0` 表示成功。 + +## 重建 / 升级 + +`Codec.lib` 不在本仓库构建,需从上游生成后回填: + +1. 克隆并按上游说明编译 ; +2. 编译目标须与 spy 一致(**x86**、静态运行库 `x86-windows-static`),否则链接会报运行库/架构不匹配; +3. 将产出的 `Codec.lib` 与 `codec.h` 覆盖回本目录。 diff --git a/WeChatFerry/smc/libCodec.a b/WeChatFerry/smc/libCodec.a deleted file mode 100644 index 13705052..00000000 Binary files a/WeChatFerry/smc/libCodec.a and /dev/null differ diff --git a/WeChatFerry/smc/libmp3lame.a b/WeChatFerry/smc/libmp3lame.a deleted file mode 100644 index 96e03ae5..00000000 Binary files a/WeChatFerry/smc/libmp3lame.a and /dev/null differ diff --git a/WeChatFerry/spy/CMakeLists.txt b/WeChatFerry/spy/CMakeLists.txt index 986c53be..337029fc 100644 --- a/WeChatFerry/spy/CMakeLists.txt +++ b/WeChatFerry/spy/CMakeLists.txt @@ -15,8 +15,8 @@ add_library(spy SHARED database_executor.h contact_manager.cpp contact_manager.h - message_handler.cpp - message_handler.h + message_receiver.cpp + message_receiver.h rpc_server.cpp rpc_server.h message_sender.cpp @@ -28,14 +28,13 @@ add_library(spy SHARED account_manager.h resource.h rpc_helper.h - sqlite3.h dllmain.cpp spy.def # Common files - ${CMAKE_SOURCE_DIR}/com/util.cpp - ${CMAKE_SOURCE_DIR}/com/util.h - ${CMAKE_SOURCE_DIR}/com/log.hpp + ${CMAKE_SOURCE_DIR}/spy/com/util.cpp + ${CMAKE_SOURCE_DIR}/spy/com/util.h + ${CMAKE_SOURCE_DIR}/spy/com/log.hpp # RPC files ${CMAKE_SOURCE_DIR}/rpc/pb_util.cpp @@ -58,7 +57,7 @@ target_link_directories(spy PRIVATE ) # Link dependencies -target_link_libraries(spy PRIVATE +target_link_libraries(spy PRIVATE Codec mp3lame version @@ -87,8 +86,8 @@ add_custom_command( # Add generated files as dependencies add_custom_target(protobuf_generation DEPENDS - ${CMAKE_SOURCE_DIR}/rpc/proto/wcf.pb.c - ${CMAKE_SOURCE_DIR}/rpc/proto/wcf.pb.h + ${CMAKE_SOURCE_DIR}/rpc/proto/wcf.pb.c + ${CMAKE_SOURCE_DIR}/rpc/proto/wcf.pb.h ) add_dependencies(spy protobuf_generation) diff --git a/WeChatFerry/spy/Spy.vcxproj b/WeChatFerry/spy/Spy.vcxproj index ca8ea825..58859c4b 100644 --- a/WeChatFerry/spy/Spy.vcxproj +++ b/WeChatFerry/spy/Spy.vcxproj @@ -1,12 +1,16 @@ - + Debug - x64 + Win32 + + + Release + Win32 - - Dev + + Debug x64 @@ -20,16 +24,24 @@ {4de80b82-5f6a-4c4c-9d16-1574308110fa} spy 10.0 + x86-windows-static x64-windows-static - + DynamicLibrary true v142 Unicode - + + DynamicLibrary + false + v142 + true + Unicode + + DynamicLibrary true v142 @@ -47,147 +59,152 @@ - + + + + - + - + true $(ProjectName)_debug + true + $(ProjectDir)..\_build\$(ProjectName)\$(Configuration)\ + + + false + true + $(ProjectDir)..\_build\$(ProjectName)\$(Configuration)\ - + true - $(ProjectName)_debug false - + true - Release - + true Release - - true - - - true - - + Level3 + true + true true - WIN32;NDEBUG;SPY_EXPORTS;_WINDOWS;_USRDLL;ENABLE_DEBUG_LOG;%(PreprocessorDefinitions) + WIN32;NDEBUG;SPY_EXPORTS;_WINDOWS;_USRDLL;ENABLE_DEBUG_LOG;_HAS_STD_BYTE=0;%(PreprocessorDefinitions) true NotUsing - stdcpp17 - $(SolutionDir)com;$(SolutionDir)rpc;$(SolutionDir)rpc\nanopb;$(SolutionDir)rpc\proto;$(SolutionDir)smc;$(SolutionDir)spy;$(SolutionDir)vcpkg_installed\x64-windows-static\x64-windows-static\include - true - false - MultiThreaded - true + $(ProjectDir)..\;$(ProjectDir)..\rpc;$(ProjectDir)..\rpc\nanopb;$(ProjectDir)..\rpc\proto;$(ProjectDir)..\smc;$(ProjectDir);$(ProjectDir)com;%(AdditionalIncludeDirectories) 4251;4731;4819 + MultiThreaded + stdcpp17 /EHa /utf-8 %(AdditionalOptions) Windows + true + true true false - - - /ignore:4099 %(AdditionalOptions) - $(SolutionDir)smc;%(AdditionalLibraryDirectories) iphlpapi.lib;wsock32.lib;ws2_32.lib;crypt32.lib;Codec.lib;%(AdditionalDependencies) - true + spy.def + $(SolutionDir)smc;%(AdditionalLibraryDirectories) + /ignore:4099 %(AdditionalOptions) - - call "$(SolutionDir)license_check.bat" -if errorlevel 1 exit 1 -cd $(SolutionDir)rpc\proto -$(SolutionDir)rpc\tool\protoc --nanopb_out=. wcf.proto - - - Generating PB files - - if not exist $(SolutionDir)Out md $(SolutionDir)Out -xcopy /y $(OutDir)$(TargetFileName) $(SolutionDir)Out -xcopy /y $(OutDir)$(TargetName).exp $(SolutionDir)Out -xcopy /y $(OutDir)$(TargetName).lib $(SolutionDir)Out -xcopy /y $(OutDir)$(TargetName).pdb $(SolutionDir)Out -xcopy /y $(SolutionDir)DISCLAIMER.md $(SolutionDir)Out -xcopy /y $(OutDir)$(TargetFileName) $(SolutionDir)..\clients\python\wcferry -xcopy /y $(SolutionDir)DISCLAIMER.md $(SolutionDir)..\clients\python\wcferry + if not exist "$(ProjectDir)..\Out" md "$(ProjectDir)..\Out" +xcopy /y "$(OutDir)$(TargetFileName)" "$(ProjectDir)..\Out\" +xcopy /y "$(OutDir)$(TargetName).exp" "$(ProjectDir)..\Out\" +xcopy /y "$(OutDir)$(TargetName).lib" "$(ProjectDir)..\Out\" +xcopy /y "$(OutDir)$(TargetName).pdb" "$(ProjectDir)..\Out\" +xcopy /y "$(OutDir)$(TargetFileName)" "$(ProjectDir)..\..\clients\python\wcferry\" Copy spy.dll + + cd /d "$(ProjectDir)..\rpc\proto" +..\tool\protoc.bat --plugin=protoc-gen-nanopb=..\tool\protoc-gen-nanopb.bat --nanopb_out=. wcf.proto + + + Generating PB files + - + Level3 + true + true true - WIN32;NDEBUG;SPY_EXPORTS;_WINDOWS;_USRDLL;ENABLE_DEBUG_LOG;ENABLE_WX_LOG;%(PreprocessorDefinitions) + WIN32;NDEBUG;SPY_EXPORTS;_WINDOWS;_USRDLL;_HAS_STD_BYTE=0;%(PreprocessorDefinitions) true NotUsing - stdcpp17 - $(SolutionDir)com;$(SolutionDir)rpc;$(SolutionDir)rpc\nanopb;$(SolutionDir)rpc\proto;$(SolutionDir)smc;$(SolutionDir)spy;$(SolutionDir)vcpkg_installed\x64-windows-static\x64-windows-static\include - true - false - MultiThreaded - true - - + $(ProjectDir)..\;$(ProjectDir)..\rpc;$(ProjectDir)..\rpc\nanopb;$(ProjectDir)..\rpc\proto;$(ProjectDir)..\smc;$(ProjectDir);$(ProjectDir)com;%(AdditionalIncludeDirectories) + 4251;4731;4819 + MultiThreaded + stdcpp17 /EHa /utf-8 %(AdditionalOptions) Windows + true + true true false - - - /ignore:4099 %(AdditionalOptions) - $(SolutionDir)smc;%(AdditionalLibraryDirectories) iphlpapi.lib;wsock32.lib;ws2_32.lib;crypt32.lib;Codec.lib;%(AdditionalDependencies) - true + spy.def + $(SolutionDir)smc;%(AdditionalLibraryDirectories) + /ignore:4099 %(AdditionalOptions) - - call "$(SolutionDir)license_check.bat" -if errorlevel 1 exit 1 -cd $(SolutionDir)rpc\proto -$(SolutionDir)rpc\tool\protoc --nanopb_out=. wcf.proto - - - Generating PB files - - if not exist $(SolutionDir)Out md $(SolutionDir)Out -xcopy /y $(OutDir)$(TargetFileName) $(SolutionDir)Out -xcopy /y $(OutDir)$(TargetName).exp $(SolutionDir)Out -xcopy /y $(OutDir)$(TargetName).lib $(SolutionDir)Out -xcopy /y $(OutDir)$(TargetName).pdb $(SolutionDir)Out -xcopy /y $(SolutionDir)DISCLAIMER.md $(SolutionDir)Out -xcopy /y $(OutDir)$(TargetFileName) $(SolutionDir)..\clients\python\wcferry -xcopy /y $(SolutionDir)DISCLAIMER.md $(SolutionDir)..\clients\python\wcferry + if not exist "$(ProjectDir)..\Out" md "$(ProjectDir)..\Out" +xcopy /y "$(OutDir)$(TargetFileName)" "$(ProjectDir)..\Out\" +xcopy /y "$(OutDir)$(TargetFileName)" "$(ProjectDir)..\..\clients\python\wcferry\" Copy spy.dll + + cd /d "$(ProjectDir)..\rpc\proto" +..\tool\protoc.bat --plugin=protoc-gen-nanopb=..\tool\protoc-gen-nanopb.bat --nanopb_out=. wcf.proto + + + Generating PB files + + + + + Level3 + true + _DEBUG;SPY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + Use + pch.h + + + Windows + true + false + spy.def + /ignore:4099 %(AdditionalOptions) + @@ -195,53 +212,21 @@ xcopy /y $(SolutionDir)DISCLAIMER.md $(SolutionDir)..\clients\python\wcferrytrue true true - WIN32;NDEBUG;SPY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + NDEBUG;SPY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) true - NotUsing - - - stdcpp17 - $(SolutionDir)com;$(SolutionDir)rpc;$(SolutionDir)rpc\nanopb;$(SolutionDir)rpc\proto;$(SolutionDir)smc;$(SolutionDir)spy;$(SolutionDir)vcpkg_installed\x64-windows-static\x64-windows-static\include - false - - 4251;4731;4819 - /EHa /utf-8 %(AdditionalOptions) - MultiThreaded + Use + pch.h Windows true true - false + true false - - - $(SolutionDir)smc;%(AdditionalLibraryDirectories) - iphlpapi.lib;wsock32.lib;ws2_32.lib;crypt32.lib;Codec.lib;%(AdditionalDependencies) - /ignore:4099 %(AdditionalOptions) + spy.def - - Generating PB files - call "$(SolutionDir)license_check.bat" -if errorlevel 1 exit 1 -cd $(SolutionDir)rpc\proto -$(SolutionDir)rpc\tool\protoc --nanopb_out=. wcf.proto - - - Copy spy.dll - if not exist $(SolutionDir)Out md $(SolutionDir)Out -xcopy /y $(OutDir)$(TargetFileName) $(SolutionDir)Out -xcopy /y $(OutDir)$(TargetName).exp $(SolutionDir)Out -xcopy /y $(OutDir)$(TargetName).lib $(SolutionDir)Out -xcopy /y $(OutDir)$(TargetName).pdb $(SolutionDir)Out -xcopy /y $(SolutionDir)DISCLAIMER.md $(SolutionDir)Out -xcopy /y $(OutDir)$(TargetFileName) $(SolutionDir)..\clients\python\wcferry -xcopy /y $(SolutionDir)DISCLAIMER.md $(SolutionDir)..\clients\python\wcferry - - - @@ -250,46 +235,52 @@ xcopy /y $(SolutionDir)DISCLAIMER.md $(SolutionDir)..\clients\python\wcferry + - - - - + + + + + + + + - - - - + - - - - - + + + + + + - + + + + - \ No newline at end of file + diff --git a/WeChatFerry/spy/Spy.vcxproj.filters b/WeChatFerry/spy/Spy.vcxproj.filters index dafe11c4..e238b779 100644 --- a/WeChatFerry/spy/Spy.vcxproj.filters +++ b/WeChatFerry/spy/Spy.vcxproj.filters @@ -18,24 +18,9 @@ - - 头文件 - 头文件 - - 头文件 - - - 头文件 - - - 头文件 - - - 头文件 - 头文件 @@ -63,31 +48,46 @@ nnrpc - + + 头文件 + + + 头文件 + + + 头文件 + + + 头文件 + + + 头文件 + + 头文件 头文件 - + 头文件 - + 头文件 - + 头文件 - + 头文件 - + 头文件 - + 头文件 - + 头文件 @@ -98,16 +98,7 @@ 源文件 - - 源文件 - - - 源文件 - - - 源文件 - - + 源文件 @@ -128,20 +119,32 @@ nnrpc + + 源文件 + + + 源文件 + + + 源文件 + 源文件 - + 源文件 - + 源文件 - + 源文件 + + 源文件 + nnrpc @@ -151,4 +154,4 @@ 资源文件 - \ No newline at end of file + diff --git a/WeChatFerry/spy/account_manager.cpp b/WeChatFerry/spy/account_manager.cpp index 232fe856..ef9c2941 100644 --- a/WeChatFerry/spy/account_manager.cpp +++ b/WeChatFerry/spy/account_manager.cpp @@ -1,99 +1,108 @@ #include "account_manager.h" -#include +#include + +#include #include "log.hpp" +#include "util.h" #include "offsets.h" #include "rpc_helper.h" #include "spy.h" -#include "util.h" + +#pragma comment(lib, "Shell32.lib") namespace account { -namespace fs = std::filesystem; namespace OsAcc = Offsets::Account; -using get_account_service_t = QWORD (*)(); -using get_data_path_t = QWORD (*)(QWORD); - -static uint64_t get_account_service() +static std::string get_default_home_path() { - static auto GetService = Spy::getFunction(OsAcc::SERVICE); - return GetService ? GetService() : 0; + wchar_t path[MAX_PATH] = { 0 }; + if (SUCCEEDED(SHGetFolderPathW(nullptr, CSIDL_PERSONAL, nullptr, SHGFP_TYPE_CURRENT, path))) { + return util::w2s(std::wstring(path)) + "\\WeChat Files\\"; + } + return ""; } -static std::string get_string_value(uint64_t base_addr, uint64_t offset) +// 读取 AccountService 对象内的 std::string 成员(容量位在 +0x14,据此判定 SSO 内联/堆) +static std::string get_string_value(uint32_t base_addr, uint32_t offset) { - uint64_t type = util::get_qword(base_addr + offset + 0x18); - return (type == 0xF) ? util::get_p_string(base_addr + offset) : util::get_pp_string(base_addr + offset); + uint32_t type = util::get_dword(base_addr + offset + 0x14); + if (type == 0xF) { + return util::get_p_string(base_addr + offset); + } else { + return util::get_pp_string(base_addr + offset); + } } -bool is_logged_in() +std::string get_home_path() { - uint64_t service_addr = get_account_service(); - return service_addr && util::get_qword(service_addr + OsAcc::LOGIN) != 0; -} - -fs::path get_home_path() -{ - static fs::path home_path; + static std::string cached_home; static std::once_flag home_once; std::call_once(home_once, []() { - WxString home {}; - if (auto getDataPath = Spy::getFunction(OsAcc::PATH)) { - getDataPath(reinterpret_cast(&home)); - if (home.wptr) { - std::wstring wstr(home.wptr, home.size); - home_path = util::w2s(std::move(wstr)); + uint32_t base = g_WeChatWinDllAddr; + if (base) { + std::string path = util::w2s(util::get_wstring(base + OsAcc::HOME)); + if (!path.empty()) { + cached_home = path + "\\WeChat Files\\"; } } + + if (cached_home.empty()) { + cached_home = get_default_home_path(); + } }); - return home_path; + return cached_home; } std::string get_self_wxid() { - static std::string cached_wxid; - static std::once_flag wxid_once; - - std::call_once(wxid_once, []() { - if (uint64_t svc = get_account_service(); svc) { - cached_wxid = get_string_value(svc, OsAcc::WXID); - if (cached_wxid.empty()) { - cached_wxid = get_string_value(svc, OsAcc::ALIAS); - } - } - }); + uint32_t base = g_WeChatWinDllAddr; + if (!base) { + return ""; + } + return get_string_value(base, OsAcc::WXID); +} - return cached_wxid; +bool is_logged_in() +{ + uint32_t base = g_WeChatWinDllAddr; + return base && util::get_dword(base + OsAcc::SERVICE) != 0; } UserInfo_t get_user_info() { UserInfo_t ui; - uint64_t service_addr = get_account_service(); - if (!service_addr) return ui; + uint32_t base = g_WeChatWinDllAddr; + if (!base) { + return ui; + } ui.wxid = get_self_wxid(); - ui.home = get_home_path().generic_string(); - ui.name = get_string_value(service_addr, OsAcc::NAME); - ui.mobile = get_string_value(service_addr, OsAcc::MOBILE); - ui.alias = get_string_value(service_addr, OsAcc::ALIAS); + ui.name = get_string_value(base, OsAcc::NAME); + ui.mobile = get_string_value(base, OsAcc::MOBILE); + ui.home = get_home_path(); + return ui; } bool rpc_is_logged_in(uint8_t *out, size_t *len) { - return fill_response(out, len, [](Response &rsp) { rsp.msg.status = is_logged_in(); }); + return fill_response(out, len, [](Response &rsp) { + rsp.msg.status = is_logged_in(); + }); } bool rpc_get_self_wxid(uint8_t *out, size_t *len) { - return fill_response( - out, len, [](Response &rsp) { rsp.msg.str = (char *)get_self_wxid().c_str(); }); + return fill_response(out, len, [](Response &rsp) { + static std::string wxid = get_self_wxid(); + rsp.msg.str = (char *)wxid.c_str(); + }); } bool rpc_get_user_info(uint8_t *out, size_t *len) @@ -104,7 +113,6 @@ bool rpc_get_user_info(uint8_t *out, size_t *len) rsp.msg.ui.name = (char *)ui.name.c_str(); rsp.msg.ui.mobile = (char *)ui.mobile.c_str(); rsp.msg.ui.home = (char *)ui.home.c_str(); - rsp.msg.ui.alias = (char *)ui.alias.c_str(); }); } diff --git a/WeChatFerry/spy/account_manager.h b/WeChatFerry/spy/account_manager.h index 4eede789..90c44d8b 100644 --- a/WeChatFerry/spy/account_manager.h +++ b/WeChatFerry/spy/account_manager.h @@ -1,7 +1,5 @@ -#pragma once +#pragma once -#include -#include #include #include "pb_types.h" @@ -9,19 +7,11 @@ namespace account { -// 登录状态 -bool is_logged_in(); - -// 获取 WeChat 数据存储路径 -std::filesystem::path get_home_path(); - -// 获取自身 wxid +std::string get_home_path(); std::string get_self_wxid(); - -// 获取用户信息 +bool is_logged_in(); UserInfo_t get_user_info(); -// RPC 方法 bool rpc_is_logged_in(uint8_t *out, size_t *len); bool rpc_get_self_wxid(uint8_t *out, size_t *len); bool rpc_get_user_info(uint8_t *out, size_t *len); diff --git a/WeChatFerry/spy/chatroom_manager.cpp b/WeChatFerry/spy/chatroom_manager.cpp index 57632831..1aade386 100644 --- a/WeChatFerry/spy/chatroom_manager.cpp +++ b/WeChatFerry/spy/chatroom_manager.cpp @@ -1,98 +1,198 @@ -#pragma execution_character_set("utf-8") +#include "chatroom_manager.h" -#include "chatroom_manager.h" +#include +#include + +#include "framework.h" #include "log.hpp" #include "offsets.h" -#include "pb_util.h" #include "rpc_helper.h" #include "spy.h" +#include "spy_types.h" #include "util.h" namespace chatroom { -namespace OsRoom = Offsets::Chatroom; -using new_t = QWORD (*)(QWORD, WxString *); -using get_mgr_t = QWORD (*)(); -using add_member_t = QWORD (*)(QWORD, QWORD, WxString *, QWORD); -using delete_member_t = QWORD (*)(QWORD, QWORD, WxString *); -using invite_members_t = QWORD (*)(const wchar_t *, QWORD, QWORD, QWORD); +namespace +{ + +struct WxStringValue { + const wchar_t *wptr; + uint32_t size; + uint32_t capacity; + const char *ptr; + uint32_t clen; +}; + +// 邀请上下文 shared_ptr(2 dword):{控制块指针, 对象指针};按值传 {0,0}=NULL +struct InviteContext { + uint32_t ctrl; + uint32_t obj; +}; + +using ChatroomManagerGetterFn = void *(*)(); +using WxStringAssignFn = void *(__thiscall *)(void *dest, const wchar_t *src, int len); +using AddMembersFn = int(__thiscall *)(void *manager, const std::vector *members, + WxStringValue roomid, int64_t reserved); +using DelMembersFn = int(__thiscall *)(void *manager, const std::vector *members, + WxStringValue roomid); +using InviteMembersFn = char(__stdcall *)(const std::vector *members, WxStringValue roomid, + InviteContext context); + +WxStringValue to_value(const WxString &value) +{ + return { value.wptr, value.size, value.capacity, value.ptr, value.clen }; +} -template -bool rpc_chatroom_common(const MemberMgmt &m, uint8_t *out, size_t *len, Func func) +// 把逗号分隔的 wxids 拆成非拥有 WxString 视图;storage 持有底层 std::wstring,须与返回值同生命周期。 +// 先建全部 std::wstring 再统一包视图,避免 vector 扩容使先前 WxString.wptr 失效。 +std::vector make_member_views(const std::string &wxids, std::vector &storage) { - int status = -1; - if (m.wxids && m.roomid) { - status = func(m.roomid, m.wxids); - } else { - LOG_ERROR("wxid 和 roomid 不能为空"); + std::wstringstream wss(util::s2w(wxids)); + while (wss.good()) { + std::wstring wstr; + getline(wss, wstr, L','); + if (!wstr.empty()) { + storage.push_back(wstr); + } } - return fill_response(out, len, [&](Response &rsp) { rsp.msg.status = status; }); + std::vector views; + views.reserve(storage.size()); + for (auto &m : storage) { + views.push_back(WxString(m)); + } + return views; } -int add_chatroom_member(const string &roomid, const string &wxids) +} // namespace + +int add_chatroom_member(const std::string &roomid, const std::string &wxids) { - auto get_chatroom_mgr = Spy::getFunction(OsRoom::MGR); - auto add_members = Spy::getFunction(OsRoom::ADD); + if (roomid.empty() || wxids.empty()) { + LOG_ERROR("Empty roomid or wxids."); + return -1; + } - WxString *wx_roomid = util::CreateWxString(roomid); + auto get_manager = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Chatroom::MGR_GETTER); + void *manager = get_manager(); + if (manager == nullptr) { + LOG_ERROR("Failed to get ChatRoomMgr."); + return -1; + } - QWORD tmp[2] = { 0 }; + // 成员列表用非拥有视图即可(doAddMemberToChatRoom 只读取并深拷贝进 net scene) + std::vector vMembers; + std::vector vWxMembers = make_member_views(wxids, vMembers); - auto split = util::parse_wxids(wxids); - auto &wx_members = split.wxWxids; - QWORD p_members = reinterpret_cast(&wx_members); + // roomid 会被 doAddMemberToChatRoom mm_free,须用 ASSIGN 建 WeChat 拥有副本 + auto assign = reinterpret_cast(g_WeChatWinDllAddr + Offsets::RichText::ASSIGN); + std::wstring wsRoomid = util::s2w(roomid); + WxString wxRoomid; + assign(&wxRoomid, wsRoomid.c_str(), -1); - return static_cast(add_members(get_chatroom_mgr(), p_members, wx_roomid, reinterpret_cast(tmp))); + LOG_DEBUG("Adding {} members[{}] to {}", vWxMembers.size(), wxids.c_str(), roomid.c_str()); + + auto add_members = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Chatroom::ADD_MEMBER); + return add_members(manager, &vWxMembers, to_value(wxRoomid), 0); } -int del_chatroom_member(const string &roomid, const string &wxids) +int del_chatroom_member(const std::string &roomid, const std::string &wxids) { - auto get_chatroom_mgr = Spy::getFunction(OsRoom::MGR); - auto del_members = Spy::getFunction(OsRoom::DEL); + if (roomid.empty() || wxids.empty()) { + LOG_ERROR("Empty roomid or wxids."); + return -1; + } + + auto get_manager = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Chatroom::MGR_GETTER); + void *manager = get_manager(); + if (manager == nullptr) { + LOG_ERROR("Failed to get ChatRoomMgr."); + return -1; + } - WxString *wx_roomid = util::CreateWxString(roomid); + std::vector vMembers; + std::vector vWxMembers = make_member_views(wxids, vMembers); - auto split = util::parse_wxids(wxids); - auto &wx_members = split.wxWxids; - QWORD p_members = reinterpret_cast(&wx_members); + // roomid 会被 doDelMemberFromChatRoom mm_free,须用 ASSIGN 建 WeChat 拥有副本 + auto assign = reinterpret_cast(g_WeChatWinDllAddr + Offsets::RichText::ASSIGN); + std::wstring wsRoomid = util::s2w(roomid); + WxString wxRoomid; + assign(&wxRoomid, wsRoomid.c_str(), -1); - return static_cast(del_members(get_chatroom_mgr(), p_members, wx_roomid)); + LOG_DEBUG("Deleting {} members[{}] from {}", vWxMembers.size(), wxids.c_str(), roomid.c_str()); + + auto del_members = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Chatroom::DEL_MEMBER); + return del_members(manager, &vWxMembers, to_value(wxRoomid)); } -int invite_chatroom_member(const string &roomid, const string &wxids) +int invite_chatroom_member(const std::string &roomid, const std::string &wxids) { - auto init_roomid = Spy::getFunction(OsRoom::NEW); - auto invite_members = Spy::getFunction(OsRoom::INV); + if (roomid.empty() || wxids.empty()) { + LOG_ERROR("Empty roomid or wxids."); + return -1; + } + + // 邀请核心不经过 manager,此处仅 warmup 确保 ChatRoomMgr 单例已构造 + auto get_manager = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Chatroom::MGR_GETTER); + if (get_manager() == nullptr) { + LOG_ERROR("Failed to get ChatRoomMgr."); + return -1; + } - wstring ws_roomid = util::s2w(roomid); - WxString wx_roomid(ws_roomid); + std::vector vMembers; + std::vector vWxMembers = make_member_views(wxids, vMembers); - QWORD tmp[2] = { 0 }; - QWORD array[4] = { 0 }; + // roomid 会被构建器 mm_free,须用 ASSIGN 建 WeChat 拥有副本 + auto assign = reinterpret_cast(g_WeChatWinDllAddr + Offsets::RichText::ASSIGN); + std::wstring wsRoomid = util::s2w(roomid); + WxString wxRoomid; + assign(&wxRoomid, wsRoomid.c_str(), -1); - auto split = util::parse_wxids(wxids); - auto &wx_members = split.wxWxids; - QWORD p_members = reinterpret_cast(&wx_members); - QWORD p_roomid = init_roomid(reinterpret_cast(&array), &wx_roomid); - LOG_BUFFER((uint8_t *)*(QWORD *)(*(QWORD *)p_members), 40); + LOG_DEBUG("Inviting {} members[{}] to {}", vWxMembers.size(), wxids.c_str(), roomid.c_str()); - return static_cast(invite_members(ws_roomid.c_str(), p_members, p_roomid, reinterpret_cast(tmp))); + // 构建器内部按 roomid 是否以 "@im.chatroom" 结尾分流普通/OpenIM,并 doScene 发送 + auto invite_members = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Chatroom::INVITE_MEMBER); + return invite_members(&vWxMembers, to_value(wxRoomid), InviteContext{ 0, 0 }); } bool rpc_add_chatroom_member(const MemberMgmt &m, uint8_t *out, size_t *len) { - return rpc_chatroom_common(m, out, len, add_chatroom_member); + return fill_response(out, len, [&m](Response &rsp) { + if ((m.roomid == NULL) || (m.wxids == NULL)) { + LOG_ERROR("Empty roomid or wxids."); + rsp.msg.status = -1; + } else { + int status = add_chatroom_member(m.roomid, m.wxids); + rsp.msg.status = status; + } + }); } bool rpc_delete_chatroom_member(const MemberMgmt &m, uint8_t *out, size_t *len) { - return rpc_chatroom_common(m, out, len, del_chatroom_member); + return fill_response(out, len, [&m](Response &rsp) { + if ((m.roomid == NULL) || (m.wxids == NULL)) { + LOG_ERROR("Empty roomid or wxids."); + rsp.msg.status = -1; + } else { + int status = del_chatroom_member(m.roomid, m.wxids); + rsp.msg.status = status; + } + }); } bool rpc_invite_chatroom_member(const MemberMgmt &m, uint8_t *out, size_t *len) { - return rpc_chatroom_common(m, out, len, invite_chatroom_member); + return fill_response(out, len, [&m](Response &rsp) { + if ((m.roomid == NULL) || (m.wxids == NULL)) { + LOG_ERROR("Empty roomid or wxids."); + rsp.msg.status = -1; + } else { + int status = invite_chatroom_member(m.roomid, m.wxids); + rsp.msg.status = status; + } + }); } } // namespace chatroom diff --git a/WeChatFerry/spy/chatroom_manager.h b/WeChatFerry/spy/chatroom_manager.h index 3be999d6..d92be4dd 100644 --- a/WeChatFerry/spy/chatroom_manager.h +++ b/WeChatFerry/spy/chatroom_manager.h @@ -1,5 +1,7 @@ #pragma once +#include +#include #include #include "wcf.pb.h" @@ -7,16 +9,10 @@ namespace chatroom { -// 添加成员到群聊 int add_chatroom_member(const std::string &roomid, const std::string &wxids); - -// 从群聊中移除成员 int del_chatroom_member(const std::string &roomid, const std::string &wxids); - -// 邀请成员加入群聊 int invite_chatroom_member(const std::string &roomid, const std::string &wxids); -// RPC 方法 bool rpc_add_chatroom_member(const MemberMgmt &m, uint8_t *out, size_t *len); bool rpc_delete_chatroom_member(const MemberMgmt &m, uint8_t *out, size_t *len); bool rpc_invite_chatroom_member(const MemberMgmt &m, uint8_t *out, size_t *len); diff --git a/WeChatFerry/spy/com/framework.h b/WeChatFerry/spy/com/framework.h new file mode 100644 index 00000000..80cbbc9b --- /dev/null +++ b/WeChatFerry/spy/com/framework.h @@ -0,0 +1,5 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN // 从 Windows 头文件中排除极少使用的内容 +// Windows 头文件 +#include diff --git a/WeChatFerry/spy/com/log.hpp b/WeChatFerry/spy/com/log.hpp new file mode 100644 index 00000000..c9bbc13f --- /dev/null +++ b/WeChatFerry/spy/com/log.hpp @@ -0,0 +1,73 @@ +#pragma once + +#include +#include +#include + +#ifdef ENABLE_DEBUG_LOG +#include + +#define SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_DEBUG +void log_buffer(uint8_t *buffer, size_t len); +#define LOG_BUFFER(buf, len) log_buffer((buf), (len)) +#else +#define LOG_BUFFER(...) (void)0 +#endif + +#include "spdlog/sinks/rotating_file_sink.h" +#include "spdlog/sinks/stdout_color_sinks.h" +#include "spdlog/spdlog.h" + +#define LOG_DEBUG(...) SPDLOG_DEBUG(__VA_ARGS__); +#define LOG_INFO(...) SPDLOG_INFO(__VA_ARGS__); +#define LOG_WARN(...) SPDLOG_WARN(__VA_ARGS__); +#define LOG_ERROR(...) SPDLOG_ERROR(__VA_ARGS__); + +#define LOGGER_NAME "WCF" +#define LOGGER_FILE_NAME "/logs/wcf.txt" +#define LOGGER_MAX_SIZE 1024 * 1024 * 10 // 10M +#define LOGGER_MAX_FILES 10 // 10 files + +inline void init_logger(const std::string &path) +{ + static std::shared_ptr logger = nullptr; + if (logger != nullptr) { + return; + } + + auto filename = std::filesystem::path(path + LOGGER_FILE_NAME).make_preferred().string(); + try { + logger = spdlog::rotating_logger_mt(LOGGER_NAME, filename, LOGGER_MAX_SIZE, LOGGER_MAX_FILES); + } catch (const spdlog::spdlog_ex &ex) { + std::wstring wmsg(ex.what(), ex.what() + strlen(ex.what())); + MessageBox(NULL, wmsg.c_str(), L"Init LOGGER ERROR", 0); + } + + spdlog::set_default_logger(logger); + logger->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%l] [%n] [%s::%#::%!] %v"); +#if SPDLOG_ACTIVE_LEVEL <= SPDLOG_LEVEL_DEBUG + spdlog::set_level(spdlog::level::debug); + logger->flush_on(spdlog::level::debug); +#else + logger->flush_on(spdlog::level::info); +#endif + LOG_DEBUG("InitLogger with debug level"); +} + +#if SPDLOG_ACTIVE_LEVEL <= SPDLOG_LEVEL_DEBUG + +#define BUF_SIZE (1024 * 1024) +static char buf[BUF_SIZE] = { 0 }; + +inline void log_buffer(uint8_t *buffer, size_t len) +{ + size_t j = sprintf_s(buf, BUF_SIZE, "BUF@%08X[%ld]: ", (uint32_t)buffer, len); + for (size_t i = 0; i < len; i++) { + j += sprintf_s(buf + j, BUF_SIZE, "%02X ", buffer[i]); + if (j > BUF_SIZE - 3) { + break; + } + } + LOG_DEBUG(buf); +} +#endif diff --git a/WeChatFerry/spy/com/util.cpp b/WeChatFerry/spy/com/util.cpp new file mode 100644 index 00000000..dbdc61ef --- /dev/null +++ b/WeChatFerry/spy/com/util.cpp @@ -0,0 +1,308 @@ +#include "util.h" + +#include "Shlwapi.h" +#include "framework.h" +#include +#include +#include +#include +#include +#include +#include + +#pragma comment(lib, "shlwapi") +#pragma comment(lib, "Version.lib") + +namespace util +{ + +std::wstring s2w(const std::string &s) +{ + if (s.empty()) + return std::wstring(); + int size_needed = MultiByteToWideChar(CP_UTF8, 0, &s[0], (int)s.size(), NULL, 0); + std::wstring ws(size_needed, 0); + MultiByteToWideChar(CP_UTF8, 0, &s[0], (int)s.size(), &ws[0], size_needed); + return ws; +} + +std::string w2s(const std::wstring &ws) +{ + if (ws.empty()) + return std::string(); + int size_needed = WideCharToMultiByte(CP_UTF8, 0, &ws[0], (int)ws.size(), NULL, 0, NULL, NULL); + std::string s(size_needed, 0); + WideCharToMultiByte(CP_UTF8, 0, &ws[0], (int)ws.size(), &s[0], size_needed, NULL, NULL); + return s; +} + +std::string gb2312_to_utf8(const char *gb2312) +{ + int size_needed = 0; + + size_needed = MultiByteToWideChar(CP_ACP, 0, gb2312, -1, NULL, 0); + std::wstring ws(size_needed, 0); + MultiByteToWideChar(CP_ACP, 0, gb2312, -1, &ws[0], size_needed); + + size_needed = WideCharToMultiByte(CP_UTF8, 0, &ws[0], -1, NULL, 0, NULL, NULL); + std::string s(size_needed, 0); + WideCharToMultiByte(CP_UTF8, 0, &ws[0], -1, &s[0], size_needed, NULL, NULL); + + return s; +} + +static int get_wechat_path(wchar_t *path) +{ + int ret = -1; + HKEY hKey = NULL; + // HKEY_CURRENT_USER\Software\Tencent\WeChat InstallPath = xx + if (ERROR_SUCCESS != RegOpenKey(HKEY_CURRENT_USER, L"Software\\Tencent\\WeChat", &hKey)) { + ret = GetLastError(); + return ret; + } + + DWORD Type = REG_SZ; + DWORD cbData = MAX_PATH * sizeof(WCHAR); + if (ERROR_SUCCESS != RegQueryValueEx(hKey, L"InstallPath", 0, &Type, (LPBYTE)path, &cbData)) { + ret = GetLastError(); + goto __exit; + } + + if (path != NULL) { + PathAppend(path, WECHAREXE); + } + +__exit: + if (hKey) { + RegCloseKey(hKey); + } + + return ERROR_SUCCESS; +} + +static int get_wechat_win_dll_path(wchar_t *path) +{ + int ret = get_wechat_path(path); + if (ret != ERROR_SUCCESS) { + return ret; + } + + PathRemoveFileSpecW(path); + PathAppendW(path, WECHATWINDLL); + if (!PathFileExists(path)) { + // 微信从(大约)3.7开始,增加了一层版本目录: [3.7.0.29] + PathRemoveFileSpec(path); + _wfinddata_t findData; + std::wstring dir = std::wstring(path) + L"\\[*.*"; + intptr_t handle = _wfindfirst(dir.c_str(), &findData); + if (handle == -1) { // 检查是否成功 + return -1; + } + std::wstring dllPath = std::wstring(path) + L"\\" + findData.name; + wcscpy_s(path, MAX_PATH, dllPath.c_str()); + PathAppend(path, WECHATWINDLL); + } + + return ret; +} + +static bool get_file_version(const wchar_t *filePath, wchar_t *version) +{ + if (wcslen(filePath) > 0 && PathFileExists(filePath)) { + VS_FIXEDFILEINFO *pVerInfo = NULL; + DWORD dwTemp, dwSize; + BYTE *pData = NULL; + UINT uLen; + + dwSize = GetFileVersionInfoSize(filePath, &dwTemp); + if (dwSize == 0) { + return false; + } + + pData = new BYTE[dwSize + 1]; + if (pData == NULL) { + return false; + } + + if (!GetFileVersionInfo(filePath, 0, dwSize, pData)) { + delete[] pData; + return false; + } + + if (!VerQueryValue(pData, TEXT("\\"), (void **)&pVerInfo, &uLen)) { + delete[] pData; + return false; + } + + DWORD verMS = pVerInfo->dwFileVersionMS; + DWORD verLS = pVerInfo->dwFileVersionLS; + DWORD major = HIWORD(verMS); + DWORD minor = LOWORD(verMS); + DWORD build = HIWORD(verLS); + DWORD revision = LOWORD(verLS); + delete[] pData; + + StringCbPrintf(version, 0x20, TEXT("%d.%d.%d.%d"), major, minor, build, revision); + + return true; + } + + return false; +} + +int get_wechat_version(wchar_t *version) +{ + WCHAR Path[MAX_PATH] = { 0 }; + + int ret = get_wechat_win_dll_path(Path); + if (ret != ERROR_SUCCESS) { + return ret; + } + + ret = get_file_version(Path, version); + + return ret; +} + +uint32_t get_wechat_pid() +{ + uint32_t pid = 0; + HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + PROCESSENTRY32 pe32 = { sizeof(PROCESSENTRY32) }; + while (Process32Next(hSnapshot, &pe32)) { + std::wstring strProcess = pe32.szExeFile; + if (strProcess == WECHAREXE) { + pid = pe32.th32ProcessID; + break; + } + } + CloseHandle(hSnapshot); + return pid; +} + +int open_wechat(uint32_t *pid, bool *launched) +{ + if (launched) { + *launched = false; + } + + *pid = get_wechat_pid(); + if (*pid) { + return ERROR_SUCCESS; + } + + int ret = -1; + STARTUPINFO si = { sizeof(si) }; + WCHAR Path[MAX_PATH] = { 0 }; + PROCESS_INFORMATION pi = { 0 }; + + ret = get_wechat_path(Path); + if (ERROR_SUCCESS != ret) { + return ret; + } + + if (!CreateProcess(NULL, Path, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi)) { + return GetLastError(); + } + + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + + *pid = pi.dwProcessId; + if (launched) { + *launched = true; + } + + return ERROR_SUCCESS; +} + +int get_wstring_by_address(uint32_t address, wchar_t *buffer, uint32_t buffer_size) +{ + uint32_t strLength = get_dword(address + 4); + if (strLength == 0) { + return 0; + } else if (strLength > buffer_size) { + strLength = buffer_size - 1; + } + + wmemcpy_s(buffer, strLength + 1, get_wstring(address), strLength + 1); + + return strLength; +} + +std::string get_string_by_address(uint32_t address) +{ + uint32_t strLength = get_dword(address + 4); + return w2s(std::wstring(get_wstring(address), strLength)); +} + +std::string get_string_by_str_addr(uint32_t addr) +{ + uint32_t strLength = get_dword(addr + 4); + return strLength ? std::string(get_string(addr), strLength) : std::string(); +} + +std::string get_string_by_wstr_addr(uint32_t addr) +{ + uint32_t strLength = get_dword(addr + 4); + return strLength ? w2s(std::wstring(get_wstring(addr), strLength)) : std::string(); +} + +uint32_t get_memory_int_by_address(void *hProcess, uint32_t address) +{ + uint32_t value = 0; + + unsigned char data[4] = { 0 }; + if (ReadProcessMemory(hProcess, (LPVOID)address, data, 4, 0)) { + value = data[0] & 0xFF; + value |= ((data[1] << 8) & 0xFF00); + value |= ((data[2] << 16) & 0xFF0000); + value |= ((data[3] << 24) & 0xFF000000); + } + + return value; +} + +std::wstring get_unicode_info_by_address(void *hProcess, uint32_t address) +{ + std::wstring value = L""; + + uint32_t strAddress = get_memory_int_by_address(hProcess, address); + uint32_t strLen = get_memory_int_by_address(hProcess, address + 0x4); + if (strLen > 500) + return value; + + wchar_t cValue[500] = { 0 }; + memset(cValue, 0, sizeof(cValue) / sizeof(wchar_t)); + if (ReadProcessMemory(hProcess, (LPVOID)strAddress, cValue, (strLen + 1) * 2, 0)) { + value = std::wstring(cValue); + } + + return value; +} + +void dbg_msg(const char *format, ...) +{ + // initialize use of the variable argument array + va_list vaArgs; + va_start(vaArgs, format); + + // reliably acquire the size + // from a copy of the variable argument array + // and a functionally reliable call to mock the formatting + va_list vaArgsCopy; + va_copy(vaArgsCopy, vaArgs); + const int iLen = std::vsnprintf(NULL, 0, format, vaArgsCopy); + va_end(vaArgsCopy); + + // return a formatted string without risking memory mismanagement + // and without assuming any compiler or platform specific behavior + std::vector zc(iLen + 1); + std::vsnprintf(zc.data(), zc.size(), format, vaArgs); + va_end(vaArgs); + std::string strText(zc.data(), iLen); + + OutputDebugStringA(strText.c_str()); +} + +} // namespace util diff --git a/WeChatFerry/spy/com/util.h b/WeChatFerry/spy/com/util.h new file mode 100644 index 00000000..61c8bee2 --- /dev/null +++ b/WeChatFerry/spy/com/util.h @@ -0,0 +1,78 @@ +#pragma once + +#include +#include + +#include "framework.h" + +#define WECHAREXE L"WeChat.exe" +#define WECHATWINDLL L"WeChatWin.dll" +#define WECHATSDKDLL L"sdk.dll" +#define WECHATINJECTDLL L"spy.dll" +#define WECHATINJECTDLL_DEBUG L"spy_debug.dll" + +typedef struct PortPath { + int port; + char path[MAX_PATH]; +} PortPath_t; + +namespace util +{ + +// Inline 函数替代宏,提供类型安全 +inline uint32_t get_dword(uint32_t addr) +{ + return addr ? *reinterpret_cast(addr) : 0; +} + +inline uint64_t get_qword(uint64_t addr) +{ + return addr ? *reinterpret_cast(addr) : 0; +} + +inline const char *get_string(uint32_t addr) +{ + return addr ? *reinterpret_cast(addr) : nullptr; +} + +inline const wchar_t *get_wstring(uint32_t addr) +{ + return addr ? *reinterpret_cast(addr) : nullptr; +} + +inline const char *get_p_string(uint32_t addr) +{ + return addr ? reinterpret_cast(addr) : nullptr; +} + +inline const wchar_t *get_p_wstring(uint32_t addr) +{ + return addr ? reinterpret_cast(addr) : nullptr; +} + +inline std::string get_pp_string(uint32_t addr) +{ + if (!addr) + return ""; + const char *ptr = *reinterpret_cast(addr); + return (ptr && *ptr) ? std::string(ptr) : ""; +} + +// 函数声明(使用 snake_case 命名) +uint32_t get_wechat_pid(); +// launched(可选出参):微信本已运行则置 false,本次新拉起则置 true, +// 供调用方决定是否需要等待微信启动。 +int open_wechat(uint32_t *pid, bool *launched = nullptr); +int get_wechat_version(wchar_t *version); +int get_wstring_by_address(uint32_t address, wchar_t *buffer, uint32_t buffer_size); +uint32_t get_memory_int_by_address(void *hProcess, uint32_t address); +std::wstring get_unicode_info_by_address(void *hProcess, uint32_t address); +std::wstring s2w(const std::string &s); +std::string w2s(const std::wstring &ws); +std::string gb2312_to_utf8(const char *gb2312); +std::string get_string_by_address(uint32_t address); +std::string get_string_by_str_addr(uint32_t addr); +std::string get_string_by_wstr_addr(uint32_t addr); +void dbg_msg(const char *format, ...); + +} // namespace util diff --git a/WeChatFerry/spy/contact_manager.cpp b/WeChatFerry/spy/contact_manager.cpp index 2863aedf..133d61ef 100644 --- a/WeChatFerry/spy/contact_manager.cpp +++ b/WeChatFerry/spy/contact_manager.cpp @@ -2,152 +2,260 @@ #include "contact_manager.h" +#include +#include +#include +#include + +#include "database_executor.h" #include "log.hpp" -#include "offsets.h" #include "pb_util.h" #include "rpc_helper.h" #include "spy.h" +#include "spy_types.h" #include "util.h" +#include "offsets.h" -using namespace std; +namespace +{ -namespace contact +struct WxStringValue { + const wchar_t *wptr; + uint32_t size; + uint32_t capacity; + const char *ptr; + uint32_t clen; +}; + +using BufferInitFn = void(__thiscall *)(void *buffer); +using BufferCleanupFn = void(__thiscall *)(void *buffer); +using WxStringAssignFn = void *(__thiscall *)(void *dest, const wchar_t *src, int len); +using AcceptNewFriendFn = int(__thiscall *)(void *buffer, const WxString *v3, void *nullbuffer, + int reserved1, uint64_t scratch, WxStringValue v4, + int scene, int reserved2); + +WxStringValue to_value(const WxString &value) +{ + return { value.wptr, value.size, value.capacity, value.ptr, value.clen }; +} + +std::string field_to_string(const DbField_t &field) +{ + return std::string(field.content.begin(), field.content.end()); +} + +std::set get_contact_columns() { -namespace OsCon = Offsets::Contact; + std::set columns; + DbRows_t rows = db::exec_db_query("MicroMsg.db", "PRAGMA table_info(Contact);"); + for (const auto &row : rows) { + for (const auto &field : row) { + if (field.column == "name") { + columns.insert(field_to_string(field)); + } + } + } + return columns; +} -using get_contact_mgr_t = QWORD (*)(); -using get_contact_list_t = QWORD (*)(QWORD, QWORD); -using func_verify_new_t = QWORD (*)(QWORD, WxString *); -using func_verify_ok_t = QWORD (*)(QWORD, WxString *, QWORD *, QWORD, QWORD, QWORD, QWORD, QWORD, WxString *); +bool has_column(const std::set &columns, const char *name) +{ + return columns.find(name) != columns.end(); +} -#define FEAT_LEN 5 -static const uint8_t FEAT_COUNTRY[FEAT_LEN] = { 0xA4, 0xD9, 0x02, 0x4A, 0x18 }; -static const uint8_t FEAT_PROVINCE[FEAT_LEN] = { 0xE2, 0xEA, 0xA8, 0xD1, 0x18 }; -static const uint8_t FEAT_CITY[FEAT_LEN] = { 0x1D, 0x02, 0x5B, 0xBF, 0x18 }; +std::string select_expr(const std::set &columns, const char *source, const char *alias, bool numeric = false) +{ + if (has_column(columns, source)) { + return std::string(source) + " AS " + alias; + } + return numeric ? ("0 AS " + std::string(alias)) : ("'' AS " + std::string(alias)); +} -static QWORD find_mem(QWORD start, QWORD end, const void *target, size_t len) +std::string escape_sql_string(const std::string &value) { - uint8_t *p = reinterpret_cast(start); - while (reinterpret_cast(p) < end) { - if (memcmp(p, target, len) == 0) { - return reinterpret_cast(p); + std::string escaped; + escaped.reserve(value.size()); + for (char ch : value) { + escaped.push_back(ch); + if (ch == '\'') { + escaped.push_back('\''); } - p++; } - return 0; + return escaped; } -static string get_cnt_string(QWORD start, QWORD end, const uint8_t *feat, size_t len) +std::string build_contact_query(const std::string *wxid) { - QWORD pfeat = find_mem(start, end, feat, len); - if (pfeat == 0) { + std::set columns = get_contact_columns(); + if (!has_column(columns, "UserName")) { return ""; } - DWORD lfeat = util::get_dword(pfeat + len); - if (lfeat <= 2) { - return ""; + std::vector fields = { + select_expr(columns, "UserName", "wxid"), + select_expr(columns, "Alias", "code"), + select_expr(columns, "Remark", "remark"), + select_expr(columns, "NickName", "name"), + select_expr(columns, "Sex", "gender", true), + select_expr(columns, "Country", "country"), + select_expr(columns, "Province", "province"), + select_expr(columns, "City", "city"), + }; + + std::string sql = "SELECT "; + for (size_t i = 0; i < fields.size(); ++i) { + if (i != 0) { + sql += ", "; + } + sql += fields[i]; + } + sql += " FROM Contact"; + + if (wxid != nullptr) { + sql += " WHERE UserName='"; + sql += escape_sql_string(*wxid); + sql += "'"; } - return util::w2s(util::get_p_wstring(pfeat + FEAT_LEN + 4, lfeat)); + return sql + ";"; } -vector get_contacts() +RpcContact_t row_to_contact(const DbRow_t &row) { - vector contacts; - auto func_get_contact_mgr = Spy::getFunction(OsCon::MGR); - auto func_get_contact_list = Spy::getFunction(OsCon::LIST); - - QWORD mgr = func_get_contact_mgr(); - QWORD addr[3] = { 0 }; - if (func_get_contact_list(mgr, reinterpret_cast(addr)) != 1) { - LOG_ERROR("get_contacts failed"); - return contacts; + RpcContact_t contact = {}; + for (const auto &field : row) { + std::string value = field_to_string(field); + if (field.column == "wxid") { + contact.wxid = value; + } else if (field.column == "code") { + contact.code = value; + } else if (field.column == "remark") { + contact.remark = value; + } else if (field.column == "name") { + contact.name = value; + } else if (field.column == "country") { + contact.country = value; + } else if (field.column == "province") { + contact.province = value; + } else if (field.column == "city") { + contact.city = value; + } else if (field.column == "gender") { + contact.gender = value.empty() ? 0 : atoi(value.c_str()); + } } - QWORD pstart = addr[0]; - QWORD pend = addr[2]; - while (pstart < pend) { - RpcContact_t cnt; - QWORD pbin = util::get_qword(pstart + OsCon::BIN); - QWORD lenbin = util::get_dword(pstart + OsCon::BIN_LEN); + return contact; +} - cnt.wxid = util::get_str_by_wstr_addr(pstart + OsCon::WXID); - cnt.code = util::get_str_by_wstr_addr(pstart + OsCon::CODE); - cnt.remark = util::get_str_by_wstr_addr(pstart + OsCon::REMARK); - cnt.name = util::get_str_by_wstr_addr(pstart + OsCon::NAME); +} // namespace - cnt.country = get_cnt_string(pbin, pbin + lenbin, FEAT_COUNTRY, FEAT_LEN); - cnt.province = get_cnt_string(pbin, pbin + lenbin, FEAT_PROVINCE, FEAT_LEN); - cnt.city = get_cnt_string(pbin, pbin + lenbin, FEAT_CITY, FEAT_LEN); +namespace contact +{ - cnt.gender = (pbin == 0) ? 0 : static_cast(*(uint8_t *)(pbin + OsCon::GENDER)); +std::vector get_contacts() +{ + std::vector contacts; - contacts.push_back(cnt); - pstart += OsCon::STEP; + // 依据 MicroMsg.db 的 Contact 表实际列动态拼 SQL(缺列以 '' / 0 占位) + std::string sql = build_contact_query(nullptr); + if (sql.empty()) { + LOG_ERROR("Failed to build contact query (missing UserName column?)."); + return contacts; } + DbRows_t rows = db::exec_db_query("MicroMsg.db", sql); + contacts.reserve(rows.size()); + for (const auto &row : rows) { + contacts.push_back(row_to_contact(row)); + } return contacts; } int accept_new_friend(const std::string &v3, const std::string &v4, int scene) { - // TODO: 备注、标签等 - auto func_new = Spy::getFunction(OsCon::VERIFY_NEW); - auto func_verify = Spy::getFunction(OsCon::VERIFY_OK); + if (g_WeChatWinDllAddr == 0) { + LOG_ERROR("WeChatWin.dll not located."); + return -1; + } + + // v3(加密 username)仅被读取(拷入 this+24),用非拥有视图即可; + // v4(ticket)会被 VerifyOK mm_free,须用 ASSIGN 建 WeChat 拥有副本。 + auto ctor = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Friend::ACCEPT_CTOR); + auto verify_ok = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Friend::VERIFY_OK); + auto dtor = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Friend::ACCEPT_DTOR); + auto assign = reinterpret_cast(g_WeChatWinDllAddr + Offsets::RichText::ASSIGN); - QWORD helper = util::get_qword(Spy::WeChatDll.load() + OsCon::ADD_FRIEND_HELPER); - QWORD fvdf = util::get_qword(Spy::WeChatDll.load() + OsCon::FVDF); + LOG_DEBUG("accept_new_friend v3: {}, v4: {}, scene: {}", v3, v4, scene); - auto pV3 = util::CreateWxString(v3); - auto pV4 = util::CreateWxString(v4); + std::wstring wsV3 = util::s2w(v3); + std::wstring wsV4 = util::s2w(v4); + WxString wxV3(wsV3); // 非拥有视图:VerifyOK 只读取 + WxString wxV4; // ticket:须为 WeChat 拥有副本(VerifyOK 末尾 mm_free 它) + assign(&wxV4, wsV4.c_str(), -1); - QWORD v4Array[4] = { 0 }; - QWORD pV4Buff = func_new(reinterpret_cast(&v4Array), pV4); + char buffer[0x40] = { 0 }; + char nullbuffer[0x3CC] = { 0 }; - char buff[0x100] = { 0 }; - memcpy(buff, &helper, sizeof(&helper)); - QWORD a1 = reinterpret_cast(&buff); + ctor(buffer); + int success = verify_ok(buffer, &wxV3, nullbuffer, 0, 0, to_value(wxV4), scene, 0); + dtor(buffer); - QWORD ret = func_verify(a1, pV3, &fvdf, 0x1D08B4, pV4Buff, 0x1, pV4Buff, scene, 0x0); - util::FreeWxString(pV3); - util::FreeWxString(pV4); + return success; +} - return static_cast(ret); // 成功返回 1 +int add_friend_by_wxid(const std::string &wxid, const std::string &msg) +{ + (void)wxid; + (void)msg; + return 0; } -RpcContact_t get_contact_by_wxid(const string &wxid) +RpcContact_t get_contact_by_wxid(const std::string &wxid) { - RpcContact_t contact; - LOG_ERROR("技术太菜,实现不了。"); + RpcContact_t contact = {}; + contact.wxid = wxid; + + // 与 get_contacts 同一条链,仅带 WHERE UserName=? 过滤单个联系人。 + std::string sql = build_contact_query(&wxid); + if (sql.empty()) { + LOG_ERROR("Failed to build contact query (missing UserName column?)."); + return contact; + } + + DbRows_t rows = db::exec_db_query("MicroMsg.db", sql); + if (!rows.empty()) { + contact = row_to_contact(rows.front()); + } return contact; } bool rpc_get_contacts(uint8_t *out, size_t *len) { - vector contacts = get_contacts(); - return fill_response(out, len, [&](Response &rsp) { + std::vector contacts = get_contacts(); + return fill_response(out, len, contacts, [](Response &rsp, auto &contacts) { rsp.msg.contacts.contacts.funcs.encode = encode_contacts; rsp.msg.contacts.contacts.arg = &contacts; }); } -bool rpc_get_contact_info(const string &wxid, uint8_t *out, size_t *len) +bool rpc_get_contact_info(const std::string &wxid, uint8_t *out, size_t *len) { - vector contacts = { get_contact_by_wxid(wxid) }; - return fill_response(out, len, [&](Response &rsp) { + std::vector contacts; + if (!wxid.empty()) { + contacts.push_back(get_contact_by_wxid(wxid)); + } + return fill_response(out, len, contacts, [](Response &rsp, auto &contacts) { rsp.msg.contacts.contacts.funcs.encode = encode_contacts; rsp.msg.contacts.contacts.arg = &contacts; }); } -bool rpc_accept_friend(const Verification &v, uint8_t *out, size_t *len) +bool rpc_accept_friend(const std::string &v3, const std::string &v4, int scene, uint8_t *out, size_t *len) { - const string v3 = v.v3 ? v.v3 : ""; - const string v4 = v.v4 ? v.v4 : ""; - int scene = v.scene; - return fill_response( - out, len, [&](Response &rsp) { rsp.msg.status = accept_new_friend(v3, v4, scene); }); + int result = accept_new_friend(v3, v4, scene); + return fill_response(out, len, [result](Response &rsp) { + rsp.msg.status = result; + }); } } // namespace contact diff --git a/WeChatFerry/spy/contact_manager.h b/WeChatFerry/spy/contact_manager.h index 440091ee..dc72ba0d 100644 --- a/WeChatFerry/spy/contact_manager.h +++ b/WeChatFerry/spy/contact_manager.h @@ -3,28 +3,18 @@ #include #include -#include "wcf.pb.h" - #include "pb_types.h" namespace contact { -// 获取所有联系人 std::vector get_contacts(); - -// 根据 wxid 获取联系人信息 -RpcContact_t get_contact_by_wxid(const std::string &wxid); - -// 接受好友请求 int accept_new_friend(const std::string &v3, const std::string &v4, int scene); +int add_friend_by_wxid(const std::string &wxid, const std::string &msg); +RpcContact_t get_contact_by_wxid(const std::string &wxid); -// 发送好友请求 -// int add_friend_by_wxid(const std::string &wxid, const std::string &msg); - -// RPC 方法 bool rpc_get_contacts(uint8_t *out, size_t *len); bool rpc_get_contact_info(const std::string &wxid, uint8_t *out, size_t *len); -bool rpc_accept_friend(const Verification &v, uint8_t *out, size_t *len); +bool rpc_accept_friend(const std::string &v3, const std::string &v4, int scene, uint8_t *out, size_t *len); } // namespace contact diff --git a/WeChatFerry/spy/database_executor.cpp b/WeChatFerry/spy/database_executor.cpp index 42e0d9a8..0a4db255 100644 --- a/WeChatFerry/spy/database_executor.cpp +++ b/WeChatFerry/spy/database_executor.cpp @@ -1,111 +1,304 @@ #include "database_executor.h" #include -#include +#include +#include +#include +#include +#include +#include #include "log.hpp" #include "offsets.h" #include "pb_util.h" #include "rpc_helper.h" -#include "spy.h" -#include "sqlite3.h" #include "util.h" +extern uint32_t g_WeChatWinDllAddr; + namespace db { -namespace OsDb = Offsets::Db; -using db_map_t = std::map; -static db_map_t db_map; +namespace OsDb = Offsets::Database; + +namespace +{ + +// SQLite 返回码与列类型 +constexpr int SQLITE_OK = 0; +constexpr int SQLITE_ROW = 100; +constexpr int SQLITE_DONE = 101; +constexpr int SQLITE_INTEGER = 1; +constexpr int SQLITE_FLOAT = 2; +constexpr int SQLITE_TEXT = 3; +constexpr int SQLITE_BLOB = 4; +constexpr int SQLITE_NULL = 5; + +// 进程内(已解密)SQLCipher/sqlite3 内部 API,均为 __cdecl +using sqlite3_prepare_v2_fn = int(__cdecl *)(void *, const char *, int, void **, const char **); +using sqlite3_step_fn = int(__cdecl *)(void *); +using sqlite3_finalize_fn = int(__cdecl *)(void *); +using sqlite3_column_count_fn = int(__cdecl *)(void *); +using sqlite3_column_name_fn = const char *(__cdecl *)(void *, int); +using sqlite3_column_type_fn = int(__cdecl *)(void *, int); +using sqlite3_column_blob_fn = const void *(__cdecl *)(void *, int); +using sqlite3_column_bytes_fn = int(__cdecl *)(void *, int); +using sqlite3_column_text_fn = const unsigned char *(__cdecl *)(void *, int); + +struct SqliteApi { + sqlite3_prepare_v2_fn prepare_v2; + sqlite3_step_fn step; + sqlite3_finalize_fn finalize; + sqlite3_column_count_fn column_count; + sqlite3_column_name_fn column_name; + sqlite3_column_type_fn column_type; + sqlite3_column_blob_fn column_blob; + sqlite3_column_bytes_fn column_bytes; + sqlite3_column_text_fn column_text; +}; + +const SqliteApi &sqlite_api() +{ + // g_WeChatWinDllAddr 在 InitSpy 期间即已就绪,RPC 线程首次调用时才构造,安全。 + static const SqliteApi api = { + reinterpret_cast(g_WeChatWinDllAddr + OsDb::PREPARE_V2), + reinterpret_cast(g_WeChatWinDllAddr + OsDb::STEP), + reinterpret_cast(g_WeChatWinDllAddr + OsDb::FINALIZE), + reinterpret_cast(g_WeChatWinDllAddr + OsDb::COLUMN_COUNT), + reinterpret_cast(g_WeChatWinDllAddr + OsDb::COLUMN_NAME), + reinterpret_cast(g_WeChatWinDllAddr + OsDb::COLUMN_TYPE), + reinterpret_cast(g_WeChatWinDllAddr + OsDb::COLUMN_BLOB), + reinterpret_cast(g_WeChatWinDllAddr + OsDb::COLUMN_BYTES), + reinterpret_cast(g_WeChatWinDllAddr + OsDb::COLUMN_TEXT), + }; + return api; +} + +// 库名 → 已解密 sqlite3* 句柄 +using db_map_t = std::map; +db_map_t db_map; + +// 读取 storage 对象内 storage+NAME 处的 std::wstring(MSVC 布局:_Bx 起始,容量位 +0x14; +// wchar_t 的 SSO 阈值为 8:容量 >= 8 走堆指针,否则内联缓冲)并取文件名(basename)。 +std::string read_db_name(uint32_t wstr_addr) +{ + uint32_t capacity = util::get_dword(wstr_addr + 0x14); + const wchar_t *buffer = (capacity >= 8) ? reinterpret_cast(util::get_dword(wstr_addr)) + : reinterpret_cast(wstr_addr); + if ((buffer == nullptr) || (*buffer == L'\0')) { + return ""; + } + + std::wstring path(buffer); + size_t pos = path.find_last_of(L"\\/"); + if (pos != std::wstring::npos) { + path = path.substr(pos + 1); + } + return util::w2s(path); +} -static void get_db_handle(QWORD base, QWORD offset) +// 读取 WxString(wptr 直接在 +0,非 MSVC std::wstring 的 SSO 联合)指向的库名,取文件名(basename)。 +// MultiDBMsgMgr 的 item 名(item+0)和媒体包装的名(StorageBase+0x4C)都是这种布局。 +std::string read_wptr_db_name(uint32_t wxstr_addr) { - auto *wsp = reinterpret_cast(*(QWORD *)(base + offset + OsDb::NAME)); - std::string dbname = util::w2s(std::wstring(wsp)); - db_map[dbname] = util::get_qword(base + offset); + const wchar_t *buffer = reinterpret_cast(util::get_dword(wxstr_addr)); + if ((buffer == nullptr) || (*buffer == L'\0')) { + return ""; + } + + std::wstring path(buffer); + size_t pos = path.find_last_of(L"\\/"); + if (pos != std::wstring::npos) { + path = path.substr(pos + 1); + } + return util::w2s(path); } -static void get_msg_db_handle(QWORD msg_mgr_addr) +// 遍历 MultiDBMsgMgr 的环形 item 数组,把 MSGn.db / MediaMSGn.db 的裸句柄并入 db_map。 +// 消息与语音多库不在 AccountStorageMgr 的主数组里,故单独枚举(未登录时管理器为 0,跳过即可)。 +void refresh_msg_db_map() { - QWORD db_index = util::get_qword(msg_mgr_addr + 0x68); - QWORD p_start = util::get_qword(msg_mgr_addr + 0x50); - for (uint32_t i = 0; i < db_index; i++) { - QWORD db_addr = util::get_qword(p_start + i * 0x08); - if (db_addr) { - // MSGi.db - std::string dbname = util::w2s(util::get_pp_wstring(db_addr)); - db_map[dbname] = util::get_qword(db_addr + 0x78); - - // MediaMsgi.db - QWORD mmdb_addr = util::get_qword(db_addr + 0x20); - std::string mmdbname = util::w2s(util::get_pp_wstring(mmdb_addr + 0x78)); - db_map[mmdbname] = util::get_qword(mmdb_addr + 0x50); + uint32_t base = g_WeChatWinDllAddr; + if (base == 0) { + return; + } + + uint32_t mgr = util::get_dword(base + OsDb::MSG_MGR); + if (mgr == 0) { + return; + } + + uint32_t ring = util::get_dword(mgr + OsDb::MSG_RING_BASE); + uint32_t cap = util::get_dword(mgr + OsDb::MSG_RING_CAP); + uint32_t head = util::get_dword(mgr + OsDb::MSG_RING_HEAD); + uint32_t count = util::get_dword(mgr + OsDb::MSG_RING_COUNT); + if ((ring == 0) || (cap == 0)) { + return; + } + + for (uint32_t i = 0; i < count; i++) { + uint32_t slot = (i + head) & (cap - 1); + uint32_t item = util::get_dword(ring + slot * 4); + if (item == 0) { + continue; + } + + // MSG 库:句柄由 Init 缓存于 item+0x60;名在 item+0(WxString wptr)。 + uint32_t msgHandle = util::get_dword(item + OsDb::MSG_ITEM_HANDLE); + std::string msgName = read_wptr_db_name(item + OsDb::MSG_ITEM_NAME); + if ((msgHandle != 0) && !msgName.empty()) { + db_map[msgName] = msgHandle; + } + + // MediaMSG 库:走媒体存储包装(item+0x14),句柄在包装 +0x38、名在包装 +0x4C。 + uint32_t media = util::get_dword(item + OsDb::MSG_ITEM_MEDIA); + if (media != 0) { + uint32_t mediaHandle = util::get_dword(media + OsDb::MEDIA_HANDLE); + std::string mediaName = read_wptr_db_name(media + OsDb::MEDIA_NAME); + if ((mediaHandle != 0) && !mediaName.empty()) { + db_map[mediaName] = mediaHandle; + } } } } -db_map_t get_db_handles() +// 遍历 AccountStorageMgr 主 storage 数组,建立 库名 → sqlite3* 映射。 +void refresh_db_map() { db_map.clear(); - QWORD db_instance_addr = util::get_qword(Spy::WeChatDll.load() + OsDb::INSTANCE); - get_db_handle(db_instance_addr, OsDb::MICROMSG); // MicroMsg.db - get_db_handle(db_instance_addr, OsDb::CHAT_MSG); // ChatMsg.db - get_db_handle(db_instance_addr, OsDb::MISC); // Misc.db - get_db_handle(db_instance_addr, OsDb::EMOTION); // Emotion.db - get_db_handle(db_instance_addr, OsDb::MEDIA); // Media.db - get_db_handle(db_instance_addr, OsDb::FUNCTION_MSG); // Function.db + uint32_t base = g_WeChatWinDllAddr; + if (base == 0) { + return; + } + + uint32_t mgr = util::get_dword(base + OsDb::INSTANCE); + if (mgr == 0) { + LOG_ERROR("AccountStorageMgr instance is null."); + return; + } - get_msg_db_handle(util::get_qword(Spy::WeChatDll.load() + OsDb::MSG_I)); // MSGi.db & MediaMsgi.db - return db_map; + uint32_t begin = util::get_dword(mgr + OsDb::START); + uint32_t end = util::get_dword(mgr + OsDb::END); + if ((begin == 0) || (end < begin)) { + return; + } + + for (uint32_t p = begin; p < end; p += 4) { + uint32_t storage = util::get_dword(p); + if (storage == 0) { + continue; + } + + uint32_t handle = util::get_dword(storage + OsDb::SLOT); + if (handle == 0) { + continue; + } + + std::string name = read_db_name(storage + OsDb::NAME); + if (name.empty()) { + continue; + } + + db_map.emplace(name, handle); + } + + // 并入 MSG / MediaMSG 多库(消息、语音查询依赖)。 + refresh_msg_db_map(); } -DbNames_t get_db_names() +uint32_t find_db_handle(const std::string &db) { if (db_map.empty()) { - db_map = get_db_handles(); + refresh_db_map(); } - DbNames_t names; - for (const auto &[k, _] : db_map) { - names.push_back(k); + auto it = db_map.find(db); + if (it != db_map.end() && it->second != 0) { + return it->second; } - return names; + + // 句柄可能因重登录等变化,重建一次再试。 + refresh_db_map(); + it = db_map.find(db); + return (it == db_map.end()) ? 0 : it->second; +} + +std::string field_to_string(const DbField_t &field) +{ + return std::string(field.content.begin(), field.content.end()); } -static int cb_get_tables(void *ret, int argc, char **argv, char **azColName) +int parse_db_index(const std::string &dbname, const char *prefix) { - auto *tables = static_cast(ret); - DbTable_t tbl; - for (int i = 0; i < argc; i++) { - if (strcmp(azColName[i], "name") == 0) { - tbl.name = argv[i] ? argv[i] : ""; - } else if (strcmp(azColName[i], "sql") == 0) { - std::string sql(argv[i]); - sql.erase(std::remove(sql.begin(), sql.end(), '\t'), sql.end()); - tbl.sql = sql; + const size_t prefix_len = strlen(prefix); + if ((dbname.size() <= prefix_len + 3) || (dbname.compare(0, prefix_len, prefix) != 0) || + (dbname.compare(dbname.size() - 3, 3, ".db") != 0)) { + return -1; + } + + std::string idx = dbname.substr(prefix_len, dbname.size() - prefix_len - 3); + if (idx.empty() || !std::all_of(idx.begin(), idx.end(), [](char ch) { return ch >= '0' && ch <= '9'; })) { + return -1; + } + + return atoi(idx.c_str()); +} + +std::vector> find_dbs_by_prefix(const char *prefix) +{ + if (db_map.empty()) { + refresh_db_map(); + } + + std::vector> result; + for (const auto &[name, handle] : db_map) { + (void)handle; + int idx = parse_db_index(name, prefix); + if (idx >= 0) { + result.emplace_back(idx, name); } } - tables->push_back(tbl); - return 0; + + std::sort(result.begin(), result.end(), [](const auto &lhs, const auto &rhs) { return lhs.first > rhs.first; }); + return result; } -DbTables_t get_db_tables(const std::string &db) +} // namespace + +DbNames_t get_db_names() { - DbTables_t tables; if (db_map.empty()) { - db_map = get_db_handles(); + refresh_db_map(); } - auto it = db_map.find(db); - if (it == db_map.end()) { - return tables; + DbNames_t names; + names.reserve(db_map.size()); + for (const auto &[name, handle] : db_map) { + (void)handle; + names.push_back(name); } + return names; +} - constexpr const char *sql = "SELECT name FROM sqlite_master WHERE type='table';"; - auto p_sqlite3_exec = Spy::getFunction(OsDb::EXEC); - p_sqlite3_exec(it->second, sql, (Sqlite3_callback)cb_get_tables, (void *)&tables, nullptr); +DbTables_t get_db_tables(const std::string &db) +{ + DbTables_t tables; + DbRows_t rows = exec_db_query(db, "SELECT name, sql FROM sqlite_master WHERE type = 'table' ORDER BY name;"); + for (const auto &row : rows) { + DbTable_t table; + for (const auto &field : row) { + if (field.column == "name") { + table.name = field_to_string(field); + } else if (field.column == "sql") { + std::string sql = field_to_string(field); + sql.erase(std::remove(sql.begin(), sql.end(), '\t'), sql.end()); + table.sql = sql; + } + } + tables.push_back(table); + } return tables; } @@ -113,165 +306,195 @@ DbRows_t exec_db_query(const std::string &db, const std::string &sql) { DbRows_t rows; - auto func_prepare = Spy::getFunction(OsDb::PREPARE); - auto func_step = Spy::getFunction(OsDb::STEP); - auto func_column_count = Spy::getFunction(OsDb::COLUMN_COUNT); - auto func_column_name = Spy::getFunction(OsDb::COLUMN_NAME); - auto func_column_type = Spy::getFunction(OsDb::COLUMN_TYPE); - auto func_column_blob = Spy::getFunction(OsDb::COLUMN_BLOB); - auto func_column_bytes = Spy::getFunction(OsDb::COLUMN_BYTES); - auto func_finalize = Spy::getFunction(OsDb::FINALIZE); - - if (db_map.empty()) { - db_map = get_db_handles(); + uint32_t handle = find_db_handle(db); + if (handle == 0) { + LOG_ERROR("Failed to get handle for database '{}'.", db); + return rows; } - auto it = db_map.find(db); - if (it == db_map.end() || it->second == 0) { - LOG_WARN("Empty handle for database '{}', retrying...", db); - db_map = get_db_handles(); - it = db_map.find(db); - if (it == db_map.end() || it->second == 0) { - LOG_ERROR("Failed to get handle for database '{}'", db); - return rows; - } - } + const SqliteApi &api = sqlite_api(); - QWORD *stmt; - int rc = func_prepare(it->second, sql.c_str(), -1, &stmt, nullptr); - if (rc != SQLITE_OK) { - LOG_ERROR("SQL prepare failed for '{}': error code {}", db, rc); + void *stmt = nullptr; + int rc = api.prepare_v2(reinterpret_cast(handle), sql.c_str(), -1, &stmt, nullptr); + if (rc != SQLITE_OK || stmt == nullptr) { + LOG_ERROR("SQL prepare failed on '{}' (rc={}).", db, rc); return rows; } - while (func_step(stmt) == SQLITE_ROW) { + while (api.step(stmt) == SQLITE_ROW) { DbRow_t row; - int col_count = func_column_count(stmt); - for (int i = 0; i < col_count; i++) { + int col_count = api.column_count(stmt); + for (int i = 0; i < col_count; ++i) { DbField_t field; - field.type = func_column_type(stmt, i); - field.column = func_column_name(stmt, i); - - int length = func_column_bytes(stmt, i); - const void *blob = func_column_blob(stmt, i); - if (length > 0 && field.type != SQLITE_NULL) { - field.content.resize(length); - std::memcpy(field.content.data(), blob, length); + field.type = api.column_type(stmt, i); + const char *colName = api.column_name(stmt, i); + field.column = (colName != nullptr) ? colName : ""; + + if (field.type != SQLITE_NULL) { + int length = api.column_bytes(stmt, i); + if (length > 0) { + const uint8_t *data = (field.type == SQLITE_BLOB) + ? reinterpret_cast(api.column_blob(stmt, i)) + : reinterpret_cast(api.column_text(stmt, i)); + if (data != nullptr) { + field.content.assign(data, data + length); + } + } } + row.push_back(field); } rows.push_back(row); } - func_finalize(stmt); + api.finalize(stmt); return rows; } int get_local_id_and_dbidx(uint64_t id, uint64_t *local_id, uint32_t *db_idx) { - if (!local_id || !db_idx) { - LOG_ERROR("Invalid pointer arguments!"); + if ((local_id == nullptr) || (db_idx == nullptr)) { return -1; } - QWORD msg_mgr_addr = util::get_qword(Spy::WeChatDll.load() + OsDb::MSG_I); - int db_index = static_cast(util::get_qword(msg_mgr_addr + 0x68)); // 总不能 int 还不够吧? - QWORD p_start = util::get_qword(msg_mgr_addr + 0x50); + for (const auto &[idx, dbname] : find_dbs_by_prefix("MSG")) { + std::string sql = "SELECT localId FROM MSG WHERE MsgSvrID=" + std::to_string(id) + " LIMIT 1;"; + DbRows_t rows = exec_db_query(dbname, sql); + if (rows.empty() || rows.front().empty()) { + continue; + } - *db_idx = 0; - for (int i = db_index - 1; i >= 0; i--) { // 从后往前遍历 - QWORD db_addr = util::get_qword(p_start + i * 0x08); - if (!db_addr) { + std::string value = field_to_string(rows.front().front()); + if (value.empty()) { continue; } - std::string dbname = util::w2s(util::get_pp_wstring(db_addr)); - db_map[dbname] = util::get_qword(db_addr + 0x78); + *local_id = _strtoui64(value.c_str(), nullptr, 10); + *db_idx = static_cast(idx); + return 0; + } - std::string sql = "SELECT localId FROM MSG WHERE MsgSvrID=" + std::to_string(id) + ";"; - DbRows_t rows = exec_db_query(dbname, sql); + return -1; +} +std::vector get_audio_data(uint64_t msg_id) +{ + for (const auto &[idx, dbname] : find_dbs_by_prefix("MediaMSG")) { + (void)idx; + std::string sql = "SELECT Buf FROM Media WHERE Reserved0=" + std::to_string(msg_id) + " LIMIT 1;"; + DbRows_t rows = exec_db_query(dbname, sql); if (rows.empty() || rows.front().empty()) { continue; } const DbField_t &field = rows.front().front(); - if (field.column != "localId" || field.type != SQLITE_INTEGER) { - continue; - } - - std::string id_str(field.content.begin(), field.content.end()); - try { - *local_id = std::stoull(id_str); - } catch (const std::exception &e) { - LOG_ERROR("Failed to parse localId: {}", e.what()); + if ((field.column != "Buf") || field.content.size() <= 1) { continue; } - *db_idx = static_cast(util::get_qword(util::get_qword(db_addr + 0x28) + 0x1E8) >> 32); - return 0; + return std::vector(field.content.begin() + 1, field.content.end()); } - return -1; + return {}; } -std::vector get_audio_data(uint64_t id) +std::string get_db_key() { - QWORD msg_mgr_addr = util::get_qword(Spy::WeChatDll.load() + OsDb::MSG_I); - int db_index = static_cast(util::get_qword(msg_mgr_addr + 0x68)); - - std::string sql = "SELECT Buf FROM Media WHERE Reserved0=" + std::to_string(id) + ";"; - for (int i = db_index - 1; i >= 0; i--) { - std::string dbname = "MediaMSG" + std::to_string(i) + ".db"; - DbRows_t rows = exec_db_query(dbname, sql); + if (db_map.empty()) { + refresh_db_map(); + } - if (rows.empty() || rows.front().empty()) { + // 同一账号所有库共用同一 32 字节生密钥,取任一已开句柄沿 SQLCipher codec 链读出即可。 + // 每段中间指针都须显式判空后再加偏移,否则 get_dword(小地址) 会崩。 + for (const auto &[name, handle] : db_map) { + (void)name; + if (handle == 0) { continue; } - const DbField_t &field = rows.front().front(); - if (field.column != "Buf" || field.content.empty()) { + uint32_t btree = util::get_dword(handle + OsDb::DB_BTREE); + if (btree == 0) { + continue; + } + uint32_t shared = util::get_dword(btree + OsDb::BTREE_SHARED); + if (shared == 0) { + continue; + } + uint32_t pager = util::get_dword(shared + OsDb::SHARED_PAGER); + if (pager == 0) { + continue; + } + uint32_t codec = util::get_dword(pager + OsDb::PAGER_CODEC); + if (codec == 0) { + continue; + } + uint32_t ctx = util::get_dword(codec + OsDb::CODEC_READCTX); + if (ctx == 0) { continue; } - // 首字节为 0x02,估计是混淆用的?去掉。 - if (field.content.front() == 0x02) { - return std::vector(field.content.begin() + 1, field.content.end()); + uint32_t key_ptr = util::get_dword(ctx + OsDb::CIPHER_PASS); + uint32_t key_len = util::get_dword(ctx + OsDb::CIPHER_PASSSZ); + if ((key_ptr == 0) || (key_len == 0) || (key_len > 64)) { + continue; } - return field.content; + // 进程内直接读生密钥字节,转小写十六进制(外部可用 PRAGMA key = "x'...'" 离线打开)。 + const uint8_t *key = reinterpret_cast(key_ptr); + static const char hex[] = "0123456789abcdef"; + std::string result; + result.reserve(key_len * 2); + for (uint32_t i = 0; i < key_len; ++i) { + result.push_back(hex[key[i] >> 4]); + result.push_back(hex[key[i] & 0x0F]); + } + return result; } - return {}; + LOG_ERROR("Failed to read database key (not logged in or no db opened)."); + return ""; } bool rpc_get_db_names(uint8_t *out, size_t *len) { - DbNames_t names = get_db_names(); - return fill_response(out, len, [&](Response &rsp) { + // 数据须活到 fill_response 内的 pb_encode(assign 返回后才编码),故置于此作用域。 + DbNames_t dbnames = get_db_names(); + return fill_response(out, len, [&dbnames](Response &rsp) { rsp.msg.dbs.names.funcs.encode = encode_dbnames; - rsp.msg.dbs.names.arg = &names; + rsp.msg.dbs.names.arg = &dbnames; }); } bool rpc_get_db_tables(const std::string &db, uint8_t *out, size_t *len) { DbTables_t tables = get_db_tables(db); - return fill_response(out, len, [&](Response &rsp) { + return fill_response(out, len, [&tables](Response &rsp) { rsp.msg.tables.tables.funcs.encode = encode_tables; rsp.msg.tables.tables.arg = &tables; }); } -bool rpc_exec_db_query(const DbQuery query, uint8_t *out, size_t *len) +bool rpc_exec_db_query(const DbQuery &query, uint8_t *out, size_t *len) { - const std::string db(query.db); - const std::string sql(query.sql); - DbRows_t rows = exec_db_query(db, sql); - return fill_response(out, len, [&](Response &rsp) { + DbRows_t rows; + if ((query.db == nullptr) || (query.sql == nullptr)) { + LOG_ERROR("Empty db or sql."); + } else { + rows = exec_db_query(query.db, query.sql); + } + return fill_response(out, len, [&rows](Response &rsp) { rsp.msg.rows.rows.funcs.encode = encode_rows; rsp.msg.rows.rows.arg = &rows; }); } +bool rpc_get_db_key(uint8_t *out, size_t *len) +{ + // key 须活到 fill_response 内的 pb_encode,故经带数据的重载按引用传入。 + std::string key = get_db_key(); + return fill_response(out, len, key, [](Response &rsp, std::string &key) { + rsp.msg.str = (char *)key.c_str(); + }); +} + } // namespace db diff --git a/WeChatFerry/spy/database_executor.h b/WeChatFerry/spy/database_executor.h index 0d50373f..0558b1f7 100644 --- a/WeChatFerry/spy/database_executor.h +++ b/WeChatFerry/spy/database_executor.h @@ -1,34 +1,28 @@ -#pragma once +#pragma once -#include +#include +#include #include #include #include "wcf.pb.h" - #include "pb_types.h" namespace db { -// 获取数据库名称列表 DbNames_t get_db_names(); - -// 获取指定数据库的表列表 DbTables_t get_db_tables(const std::string &db); - -// 执行 SQL 查询 DbRows_t exec_db_query(const std::string &db, const std::string &sql); - -// 获取本地消息 ID 和数据库索引 int get_local_id_and_dbidx(uint64_t id, uint64_t *local_id, uint32_t *db_idx); - -// 获取音频数据 std::vector get_audio_data(uint64_t msg_id); -// RPC 方法 +// 数据库生密钥:32 字节,返回小写十六进制串,供外部离线解密 .db +std::string get_db_key(); + bool rpc_get_db_names(uint8_t *out, size_t *len); bool rpc_get_db_tables(const std::string &db, uint8_t *out, size_t *len); -bool rpc_exec_db_query(const DbQuery query, uint8_t *out, size_t *len); +bool rpc_exec_db_query(const DbQuery &query, uint8_t *out, size_t *len); +bool rpc_get_db_key(uint8_t *out, size_t *len); } // namespace db diff --git a/WeChatFerry/spy/dllmain.cpp b/WeChatFerry/spy/dllmain.cpp index 66475e48..580ec4ca 100644 --- a/WeChatFerry/spy/dllmain.cpp +++ b/WeChatFerry/spy/dllmain.cpp @@ -1,5 +1,9 @@ // dllmain.cpp : 定义 DLL 应用程序的入口点。 #include "framework.h" +#include +#include + +#include "spy.h" BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { diff --git a/WeChatFerry/spy/message_handler.cpp b/WeChatFerry/spy/message_handler.cpp deleted file mode 100644 index 851584f7..00000000 --- a/WeChatFerry/spy/message_handler.cpp +++ /dev/null @@ -1,282 +0,0 @@ -#include "message_handler.h" - -#include -#include -#include -#include - -#include "account_manager.h" -#include "log.hpp" -#include "offsets.h" -#include "pb_util.h" -#include "rpc_helper.h" -#include "spy.h" -#include "util.h" - -namespace message -{ - -namespace fs = std::filesystem; - -namespace OsLog = Offsets::Message::Log; -namespace OsRecv = Offsets::Message::Receive; - -QWORD Handler::DispatchMsg(QWORD arg1, QWORD arg2) -{ - auto &handler = getInstance(); - WxMsg_t wxMsg = {}; - try { - wxMsg.id = util::get_qword(arg2 + OsRecv::ID); - wxMsg.type = util::get_dword(arg2 + OsRecv::TYPE); - wxMsg.is_self = util::get_dword(arg2 + OsRecv::SELF); - wxMsg.ts = util::get_dword(arg2 + OsRecv::TIMESTAMP); - wxMsg.content = util::get_str_by_wstr_addr(arg2 + OsRecv::CONTENT); - wxMsg.sign = util::get_str_by_wstr_addr(arg2 + OsRecv::SIGN); - wxMsg.xml = util::get_str_by_wstr_addr(arg2 + OsRecv::XML); - wxMsg.roomid = util::get_str_by_wstr_addr(arg2 + OsRecv::ROOMID); - - if (wxMsg.roomid.find("@chatroom") != std::string::npos) { // 群 ID 的格式为 xxxxxxxxxxx@chatroom - wxMsg.is_group = true; - wxMsg.sender = wxMsg.is_self ? account::get_self_wxid() : util::get_str_by_wstr_addr(arg2 + OsRecv::WXID); - } else { - wxMsg.is_group = false; - wxMsg.sender = wxMsg.is_self ? account::get_self_wxid() : wxMsg.roomid; - } - - fs::path thumb = util::get_str_by_wstr_addr(arg2 + OsRecv::THUMB); - if (!thumb.empty()) { - wxMsg.thumb = (account::get_home_path() / thumb).generic_string(); - } - - fs::path extra = util::get_str_by_wstr_addr(arg2 + OsRecv::EXTRA); - if (!extra.empty()) { - wxMsg.extra = (account::get_home_path() / extra).generic_string(); - } - LOG_DEBUG("{}", wxMsg.content); - } catch (const std::exception &e) { - LOG_ERROR(util::gb2312_to_utf8(e.what())); - } - - { - std::unique_lock lock(handler.mutex_); - handler.msgQueue_.push(wxMsg); // 推送到队列 - } - - handler.cv_.notify_all(); - return handler.realRecvMsg(arg1, arg2); -} - -QWORD Handler::PrintWxLog(QWORD a1, QWORD a2, QWORD a3, QWORD a4, QWORD a5, QWORD a6, QWORD a7, QWORD a8, QWORD a9, - QWORD a10, QWORD a11, QWORD a12) -{ - auto &handler = getInstance(); - - QWORD p = handler.realWxLog(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12); - if (p == 0 || p == 1) { - return p; - } - - LOG_INFO("【WX】\n{}", util::gb2312_to_utf8((char *)p)); - return p; -} - -QWORD Handler::DispatchPyq(QWORD arg1, QWORD arg2, QWORD arg3) -{ - auto &handler = getInstance(); - QWORD startAddr = *(QWORD *)(arg2 + OsRecv::PYQ_START); - QWORD endAddr = *(QWORD *)(arg2 + OsRecv::PYQ_END); - - if (startAddr == 0) { - return 0; - } - - while (startAddr < endAddr) { - WxMsg_t wxMsg; - - wxMsg.type = 0x00; - wxMsg.is_self = false; - wxMsg.is_group = false; - wxMsg.id = util::get_qword(startAddr); - wxMsg.ts = util::get_dword(startAddr + OsRecv::PYQ_TS); - wxMsg.xml = util::get_str_by_wstr_addr(startAddr + OsRecv::PYQ_XML); - wxMsg.sender = util::get_str_by_wstr_addr(startAddr + OsRecv::PYQ_SENDER); - wxMsg.content = util::get_str_by_wstr_addr(startAddr + OsRecv::PYQ_CONTENT); - - { - std::unique_lock lock(handler.mutex_); - handler.msgQueue_.push(wxMsg); - } - - handler.cv_.notify_all(); - startAddr += 0x1618; - } - - return handler.realRecvPyq(arg1, arg2, arg3); -} - -Handler &Handler::getInstance() -{ - static Handler instance; - return instance; -} - -Handler::Handler() -{ - isLogging = false; - isListeningMsg = false; - isListeningPyq = false; -} - -Handler::~Handler() -{ - DisableLog(); - UnListenMsg(); - UnListenPyq(); -} - -MsgTypes_t Handler::GetMsgTypes() -{ - return { { 0x00, "朋友圈消息" }, - { 0x01, "文字" }, - { 0x03, "图片" }, - { 0x22, "语音" }, - { 0x25, "好友确认" }, - { 0x28, "POSSIBLEFRIEND_MSG" }, - { 0x2A, "名片" }, - { 0x2B, "视频" }, - { 0x2F, "石头剪刀布 | 表情图片" }, - { 0x30, "位置" }, - { 0x31, "共享实时位置、文件、转账、链接" }, - { 0x32, "VOIPMSG" }, - { 0x33, "微信初始化" }, - { 0x34, "VOIPNOTIFY" }, - { 0x35, "VOIPINVITE" }, - { 0x3E, "小视频" }, - { 0x42, "微信红包" }, - { 0x270F, "SYSNOTICE" }, - { 0x2710, "红包、系统消息" }, - { 0x2712, "撤回消息" }, - { 0x100031, "搜狗表情" }, - { 0x1000031, "链接" }, - { 0x1A000031, "微信红包" }, - { 0x20010031, "红包封面" }, - { 0x2D000031, "视频号视频" }, - { 0x2E000031, "视频号名片" }, - { 0x31000031, "引用消息" }, - { 0x37000031, "拍一拍" }, - { 0x3A000031, "视频号直播" }, - { 0x3A100031, "商品链接" }, - { 0x3A200031, "视频号直播" }, - { 0x3E000031, "音乐链接" }, - { 0x41000031, "文件" } }; -} - -std::optional Handler::popMessage() -{ - std::lock_guard lock(mutex_); - if (msgQueue_.empty()) { - return std::nullopt; - } - WxMsg_t msg = std::move(msgQueue_.front()); - msgQueue_.pop(); - return msg; -} - -int Handler::EnableLog() -{ - if (isLogging) return 1; - - pLogLevel = reinterpret_cast(Spy::WeChatDll.load() + OsLog::LEVEL); - funcWxLog = Spy::getFunction(OsLog::CALL); - - if (InitializeHook() != MH_OK) return -1; - if (MH_CreateHook(reinterpret_cast(funcWxLog), reinterpret_cast(&PrintWxLog), reinterpret_cast(&realWxLog)) != MH_OK) return -2; - if (MH_EnableHook(reinterpret_cast(funcWxLog)) != MH_OK) return -3; - - *pLogLevel = 0; - isLogging = true; - return 0; -} - -int Handler::DisableLog() -{ - if (!isLogging) return 1; - if (MH_DisableHook(reinterpret_cast(funcWxLog)) != MH_OK) return -1; - if (MH_RemoveHook(reinterpret_cast(funcWxLog)) != MH_OK) return -2; - if (UninitializeHook() != MH_OK) return -3; - *pLogLevel = 6; - isLogging = false; - return 0; -} - -int Handler::ListenMsg() -{ - if (isListeningMsg) return 1; - - funcRecvMsg = Spy::getFunction(OsRecv::CALL); - if (InitializeHook() != MH_OK) return -1; - if (MH_CreateHook(reinterpret_cast(funcRecvMsg), reinterpret_cast(&DispatchMsg), reinterpret_cast(&realRecvMsg)) != MH_OK) return -2; - if (MH_EnableHook(reinterpret_cast(funcRecvMsg)) != MH_OK) return -3; - - isListeningMsg = true; - return 0; -} - -int Handler::UnListenMsg() -{ - if (!isListeningMsg) return 1; - if (MH_DisableHook(reinterpret_cast(funcRecvMsg)) != MH_OK) return -1; - if (MH_RemoveHook(reinterpret_cast(funcRecvMsg)) != MH_OK) return -2; - if (UninitializeHook() != MH_OK) return -3; - isListeningMsg = false; - return 0; -} - -int Handler::ListenPyq() -{ - if (isListeningPyq) return 1; - - funcRecvPyq = Spy::getFunction(OsRecv::PYQ_CALL); - if (InitializeHook() != MH_OK) return -1; - if (MH_CreateHook(reinterpret_cast(funcRecvPyq), reinterpret_cast(&DispatchPyq), reinterpret_cast(&realRecvPyq)) != MH_OK) return -1; - if (MH_EnableHook(reinterpret_cast(funcRecvPyq)) != MH_OK) return -1; - - isListeningPyq = true; - return 0; -} - -int Handler::UnListenPyq() -{ - if (!isListeningPyq) return 1; - if (MH_DisableHook(reinterpret_cast(funcRecvPyq)) != MH_OK) return -1; - if (MH_RemoveHook(reinterpret_cast(funcRecvPyq)) != MH_OK) return -2; - if (UninitializeHook() != MH_OK) return -3; - isListeningPyq = false; - return 0; -} - -MH_STATUS Handler::InitializeHook() -{ - if (isMH_Initialized) return MH_OK; - MH_STATUS status = MH_Initialize(); - if (status == MH_OK) isMH_Initialized = true; - return status; -} - -MH_STATUS Handler::UninitializeHook() -{ - if (!isMH_Initialized || isLogging || isListeningMsg || isListeningPyq) return MH_OK; - MH_STATUS status = MH_Uninitialize(); - if (status == MH_OK) isMH_Initialized = false; - return status; -} - -bool Handler::rpc_get_msg_types(uint8_t *out, size_t *len) -{ - MsgTypes_t types = GetMsgTypes(); - return fill_response(out, len, [&](Response &rsp) { - rsp.msg.types.types.funcs.encode = encode_types; - rsp.msg.types.types.arg = &types; - }); -} -} diff --git a/WeChatFerry/spy/message_handler.h b/WeChatFerry/spy/message_handler.h deleted file mode 100644 index 53e3d1a2..00000000 --- a/WeChatFerry/spy/message_handler.h +++ /dev/null @@ -1,74 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include "MinHook.h" - -#include "pb_types.h" -#include "spy_types.h" - -namespace message -{ - -class Handler -{ -public: - static Handler &getInstance(); - - // 0: 成功, -1: 失败, 1: 已经开启 - int EnableLog(); - int DisableLog(); - int ListenPyq(); - int UnListenPyq(); - int ListenMsg(); - int UnListenMsg(); - - MsgTypes_t GetMsgTypes(); - - bool isLoggingEnabled() const { return isLogging.load(); } - bool isMessageListening() const { return isListeningMsg.load(); } - bool isPyqListening() const { return isListeningPyq.load(); } - - std::optional popMessage(); - std::condition_variable &getConditionVariable() { return cv_; }; - std::mutex &getMutex() { return mutex_; }; - - bool rpc_get_msg_types(uint8_t *out, size_t *len); - -private: - Handler(); - ~Handler(); - - mutable std::mutex mutex_; - std::condition_variable cv_; - std::queue msgQueue_; - - std::atomic isLogging { false }; - std::atomic isListeningMsg { false }; - std::atomic isListeningPyq { false }; - - using funcRecvMsg_t = QWORD (*)(QWORD, QWORD); - using funcWxLog_t = QWORD (*)(QWORD, QWORD, QWORD, QWORD, QWORD, QWORD, QWORD, QWORD, QWORD, QWORD, QWORD, QWORD); - using funcRecvPyq_t = QWORD (*)(QWORD, QWORD, QWORD); - - uint32_t *pLogLevel; - funcWxLog_t funcWxLog, realWxLog; - funcRecvMsg_t funcRecvMsg, realRecvMsg; - funcRecvPyq_t funcRecvPyq, realRecvPyq; - - bool isMH_Initialized { false }; - - MH_STATUS InitializeHook(); - MH_STATUS UninitializeHook(); - - static QWORD DispatchMsg(QWORD arg1, QWORD arg2); - static QWORD PrintWxLog(QWORD a1, QWORD a2, QWORD a3, QWORD a4, QWORD a5, QWORD a6, QWORD a7, QWORD a8, QWORD a9, - QWORD a10, QWORD a11, QWORD a12); - static QWORD DispatchPyq(QWORD arg1, QWORD arg2, QWORD arg3); -}; - -} // namespace message diff --git a/WeChatFerry/spy/message_receiver.cpp b/WeChatFerry/spy/message_receiver.cpp new file mode 100644 index 00000000..ac9b880f --- /dev/null +++ b/WeChatFerry/spy/message_receiver.cpp @@ -0,0 +1,351 @@ +#pragma execution_character_set("utf-8") + +#include "message_receiver.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "account_manager.h" +#include "log.hpp" +#include "offsets.h" +#include "pb_util.h" +#include "rpc_helper.h" +#include "spy.h" +#include "util.h" + +extern bool gIsListening, gIsListeningPyq; +extern std::mutex gMutex; +extern std::condition_variable gCV; +extern std::queue gMsgQueue; + +namespace +{ + +using ReceiveMessageFn = uintptr_t(__thiscall *)(void *msg); +// flag 必须原样透传:OnSnsTimeLineSceneFinish 有 2 个调用者,丢参会破坏另一条(非接收)路径 +using ReceivePyqFn = uintptr_t(__thiscall *)(void *self, uint32_t data, uint32_t flag); + +struct DetourHook { + uint32_t target = 0; + void *trampoline = nullptr; + unsigned char saved[5] = { 0 }; + bool installed = false; +}; + +ReceiveMessageFn gRealReceiveMessage = nullptr; +ReceivePyqFn gRealReceivePyq = nullptr; +DetourHook gMessageHook; +DetourHook gPyqHook; + +MsgTypes_t build_msg_types() +{ + return { { 0x00, "moments" }, + { 0x01, "text" }, + { 0x03, "image" }, + { 0x22, "voice" }, + { 0x25, "friend_confirm" }, + { 0x28, "possible_friend" }, + { 0x2A, "card" }, + { 0x2B, "video" }, + { 0x2F, "emoji_or_game" }, + { 0x30, "location" }, + { 0x31, "app_msg" }, + { 0x32, "voip_msg" }, + { 0x33, "init" }, + { 0x34, "voip_notify" }, + { 0x35, "voip_invite" }, + { 0x3E, "short_video" }, + { 0x42, "red_packet" }, + { 0x270F, "sys_notice" }, + { 0x2710, "system_or_red_packet" }, + { 0x2712, "recalled" }, + { 0x100031, "sogou_emoji" }, + { 0x1000031, "link" }, + { 0x1A000031, "wechat_red_packet" }, + { 0x20010031, "packet_cover" }, + { 0x2D000031, "channels_video" }, + { 0x2E000031, "channels_card" }, + { 0x31000031, "quote" }, + { 0x37000031, "pat" }, + { 0x3A000031, "channels_live" }, + { 0x3A100031, "product_link" }, + { 0x3A200031, "channels_live_2" }, + { 0x3E000031, "music_link" }, + { 0x41000031, "file" } }; +} + +bool write_jump(uint32_t src, const void *dst) +{ + DWORD old_protect = 0; + if (!VirtualProtect(reinterpret_cast(src), 5, PAGE_EXECUTE_READWRITE, &old_protect)) { + return false; + } + + unsigned char jump[5] = { 0xE9, 0, 0, 0, 0 }; + *reinterpret_cast(&jump[1]) = reinterpret_cast(dst) - src - 5; + std::memcpy(reinterpret_cast(src), jump, sizeof(jump)); + FlushInstructionCache(GetCurrentProcess(), reinterpret_cast(src), sizeof(jump)); + + DWORD restored = 0; + VirtualProtect(reinterpret_cast(src), 5, old_protect, &restored); + return true; +} + +bool install_hook(DetourHook &hook, uint32_t target, const void *replacement, void **original) +{ + if (hook.installed) { + *original = hook.trampoline; + return true; + } + + hook.target = target; + std::memcpy(hook.saved, reinterpret_cast(target), sizeof(hook.saved)); + + auto trampoline = static_cast( + VirtualAlloc(nullptr, 16, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)); + if (trampoline == nullptr) { + LOG_ERROR("VirtualAlloc failed for trampoline."); + return false; + } + + std::memcpy(trampoline, hook.saved, sizeof(hook.saved)); + trampoline[5] = 0xE9; + *reinterpret_cast(trampoline + 6) = (target + 5) - (reinterpret_cast(trampoline) + 10); + + if (!write_jump(target, replacement)) { + VirtualFree(trampoline, 0, MEM_RELEASE); + LOG_ERROR("Failed to patch hook target: 0x{:08X}", target); + return false; + } + + hook.trampoline = trampoline; + hook.installed = true; + *original = trampoline; + return true; +} + +void remove_hook(DetourHook &hook) +{ + if (!hook.installed) { + return; + } + + DWORD old_protect = 0; + if (VirtualProtect(reinterpret_cast(hook.target), 5, PAGE_EXECUTE_READWRITE, &old_protect)) { + std::memcpy(reinterpret_cast(hook.target), hook.saved, sizeof(hook.saved)); + FlushInstructionCache(GetCurrentProcess(), reinterpret_cast(hook.target), sizeof(hook.saved)); + DWORD restored = 0; + VirtualProtect(reinterpret_cast(hook.target), 5, old_protect, &restored); + } else { + LOG_ERROR("Failed to restore hook target: 0x{:08X}", hook.target); + } + + if (hook.trampoline != nullptr) { + VirtualFree(hook.trampoline, 0, MEM_RELEASE); + } + + hook.target = 0; + hook.trampoline = nullptr; + hook.installed = false; + std::memset(hook.saved, 0, sizeof(hook.saved)); +} + +void dispatch_msg(uint32_t reg) +{ + WxMsg_t wx_msg = {}; + try { + wx_msg.id = util::get_qword(reg + Offsets::Message::Receive::MSG_ID); + wx_msg.type = util::get_dword(reg + Offsets::Message::Receive::TYPE); + wx_msg.is_self = util::get_dword(reg + Offsets::Message::Receive::IS_SELF); + wx_msg.ts = util::get_dword(reg + Offsets::Message::Receive::TS); + wx_msg.content = util::get_string_by_wstr_addr(reg + Offsets::Message::Receive::CONTENT); + wx_msg.sign = util::get_string_by_str_addr(reg + Offsets::Message::Receive::SIGN); + wx_msg.xml = util::get_string_by_str_addr(reg + Offsets::Message::Receive::MSG_XML); + + std::string roomid = util::get_string_by_wstr_addr(reg + Offsets::Message::Receive::ROOM_ID); + if (roomid.find("@chatroom") != std::string::npos) { + wx_msg.is_group = true; + wx_msg.roomid = roomid; + wx_msg.sender = wx_msg.is_self ? account::get_self_wxid() + : util::get_string_by_str_addr(reg + Offsets::Message::Receive::WXID); + } else { + wx_msg.is_group = false; + wx_msg.sender = wx_msg.is_self ? account::get_self_wxid() : roomid; + } + + wx_msg.thumb = util::get_string_by_str_addr(reg + Offsets::Message::Receive::THUMB); + if (!wx_msg.thumb.empty()) { + wx_msg.thumb = account::get_home_path() + wx_msg.thumb; + std::replace(wx_msg.thumb.begin(), wx_msg.thumb.end(), '\\', '/'); + } + + wx_msg.extra = util::get_string_by_str_addr(reg + Offsets::Message::Receive::EXTRA); + if (!wx_msg.extra.empty()) { + wx_msg.extra = account::get_home_path() + wx_msg.extra; + std::replace(wx_msg.extra.begin(), wx_msg.extra.end(), '\\', '/'); + } + } catch (const std::exception &e) { + LOG_ERROR(util::gb2312_to_utf8(e.what())); + } catch (...) { + LOG_ERROR("Unknown exception."); + } + + { + std::unique_lock lock(gMutex); + gMsgQueue.push(wx_msg); + } + + gCV.notify_all(); +} + +uintptr_t __fastcall receive_message_hook(void *msg, void *) +{ + // ~ChatMsg 有大量调用点,按“返回地址==HOOK+5”过滤,仅当本次由收消息的定点 call 触发时才 dispatch + if (reinterpret_cast(_ReturnAddress()) + == g_WeChatWinDllAddr + Offsets::Message::Receive::HOOK + 5) { + dispatch_msg(reinterpret_cast(msg)); + } + return gRealReceiveMessage ? gRealReceiveMessage(msg) : 0; +} + +void listen_message() +{ + if (gIsListening || (g_WeChatWinDllAddr == 0)) { + return; + } + + uint32_t target = g_WeChatWinDllAddr + Offsets::Message::Receive::CALL; + if (install_hook(gMessageHook, target, reinterpret_cast(receive_message_hook), + reinterpret_cast(&gRealReceiveMessage))) { + gIsListening = true; + } +} + +void unlisten_message() +{ + if (!gIsListening) { + return; + } + + remove_hook(gMessageHook); + gRealReceiveMessage = nullptr; + gIsListening = false; +} + +void dispatch_pyq(uint32_t reg) +{ + uint32_t start_addr = *reinterpret_cast(reg + Offsets::Moments::START); + uint32_t end_addr = *reinterpret_cast(reg + Offsets::Moments::END); + + if (start_addr == 0) { + return; + } + + while (start_addr < end_addr) { + WxMsg_t wx_msg = {}; + + wx_msg.type = 0x00; + wx_msg.is_self = false; + wx_msg.is_group = false; + wx_msg.id = util::get_qword(start_addr); + wx_msg.ts = util::get_dword(start_addr + Offsets::Moments::TS); + wx_msg.xml = util::get_string_by_wstr_addr(start_addr + Offsets::Moments::XML); + wx_msg.sender = util::get_string_by_wstr_addr(start_addr + Offsets::Moments::WXID); + wx_msg.content = util::get_string_by_wstr_addr(start_addr + Offsets::Moments::CONTENT); + + { + std::unique_lock lock(gMutex); + gMsgQueue.push(wx_msg); + } + + gCV.notify_all(); + start_addr += Offsets::Moments::STEP; + } +} + +uintptr_t __fastcall receive_pyq_hook(void *self, void *, uint32_t data, uint32_t flag) +{ + // OnSnsTimeLineSceneFinish 有 2 个调用者,仅当由 OnProcessTimelineResp 定点触发(返回地址==HOOK+5) + // 才是"刚收到一批朋友圈",此时 data 即容器指针 + if (reinterpret_cast(_ReturnAddress()) + == g_WeChatWinDllAddr + Offsets::Moments::HOOK + 5) { + dispatch_pyq(data); + } + return gRealReceivePyq ? gRealReceivePyq(self, data, flag) : 0; +} + +void listen_pyq() +{ + if (gIsListeningPyq || (g_WeChatWinDllAddr == 0)) { + return; + } + + uint32_t target = g_WeChatWinDllAddr + Offsets::Moments::CALL; + if (install_hook(gPyqHook, target, reinterpret_cast(receive_pyq_hook), + reinterpret_cast(&gRealReceivePyq))) { + gIsListeningPyq = true; + } +} + +void unlisten_pyq() +{ + if (!gIsListeningPyq) { + return; + } + + remove_hook(gPyqHook); + gRealReceivePyq = nullptr; + gIsListeningPyq = false; +} + +} // namespace + +namespace message +{ + +MsgTypes_t get_msg_types() +{ + return build_msg_types(); +} + +bool rpc_get_msg_types(uint8_t *out, size_t *len) +{ + return fill_response(out, len, [](Response &rsp) { + MsgTypes_t types = get_msg_types(); + rsp.msg.types.types.funcs.encode = encode_types; + rsp.msg.types.types.arg = &types; + }); +} + +bool rpc_enable_recv_txt(bool pyq, uint8_t *out, size_t *len) +{ + return fill_response(out, len, [pyq](Response &rsp) { + if (pyq) { + listen_pyq(); + } else { + listen_message(); + } + rsp.msg.status = 0; + }); +} + +bool rpc_disable_recv_txt(uint8_t *out, size_t *len) +{ + return fill_response(out, len, [](Response &rsp) { + stop_receiving(); + rsp.msg.status = 0; + }); +} + +void stop_receiving() +{ + unlisten_message(); + unlisten_pyq(); +} + +} // namespace message diff --git a/WeChatFerry/spy/message_receiver.h b/WeChatFerry/spy/message_receiver.h new file mode 100644 index 00000000..1d7f7675 --- /dev/null +++ b/WeChatFerry/spy/message_receiver.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +#include "pb_types.h" + +namespace message +{ + +MsgTypes_t get_msg_types(); +bool rpc_get_msg_types(uint8_t *out, size_t *len); +bool rpc_enable_recv_txt(bool pyq, uint8_t *out, size_t *len); +bool rpc_disable_recv_txt(uint8_t *out, size_t *len); +void stop_receiving(); + +} // namespace message diff --git a/WeChatFerry/spy/message_sender.cpp b/WeChatFerry/spy/message_sender.cpp index 544c7471..b510f467 100644 --- a/WeChatFerry/spy/message_sender.cpp +++ b/WeChatFerry/spy/message_sender.cpp @@ -1,10 +1,12 @@ #include "message_sender.h" +#include #include #include #include "account_manager.h" #include "database_executor.h" +#include "framework.h" #include "log.hpp" #include "offsets.h" #include "rpc_helper.h" @@ -12,351 +14,454 @@ #include "spy_types.h" #include "util.h" -namespace message -{ - -namespace OsSend = Offsets::Message::Send; +namespace fs = std::filesystem; -Sender &Sender::get_instance() +namespace { - static Sender instance; - return instance; -} -Sender::Sender() +struct WxStringValue { + const wchar_t *wptr; + uint32_t size; + uint32_t capacity; + const char *ptr; + uint32_t clen; +}; + +using PatManagerGetterFn = void *(*)(); +using SendPatFn = uint8_t(__fastcall *)(WxString *roomid, WxString *wxid, void *manager, + uint32_t reserved1, uint32_t reserved2); +using BufferInitFn = void(__thiscall *)(void *buffer); +using BufferCleanupFn = void(__thiscall *)(void *buffer); +using SendMgrGetterFn = void *(*)(); +using InitGlobalFn = void (*)(); +using SendTextFn = int(__fastcall *)(void *buffer, const WxString *wxid, const WxString *msg, + const std::vector *at_wxids, uint32_t flag1, + uint32_t zero1, uint32_t zero2, uint32_t zero3); +using SendImageFn = void *(__thiscall *)(void *manager, void *buffer, const WxString *receiver, + const WxString *path, void *options); +using SendFileFn = void *(__thiscall *)(void *manager, void *buffer, + WxStringValue receiver, WxStringValue path, int flag1, + WxStringValue empty1, int flag2, + WxStringValue empty2, int zero1, int zero2, + WxStringValue empty3, int flag3, int flag4); +using RichTextManagerGetterFn = void *(*)(); +using WxStringAssignFn = void *(__thiscall *)(void *dest, const wchar_t *src, int len); +using SendRichTextFn = int(__thiscall *)(void *manager, WxStringValue receiver, void *buffer); +using ChatMsgCtorFn = void *(__thiscall *)(void *buffer); +using SendAppMsgXmlFn = char(__fastcall *)(void *chatmsg, const WxString *from, const WxString *receiver, + const WxString *content, const WxString *empty1, + const WxString *path, int type, int flag, + const WxString *empty2, int zero); +using ForwardMsgFn = uint8_t(__fastcall *)(int scene, int msg_ptr, WxStringValue receiver, + uint32_t local_id, uint32_t db_idx); +using SendEmotionFn = int(__thiscall *)(void *manager, WxStringValue path, WxStringValue null2, + WxStringValue wxid, int type, WxStringValue null1, + int zero, void *buffer); +using EmoMgrGetterFn = void *(*)(); + +struct RichTextData { + std::string name; + std::string account; + std::string title; + std::string digest; + std::string url; + std::string thumburl; + std::string receiver; +}; + +WxStringValue to_value(const WxString &value) { - func_new_chat_msg = Spy::getFunction(OsSend::INSTANCE); - func_free_chat_msg = Spy::getFunction(OsSend::FREE); - func_send_msg_mgr = Spy::getFunction(OsSend::MGR); - func_send_text = Spy::getFunction(OsSend::TEXT); - func_send_image = Spy::getFunction(OsSend::IMAGE); - func_get_app_mgr = Spy::getFunction(OsSend::APP_MGR); - func_send_file = Spy::getFunction(OsSend::FILE); - func_new_mmreader = Spy::getFunction(OsSend::NEW_MM_READER); - func_free_mmreader = Spy::getFunction(OsSend::FREE_MM_READER); - func_send_rich_text = Spy::getFunction(OsSend::RICH_TEXT); - func_send_pat = Spy::getFunction(OsSend::PAT); - func_forward = Spy::getFunction(OsSend::FORWARD); - func_get_emotion_mgr = Spy::getFunction(OsSend::EMOTION_MGR); - func_send_emotion = Spy::getFunction(OsSend::EMOTION); - func_send_xml = Spy::getFunction(OsSend::XML); - func_xml_buf_sign = Spy::getFunction(OsSend::XML_BUF_SIGN); + return { value.wptr, value.size, value.capacity, value.ptr, value.clen }; } -void Sender::send_text(const std::string &wxid, const std::string &msg, const std::string &at_wxids) -{ - util::WxStringHolder holderMsg(msg); - util::WxStringHolder holderWxid(wxid); - - auto wxAtWxids = util::parse_wxids(at_wxids).wxWxids; - QWORD pWxAtWxids = wxAtWxids.empty() ? 0 : reinterpret_cast(&wxAtWxids); - - char buffer[1104] = { 0 }; - func_send_msg_mgr(); - func_send_text(reinterpret_cast(&buffer), &holderWxid.wx, &holderMsg.wx, pWxAtWxids, 1, 1, 0, 0); - func_free_chat_msg(reinterpret_cast(&buffer)); -} +} // namespace -void Sender::send_image(const std::string &wxid, const std::string &path) +namespace message { - util::WxStringHolder holderWxid(wxid); - util::WxStringHolder holderPath(path); - - char msg[1192] = { 0 }; - char msgTmp[1192] = { 0 }; - QWORD *flag[10] = { 0 }; - - QWORD tmp1 = 1, tmp2 = 0, tmp3 = 0; - QWORD pMsgTmp = func_new_chat_msg((QWORD)(&msgTmp)); - flag[0] = reinterpret_cast(tmp1); - flag[1] = reinterpret_cast(pMsgTmp); - flag[8] = &tmp2; - flag[9] = &tmp3; - - QWORD pMsg = func_new_chat_msg((QWORD)(&msg)); - QWORD sendMgr = func_send_msg_mgr(); - func_send_image(sendMgr, pMsg, &holderWxid.wx, &holderPath.wx, reinterpret_cast(&flag)); - - func_free_chat_msg(pMsg); - func_free_chat_msg(pMsgTmp); -} -void Sender::send_file(const std::string &wxid, const std::string &path) +void send_text(const std::string &wxid, const std::string &msg, const std::string &at_wxids) { - WxString *wxWxid = util::CreateWxString(wxid); - WxString *wxPath = util::CreateWxString(path); - if (!wxWxid || !wxPath) { - util::FreeWxString(wxWxid); - util::FreeWxString(wxPath); + if (g_WeChatWinDllAddr == 0) { + LOG_ERROR("WeChatWin.dll not located."); return; } - char *chat_msg = reinterpret_cast(util::AllocFromHeap(0x460)); - if (!chat_msg) { - util::FreeWxString(wxWxid); - util::FreeWxString(wxPath); - return; + auto init_global = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Message::Send::SEND_MGR_GETTER); + auto send_msg = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Message::Send::SEND_MSG); + auto cleanup = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Message::Send::CHATMSG_DTOR); + + std::wstring wsWxid = util::s2w(wxid); + std::wstring wsMsg = util::s2w(msg); + WxString wxWxid(wsWxid); + WxString wxMsg(wsMsg); + + // 先落全部 wstring 再包 WxString 视图,避免 vector 扩容使先前取的地址失效 + std::vector vAtWxids; + std::vector vWxAtWxids; + if (!at_wxids.empty()) { + std::wstringstream wss(util::s2w(at_wxids)); + std::wstring wstr; + while (std::getline(wss, wstr, L',')) { + if (!wstr.empty()) { + vAtWxids.push_back(wstr); + } + } + vWxAtWxids.reserve(vAtWxids.size()); + for (std::wstring &w : vAtWxids) { + vWxAtWxids.push_back(WxString(w)); + } } - QWORD *tmp1 = util::AllocBuffer(4); - QWORD *tmp2 = util::AllocBuffer(4); - QWORD *tmp3 = util::AllocBuffer(4); - if (!tmp1 || !tmp2 || !tmp3) { - func_free_chat_msg(reinterpret_cast(chat_msg)); - util::FreeBuffer(chat_msg); - util::FreeBuffer(tmp1); - util::FreeBuffer(tmp2); - util::FreeBuffer(tmp3); - util::FreeWxString(wxWxid); - util::FreeWxString(wxPath); + char buffer[0x2D8] = { 0 }; + init_global(); + send_msg(buffer, &wxWxid, &wxMsg, &vWxAtWxids, 1, 0, 0, 0); + cleanup(buffer); +} + +void send_image(const std::string &wxid, const std::string &path) +{ + if (g_WeChatWinDllAddr == 0) { + LOG_ERROR("WeChatWin.dll not located."); return; } - QWORD app_mgr = func_get_app_mgr(); - func_send_file(app_mgr, chat_msg, wxWxid, wxPath, 1, tmp1, 0, tmp2, 0, tmp3, 0, 0xC); - func_free_chat_msg(reinterpret_cast(chat_msg)); - - util::FreeBuffer(chat_msg); - util::FreeBuffer(tmp1); - util::FreeBuffer(tmp2); - util::FreeBuffer(tmp3); - util::FreeWxString(wxWxid); - util::FreeWxString(wxPath); + auto getter = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Message::Send::SEND_MGR_GETTER); + auto send_img = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Message::Send::SEND_IMAGE); + auto cleanup = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Message::Send::CHATMSG_DTOR); + + std::wstring wsWxid = util::s2w(wxid); + std::wstring wsPath = util::s2w(path); + WxString wxWxid(wsWxid); + WxString wxPath(wsPath); + + // 普通图片:type=1,caption 为空,两个附加串(source/appinfo)指向空 WxString,其余全 0 + WxString extra1; + WxString extra2; + uint8_t options[0x40] = { 0 }; + *reinterpret_cast(options + 0x00) = 1; + *reinterpret_cast(options + 0x2C) = &extra1; + *reinterpret_cast(options + 0x30) = &extra2; + + char buffer[0x2D8] = { 0 }; + void *manager = getter(); + send_img(manager, buffer, &wxWxid, &wxPath, options); + cleanup(buffer); } -void Sender::send_xml(const std::string &receiver, const std::string &xml, const std::string &path, uint64_t type) +void send_file(const std::string &wxid, const std::string &path) { - char buff1[0x500] = { 0 }; - char buff2[0x500] = { 0 }; - char buff3[0x1C] = { 0 }; - - auto pBuff1 = func_new_chat_msg(reinterpret_cast(buff1)); - auto pBuff2 = func_new_chat_msg(reinterpret_cast(buff2)); - auto pBuff3 = reinterpret_cast(&buff3); - - QWORD array[4] = { 0 }; + if (g_WeChatWinDllAddr == 0) { + LOG_ERROR("WeChatWin.dll not located."); + return; + } - auto sign = func_xml_buf_sign(pBuff2, reinterpret_cast(&array), 0x1); + auto getter = reinterpret_cast(g_WeChatWinDllAddr + Offsets::RichText::GETTER); + auto assign = reinterpret_cast(g_WeChatWinDllAddr + Offsets::RichText::ASSIGN); + auto send_file = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Message::Send::SEND_FILE); + auto cleanup = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Message::Send::CHATMSG_DTOR); + + std::wstring wsWxid = util::s2w(wxid); + std::wstring wsPath = util::s2w(path); + + // receiver/path 及三个空串都会被 sendFile mm_free,须用 ASSIGN 建 WeChat 拥有副本 + WxString wxReceiver; + WxString wxPath; + WxString wxEmpty1; + WxString wxEmpty2; + WxString wxEmpty3; + assign(&wxReceiver, wsWxid.c_str(), -1); + assign(&wxPath, wsPath.c_str(), -1); + assign(&wxEmpty1, L"", -1); + assign(&wxEmpty2, L"", -1); + assign(&wxEmpty3, L"", -1); + + char buffer[0x2D8] = { 0 }; + void *manager = getter(); + send_file(manager, buffer, to_value(wxReceiver), to_value(wxPath), 1, to_value(wxEmpty1), 0, + to_value(wxEmpty2), 0, 0, to_value(wxEmpty3), 0, 0); + cleanup(buffer); +} - util::WxStringHolder to(receiver); - util::WxStringHolder body(xml); - util::WxStringHolder thumb(path); - util::WxStringHolder from(account::get_self_wxid()); +void send_xml(const std::string &receiver, const std::string &xml, const std::string &path, int type) +{ + if (g_WeChatWinDllAddr == 0) { + LOG_ERROR("WeChatWin.dll not located."); + return; + } - func_send_xml(pBuff1, &from.wx, &to.wx, &body.wx, &thumb.wx, pBuff3, type, 0x4, sign, pBuff2); - func_free_chat_msg(pBuff1); - func_free_chat_msg(pBuff2); + // 发送核心会往 ChatMsg 字段 assign,故须先 CHATMSG_CTOR 真正构造、不能只置零;flag=1 跳过内部 构造 + auto ctor = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Message::Send::CHATMSG_CTOR); + auto send = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Message::Send::SEND_APPMSG); + auto dtor = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Message::Send::CHATMSG_DTOR); + + std::wstring wsFrom = util::s2w(account::get_self_wxid()); // from 非空是发送核心的入口守卫要求 + std::wstring wsReceiver = util::s2w(receiver); + std::wstring wsXml = util::s2w(xml); + std::wstring wsPath = util::s2w(path); + std::wstring wsEmpty; + + WxString wxFrom(wsFrom); + WxString wxReceiver(wsReceiver); + WxString wxXml(wsXml); + WxString wxPath(wsPath); + WxString wxEmpty1(wsEmpty); + WxString wxEmpty2(wsEmpty); + + char buffer[0x500] = { 0 }; + ctor(buffer); + send(buffer, &wxFrom, &wxReceiver, &wxXml, &wxEmpty1, &wxPath, type, 1, &wxEmpty2, 0); + dtor(buffer); } -void Sender::send_emotion(const std::string &wxid, const std::string &path) +void send_emotion(const std::string &wxid, const std::string &path) { - WxString *wxWxid = util::CreateWxString(wxid); - WxString *wxPath = util::CreateWxString(path); - QWORD *buff = util::AllocBuffer(8); - if (!wxWxid || !wxPath || !buff) { - util::FreeWxString(wxWxid); - util::FreeWxString(wxPath); - util::FreeBuffer(buff); + if (g_WeChatWinDllAddr == 0) { + LOG_ERROR("WeChatWin.dll not located."); return; } - QWORD mgr = func_get_emotion_mgr(); - func_send_emotion(mgr, wxPath, buff, wxWxid, 2, buff, 0, buff); - util::FreeBuffer(buff); - util::FreeWxString(wxWxid); - util::FreeWxString(wxPath); + // buffer 为 0x1C 置零小结构([+4]=size 置零走"从文件发送"正常路径),非 ChatMsg、无 dtor + auto getter = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Message::Send::EMO_MGR_GETTER); + auto assign = reinterpret_cast(g_WeChatWinDllAddr + Offsets::RichText::ASSIGN); + auto send = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Message::Send::SEND_CUSTOM_EMOTION); + + std::wstring wsWxid = util::s2w(wxid); + std::wstring wsPath = util::s2w(path); + + // path/wxid 及两个空串都会被 sendCustomEmotion mm_free,须用 ASSIGN 建 WeChat 拥有副本 + WxString wxPath; + WxString wxWxid; + WxString wxEmpty1; + WxString wxEmpty2; + assign(&wxPath, wsPath.c_str(), -1); + assign(&wxWxid, wsWxid.c_str(), -1); + assign(&wxEmpty1, L"", -1); + assign(&wxEmpty2, L"", -1); + + char buffer[0x1C] = { 0 }; + void *manager = getter(); + send(manager, to_value(wxPath), to_value(wxEmpty1), to_value(wxWxid), 2, to_value(wxEmpty2), 0, buffer); } -int Sender::send_rich_text(const RichText &rt) +int send_rich_text(const RichText &rt) { - QWORD status = -1; - - char *buff = util::AllocBuffer(0x3F0); - WxString *pReceiver = util::CreateWxString(rt.receiver); - WxString *pTitle = util::CreateWxString(rt.title); - WxString *pUrl = util::CreateWxString(rt.url); - WxString *pThumburl = util::CreateWxString(rt.thumburl); - WxString *pDigest = util::CreateWxString(rt.digest); - WxString *pAccount = util::CreateWxString(rt.account); - WxString *pName = util::CreateWxString(rt.name); - if (!buff || !pReceiver || !pTitle || !pUrl || !pThumburl || !pDigest || !pAccount || !pName) { - util::FreeWxString(pReceiver); - util::FreeWxString(pTitle); - util::FreeWxString(pUrl); - util::FreeWxString(pThumburl); - util::FreeWxString(pDigest); - util::FreeWxString(pAccount); - util::FreeWxString(pName); - util::FreeBuffer(buff); - LOG_ERROR("Out of Memory..."); - return static_cast(status); + if (g_WeChatWinDllAddr == 0) { + LOG_ERROR("WeChatWin.dll not located."); + return -1; } - func_new_mmreader(reinterpret_cast(buff)); - memcpy(buff + 0x8, pTitle, sizeof(WxString)); - memcpy(buff + 0x48, pUrl, sizeof(WxString)); - memcpy(buff + 0xB0, pThumburl, sizeof(WxString)); - memcpy(buff + 0xF0, pDigest, sizeof(WxString)); - memcpy(buff + 0x2C0, pAccount, sizeof(WxString)); - memcpy(buff + 0x2E0, pName, sizeof(WxString)); - - status = func_send_rich_text(func_get_app_mgr(), pReceiver, buff); - func_free_mmreader(reinterpret_cast(buff)); - - // TODO: 验证是否有内存泄露 - // util::FreeWxString(pReceiver); - // util::FreeWxString(pTitle); - // util::FreeWxString(pUrl); - // util::FreeWxString(pThumburl); - // util::FreeWxString(pDigest); - // util::FreeWxString(pAccount); - // util::FreeWxString(pName); - util::FreeBuffer(buff); - - return static_cast(status); + RichTextData data; + data.name = rt.name ? rt.name : ""; + data.account = rt.account ? rt.account : ""; + data.title = rt.title ? rt.title : ""; + data.digest = rt.digest ? rt.digest : ""; + data.url = rt.url ? rt.url : ""; + data.thumburl = rt.thumburl ? rt.thumburl : ""; + data.receiver = rt.receiver ? rt.receiver : ""; + + if (data.receiver.empty()) { + LOG_ERROR("Empty receiver."); + return -1; + } + + auto ctor = reinterpret_cast(g_WeChatWinDllAddr + Offsets::RichText::CTOR); + auto assign = reinterpret_cast(g_WeChatWinDllAddr + Offsets::RichText::ASSIGN); + auto getter = reinterpret_cast(g_WeChatWinDllAddr + Offsets::RichText::GETTER); + auto send = reinterpret_cast(g_WeChatWinDllAddr + Offsets::RichText::SEND); + auto cleanup = reinterpret_cast(g_WeChatWinDllAddr + Offsets::RichText::DTOR); + + std::wstring wsTitle = util::s2w(data.title); + std::wstring wsUrl = util::s2w(data.url); + std::wstring wsThumburl = util::s2w(data.thumburl); + std::wstring wsDigest = util::s2w(data.digest); + std::wstring wsAccount = util::s2w(data.account); + std::wstring wsName = util::s2w(data.name); + std::wstring wsReceiver = util::s2w(data.receiver); + + char buff[Offsets::RichText::OBJ_SIZE] = { 0 }; + ctor(buff); + assign(buff + Offsets::RichText::F_TITLE, wsTitle.c_str(), -1); + assign(buff + Offsets::RichText::F_URL, wsUrl.c_str(), -1); + assign(buff + Offsets::RichText::F_THUMBURL, wsThumburl.c_str(), -1); + assign(buff + Offsets::RichText::F_DIGEST, wsDigest.c_str(), -1); + assign(buff + Offsets::RichText::F_ACCOUNT, wsAccount.c_str(), -1); + assign(buff + Offsets::RichText::F_NAME, wsName.c_str(), -1); + + // 收件人须为 WeChat 拥有副本(SEND 内部 mm_free) + WxString wxReceiver; + assign(&wxReceiver, wsReceiver.c_str(), -1); + + void *manager = getter(); + int status = send(manager, to_value(wxReceiver), buff); + cleanup(buff); + + return status; } -int Sender::send_pat(const std::string &roomid, const std::string &wxid) +int send_pat(const std::string &roomid, const std::string &wxid) { - QWORD status = -1; + if (g_WeChatWinDllAddr == 0) { + LOG_ERROR("WeChatWin.dll not located."); + return -1; + } + + // getter 返回值仅作 SendPatMsg 的 SEH 帧 token 透传(不解引用);两个废弃栈参用 0 占位 + auto getter = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Pat::MGR_GETTER); + auto send_pat_msg = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Pat::SEND_PAT); - util::WxStringHolder holderRoom(roomid); - util::WxStringHolder holderWxid(wxid); + std::wstring wsRoomid = util::s2w(roomid); + std::wstring wsWxid = util::s2w(wxid); + WxString wxRoomid(wsRoomid); + WxString wxWxid(wsWxid); - status = func_send_pat(&holderRoom.wx, &holderWxid.wx); + void *manager = getter(); + uint8_t status = send_pat_msg(&wxRoomid, &wxWxid, manager, 0, 0); - return static_cast(status); + return status ? 0 : -1; } -int Sender::forward(QWORD msgid, const std::string &receiver) +int forward(uint64_t msgid, const std::string &receiver) { - uint32_t dbIdx = 0; - QWORD localId = 0; + if (g_WeChatWinDllAddr == 0) { + LOG_ERROR("WeChatWin.dll not located."); + return -1; + } + // 由 MsgSvrID 反查进程内 (localId, dbIdx) + uint64_t localId = 0; + uint32_t dbIdx = 0; if (db::get_local_id_and_dbidx(msgid, &localId, &dbIdx) != 0) { - LOG_ERROR("Failed to get localId, Please check id: {}", msgid); + LOG_ERROR("Failed to get localId, Please check id: {}", std::to_string(msgid)); return -1; } - LARGE_INTEGER l; - l.HighPart = dbIdx; - l.LowPart = static_cast(localId); + // msg_ptr 传 0 → 内部按 (localId, dbIdx) 加载源消息;scene=5 仅作转发场景统计元数据。 + // 收件人须为 WeChat 拥有副本(forwordMsg 末尾 mm_free 它),用 ASSIGN 深拷贝。 + auto assign = reinterpret_cast(g_WeChatWinDllAddr + Offsets::RichText::ASSIGN); + auto forward_msg = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Forward::FORWARD_MSG); + + std::wstring wsReceiver = util::s2w(receiver); + WxString wxReceiver; + assign(&wxReceiver, wsReceiver.c_str(), -1); - WxString *pReceiver = util::CreateWxString(receiver); + uint8_t status = forward_msg(5, 0, to_value(wxReceiver), static_cast(localId), dbIdx); - return static_cast(func_forward(pReceiver, l.QuadPart, 0x4, 0x0)); + return status ? 0 : -1; } -// RPC 方法 -bool Sender::rpc_send_text(const TextMsg &text, uint8_t *out, size_t *len) +bool rpc_send_text(const TextMsg &text, uint8_t *out, size_t *len) { - return fill_response(out, len, [&](Response &rsp) { - if (text.msg == nullptr || text.receiver == nullptr || strlen(text.msg) == 0 || strlen(text.receiver) == 0) { + return fill_response(out, len, [&text](Response &rsp) { + if ((text.msg == NULL) || (text.receiver == NULL)) { LOG_ERROR("Empty message or receiver."); rsp.msg.status = -1; } else { - send_text(text.receiver, text.msg, text.aters ? text.aters : ""); + std::string msg(text.msg); + std::string receiver(text.receiver); + std::string aters(text.aters ? text.aters : ""); + send_text(receiver, msg, aters); rsp.msg.status = 0; } }); } -bool Sender::rpc_send_image(const PathMsg &file, uint8_t *out, size_t *len) +bool rpc_send_image(const PathMsg &file, uint8_t *out, size_t *len) { - std::string path(file.path); - std::string receiver(file.receiver); - return fill_response(out, len, [&](Response &rsp) { - if (path.empty() || receiver.empty()) { + return fill_response(out, len, [&file](Response &rsp) { + if ((file.path == NULL) || (file.receiver == NULL)) { LOG_ERROR("Empty path or receiver."); rsp.msg.status = -1; + } else if (!fs::exists(file.path)) { + LOG_ERROR("Path does not exist: {}", file.path); + rsp.msg.status = -2; } else { - send_image(receiver, path); + send_image(file.receiver, file.path); rsp.msg.status = 0; } }); } -bool Sender::rpc_send_file(const PathMsg &file, uint8_t *out, size_t *len) +bool rpc_send_file(const PathMsg &file, uint8_t *out, size_t *len) { - std::string path(file.path); - std::string receiver(file.receiver); - return fill_response(out, len, [&](Response &rsp) { - if (path.empty() || receiver.empty()) { + return fill_response(out, len, [&file](Response &rsp) { + if ((file.path == NULL) || (file.receiver == NULL)) { LOG_ERROR("Empty path or receiver."); rsp.msg.status = -1; + } else if (!fs::exists(file.path)) { + LOG_ERROR("Path does not exist: {}", file.path); + rsp.msg.status = -2; } else { - send_file(receiver, path); + send_file(file.receiver, file.path); rsp.msg.status = 0; } }); } -bool Sender::rpc_send_emotion(const PathMsg &file, uint8_t *out, size_t *len) +bool rpc_send_emotion(const PathMsg &file, uint8_t *out, size_t *len) { - std::string path(file.path); - std::string receiver(file.receiver); - return fill_response(out, len, [&](Response &rsp) { - if (path.empty() || receiver.empty()) { + return fill_response(out, len, [&file](Response &rsp) { + if ((file.path == NULL) || (file.receiver == NULL)) { LOG_ERROR("Empty path or receiver."); rsp.msg.status = -1; + } else if (!fs::exists(file.path)) { + LOG_ERROR("Path does not exist: {}", file.path); + rsp.msg.status = -2; } else { - send_emotion(receiver, path); + send_emotion(file.receiver, file.path); rsp.msg.status = 0; } }); } -bool Sender::rpc_send_xml(const XmlMsg &xml, uint8_t *out, size_t *len) +bool rpc_send_xml(const XmlMsg &xml, uint8_t *out, size_t *len) { - return fill_response(out, len, [&](Response &rsp) { - if (xml.content == nullptr || xml.receiver == nullptr) { + return fill_response(out, len, [&xml](Response &rsp) { + if ((xml.content == NULL) || (xml.receiver == NULL)) { LOG_ERROR("Empty content or receiver."); rsp.msg.status = -1; } else { - send_xml(xml.receiver, xml.content, xml.path, xml.type); + std::string content(xml.content); + std::string receiver(xml.receiver); + std::string path(xml.path ? xml.path : ""); + int type = xml.type; + send_xml(receiver, content, path, type); rsp.msg.status = 0; } }); } -bool Sender::rpc_send_rich_text(const RichText &rt, uint8_t *out, size_t *len) +bool rpc_send_rich_text(const RichText &rt, uint8_t *out, size_t *len) { - return fill_response(out, len, [&](Response &rsp) { - if (rt.receiver == nullptr) { - LOG_ERROR("Empty receiver."); - rsp.msg.status = -1; - } else { - rsp.msg.status = send_rich_text(rt); - } + return fill_response(out, len, [&rt](Response &rsp) { + int status = send_rich_text(rt); + rsp.msg.status = status; }); } -bool Sender::rpc_send_pat(const PatMsg &pat, uint8_t *out, size_t *len) +bool rpc_send_pat(const PatMsg &pat, uint8_t *out, size_t *len) { - std::string wxid(pat.wxid); - std::string roomid(pat.roomid); - return fill_response(out, len, [&](Response &rsp) { - if (roomid.empty() || wxid.empty()) { + return fill_response(out, len, [&pat](Response &rsp) { + if ((pat.roomid == NULL) || (pat.wxid == NULL)) { LOG_ERROR("Empty roomid or wxid."); rsp.msg.status = -1; } else { - rsp.msg.status = send_pat(roomid, wxid); + int status = send_pat(pat.roomid, pat.wxid); + rsp.msg.status = status; } }); } -bool Sender::rpc_forward(const ForwardMsg &fm, uint8_t *out, size_t *len) +bool rpc_forward(const ForwardMsg &fm, uint8_t *out, size_t *len) { - uint64_t msgid = fm.id; - std::string receiver(fm.receiver); - return fill_response(out, len, [&](Response &rsp) { - if (receiver.empty()) { + return fill_response(out, len, [&fm](Response &rsp) { + if (fm.receiver == NULL) { LOG_ERROR("Empty receiver."); rsp.msg.status = -1; } else { - rsp.msg.status = forward(msgid, receiver); + int status = forward(fm.id, fm.receiver); + rsp.msg.status = status; } }); } -} +} // namespace message diff --git a/WeChatFerry/spy/message_sender.h b/WeChatFerry/spy/message_sender.h index c724d09e..5388e727 100644 --- a/WeChatFerry/spy/message_sender.h +++ b/WeChatFerry/spy/message_sender.h @@ -1,82 +1,30 @@ -#pragma once +#pragma once -#include +#include +#include #include -#include #include "wcf.pb.h" -#include "spy_types.h" - namespace message { -class Sender -{ -public: - static Sender &get_instance(); - - void send_text(const std::string &wxid, const std::string &msg, const std::string &at_wxids = ""); - void send_image(const std::string &wxid, const std::string &path); - void send_file(const std::string &wxid, const std::string &path); - void send_xml(const std::string &receiver, const std::string &xml, const std::string &path, uint64_t type); - void send_emotion(const std::string &wxid, const std::string &path); - int send_rich_text(const RichText &rt); - int send_pat(const std::string &roomid, const std::string &wxid); - int forward(uint64_t msgid, const std::string &receiver); - - // RPC 方法 - bool rpc_send_text(const TextMsg &text, uint8_t *out, size_t *len); - bool rpc_send_image(const PathMsg &file, uint8_t *out, size_t *len); - bool rpc_send_file(const PathMsg &file, uint8_t *out, size_t *len); - bool rpc_send_emotion(const PathMsg &file, uint8_t *out, size_t *len); - bool rpc_send_xml(const XmlMsg &rt, uint8_t *out, size_t *len); - bool rpc_send_rich_text(const RichText &rt, uint8_t *out, size_t *len); - bool rpc_send_pat(const PatMsg &pat, uint8_t *out, size_t *len); - bool rpc_forward(const ForwardMsg &fm, uint8_t *out, size_t *len); - -private: - Sender(); - ~Sender() = default; - - Sender(const Sender &) = delete; - Sender &operator=(const Sender &) = delete; - - using New_t = QWORD (*)(QWORD); - using Free_t = QWORD (*)(QWORD); - using SendMsgMgr_t = QWORD (*)(); - using GetAppMgr_t = QWORD (*)(); - using SendText_t = QWORD (*)(QWORD, WxString *, WxString *, QWORD, QWORD, QWORD, QWORD, QWORD); - using SendImage_t = QWORD (*)(QWORD, QWORD, WxString *, WxString *, QWORD); - using SendFile_t = QWORD (*)(QWORD, char *, WxString *, WxString *, QWORD, QWORD *, QWORD, QWORD *, QWORD, QWORD *, - QWORD, QWORD); - using SendRichText_t = QWORD (*)(QWORD, WxString *, char *); - using SendPat_t = QWORD (*)(WxString *, WxString *); - using Forward_t = QWORD (*)(WxString *, QWORD, QWORD, QWORD); - using GetEmotionMgr_t = QWORD (*)(); - using SendEmotion_t = QWORD (*)(QWORD, WxString *, QWORD *, WxString *, QWORD, QWORD *, QWORD, QWORD *); - using XmlBufSign_t = QWORD (*)(QWORD, QWORD, QWORD); - using SendXml_t - = QWORD (*)(QWORD, WxString *, WxString *, WxString *, WxString *, QWORD, QWORD, QWORD, QWORD, QWORD); - - New_t func_new_chat_msg; - Free_t func_free_chat_msg; - SendMsgMgr_t func_send_msg_mgr; - GetAppMgr_t func_get_app_mgr; - SendText_t func_send_text; - SendImage_t func_send_image; - SendFile_t func_send_file; - New_t func_new_mmreader; - Free_t func_free_mmreader; - SendRichText_t func_send_rich_text; - SendPat_t func_send_pat; - Forward_t func_forward; - GetEmotionMgr_t func_get_emotion_mgr; - SendEmotion_t func_send_emotion; - XmlBufSign_t func_xml_buf_sign; - SendXml_t func_send_xml; - - std::unique_ptr new_wx_string(const char *str); - std::unique_ptr new_wx_string(const std::string &str); -}; -} +void send_text(const std::string &wxid, const std::string &msg, const std::string &at_wxids = ""); +void send_image(const std::string &wxid, const std::string &path); +void send_file(const std::string &wxid, const std::string &path); +void send_xml(const std::string &receiver, const std::string &xml, const std::string &path, int type); +void send_emotion(const std::string &wxid, const std::string &path); +int send_rich_text(const RichText &rt); +int send_pat(const std::string &roomid, const std::string &wxid); +int forward(uint64_t msgid, const std::string &receiver); + +bool rpc_send_text(const TextMsg &text, uint8_t *out, size_t *len); +bool rpc_send_image(const PathMsg &file, uint8_t *out, size_t *len); +bool rpc_send_file(const PathMsg &file, uint8_t *out, size_t *len); +bool rpc_send_emotion(const PathMsg &file, uint8_t *out, size_t *len); +bool rpc_send_xml(const XmlMsg &xml, uint8_t *out, size_t *len); +bool rpc_send_rich_text(const RichText &rt, uint8_t *out, size_t *len); +bool rpc_send_pat(const PatMsg &pat, uint8_t *out, size_t *len); +bool rpc_forward(const ForwardMsg &fm, uint8_t *out, size_t *len); + +} // namespace message diff --git a/WeChatFerry/spy/misc_manager.cpp b/WeChatFerry/spy/misc_manager.cpp index 7a7f4456..d8bd02bb 100644 --- a/WeChatFerry/spy/misc_manager.cpp +++ b/WeChatFerry/spy/misc_manager.cpp @@ -1,135 +1,184 @@ #pragma warning(disable : 4244) + #include "misc_manager.h" +#include #include #include +#include #include "codec.h" #include "database_executor.h" +#include "framework.h" #include "log.hpp" -#include "message_handler.h" #include "offsets.h" +#include "pb_util.h" #include "rpc_helper.h" -#include "rpc_server.h" -#include "spy.h" #include "spy_types.h" #include "util.h" -namespace misc -{ -using namespace std; -namespace fs = std::filesystem; -namespace OsMisc = Offsets::Misc; -namespace OsSns = Offsets::Misc::Sns; - -using get_sns_data_mgr_t = QWORD (*)(); -using get_sns_timeline_mgr_t = QWORD (*)(); -using get_sns_first_page_t = QWORD (*)(QWORD, QWORD, QWORD); -using get_sns_next_page_scene_t = QWORD (*)(QWORD, QWORD); -using get_chat_mgr_t = QWORD (*)(); -using new_chat_msg_t = QWORD (*)(char *); -using free_chat_msg_t = QWORD (*)(QWORD); -using get_pre_download_mgr_t = QWORD (*)(); -using get_mgr_by_prefix_localid_t = QWORD (*)(QWORD, QWORD); -using push_attach_task_t = QWORD (*)(QWORD, QWORD, QWORD, QWORD); -using get_ocr_manager_t = QWORD (*)(); -using do_ocr_task_t = QWORD (*)(QWORD, QWORD, QWORD, QWORD, QWORD, QWORD); -using get_qr_code_mgr_t = QWORD (*)(); - -struct ImagePattern { - uint8_t header1_candidate; - uint8_t header2_expected; - const char *extension; -}; +#define HEADER_PNG1 0x89 +#define HEADER_PNG2 0x50 +#define HEADER_JPG1 0xFF +#define HEADER_JPG2 0xD8 +#define HEADER_GIF1 0x47 +#define HEADER_GIF2 0x49 -static constexpr ImagePattern patterns[] = { - { 0x89, 0x50, ".png" }, - { 0xFF, 0xD8, ".jpg" }, - { 0x47, 0x49, ".gif" }, -}; +namespace fs = std::filesystem; -static std::string detect_image_extension(uint8_t header1, uint8_t header2, uint8_t *key) +extern bool gIsListeningPyq; +extern uint32_t g_WeChatWinDllAddr; + +namespace { - for (const auto &pat : patterns) { - *key = pat.header1_candidate ^ header1; - if ((pat.header2_expected ^ *key) == header2) { - return pat.extension; - } +using ManagerGetterFn = void *(*)(); +using BufferInitFn = void(__thiscall *)(void *buffer); +using BufferCleanupFn = void(__thiscall *)(void *buffer); +using WarmupFn = void (*)(); +using RefreshFirstPageFn = int(__thiscall *)(void *manager, int forward); +using RefreshNextPageFn = int(__thiscall *)(void *manager, uint32_t id_low, uint32_t id_high); +using RunOcrFn = int(__thiscall *)(void *manager, const WxString *path, int reserved, + void *result_list, uint32_t *found_flag, + const WxString *null_obj); +using OperatorNewFn = void *(__cdecl *)(size_t); +using OcrResultDtorFn = void(__thiscall *)(void *result_list); +using RefreshLoginQrCodeFn = void(__thiscall *)(void *manager); +using LoadAttachmentMetaFn = int(__thiscall *)(void *buffer, uint32_t local_id, uint32_t db_idx); +using DownloadAttachmentFn = int(__thiscall *)(void *manager, void *buffer, int reserved, int sync, + int user_clicked); +using WxStringAssignFn = void *(__thiscall *)(void *dest, const wchar_t *src, int len); +using RevokeMessageFn = int(__thiscall *)(void *manager, void *chat_msg); +using ReceiveTransferFn = int(__fastcall *)(void *pay_info, const WxString *wxid, uint64_t scratch, + int confirm); + +std::string get_key(uint8_t header1, uint8_t header2, uint8_t *key) +{ + *key = HEADER_PNG1 ^ header1; + if ((HEADER_PNG2 ^ *key) == header2) { + return ".png"; + } + + *key = HEADER_JPG1 ^ header1; + if ((HEADER_JPG2 ^ *key) == header2) { + return ".jpg"; } - LOG_ERROR("未知类型:{:02x} {:02x}", header1, header2); + + *key = HEADER_GIF1 ^ header1; + if ((HEADER_GIF2 ^ *key) == header2) { + return ".gif"; + } + return ""; } -std::string decrypt_image(const fs::path &src, const fs::path &dst_dir) +int get_first_page() { - if (!fs::exists(src)) { - LOG_ERROR("文件不存在: {}", src.string()); - return ""; + int rv = -1; + + auto getMomentsManager = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Moments::MGR_GETTER); + auto requestFirstPage = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Moments::GET_FIRST_PAGE); + + void *manager = getMomentsManager(); + if (manager != nullptr) { + rv = requestFirstPage(manager, 1); } - std::ifstream in(src, std::ios::binary); - if (!in) { - LOG_ERROR("无法打开文件: {}", src.string()); - return ""; + return rv; +} + +int get_next_page(uint64_t id) +{ + int rv = -1; + + auto getMomentsManager = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Moments::MGR_GETTER); + auto requestNextPage = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Moments::GET_NEXT_PAGE); + + void *manager = getMomentsManager(); + if (manager != nullptr) { + rv = requestNextPage(manager, static_cast(id), static_cast(id >> 32)); } - std::vector buffer(std::istreambuf_iterator(in), {}); - if (buffer.size() < 2) return ""; + return rv; +} - uint8_t key = 0x00; - auto ext = detect_image_extension(buffer[0], buffer[1], &key); - if (ext.empty()) { - LOG_ERROR("无法检测文件类型."); +} // namespace + +namespace misc +{ + +std::string decrypt_image(const std::string &src, const std::string &dir) +{ + // 微信图片是原图逐字节异或同一单字节密钥;由前两字节与已知图片头(PNG/JPG/GIF)反推密钥 + if (!fs::exists(src)) { return ""; } - std::for_each(buffer.begin(), buffer.end(), [key](char &c) { c ^= key; }); + std::ifstream in(src.c_str(), std::ios::binary); + if (!in.is_open()) { + LOG_ERROR("Failed to read file {}", src); + return ""; + } - fs::path dst_path = dst_dir / (src.stem().string() + ext); - if (!fs::exists(dst_dir)) fs::create_directories(dst_dir); + std::filebuf *pfb = in.rdbuf(); + size_t size = pfb->pubseekoff(0, std::ios::end, std::ios::in); + pfb->pubseekpos(0, std::ios::in); - std::ofstream out(dst_path, std::ios::binary); - if (!out) { - LOG_ERROR("写入文件失败: {}", dst_path.generic_string()); + std::vector buff; + buff.resize(size); + char *pBuf = buff.data(); + pfb->sgetn(pBuf, size); + in.close(); + + if (size < 2) { + LOG_ERROR("File too small: {}", src); return ""; } - out.write(buffer.data(), buffer.size()); - return dst_path.generic_string(); -} - -static int get_first_page() -{ - int status = -1; + uint8_t key = 0x00; + std::string ext = get_key(pBuf[0], pBuf[1], &key); + if (ext.empty()) { + LOG_ERROR("Failed to get key."); + return ""; + } - auto GetSNSDataMgr = Spy::getFunction(OsSns::DATA_MGR); - auto GetSNSFirstPage = Spy::getFunction(OsSns::FIRST); + for (size_t i = 0; i < size; i++) { + pBuf[i] ^= key; + } - QWORD buff[16] = { 0 }; - QWORD mgr = GetSNSDataMgr(); - status = (int)GetSNSFirstPage(mgr, (QWORD)&buff, 1); + std::string dst = ""; - return status; -} + try { + if (dir.empty()) { + dst = fs::path(src).replace_extension(ext).string(); + } else { + dst = (dir.back() == '\\' || dir.back() == '/') ? dir : (dir + "/"); + dst += fs::path(src).stem().string() + ext; + } -static int get_next_page(QWORD id) -{ - int status = -1; + std::replace(dst.begin(), dst.end(), '\\', '/'); + } catch (const std::exception &e) { + LOG_ERROR(util::gb2312_to_utf8(e.what())); + } catch (...) { + LOG_ERROR("Unknow exception."); + return ""; + } - auto GetSnsTimeLineMgr = Spy::getFunction(OsSns::TIMELINE); - auto GetSNSNextPageScene = Spy::getFunction(OsSns::NEXT); + std::ofstream out(dst.c_str(), std::ios::binary); + if (!out.is_open()) { + LOG_ERROR("Failed to write file {}", dst); + return ""; + } - QWORD mgr = GetSnsTimeLineMgr(); - status = (int)GetSNSNextPageScene(mgr, id); + out.write(pBuf, size); + out.close(); - return status; + return dst; } int refresh_pyq(uint64_t id) { - auto &msgHandler = message::Handler::getInstance(); - if (!msgHandler.isPyqListening()) { + if (!gIsListeningPyq) { LOG_ERROR("没有启动朋友圈消息接收,参考:enable_receiving_msg"); return -1; } @@ -141,293 +190,388 @@ int refresh_pyq(uint64_t id) return get_next_page(id); } -/******************************************************************************* - * 都说我不写注释,写一下吧 - * 其实也没啥好写的,就是下载资源 - * 主要介绍一下几个参数: - * id:好理解,消息 id - * thumb:图片或者视频的缩略图路径;如果是视频,后缀为 mp4 后就是存在路径了 - * extra:图片、文件的路径 - *******************************************************************************/ -int download_attachment(uint64_t id, const fs::path &thumb, const fs::path &extra) +int download_attachment(uint64_t id, const std::string &thumb, const std::string &extra) { + if (g_WeChatWinDllAddr == 0) { + LOG_ERROR("WeChatWin.dll not located."); + return -1; + } + int status = -1; - QWORD localId; + uint64_t localId; uint32_t dbIdx; - if (fs::exists(extra)) { // 第一道,不重复下载。TODO: 通过文件大小来判断 - LOG_WARN("文件已存在:{}", extra.generic_string()); + if (fs::exists(extra)) { return 0; } if (db::get_local_id_and_dbidx(id, &localId, &dbIdx) != 0) { - LOG_ERROR("获取 localId 失败, 请检查消息 id: {} 是否正确", to_string(id)); + LOG_ERROR("Failed to get localId, Please check id: {}", to_string(id)); return status; } - auto NewChatMsg = Spy::getFunction(OsMisc::INSATNCE); - auto FreeChatMsg = Spy::getFunction(OsMisc::FREE); - auto GetChatMgr = Spy::getFunction(OsMisc::CHAT_MGR); - auto GetPreDownLoadMgr = Spy::getFunction(OsMisc::PRE_DOWNLOAD_MGR); - auto PushAttachTask = Spy::getFunction(OsMisc::PUSH_ATTACH_TASK); - auto GetMgrByPrefixLocalId = Spy::getFunction(OsMisc::PRE_LOCAL_ID_MGR); + // 加载消息进 ChatMsg → 依类型算落盘路径写入其字段 → 提交下载 → 析构 + char buff[0x2D8] = { 0 }; + auto initChatMsg = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Message::Send::CHATMSG_CTOR); + auto warmup = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Attachment::WARMUP); + auto loadMsg = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Attachment::LOAD_MSG); + auto getManager = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Attachment::MGR_GETTER); + auto pushTask = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Attachment::PUSH_TASK); + auto assignPath = reinterpret_cast(g_WeChatWinDllAddr + Offsets::RichText::ASSIGN); + auto destroyMsg = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Message::Send::CHATMSG_DTOR); - LARGE_INTEGER l; - l.HighPart = dbIdx; - l.LowPart = (DWORD)localId; + initChatMsg(buff); + warmup(); + loadMsg(buff, static_cast(localId), dbIdx); - char *buff = util::AllocBuffer(0x460); - if (buff == nullptr) { - LOG_ERROR("申请内存失败."); - return status; - } + uint32_t type = util::get_dword((uint32_t)(buff + Offsets::Message::Receive::TYPE)); - QWORD pChatMsg = NewChatMsg(buff); - GetChatMgr(); - GetMgrByPrefixLocalId(l.QuadPart, pChatMsg); - QWORD type = util::get_dword(reinterpret_cast(buff) + 0x38); + std::string save_path = ""; + std::string thumb_path = ""; - fs::path save_path, thumb_path; switch (type) { - case 0x03: { // Image: extra + case 0x03: // 图片 save_path = extra; break; - } - case 0x3E: - case 0x2B: { // Video: thumb + case 0x3E: // 视频 + case 0x2B: thumb_path = thumb; - save_path = fs::path(thumb).replace_extension("mp4"); + save_path = fs::path(thumb).replace_extension("mp4").string(); break; - } - case 0x31: { // File: extra + case 0x31: // 文件 save_path = extra; break; - } default: - LOG_ERROR("不支持的文件类型: {}", type); - return -2; + break; } - if (fs::exists(save_path)) { // 不重复下载。TODO: 通过文件大小来判断 + if (fs::exists(save_path)) { + destroyMsg(buff); return 0; } - LOG_DEBUG("保存路径: {}", save_path.generic_string()); - // 创建父目录,由于路径来源于微信,不做检查 - fs::create_directory(save_path.parent_path()); + LOG_DEBUG("path: {}", save_path); + fs::create_directory(fs::path(save_path).parent_path().string()); - int temp = 1; - auto wx_save_path = util::CreateWxString(save_path.make_preferred().string()); - auto wx_thumb_path = util::CreateWxString(thumb_path.make_preferred().string()); + std::wstring wsSavePath = util::s2w(save_path); + std::wstring wsThumbPath = util::s2w(thumb_path); - memcpy(&buff[0x280], wx_thumb_path, sizeof(WxString)); - memcpy(&buff[0x2A0], wx_save_path, sizeof(WxString)); - memcpy(&buff[0x40C], &temp, sizeof(temp)); + // 路径字段会被 ~ChatMsg mm_free,须用 ASSIGN 建 WeChat 拥有副本;init 标志置 1 跳过重复子对象初始化 + assignPath(buff + Offsets::Attachment::F_THUMB_PATH, wsThumbPath.c_str(), -1); + assignPath(buff + Offsets::Attachment::F_SAVE_PATH, wsSavePath.c_str(), -1); + *reinterpret_cast(buff + Offsets::Attachment::F_INITED_FLAG) = 1; - QWORD mgr = GetPreDownLoadMgr(); - status = (int)PushAttachTask(mgr, pChatMsg, 0, 1); - FreeChatMsg(pChatMsg); - util::FreeBuffer(buff); + void *manager = getManager(); + if (manager != nullptr) { + status = pushTask(manager, buff, 0, 1, 0); + } + destroyMsg(buff); return status; } -std::string get_audio(uint64_t id, const fs::path &dir) +int revoke_message(uint64_t id) { - if (!fs::exists(dir)) fs::create_directories(dir); + int status = -1; + uint64_t localId; + uint32_t dbIdx; + if (db::get_local_id_and_dbidx(id, &localId, &dbIdx) != 0) { + LOG_ERROR("Failed to get localId, Please check id: {}", to_string(id)); + return status; + } - fs::path mp3path = dir / (std::to_string(id) + ".mp3"); - if (fs::exists(mp3path)) return mp3path.generic_string(); + // ChatMsg 精确 0x2D8 字节(同发送侧),栈上构造后按 localId/dbIdx 加载再撤回。 + char chat_msg[0x2D8] = { 0 }; - auto silk = db::get_audio_data(id); - if (silk.empty()) { - LOG_ERROR("没有获取到语音数据."); - return ""; + auto initChatMsg = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Message::Send::CHATMSG_CTOR); + auto loadMsg = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Attachment::LOAD_MSG); + auto getRevokeMgr = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Revoke::MGR_GETTER); + auto revokeMessageFn = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Revoke::REVOKE_MSG); + auto destroyChatMsg = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Message::Send::CHATMSG_DTOR); + + initChatMsg(chat_msg); + // GetMgrByPrefixLocalId 内部自初始化 ChatMgr,无需单独 warmup + loadMsg(chat_msg, static_cast(localId), dbIdx); + + void *manager = getRevokeMgr(); + if (manager != nullptr) { + status = revokeMessageFn(manager, chat_msg); } + destroyChatMsg(chat_msg); - Silk2Mp3(silk, mp3path.generic_string(), 24000); - return mp3path.generic_string(); + return status; } -std::string get_pcm_audio(uint64_t id, const fs::path &dir, int32_t sr) +std::string get_audio(uint64_t id, const std::string &dir) { - if (!fs::exists(dir)) fs::create_directories(dir); + // 语音存于 MediaMSGn.db 的 Media 表(Buf 列为 silk 编码,首字节为标志需跳过);取出后转 mp3 落盘。 + // 依赖数据库层的 MSG 多库枚举(MultiDBMsgMgr),否则 get_audio_data 查不到 MediaMSG 库。 + if (dir.empty()) { + LOG_ERROR("Empty dir."); + return ""; + } - fs::path pcmpath = dir / (std::to_string(id) + ".pcm"); - if (fs::exists(pcmpath)) return pcmpath.generic_string(); + std::string mp3path = (dir.back() == '\\' || dir.back() == '/') ? dir : (dir + "/"); + mp3path += to_string(id) + ".mp3"; + std::replace(mp3path.begin(), mp3path.end(), '\\', '/'); + if (fs::exists(mp3path)) { + return mp3path; + } - auto silk = db::get_audio_data(id); + std::vector silk = db::get_audio_data(id); if (silk.empty()) { - LOG_ERROR("没有获取到语音数据."); + LOG_ERROR("Empty audio data."); return ""; } - std::vector pcm; - SilkDecode(silk, pcm, sr); - - std::ofstream out(pcmpath, std::ios::binary); - if (!out) { - LOG_ERROR("创建文件失败: {}", pcmpath.generic_string()); + if (Silk2Mp3(silk, mp3path, 24000) != 0 || !fs::exists(mp3path)) { return ""; } - out.write(reinterpret_cast(pcm.data()), pcm.size()); - return pcmpath.generic_string(); + return mp3path; } -OcrResult_t get_ocr_result(const fs::path &path) +// OCR 结果链表头:std::list 头(哨兵指针 + 计数)后接缓存命中时被回填的标量字段,须留足空间避免越界 +struct OcrResultList { + void *head; // +0x00 哨兵结点(自环空链表) + uint32_t count; // +0x04 结点数 + uint32_t f8; // +0x08 缓存命中回填 + uint32_t fc; // +0x0C + uint64_t f10; // +0x10 + uint64_t pad; // +0x18 冗余,覆盖缓存命中的整段写入 +}; + +OcrResult_t get_ocr_result(const std::string &path) { OcrResult_t ret = { -1, "" }; -#if 0 // 参数没调好,会抛异常,看看有没有好心人来修复 + if (!fs::exists(path)) { LOG_ERROR("Can not find: {}", path); return ret; } - get_ocr_manager_t GetOCRManager = (get_ocr_manager_t)(g_WeChatWinDllAddr + 0x1D6C3C0); - do_ocr_task_t DoOCRTask = (do_ocr_task_t)(g_WeChatWinDllAddr + 0x2D10BC0); + std::wstring wsPath = util::s2w(fs::path(path).make_preferred().string()); - QWORD unk1 = 0, unk2 = 0, unused = 0; - QWORD *pUnk1 = &unk1; - QWORD *pUnk2 = &unk2; - // 路径分隔符有要求,必须为 `\` - wstring wsPath = util::s2w(fs::path(path).make_preferred().string()); WxString wxPath(wsPath); - vector *pv = (vector *)HeapAlloc(GetProcessHeap(), 0, 0x20); - RawVector_t *pRv = (RawVector_t *)pv; - pRv->finish = pRv->start; - char buff[0x98] = { 0 }; - memcpy(buff, &pRv->start, sizeof(QWORD)); - - QWORD mgr = GetOCRManager(); - ret.status = (int)DoOCRTask(mgr, (QWORD)&wxPath, unused, (QWORD)buff, (QWORD)&pUnk1, (QWORD)&pUnk2); - - QWORD count = util::get_qword(buff + 0x8); - if (count > 0) { - QWORD header = util::get_qword(buff); - for (QWORD i = 0; i < count; i++) { - QWORD content = util::get_qword(header); - ret.result += util::w2s(get_pp_wstring(content + 0x28)); - ret.result += "\n"; - header = content; - } + WxString nullObj; + + auto opNew = reinterpret_cast(g_WeChatWinDllAddr + Offsets::OCR::RESULT_NEW); + auto getOcrManager = reinterpret_cast(g_WeChatWinDllAddr + Offsets::OCR::MGR_GETTER); + auto runOcr = reinterpret_cast(g_WeChatWinDllAddr + Offsets::OCR::RUN_OCR); + auto destroyList = reinterpret_cast(g_WeChatWinDllAddr + Offsets::OCR::RESULT_DTOR); + + // 构造合法空链表:哨兵结点自环(与 WeChat 内联初始化一致), + // 缓存命中路径 DoOCRTask 会 splice 结点,无有效哨兵会崩。 + OcrResultList buffer = { 0 }; + void **sentinel = static_cast(opNew(0x58)); + if (sentinel == nullptr) { + LOG_ERROR("Failed to alloc OCR result buffer."); + return ret; } -#endif - return ret; -} + sentinel[0] = sentinel; // next -> self + sentinel[1] = sentinel; // prev -> self + buffer.head = sentinel; -int revoke_message(uint64_t id) -{ - int status = -1; -#if 0 // 这个挺鸡肋的,因为自己发的消息没法直接获得 msgid,就这样吧 - QWORD localId; - uint32_t dbIdx; - if (GetLocalIdandDbidx(id, &localId, &dbIdx) != 0) { - LOG_ERROR("Failed to get localId, Please check id: {}", to_string(id)); - return status; + uint32_t found = 0; + int status = -1; + + void *manager = getOcrManager(); + if (manager != nullptr) { + status = runOcr(manager, &wxPath, 0, &buffer, &found, &nullObj); } -#endif - return status; + + // DoOCRTask 缓存命中返回 0 并同步回填链表;异步入队返回非 0 task_id(此时无同步结果)。 + if (status != 0) { + destroyList(&buffer); + return ret; + } + + ret.status = status; + + uint32_t addr = reinterpret_cast(&buffer); + uint32_t header = util::get_dword(addr); // 哨兵/首结点 + uint32_t num = util::get_dword(addr + 0x4); // 结点数 + for (uint32_t i = 0; i < num; i++) { + uint32_t content = util::get_dword(header); // 下一结点 + ret.result += util::w2s(util::get_wstring(content + 0x14)); // 结点内文本 WxString.wptr + ret.result += "\n"; + header = content; + } + + destroyList(&buffer); + return ret; } std::string get_login_url() { - std::string uri; - auto get_qr_code_mgr = Spy::getFunction(OsMisc::QR_CODE); + if (g_WeChatWinDllAddr == 0) { + LOG_ERROR("WeChatWin.dll not located."); + return ""; + } - uint64_t addr = get_qr_code_mgr() + 0x68; - uint64_t len = *(uint64_t *)(addr + 0x10); - if (len == 0) { - LOG_ERROR("获取二维码失败."); - return uri; + // 已登录则无二维码可刷(SERVICE != 0 即在线,与 account::is_logged_in 语义一致)。 + if (util::get_dword(g_WeChatWinDllAddr + Offsets::Account::SERVICE) != 0) { + LOG_DEBUG("Already logged in, no QR code to refresh."); + return ""; } - if (*(uint64_t *)(addr + 0x18) == 0xF) { - uri = std::string((char *)addr, len); - } else { - uri = std::string(*(char **)(addr), len); + // getQRCodeImage 经 doScene 异步获取,完成后把登录 uuid 回填到管理器 +8 的 std::string + auto getQrCodeManager = reinterpret_cast(g_WeChatWinDllAddr + Offsets::QRCode::MGR_GETTER); + auto getQrCodeImage = reinterpret_cast(g_WeChatWinDllAddr + Offsets::QRCode::GET_QRCODE); + + void *manager = getQrCodeManager(); + if (manager == nullptr) { + LOG_ERROR("Failed to get QRCodeLoginMgr."); + return ""; + } + getQrCodeImage(manager); + + // uuid 存于管理器 +8 的 MSVC std::string(size@+0x10、cap@+0x14);异步回填故轮询 size。 + uint32_t strAddr = g_WeChatWinDllAddr + Offsets::QRCode::URL; + uint8_t cnt = 0; + while (util::get_dword(strAddr + 0x10) == 0) { + if (cnt > 5) { + LOG_ERROR("Refresh QR Code timeout."); + return ""; + } + Sleep(1000); + cnt++; } - return "http://weixin.qq.com/x/" + uri; + // 短 uuid 走 SSO(地址即缓冲区),容量 >= 16 时 +0 处为堆指针。 + uint32_t size = util::get_dword(strAddr + 0x10); + uint32_t cap = util::get_dword(strAddr + 0x14); + const char *uuid = (cap >= 16) ? *reinterpret_cast(strAddr) + : reinterpret_cast(strAddr); + if (uuid == nullptr) { + return ""; + } + return "http://weixin.qq.com/x/" + std::string(uuid, size); } int receive_transfer(const std::string &wxid, const std::string &transferid, const std::string &transactionid) { - LOG_ERROR("技术太菜,实现不了。"); - return -1; + if (g_WeChatWinDllAddr == 0) { + LOG_ERROR("WeChatWin.dll not located."); + return -1; + } + + int rv = -1; + + // WCPayInfo 带虚表且约 0x158 字节;先默认构造再逐字段 ASSIGN,最后析构。缓冲取 0x160 冗余覆盖。 + char payInfo[0x160] = { 0 }; + + std::wstring wsWxid = util::s2w(wxid); + std::wstring wsTfid = util::s2w(transferid); + std::wstring wsTaid = util::s2w(transactionid); + + // wxid 仅被受理入口读取(内部转 std::string、真实调用者调用后自行 free),故用非拥有 WxString 视图。 + WxString wxWxid(wsWxid); + + LOG_DEBUG("Receiving transfer, from: {}, transferid: {}, transactionid: {}", wxid, transferid, transactionid); + + auto initPayInfo = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Transfer::PAY_INFO_CTOR); + auto receiveTransfer = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Transfer::RECV_TRANSFER); + auto assignField = reinterpret_cast(g_WeChatWinDllAddr + Offsets::RichText::ASSIGN); + auto destroyPayInfo = reinterpret_cast(g_WeChatWinDllAddr + Offsets::Transfer::PAY_INFO_DTOR); + + // 受理入口会拷贝构造 payInfo,故所有成员须先默认构造初始化 + initPayInfo(payInfo); + + *reinterpret_cast(payInfo + Offsets::Transfer::F_PAYSUBTYPE) = 1; + *reinterpret_cast(payInfo + Offsets::Transfer::F_EFFECTIVE_DATE) = 1; + + // 交易号/转账单号会被析构器 mm_free,须用 ASSIGN 建 WeChat 拥有副本 + assignField(payInfo + Offsets::Transfer::F_TRANSACTION_ID, wsTaid.c_str(), -1); + assignField(payInfo + Offsets::Transfer::F_TRANSFER_ID, wsTfid.c_str(), -1); + + // confirm=1 表示接受/领取;scratch 为废弃占位传 0。 + rv = receiveTransfer(payInfo, &wxWxid, 0, 1); + destroyPayInfo(payInfo); + + return rv; } bool rpc_get_audio(const AudioMsg &am, uint8_t *out, size_t *len) { - return fill_response( - out, len, [&](Response &rsp) { rsp.msg.str = (char *)get_audio(am.id, am.dir).c_str(); }); + return fill_response(out, len, [&am](Response &rsp) { + std::string path = ""; + if (am.dir == NULL) { + LOG_ERROR("Empty dir."); + } else { + path = get_audio(am.id, am.dir); + } + rsp.msg.str = (char *)path.c_str(); + }); } -bool rpc_get_pcm_audio(uint64_t id, const fs::path &dir, int32_t sr, uint8_t *out, size_t *len) { return false; } - bool rpc_decrypt_image(const DecPath &dec, uint8_t *out, size_t *len) { - return fill_response( - out, len, [&](Response &rsp) { rsp.msg.str = (char *)decrypt_image(dec.src, dec.dst).c_str(); }); -} - -bool rpc_get_login_url(uint8_t *out, size_t *len) -{ - return fill_response( - out, len, [&](Response &rsp) { rsp.msg.str = (char *)get_login_url().c_str(); }); + return fill_response(out, len, [&dec](Response &rsp) { + std::string path = ""; + if ((dec.src == NULL) || (dec.dst == NULL)) { + LOG_ERROR("Empty src or dst."); + } else { + path = decrypt_image(dec.src, dec.dst); + } + rsp.msg.str = (char *)path.c_str(); + }); } bool rpc_refresh_pyq(uint64_t id, uint8_t *out, size_t *len) { - return fill_response(out, len, - [&](Response &rsp) { rsp.msg.status = refresh_pyq(id); }); + return fill_response(out, len, [id](Response &rsp) { + int status = refresh_pyq(id); + rsp.msg.status = status; + }); } bool rpc_download_attachment(const AttachMsg &att, uint8_t *out, size_t *len) { - int status = -1; - if (att.thumb || att.extra) { + return fill_response(out, len, [&att](Response &rsp) { std::string thumb = att.thumb ? att.thumb : ""; std::string extra = att.extra ? att.extra : ""; - status = download_attachment(att.id, thumb, extra); - } else { - LOG_ERROR("文件地址不能全为空"); - } - - return fill_response(out, len, [&](Response &rsp) { rsp.msg.status = status; }); + int status = download_attachment(att.id, thumb, extra); + rsp.msg.status = status; + }); } bool rpc_revoke_message(uint64_t id, uint8_t *out, size_t *len) { - return fill_response(out, len, - [&](Response &rsp) { rsp.msg.status = revoke_message(id); }); + return fill_response(out, len, [id](Response &rsp) { + int status = revoke_message(id); + rsp.msg.status = status; + }); } -bool rpc_get_ocr_result(const fs::path &path, uint8_t *out, size_t *len) +bool rpc_get_ocr_result(const std::string &path, uint8_t *out, size_t *len) { - auto ret = get_ocr_result(path); - return fill_response(out, len, [&](Response &rsp) { - rsp.msg.ocr.status = ret.status; - rsp.msg.ocr.result = (char *)ret.result.c_str(); + return fill_response(out, len, [&path](Response &rsp) { + OcrResult_t ocr = get_ocr_result(path); + rsp.msg.ocr.status = ocr.status; + rsp.msg.ocr.result = (char *)ocr.result.c_str(); }); } -bool rpc_receive_transfer(const Transfer &tf, uint8_t *out, size_t *len) +bool rpc_get_login_url(uint8_t *out, size_t *len) { - return fill_response( - out, len, [&](Response &rsp) { rsp.msg.status = receive_transfer(tf.wxid, tf.tfid, tf.taid); }); + return fill_response(out, len, [](Response &rsp) { + std::string url = get_login_url(); + rsp.msg.str = (char *)url.c_str(); + }); } -bool rpc_shutdown(uint8_t *out, size_t *len) +bool rpc_receive_transfer(const Transfer &tf, uint8_t *out, size_t *len) { - return fill_response(out, len, [&](Response &rsp) { - rsp.msg.status = 0; - std::thread([]() { - Sleep(100); - RpcServer::destroyInstance(); - Spy::Cleanup(); - }).detach(); - return true; + return fill_response(out, len, [&tf](Response &rsp) { + if ((tf.wxid == NULL) || (tf.tfid == NULL) || (tf.taid == NULL)) { + LOG_ERROR("Empty wxid, tfid or taid."); + rsp.msg.status = -1; + } else { + int status = receive_transfer(tf.wxid, tf.tfid, tf.taid); + rsp.msg.status = status; + } }); } + } // namespace misc diff --git a/WeChatFerry/spy/misc_manager.h b/WeChatFerry/spy/misc_manager.h index 1351438a..3012672b 100644 --- a/WeChatFerry/spy/misc_manager.h +++ b/WeChatFerry/spy/misc_manager.h @@ -1,41 +1,31 @@ #pragma once #include -#include -#include +#include #include -#include #include "wcf.pb.h" - #include "pb_types.h" namespace misc { -std::string get_audio(uint64_t id, const std::filesystem::path &dir); -std::string get_pcm_audio(uint64_t id, const std::filesystem::path &dir, int32_t sr); -std::string decrypt_image(const std::filesystem::path &src, const std::filesystem::path &dst); -std::string get_login_url(); - +std::string get_audio(uint64_t id, const std::string &dir); +std::string decrypt_image(const std::string &src, const std::string &dst); int refresh_pyq(uint64_t id); -int download_attachment(uint64_t id, const std::filesystem::path &thumb, const std::filesystem::path &extra); +int download_attachment(uint64_t id, const std::string &thumb, const std::string &extra); int revoke_message(uint64_t id); - -OcrResult_t get_ocr_result(const std::filesystem::path &path); +OcrResult_t get_ocr_result(const std::string &path); +std::string get_login_url(); int receive_transfer(const std::string &wxid, const std::string &transferid, const std::string &transactionid); -// RPC -// clang-format off bool rpc_get_audio(const AudioMsg &am, uint8_t *out, size_t *len); -bool rpc_get_pcm_audio(uint64_t id, const std::filesystem::path &dir, int32_t sr, uint8_t *out, size_t *len); bool rpc_decrypt_image(const DecPath &dec, uint8_t *out, size_t *len); -bool rpc_get_login_url(uint8_t *out, size_t *len); bool rpc_refresh_pyq(uint64_t id, uint8_t *out, size_t *len); bool rpc_download_attachment(const AttachMsg &att, uint8_t *out, size_t *len); bool rpc_revoke_message(uint64_t id, uint8_t *out, size_t *len); -bool rpc_get_ocr_result(const std::filesystem::path &path, uint8_t *out, size_t *len); +bool rpc_get_ocr_result(const std::string &path, uint8_t *out, size_t *len); +bool rpc_get_login_url(uint8_t *out, size_t *len); bool rpc_receive_transfer(const Transfer &tf, uint8_t *out, size_t *len); -bool rpc_shutdown(uint8_t *out, size_t *len); -// clang-format on + } // namespace misc diff --git a/WeChatFerry/spy/offsets.h b/WeChatFerry/spy/offsets.h index a29370e7..ec9aea4e 100644 --- a/WeChatFerry/spy/offsets.h +++ b/WeChatFerry/spy/offsets.h @@ -2,155 +2,269 @@ #include +// x86 平台偏移地址定义 +// 支持微信版本: 3.9.12.56 + namespace Offsets { namespace Account { - constexpr uint64_t SERVICE = 0x1B5CA40; // 账户服务 - constexpr uint64_t PATH = 0x25F4A40; // 数据路径 - constexpr uint64_t WXID = 0x80; // WXID - constexpr uint64_t NAME = 0x1E8; // 昵称 - constexpr uint64_t MOBILE = 0x128; // 手机号 - constexpr uint64_t LOGIN = 0x7F8; // 登录状态 - constexpr uint64_t ALIAS = 0x108; // 修改后的WXID -} + constexpr uint32_t SERVICE = 0x4368508; // 登录状态服务(登录后非零) + constexpr uint32_t WXID = 0x4368354; // 微信ID(std::string,容量位 WXID+0x14) + constexpr uint32_t NAME = 0x4368460; // 昵称(std::string) + constexpr uint32_t MOBILE = 0x43683D0; // 手机号(std::string) + constexpr uint32_t HOME = 0x434BD64; // 数据目录(wchar_t* 指向文档父目录) + constexpr uint32_t LOGIN = 0x0; // 登录状态偏移(相对 SERVICE) +} // namespace Account -namespace Chatroom +namespace Message { - constexpr uint64_t MGR = 0x1B8AE40; - constexpr uint64_t NEW = 0x262D800; - constexpr uint64_t DEL = 0x2163070; - constexpr uint64_t ADD = 0x2162A30; - constexpr uint64_t INV = 0x2162410; -} + namespace Send + { + // 共享辅助: + constexpr uint32_t SEND_MGR_GETTER = 0x1197AC0; // SendMessageMgr 单例 getter + constexpr uint32_t CHATMSG_DTOR = 0x1199010; // ChatMsg::~ChatMsg(对象恰好 0x2D8 字节,=Receive::CALL) + + // 发送文本: + constexpr uint32_t SEND_MSG = 0x1783C10; // SendMessageMgr::sendMsg(__fastcall,ecx=buffer/edx=wxid,6 栈参 msg/at/1/0/0/0) + + // 发送图片(getter/dtor 与文本发送共用): + constexpr uint32_t SEND_IMAGE = 0x1783120; // SendMessageMgr 图片提交叶子(__thiscall(mgr,buf,receiver,path,options);options 布局见 message_sender.cpp) + + // 发送文件:走 AppMsgMgr::sendFile(getter=RichText::GETTER、assign=RichText::ASSIGN、dtor=CHATMSG_DTOR 复用)。 + // __usercall 建模为 __thiscall(ecx=manager,其余全部栈参);receiver/path 及三个空串会被 mm_free,须用 ASSIGN 建拥有副本。 + constexpr uint32_t SEND_FILE = 0x1621EF0; // AppMsgMgr::sendFile 提交叶子 + + // send_xml 发原始 appmsg XML(type 为 appmsg 子类型,如 0x21 小程序)。__usercall 建模为 __fastcall(ecx=ChatMsg, edx=from), + // 原始 XML 走 a4→content、type→a7、flag=1 跳过内部 ;各 WxString 仅被读取深拷贝进 ChatMsg(用非拥有视图即可)。 + constexpr uint32_t SEND_APPMSG = 0x1621280; // appmsg 发送核心(多入口共用) + constexpr uint32_t CHATMSG_CTOR = 0x1A0ED0; // ChatMsg 默认构造器(__thiscall(this)→this) + + // send_emotion 发本地自定义表情。SEND_CUSTOM_EMOTION 为 __thiscall(ecx=manager, retn 0x5C 被调清栈); + // path/wxid 及两个空串会被 mm_free,须用 ASSIGN 建拥有副本。buffer 为 0x1C 置零小结构,非 ChatMsg、无 dtor。 + constexpr uint32_t EMO_MGR_GETTER = 0x1208250; // CustomSmileyMgr 单例 getter(返回 &对象本体) + constexpr uint32_t SEND_CUSTOM_EMOTION = 0x6C10C0; // CustomSmileyMgr::sendCustomEmotion 发送叶子 + } // namespace Send + + namespace Receive + { + // 收消息定点:hook ~ChatMsg 入口后按“返回地址==HOOK+5”过滤,仅当来自 doAddMsg 尾部那条 call 时才 dispatch + constexpr uint32_t HOOK = 0x17B93B5; // 定点 call 站点(返回地址过滤用 HOOK+5) + constexpr uint32_t CALL = 0x1199010; // ChatMsg::~ChatMsg 入口(detour 目标;前 5 字节可安全搬进 trampoline) + // ---- ChatMsg 对象字段(相对已解析 ChatMsg 基址)---- + constexpr uint32_t MSG_ID = 0x30; + constexpr uint32_t TYPE = 0x38; + constexpr uint32_t IS_SELF = 0x3C; + constexpr uint32_t TS = 0x44; + constexpr uint32_t ROOM_ID = 0x48; // std::wstring + constexpr uint32_t CONTENT = 0x70; // std::wstring + constexpr uint32_t WXID = 0x188; // std::string + constexpr uint32_t SIGN = 0x19C; + constexpr uint32_t THUMB = 0x1B0; + constexpr uint32_t EXTRA = 0x1C4; + constexpr uint32_t MSG_XML = 0x204; + } // namespace Receive +} // namespace Message namespace Contact { - constexpr uint64_t MGR = 0x1B489D0; - constexpr uint64_t LIST = 0x21ACBE0; - constexpr uint64_t BIN = 0x200; - constexpr uint64_t BIN_LEN = 0x208; - constexpr uint64_t WXID = 0x10; - constexpr uint64_t CODE = 0x30; - constexpr uint64_t REMARK = 0x80; - constexpr uint64_t NAME = 0xA0; - constexpr uint64_t GENDER = 0x0E; - constexpr uint64_t STEP = 0x6A8; - - constexpr uint64_t VERIFY_NEW = Chatroom::NEW; - constexpr uint64_t VERIFY_OK = 0x1F48850; - constexpr uint64_t ADD_FRIEND_HELPER = 0x4F7FB18; // a1 - constexpr uint64_t FVDF = 0x4F9DE28; // FriendVeriyDialogFragment -} - -namespace Db -{ - constexpr uint64_t INSTANCE = 0x59D2008; // 数据库实例地址 - constexpr uint64_t MSG_I = 0x5A30158; // MSGi.db & MediaMsgi.db - constexpr uint64_t MICROMSG = 0xB8; - constexpr uint64_t CHAT_MSG = 0x2C8; - constexpr uint64_t MISC = 0x5F0; - constexpr uint64_t EMOTION = 0x15F0; - constexpr uint64_t MEDIA = 0xF48; - constexpr uint64_t BIZCHAT_MSG = 0x1AC0; - constexpr uint64_t FUNCTION_MSG = 0x1B98; - constexpr uint64_t NAME = 0x28; - - // SQLITE3 - constexpr uint64_t EXEC = 0x3A820A0; - // constexpr uint64_t BACKUP_INIT = EXEC - 0x1D113E0; - constexpr uint64_t PREPARE = EXEC + 0x7CB0; - // constexpr uint64_t OPEN = EXEC - 0x1CA2430; - // constexpr uint64_t BACKUP_STEP = EXEC - 0x1D110A0; - // constexpr uint64_t BACKUP_REMAINING = EXEC - 0x1D10880; - // constexpr uint64_t BACKUP_PAGECOUNT = EXEC - 0x1D10890; - // constexpr uint64_t BACKUP_FINISH = EXEC - 0x1D10940; - // constexpr uint64_t SLEEP = EXEC - 0x1CA1BB0; - // constexpr uint64_t ERRCODE = EXEC - 0x1CA3770; - // constexpr uint64_t CLOSE = EXEC - 0x1CA4FD0; - constexpr uint64_t STEP = EXEC - 0x3C000; - constexpr uint64_t COLUMN_COUNT = EXEC - 0x3B7E0; - constexpr uint64_t COLUMN_NAME = EXEC - 0x3ADE0; - constexpr uint64_t COLUMN_TYPE = EXEC - 0x3AF90; - constexpr uint64_t COLUMN_BLOB = EXEC - 0x3B7B0; - constexpr uint64_t COLUMN_BYTES = EXEC - 0x3B6C0; - constexpr uint64_t FINALIZE = EXEC - 0x3CF50; -} + constexpr uint32_t BASE = 0x75A4A0; + constexpr uint32_t HEAD = 0xC089F0; + constexpr uint32_t WXID = 0x10; + constexpr uint32_t CODE = 0x24; + constexpr uint32_t REMARK = 0x58; + constexpr uint32_t NAME = 0x6C; + constexpr uint32_t GENDER = 0x0E; + constexpr uint32_t COUNTRY = 0x00; + constexpr uint32_t PROVINCE = 0x00; + constexpr uint32_t CITY = 0x00; +} // namespace Contact -namespace Message +namespace Database { - namespace Log - { - constexpr uint64_t LEVEL = 0x578DF28; // 日志级别 - constexpr uint64_t CALL = 0x2627590; // 日志函数 - } + // ---- 裸 sqlite3 API(RVA,运行时 + g_WeChatWinDllAddr)---- + // 3.9.12.56 走进程内已解密句柄:从已开库管理器取裸 sqlite3*,直接调下列裸 sqlite 函数。 + constexpr uint32_t PREPARE_V2 = 0x2A97C50; // sqlite3_prepare_v2(db, zSql, nByte, &ppStmt, &pzTail) + constexpr uint32_t STEP = 0x2A5E2E0; // sqlite3_step(stmt) → 100=ROW/101=DONE + constexpr uint32_t FINALIZE = 0x2A5D330; // sqlite3_finalize(stmt) + constexpr uint32_t COLUMN_COUNT = 0x2A5E840; // sqlite3_column_count(stmt) + constexpr uint32_t COLUMN_NAME = 0x2A5EEB0; // sqlite3_column_name(stmt, iCol) + constexpr uint32_t COLUMN_TYPE = 0x2A5EDA0; // sqlite3_column_type(stmt, iCol) 1I/2F/3T/4B/5NULL + constexpr uint32_t COLUMN_BLOB = 0x2A5E8E0; // sqlite3_column_blob(stmt, iCol) + constexpr uint32_t COLUMN_BYTES = 0x2A5E970; // sqlite3_column_bytes(stmt, iCol) + constexpr uint32_t COLUMN_TEXT = 0x2A5EC60; // sqlite3_column_text(stmt, iCol) - namespace Receive - { - constexpr uint64_t CALL = 0x214C6C0; // 接收消息 Call - constexpr uint64_t ID = 0x30; // 消息 ID - constexpr uint64_t TYPE = 0x38; // 消息类型 - constexpr uint64_t SELF = 0x3C; // 消息是否来自自己 - constexpr uint64_t TIMESTAMP = 0x44; // 消息时间戳 - constexpr uint64_t ROOMID = 0x48; // 群聊 ID(或者发送者 wxid) - constexpr uint64_t CONTENT = 0x88; // 消息内容 - constexpr uint64_t WXID = 0x240; // 发送者 wxid - constexpr uint64_t SIGN = 0x260; // 消息签名 - constexpr uint64_t THUMB = 0x280; // 缩略图路径 - constexpr uint64_t EXTRA = 0x2A0; // 原图路径 - constexpr uint64_t XML = 0x308; // 消息 XML - - constexpr uint64_t PYQ_CALL = 0x2E621D0; // 接收朋友圈 Call - constexpr uint64_t PYQ_START = 0x30; // 开始地址 - constexpr uint64_t PYQ_END = 0x38; // 结束地址 - constexpr uint64_t PYQ_SENDER = 0x18; // 发布者 - constexpr uint64_t PYQ_TS = 0x38; // 时间戳 - constexpr uint64_t PYQ_CONTENT = 0x48; // 文本内容 - constexpr uint64_t PYQ_XML = 0x9B8; // 其他内容 - } + // ---- 已打开数据库管理器(进程内已解密句柄,扁平枚举)---- + // managerObj = *(base + INSTANCE);[managerObj+START, managerObj+END) 逐 4 字节遍历得 storage 对象指针; + // storage+SLOT = 裸 sqlite3*,storage+NAME = 库路径 std::wstring。 + constexpr uint32_t INSTANCE = 0x4327610; // AccountStorageMgr 单例(存管理器对象指针) + constexpr uint32_t START = 0x1430; // managerObj → 主 storage 数组 begin 指针 + constexpr uint32_t END = 0x1434; // managerObj → 主 storage 数组 end 指针 + constexpr uint32_t SLOT = 0x34; // storage → 裸 sqlite3* 句柄 + constexpr uint32_t NAME = 0x4C; // storage → 库路径 std::wstring(容量位 +0x14) - namespace Send - { - constexpr uint64_t MGR = 0x1B5B210; - constexpr uint64_t INSTANCE = 0x1B653B0; - constexpr uint64_t FREE = 0x1B5CAC0; - constexpr uint64_t TEXT = 0x22D4A90; - constexpr uint64_t IMAGE = 0x22CA2A0; - constexpr uint64_t APP_MGR = 0x1B601E0; - constexpr uint64_t FILE = 0x20DE200; - constexpr uint64_t XML = 0x20DD330; - constexpr uint64_t XML_BUF_SIGN = 0x2503760; - constexpr uint64_t EMOTION_MGR = 0x1BD6300; - constexpr uint64_t EMOTION = 0x21C2EE0; - - constexpr uint64_t NEW_MM_READER = 0x1B64900; - constexpr uint64_t FREE_MM_READER = 0x1B63CD0; - constexpr uint64_t RICH_TEXT = 0x20E81E0; - - constexpr uint64_t PAT = 0x2CCDDC0; - - constexpr uint64_t FORWARD = 0x22D4010; - } -} - -namespace Misc -{ - constexpr uint64_t QR_CODE = 0x202D3C0; - - constexpr uint64_t INSATNCE = Message::Send::INSTANCE; - constexpr uint64_t FREE = Message::Send::FREE; - constexpr uint64_t CHAT_MGR = 0x1B8E930; - constexpr uint64_t PRE_LOCAL_ID_MGR = 0x214D430; - constexpr uint64_t PRE_DOWNLOAD_MGR = 0x1C17660; - constexpr uint64_t PUSH_ATTACH_TASK = 0x1CE8500; - - namespace Sns - { - constexpr uint64_t DATA_MGR = 0x21F00D0; - constexpr uint64_t TIMELINE = 0x2DD2320; - constexpr uint64_t FIRST = 0x2E40810; - constexpr uint64_t NEXT = 0x2E663C0; - } -} -} + // ---- MSG / MediaMSG 多库(MultiDBMsgMgr,独立于上面的主 storage 数组)---- + // 消息与语音库由 MultiDBMsgMgr 单例的环形 item 指针数组管理(惰性构造,未登录时 mgr 为 0): + // 逻辑索引 i 的 item = *(base + 4 * ((i + head) & (cap - 1))),cap 为 2 的幂。 + constexpr uint32_t MSG_MGR = 0x436A8CC; // MultiDBMsgMgr 单例对象指针 + constexpr uint32_t MSG_RING_BASE = 0x2C; // mgr → 环形 item 指针数组基址 + constexpr uint32_t MSG_RING_CAP = 0x30; // mgr → 环形容量(2 的幂) + constexpr uint32_t MSG_RING_HEAD = 0x34; // mgr → 环形头索引 + constexpr uint32_t MSG_RING_COUNT = 0x38; // mgr → item 数量 + constexpr uint32_t MSG_ITEM_NAME = 0x00; // item → MSGn.db 名 WxString(wptr@0) + constexpr uint32_t MSG_ITEM_HANDLE = 0x60; // item → MSG 裸 sqlite3* + constexpr uint32_t MSG_ITEM_MEDIA = 0x14; // item → MediaMSG 存储包装(StorageBase 派生) + constexpr uint32_t MEDIA_NAME = 0x4C; // 媒体包装 → MediaMSGn.db 名 WxString(wptr@0) + constexpr uint32_t MEDIA_HANDLE = 0x38; // 媒体包装 → 裸 sqlite3* + + // ---- 生库密钥(SQLCipher codec 链,供离线解密)---- + // 同一账号所有库共用同一 32 字节生密钥,从任一已开库句柄沿下链即可读出(无需 Hook): + // db → +DB_BTREE → +BTREE_SHARED → +SHARED_PAGER → +PAGER_CODEC → +CODEC_READCTX=cipher_ctx + // → 密钥指针 +CIPHER_PASS、长度 +CIPHER_PASSSZ + // 注:均为结构体字段偏移(非 RVA),使用时不加 g_WeChatWinDllAddr;仅登录并打开过库后才有效。 + constexpr uint32_t DB_BTREE = 0x14; // sqlite3* → aDb[0].pBt + constexpr uint32_t BTREE_SHARED = 0x04; // Btree → BtShared + constexpr uint32_t SHARED_PAGER = 0x00; // BtShared → Pager(首字段) + constexpr uint32_t PAGER_CODEC = 0xDC; // Pager → codec(pCodec) + constexpr uint32_t CODEC_READCTX = 0x54; // codec → read cipher_ctx + constexpr uint32_t CIPHER_PASS = 0x10; // cipher_ctx → 生密钥指针(pass) + constexpr uint32_t CIPHER_PASSSZ = 0x04; // cipher_ctx → 生密钥字节数(pass_sz) +} // namespace Database + +namespace Friend +{ + // 通过好友申请:走 AddFriendHelper。VerifyOK 为纯 __thiscall(ecx=this, retn 0x30 被调清栈); + // v3(加密 username)仅被读取用非拥有视图,v4(ticket)会被 mm_free 须用 ASSIGN 建拥有副本。 + constexpr uint32_t ACCEPT_CTOR = 0x4CBD20; // AddFriendHelper 构造器 + constexpr uint32_t VERIFY_OK = 0x4CCE70; // AddFriendHelper::VerifyOK + constexpr uint32_t ACCEPT_DTOR = 0x4CBE40; // AddFriendHelper 析构器 +} // namespace Friend + +namespace Chatroom +{ + // ChatRoomMgr 单例 getter,add/del/invite 三条链共用;roomid 均会被 mm_free,须用 ASSIGN 建拥有副本。 + constexpr uint32_t MGR_GETTER = 0x11C43A0; + constexpr uint32_t ADD_MEMBER = 0x167F680; // ChatRoomMgr::doAddMemberToChatRoom(__thiscall, retn 0x20) + constexpr uint32_t DEL_MEMBER = 0x167FBD0; // ChatRoomMgr::doDelMemberFromChatRoom(__thiscall, retn 0x18) + // NetSceneInviteChatRoomMember 构建器 + 内部 doScene 发送(__stdcall(members, roomid 按值, 上下文 shared_ptr 按值)); + // 内部按 roomid 是否以 "@im.chatroom" 结尾分流普通/OpenIM,上下文 shared_ptr 传 {0,0}。 + constexpr uint32_t INVITE_MEMBER = 0x167F280; +} // namespace Chatroom + +namespace Transfer +{ + // 领取转账走 WCPayInfo(带虚表)+ TenPayTransfer 受理入口。RECV_TRANSFER __usercall 建模为 __fastcall(ecx=payInfo, edx=wxid); + // wxid 只读用非拥有视图,transactionid/transferid 用 ASSIGN 建拥有副本(由 PAY_INFO_DTOR mm_free)。 + constexpr uint32_t PAY_INFO_CTOR = 0x120AF00; // WCPayInfo 默认构造器 + constexpr uint32_t RECV_TRANSFER = 0x120DE350; // TenPayTransfer 受理入口(confirm=1 接受/=0 拒绝) + constexpr uint32_t PAY_INFO_DTOR = 0x111D4570; // WCPayInfo 析构器 + + // WCPayInfo 字段偏移。 + constexpr uint32_t F_PAYSUBTYPE = 0x04; // 支付子类型 int + constexpr uint32_t F_TRANSACTION_ID = 0x1C; // 交易号 WxString + constexpr uint32_t F_TRANSFER_ID = 0x38; // 转账单号 WxString + constexpr uint32_t F_EFFECTIVE_DATE = 0x4C; // 生效标志 int +} // namespace Transfer + +namespace Moments +{ + // 接收路径:hook OnSnsTimeLineSceneFinish 入口后按返回地址==HOOK+5 过滤,此时 a2 即容器指针 + constexpr uint32_t HOOK = 0x1FDB1E5; + constexpr uint32_t CALL = 0x1FDB4D0; // SnsTimeLineMgr::OnSnsTimeLineSceneFinish 入口(2 个调用者) + // 刷新路径(refresh_pyq):两个 Scene 构建器内部自建 NetScene 经 doScene 直接发送 + constexpr uint32_t MGR_GETTER = 0x1F77D90; // SnsTimeLineMgr 单例 getter + constexpr uint32_t GET_FIRST_PAGE = 0x1FDE8A0; // TryGetFirstPageScene(__thiscall(manager, forward), retn 4) + constexpr uint32_t GET_NEXT_PAGE = 0x1FDED90; // GetNextPageScene(__thiscall(manager, id_low, id_high), retn 8) + // 容器字段:主 feed 数组 begin/end 指针(容器 = a2) + constexpr uint32_t START = 0x20; + constexpr uint32_t END = 0x24; + // 单个 SnsObject(元素,STEP 字节)内字段: + constexpr uint32_t TS = 0x2C; + constexpr uint32_t WXID = 0x18; // 内层子对象首个 std::string + constexpr uint32_t CONTENT = 0x3C; // 内层子对象第 2 个 std::string + constexpr uint32_t XML = 0x64C; + constexpr uint32_t STEP = 0xE18; // 单个 SnsObject 大小 +} // namespace Moments + +namespace Attachment +{ + // 下载附件走 PreDownLoadMgr(ChatMsg 缓冲复用 Send::CHATMSG_CTOR/DTOR,路径拷贝复用 RichText::ASSIGN)。 + // LOAD_MSG(ChatMgr::GetMgrByPrefixLocalId)__usercall 建模为 __thiscall,与 forward 共用; + // PUSH_TASK 纯 __thiscall(retn 0x10),调用形如 push_attach_task(manager, buffer, 0, 1, 0)。 + constexpr uint32_t WARMUP = 0x11C7290; // ChatMgr 懒初始化 + constexpr uint32_t LOAD_MSG = 0x166FE00; // ChatMgr::GetMgrByPrefixLocalId(按 localId/dbIdx 加载消息进 ChatMsg) + constexpr uint32_t MGR_GETTER = 0x1240DE0; // PreDownLoadMgr 单例 getter + constexpr uint32_t PUSH_TASK = 0x12DA8A0; // PreDownLoadMgr::push_attach_task + + // ChatMsg 内下载目标字段(type 读 Message::Receive::TYPE)。路径字段会被 ~ChatMsg mm_free,须用 ASSIGN 建拥有副本。 + constexpr uint32_t F_THUMB_PATH = 0x1A4; // 缩略图落盘路径 WxString + constexpr uint32_t F_SAVE_PATH = 0x1B8; // 附件落盘路径 WxString + constexpr uint32_t F_INITED_FLAG = 0x2AC; // "子对象已初始化"标志(置 1 跳过重复 init) +} // namespace Attachment + +namespace Revoke +{ + // 撤回消息走 ChatRevokeMgr::revokeMsg(ChatMsg 缓冲复用 Send::CHATMSG_CTOR/DTOR,加载消息复用 Attachment::LOAD_MSG)。 + // REVOKE_MSG 纯 __thiscall(ecx=manager, chat_msg, retn 4)。 + constexpr uint32_t MGR_GETTER = 0x1245280; // ChatRevokeMgr 单例 getter(返回 &对象本体) + constexpr uint32_t REVOKE_MSG = 0x12469F0; // ChatRevokeMgr::revokeMsg +} // namespace Revoke + +namespace RichText +{ + // 发送消息卡片:业务对象 MMReaderItem + AppMsgMgr 单例。 + // 编排:CTOR(buff) → 用 ASSIGN 逐字段深拷贝 → GETTER()=manager → SEND(manager, receiver 按值, buff) → DTOR(buff)。 + // receiver 会被 SEND mm_free,须用 ASSIGN 建 WeChat 拥有副本。 + constexpr uint32_t CTOR = 0x11A0220; // MMReaderItem::MMReaderItem(__thiscall(this)) + constexpr uint32_t GETTER = 0x119C990; // AppMsgMgr 单例 getter + constexpr uint32_t ASSIGN = 0x19DAF20; // WxString::assign(src,len)(__thiscall, mm_realloc 深拷贝) + constexpr uint32_t SEND = 0x1628DC0; // AppMsgMgr::sendAppMsg(__thiscall(manager, WxString receiver 按值, MMReaderItem* buff)) + constexpr uint32_t DTOR = 0x119F530; // MMReaderItem::~MMReaderItem 完整析构 + + // MMReaderItem 内 WxString 成员偏移(=该成员 wptr 的字节偏移)。 + constexpr uint32_t F_TITLE = 0x4; // 标题 + constexpr uint32_t F_URL = 0x2C; // 链接 + constexpr uint32_t F_THUMBURL = 0x6C; // 缩略图 URL + constexpr uint32_t F_DIGEST = 0x94; // 摘要 + constexpr uint32_t F_ACCOUNT = 0x1B4; // 源用户名 + constexpr uint32_t F_NAME = 0x1C8; // 源显示名 + constexpr uint32_t OBJ_SIZE = 0x280; // MMReaderItem 栈缓冲大小 +} // namespace RichText + +namespace Pat +{ + // 拍一拍走 PatMgr。SEND_PAT(PatMgr::SendPatMsg)__usercall 建模为 __fastcall(ecx=roomid, edx=wxid), + // 栈参 manager/0/0,返回 al 非零即成功。 + constexpr uint32_t MGR_GETTER = 0x124A670; + constexpr uint32_t SEND_PAT = 0x1EB5D50; +} // namespace Pat + +namespace OCR +{ + // 图片 OCR 走 OCRManager。RUN_OCR(DoOCRTask)__usercall 建模为 __thiscall(ecx=manager, 5 栈参),返回 task_id; + // 异步:仅缓存命中时同步回填 result_list 并置 found_flag=1、返回 0(结点文本 WxString 在 node+0x14)。 + constexpr uint32_t MGR_GETTER = 0x12B5F50; + constexpr uint32_t RUN_OCR = 0x1E7A2E0; + constexpr uint32_t RESULT_NEW = 0x3234B97; // 全局 operator new(分配链表哨兵结点,与 RESULT_DTOR 配对) + constexpr uint32_t RESULT_DTOR = 0x12B6140; // OCR 结果链表析构器 +} // namespace OCR + +namespace Forward +{ + // 转发消息走 SendMessageMgr::forwordMsg。__usercall 建模为 __fastcall(ecx=scene, edx=msgPtr), + // 栈参 receiver 按值 + localId + dbIdx;msgPtr=0 走加载路径、scene=5 仅统计元数据; + // receiver 会被末尾 mm_free,须用 RichText::ASSIGN 建拥有副本。 + constexpr uint32_t FORWARD_MSG = 0x1783230; // SendMessageMgr::forwordMsg +} // namespace Forward + +namespace QRCode +{ + // 刷新登录二维码走 QRCodeLoginMgr。getQRCodeImage 经 doScene 异步获取,完成后回填 uuid; + // URL 是 MSVC std::string(size@+0x10、cap@+0x14,短串走 SSO 地址即缓冲区、长串 +0 处为堆指针)。 + constexpr uint32_t MGR_GETTER = 0x1589600; // QRCodeLoginMgr 单例 getter + constexpr uint32_t GET_QRCODE = 0x17725A0; // QRCodeLoginMgr::getQRCodeImage + constexpr uint32_t URL = 0x436C398; // 登录 uuid std::string(管理器 +8) +} // namespace QRCode + +} // namespace Offsets diff --git a/WeChatFerry/spy/rpc_helper.h b/WeChatFerry/spy/rpc_helper.h index 0ecbdcbb..9948d1d6 100644 --- a/WeChatFerry/spy/rpc_helper.h +++ b/WeChatFerry/spy/rpc_helper.h @@ -1,59 +1,58 @@ -#pragma once +#pragma once #include -#define MAGIC_ENUM_RANGE_MIN 0 -#define MAGIC_ENUM_RANGE_MAX 256 -#include - #include "wcf.pb.h" - -#include "log.hpp" #include "pb_encode.h" #include "pb_types.h" +#include "log.hpp" -static const std::unordered_map rpc_tag_map - = { { Functions_FUNC_IS_LOGIN, Response_status_tag }, - { Functions_FUNC_GET_SELF_WXID, Response_str_tag }, - { Functions_FUNC_GET_USER_INFO, Response_ui_tag }, - { Functions_FUNC_GET_MSG_TYPES, Response_types_tag }, - { Functions_FUNC_GET_CONTACTS, Response_contacts_tag }, - { Functions_FUNC_GET_DB_NAMES, Response_dbs_tag }, - { Functions_FUNC_GET_DB_TABLES, Response_tables_tag }, - { Functions_FUNC_GET_AUDIO_MSG, Response_str_tag }, - { Functions_FUNC_SEND_TXT, Response_status_tag }, - { Functions_FUNC_SEND_IMG, Response_status_tag }, - { Functions_FUNC_SEND_FILE, Response_status_tag }, - { Functions_FUNC_SEND_XML, Response_status_tag }, - { Functions_FUNC_SEND_RICH_TXT, Response_status_tag }, - { Functions_FUNC_SEND_PAT_MSG, Response_status_tag }, - { Functions_FUNC_FORWARD_MSG, Response_status_tag }, - { Functions_FUNC_SEND_EMOTION, Response_status_tag }, - { Functions_FUNC_ENABLE_RECV_TXT, Response_status_tag }, - { Functions_FUNC_DISABLE_RECV_TXT, Response_status_tag }, - { Functions_FUNC_EXEC_DB_QUERY, Response_rows_tag }, - { Functions_FUNC_REFRESH_PYQ, Response_status_tag }, - { Functions_FUNC_DOWNLOAD_ATTACH, Response_status_tag }, - { Functions_FUNC_GET_CONTACT_INFO, Response_contacts_tag }, - { Functions_FUNC_ACCEPT_FRIEND, Response_status_tag }, - { Functions_FUNC_RECV_TRANSFER, Response_status_tag }, - { Functions_FUNC_REVOKE_MSG, Response_status_tag }, - { Functions_FUNC_REFRESH_QRCODE, Response_str_tag }, - { Functions_FUNC_DECRYPT_IMAGE, Response_str_tag }, - { Functions_FUNC_EXEC_OCR, Response_ocr_tag }, - { Functions_FUNC_ADD_ROOM_MEMBERS, Response_status_tag }, - { Functions_FUNC_DEL_ROOM_MEMBERS, Response_status_tag }, - { Functions_FUNC_INV_ROOM_MEMBERS, Response_status_tag }, - { Functions_FUNC_SHUTDOWN, Response_status_tag } }; +// RPC 函数类型到响应标签的映射表 +static const std::unordered_map rpc_tag_map = { + { Functions_FUNC_IS_LOGIN, Response_status_tag }, + { Functions_FUNC_GET_SELF_WXID, Response_str_tag }, + { Functions_FUNC_GET_USER_INFO, Response_ui_tag }, + { Functions_FUNC_GET_MSG_TYPES, Response_types_tag }, + { Functions_FUNC_GET_CONTACTS, Response_contacts_tag }, + { Functions_FUNC_GET_DB_NAMES, Response_dbs_tag }, + { Functions_FUNC_GET_DB_TABLES, Response_tables_tag }, + { Functions_FUNC_GET_AUDIO_MSG, Response_str_tag }, + { Functions_FUNC_GET_DB_KEY, Response_str_tag }, + { Functions_FUNC_SEND_TXT, Response_status_tag }, + { Functions_FUNC_SEND_IMG, Response_status_tag }, + { Functions_FUNC_SEND_FILE, Response_status_tag }, + { Functions_FUNC_SEND_XML, Response_status_tag }, + { Functions_FUNC_SEND_RICH_TXT, Response_status_tag }, + { Functions_FUNC_SEND_PAT_MSG, Response_status_tag }, + { Functions_FUNC_FORWARD_MSG, Response_status_tag }, + { Functions_FUNC_SEND_EMOTION, Response_status_tag }, + { Functions_FUNC_ENABLE_RECV_TXT, Response_status_tag }, + { Functions_FUNC_DISABLE_RECV_TXT, Response_status_tag }, + { Functions_FUNC_EXEC_DB_QUERY, Response_rows_tag }, + { Functions_FUNC_REFRESH_PYQ, Response_status_tag }, + { Functions_FUNC_DOWNLOAD_ATTACH, Response_status_tag }, + { Functions_FUNC_GET_CONTACT_INFO, Response_contacts_tag }, + { Functions_FUNC_ACCEPT_FRIEND, Response_status_tag }, + { Functions_FUNC_RECV_TRANSFER, Response_status_tag }, + { Functions_FUNC_REVOKE_MSG, Response_status_tag }, + { Functions_FUNC_REFRESH_QRCODE, Response_str_tag }, + { Functions_FUNC_DECRYPT_IMAGE, Response_str_tag }, + { Functions_FUNC_EXEC_OCR, Response_ocr_tag }, + { Functions_FUNC_ADD_ROOM_MEMBERS, Response_status_tag }, + { Functions_FUNC_DEL_ROOM_MEMBERS, Response_status_tag }, + { Functions_FUNC_INV_ROOM_MEMBERS, Response_status_tag } +}; -template bool fill_response(uint8_t *out, size_t *len, AssignFunc assign) +// 填充并编码 RPC 响应(无额外数据) +template +bool fill_response(uint8_t *out, size_t *len, AssignFunc assign) { Response rsp = Response_init_default; rsp.func = FuncType; auto it = rpc_tag_map.find(FuncType); if (it == rpc_tag_map.end()) { - LOG_ERROR("Unknown function type: {}", magic_enum::enum_name(rsp.func)); + LOG_ERROR("Unknown function type: %d", static_cast(FuncType)); return false; } rsp.which_msg = it->second; @@ -62,13 +61,14 @@ template bool fill_response(uint8_t *o pb_ostream_t stream = pb_ostream_from_buffer(out, *len); if (!pb_encode(&stream, Response_fields, &rsp)) { - LOG_ERROR("Encoding failed: {}", PB_GET_ERROR(&stream)); + LOG_ERROR("Encoding failed: %s", PB_GET_ERROR(&stream)); return false; } *len = stream.bytes_written; return true; } +// 填充并编码 RPC 响应(额外数据须活到编码完成,故按引用传入) template bool fill_response(uint8_t *out, size_t *len, DataType &&data, AssignFunc &&assign) { @@ -77,7 +77,7 @@ bool fill_response(uint8_t *out, size_t *len, DataType &&data, AssignFunc &&assi auto it = rpc_tag_map.find(FuncType); if (it == rpc_tag_map.end()) { - LOG_ERROR("Unknown function type: {}", magic_enum::enum_name(rsp.func)); + LOG_ERROR("Unknown function type: %d", static_cast(FuncType)); return false; } rsp.which_msg = it->second; @@ -86,7 +86,7 @@ bool fill_response(uint8_t *out, size_t *len, DataType &&data, AssignFunc &&assi pb_ostream_t stream = pb_ostream_from_buffer(out, *len); if (!pb_encode(&stream, Response_fields, &rsp)) { - LOG_ERROR("Encoding failed: {}", PB_GET_ERROR(&stream)); + LOG_ERROR("Encoding failed: %s", PB_GET_ERROR(&stream)); return false; } *len = stream.bytes_written; diff --git a/WeChatFerry/spy/rpc_server.cpp b/WeChatFerry/spy/rpc_server.cpp index 354f7da5..f9f12b52 100644 --- a/WeChatFerry/spy/rpc_server.cpp +++ b/WeChatFerry/spy/rpc_server.cpp @@ -1,7 +1,5 @@ #pragma warning(disable : 4251) -#include "rpc_server.h" - #include #include #include @@ -12,267 +10,175 @@ #include #include #include - +#include + +#if __has_include() +#include +#elif __has_include() +#include +#else +#error "magic_enum header not found" +#endif +#include #include #include +#include "wcf.pb.h" + #include "account_manager.h" #include "chatroom_manager.h" #include "contact_manager.h" #include "database_executor.h" #include "log.hpp" -#include "message_handler.h" +#include "message_receiver.h" #include "message_sender.h" #include "misc_manager.h" #include "pb_types.h" #include "pb_util.h" #include "rpc_helper.h" +#include "rpc_server.h" #include "spy.h" #include "spy_types.h" #include "util.h" -namespace fs = std::filesystem; - -constexpr size_t DEFAULT_BUF_SIZE = 16 * 1024 * 1024; - -std::unique_ptr RpcServer::instance_ = nullptr; - -RpcServer &RpcServer::getInstance() -{ - if (!instance_) { - instance_.reset(new RpcServer()); - } - return *instance_; -} - -void RpcServer::destroyInstance() -{ - if (instance_) { - instance_->stop(); - instance_.reset(); - } -} - -RpcServer::RpcServer(int port) - : port_(port), handler_(message::Handler::getInstance()), sender_(message::Sender::get_instance()) -{ - LOG_DEBUG("RpcServer 构造: 端口 {}", port_); -} - -RpcServer::~RpcServer() -{ - stop(); - LOG_DEBUG("RpcServer 被析构,释放所有资源"); -} - -std::string RpcServer::build_url(int port) -{ - return std::string(RpcServer::RPC_SERVER_ADDRESS) + ":" + std::to_string(port); -} - -int RpcServer::start(int port) -{ - if (isRunning_.load()) { - LOG_WARN("RPC 服务已在运行"); - return 1; - } - - port_ = port; - isRunning_ = true; - - try { - cmdThread_ = std::thread(&RpcServer::run_rpc_server, this); - } catch (const std::exception &e) { - LOG_ERROR("启动 RPC 服务器失败: {}", e.what()); - isRunning_ = false; - return -2; - } -#if ENABLE_WX_LOG - handler_.EnableLog(); -#endif - LOG_INFO("RPC 服务器成功启动,监听端口: {}", port_); - return 0; -} - -int RpcServer::stop() -{ - if (!isRunning_.load()) { - LOG_WARN("RPC 服务未运行"); - return 1; - } - isRunning_ = false; - - handler_.UnListenPyq(); - handler_.UnListenMsg(); -#if ENABLE_WX_LOG - handler_.DisableLog(); -#endif - nng_fini(); - if (cmdThread_.joinable()) { - LOG_DEBUG("等待命令线程关闭"); - cmdThread_.join(); - } - LOG_DEBUG("命令线程已经关闭"); - - if (msgThread_.joinable()) { - LOG_DEBUG("等待消息线程关闭"); - msgThread_.join(); - } - LOG_DEBUG("消息线程已经关闭"); - LOG_INFO("RPC 服务已停止"); - return 0; -} - -void RpcServer::on_message_callback() -{ - try { - int rv; - nng_socket msgSock = NNG_SOCKET_INITIALIZER; - Response rsp = Response_init_default; - rsp.func = Functions_FUNC_ENABLE_RECV_TXT; - rsp.which_msg = Response_wxmsg_tag; - std::vector msgBuffer(DEFAULT_BUF_SIZE); - - pb_ostream_t stream = pb_ostream_from_buffer(msgBuffer.data(), msgBuffer.size()); - - std::string url = build_url(port_ + 1); - if ((rv = nng_pair1_open(&msgSock)) != 0) { - LOG_ERROR("nng_pair0_open error {}", nng_strerror(rv)); - return; - } - - if ((rv = nng_listen(msgSock, url.c_str(), NULL, 0)) != 0) { - LOG_ERROR("nng_listen error {}", nng_strerror(rv)); - return; - } - - if ((rv = nng_setopt_ms(msgSock, NNG_OPT_SENDTIMEO, 5000)) != 0) { - LOG_ERROR("nng_setopt_ms: {}", nng_strerror(rv)); - return; - } - - while (handler_.isMessageListening()) { - std::optional msgOpt; - { - std::unique_lock lock(handler_.getMutex()); - bool hasMessage - = handler_.getConditionVariable().wait_for(lock, std::chrono::milliseconds(1000), [&]() { - lock.unlock(); - msgOpt = handler_.popMessage(); - lock.lock(); - return msgOpt.has_value(); - }); - - if (!hasMessage) { - continue; - } - } +#define URL_SIZE 20 +#define BASE_URL "tcp://0.0.0.0" +#define G_BUF_SIZE (16 * 1024 * 1024) - if (!msgOpt.has_value()) { - LOG_WARN("popMessage returned empty after wait_for success."); - continue; - } +namespace fs = std::filesystem; - WxMsg_t wxmsg = std::move(msgOpt.value()); - rsp.msg.wxmsg.id = wxmsg.id; - rsp.msg.wxmsg.is_self = wxmsg.is_self; - rsp.msg.wxmsg.is_group = wxmsg.is_group; - rsp.msg.wxmsg.type = wxmsg.type; - rsp.msg.wxmsg.ts = wxmsg.ts; - rsp.msg.wxmsg.roomid = (char *)wxmsg.roomid.c_str(); - rsp.msg.wxmsg.content = (char *)wxmsg.content.c_str(); - rsp.msg.wxmsg.sender = (char *)wxmsg.sender.c_str(); - rsp.msg.wxmsg.sign = (char *)wxmsg.sign.c_str(); - rsp.msg.wxmsg.thumb = (char *)wxmsg.thumb.c_str(); - rsp.msg.wxmsg.extra = (char *)wxmsg.extra.c_str(); - rsp.msg.wxmsg.xml = (char *)wxmsg.xml.c_str(); - - LOG_DEBUG("Push msg: {}", wxmsg.content); - pb_ostream_t stream = pb_ostream_from_buffer(msgBuffer.data(), msgBuffer.size()); - if (!pb_encode(&stream, Response_fields, &rsp)) { - LOG_ERROR("Encoding failed: {}", PB_GET_ERROR(&stream)); - continue; - } +extern int IsLogin(void); // Defined in spy.cpp - rv = nng_send(msgSock, msgBuffer.data(), stream.bytes_written, 0); - if (rv != 0) { - LOG_ERROR("msgSock-nng_send: {}", nng_strerror(rv)); - } - LOG_DEBUG("Send data length {}", stream.bytes_written); - } - nng_close(msgSock); - LOG_DEBUG("Leave MSG Server."); - } catch (const std::exception &e) { - LOG_ERROR("Fatal exception in on_message_callback: {}", e.what()); - } catch (...) { - LOG_ERROR("Unknown fatal exception in on_message_callback."); - } -} +bool gIsListening = false; +bool gIsListeningPyq = false; +std::mutex gMutex; +std::condition_variable gCV; +std::queue gMsgQueue; -bool RpcServer::start_message_listener(bool pyq, uint8_t *out, size_t *len) -{ - return fill_response(out, len, [&](Response &rsp) { - rsp.msg.status = handler_.ListenMsg(); - if (rsp.msg.status >= 0) { - if (pyq) { - handler_.ListenPyq(); - } - msgThread_ = std::thread(&RpcServer::on_message_callback, this); - } - }); -} +static int lport = 0; +static HANDLE lRpcThread = NULL; +static bool lIsRunning = false; +static nng_socket cmdSock, msgSock; // TODO: 断开检测 +static uint8_t gBuffer[G_BUF_SIZE] = { 0 }; -bool RpcServer::stop_message_listener(uint8_t *out, size_t *len) -{ - return fill_response(out, len, [&](Response &rsp) { - rsp.msg.status = handler_.UnListenMsg(); - if (rsp.msg.status >= 0) { - handler_.UnListenPyq(); - if (msgThread_.joinable()) { - msgThread_.join(); - } - } - }); -} +// Functions 枚举 → 处理函数映射表 +using RpcFunctionHandler = std::function; -const std::unordered_map RpcServer::rpcFunctionMap = { +static const std::unordered_map rpcFunctionMap = { // clang-format off - { Functions_FUNC_IS_LOGIN, [](const Request &r, uint8_t *out, size_t *len) { return account::rpc_is_logged_in(out, len); } }, - { Functions_FUNC_GET_SELF_WXID, [](const Request &r, uint8_t *out, size_t *len) { return account::rpc_get_self_wxid(out, len); } }, - { Functions_FUNC_GET_USER_INFO, [](const Request &r, uint8_t *out, size_t *len) { return account::rpc_get_user_info(out, len); } }, - { Functions_FUNC_GET_MSG_TYPES, [](const Request &r, uint8_t *out, size_t *len) { return RpcServer::getInstance().handler_.rpc_get_msg_types(out, len); } }, - { Functions_FUNC_ENABLE_RECV_TXT, [](const Request &r, uint8_t *out, size_t *len) { return RpcServer::getInstance().start_message_listener(r.msg.flag, out, len); } }, - { Functions_FUNC_DISABLE_RECV_TXT, [](const Request &r, uint8_t *out, size_t *len) { return RpcServer::getInstance().stop_message_listener(out, len); } }, - { Functions_FUNC_GET_CONTACTS, [](const Request &r, uint8_t *out, size_t *len) { return contact::rpc_get_contacts(out, len); } }, - { Functions_FUNC_GET_DB_NAMES, [](const Request &r, uint8_t *out, size_t *len) { return db::rpc_get_db_names(out, len); } }, - { Functions_FUNC_GET_DB_TABLES, [](const Request &r, uint8_t *out, size_t *len) { return db::rpc_get_db_tables(r.msg.str, out, len); } }, - { Functions_FUNC_GET_AUDIO_MSG, [](const Request &r, uint8_t *out, size_t *len) { return misc::rpc_get_audio(r.msg.am, out, len); } }, - { Functions_FUNC_SEND_TXT, [](const Request &r, uint8_t *out, size_t *len) { return RpcServer::getInstance().sender_.rpc_send_text(r.msg.txt, out, len); } }, - { Functions_FUNC_SEND_IMG, [](const Request &r, uint8_t *out, size_t *len) { return RpcServer::getInstance().sender_.rpc_send_image(r.msg.file, out, len); } }, - { Functions_FUNC_SEND_FILE, [](const Request &r, uint8_t *out, size_t *len) { return RpcServer::getInstance().sender_.rpc_send_file(r.msg.file, out, len); } }, - { Functions_FUNC_SEND_XML, [](const Request &r, uint8_t *out, size_t *len) { return RpcServer::getInstance().sender_.rpc_send_xml(r.msg.xml, out, len); } }, - { Functions_FUNC_SEND_EMOTION, [](const Request &r, uint8_t *out, size_t *len) { return RpcServer::getInstance().sender_.rpc_send_emotion(r.msg.file, out, len); } }, - { Functions_FUNC_SEND_RICH_TXT, [](const Request &r, uint8_t *out, size_t *len) { return RpcServer::getInstance().sender_.rpc_send_rich_text(r.msg.rt, out, len); } }, - { Functions_FUNC_SEND_PAT_MSG, [](const Request &r, uint8_t *out, size_t *len) { return RpcServer::getInstance().sender_.rpc_send_pat(r.msg.pm, out, len); } }, - { Functions_FUNC_FORWARD_MSG, [](const Request &r, uint8_t *out, size_t *len) { return RpcServer::getInstance().sender_.rpc_forward(r.msg.fm, out, len); } }, - { Functions_FUNC_EXEC_DB_QUERY, [](const Request &r, uint8_t *out, size_t *len) { return db::rpc_exec_db_query(r.msg.query, out, len); } }, - { Functions_FUNC_ACCEPT_FRIEND, [](const Request &r, uint8_t *out, size_t *len) { return contact::rpc_accept_friend(r.msg.v, out, len); } }, - { Functions_FUNC_RECV_TRANSFER, [](const Request &r, uint8_t *out, size_t *len) { return misc::rpc_receive_transfer(r.msg.tf, out, len); } }, - { Functions_FUNC_REFRESH_PYQ, [](const Request &r, uint8_t *out, size_t *len) { return misc::rpc_refresh_pyq(r.msg.ui64, out, len); } }, - { Functions_FUNC_DOWNLOAD_ATTACH, [](const Request &r, uint8_t *out, size_t *len) { return misc::rpc_download_attachment(r.msg.att, out, len); } }, - { Functions_FUNC_GET_CONTACT_INFO, [](const Request &r, uint8_t *out, size_t *len) { return contact::rpc_get_contact_info(r.msg.str, out, len); } }, - { Functions_FUNC_REVOKE_MSG, [](const Request &r, uint8_t *out, size_t *len) { return misc::rpc_revoke_message(r.msg.ui64, out, len); } }, - { Functions_FUNC_REFRESH_QRCODE, [](const Request &r, uint8_t *out, size_t *len) { return misc::rpc_get_login_url(out, len); } }, - { Functions_FUNC_DECRYPT_IMAGE, [](const Request &r, uint8_t *out, size_t *len) { return misc::rpc_decrypt_image(r.msg.dec, out, len); } }, - { Functions_FUNC_EXEC_OCR, [](const Request &r, uint8_t *out, size_t *len) { return misc::rpc_get_ocr_result(r.msg.str, out, len); } }, - { Functions_FUNC_ADD_ROOM_MEMBERS, [](const Request &r, uint8_t *out, size_t *len) { return chatroom::rpc_add_chatroom_member(r.msg.m, out, len); } }, - { Functions_FUNC_DEL_ROOM_MEMBERS, [](const Request &r, uint8_t *out, size_t *len) { return chatroom::rpc_delete_chatroom_member(r.msg.m, out, len); } }, - { Functions_FUNC_INV_ROOM_MEMBERS, [](const Request &r, uint8_t *out, size_t *len) { return chatroom::rpc_invite_chatroom_member(r.msg.m, out, len); } }, - { Functions_FUNC_SHUTDOWN, [](const Request &r, uint8_t *out, size_t *len) { return misc::rpc_shutdown(out, len); }} + // Account 模块 + { Functions_FUNC_IS_LOGIN, [](const Request &r, uint8_t *out, size_t *len) { + return account::rpc_is_logged_in(out, len); + }}, + { Functions_FUNC_GET_SELF_WXID, [](const Request &r, uint8_t *out, size_t *len) { + return account::rpc_get_self_wxid(out, len); + }}, + { Functions_FUNC_GET_USER_INFO, [](const Request &r, uint8_t *out, size_t *len) { + return account::rpc_get_user_info(out, len); + }}, + + // Contact 模块 + { Functions_FUNC_GET_CONTACTS, [](const Request &r, uint8_t *out, size_t *len) { + return contact::rpc_get_contacts(out, len); + }}, + { Functions_FUNC_GET_CONTACT_INFO, [](const Request &r, uint8_t *out, size_t *len) { + return contact::rpc_get_contact_info(r.msg.str, out, len); + }}, + { Functions_FUNC_ACCEPT_FRIEND, [](const Request &r, uint8_t *out, size_t *len) { + return contact::rpc_accept_friend(r.msg.v.v3, r.msg.v.v4, r.msg.v.scene, out, len); + }}, + + // Message Handler 模块 + { Functions_FUNC_GET_MSG_TYPES, [](const Request &r, uint8_t *out, size_t *len) { + return message::rpc_get_msg_types(out, len); + }}, + { Functions_FUNC_ENABLE_RECV_TXT, [](const Request &r, uint8_t *out, size_t *len) { + return message::rpc_enable_recv_txt(r.msg.flag, out, len); + }}, + { Functions_FUNC_DISABLE_RECV_TXT, [](const Request &r, uint8_t *out, size_t *len) { + return message::rpc_disable_recv_txt(out, len); + }}, + + // Message Sender 模块 + { Functions_FUNC_SEND_TXT, [](const Request &r, uint8_t *out, size_t *len) { + return message::rpc_send_text(r.msg.txt, out, len); + }}, + { Functions_FUNC_SEND_IMG, [](const Request &r, uint8_t *out, size_t *len) { + return message::rpc_send_image(r.msg.file, out, len); + }}, + { Functions_FUNC_SEND_FILE, [](const Request &r, uint8_t *out, size_t *len) { + return message::rpc_send_file(r.msg.file, out, len); + }}, + { Functions_FUNC_SEND_XML, [](const Request &r, uint8_t *out, size_t *len) { + return message::rpc_send_xml(r.msg.xml, out, len); + }}, + { Functions_FUNC_SEND_EMOTION, [](const Request &r, uint8_t *out, size_t *len) { + return message::rpc_send_emotion(r.msg.file, out, len); + }}, + { Functions_FUNC_SEND_RICH_TXT, [](const Request &r, uint8_t *out, size_t *len) { + return message::rpc_send_rich_text(r.msg.rt, out, len); + }}, + { Functions_FUNC_SEND_PAT_MSG, [](const Request &r, uint8_t *out, size_t *len) { + return message::rpc_send_pat(r.msg.pm, out, len); + }}, + { Functions_FUNC_FORWARD_MSG, [](const Request &r, uint8_t *out, size_t *len) { + return message::rpc_forward(r.msg.fm, out, len); + }}, + + // Database Executor 模块 + { Functions_FUNC_GET_DB_NAMES, [](const Request &r, uint8_t *out, size_t *len) { + return db::rpc_get_db_names(out, len); + }}, + { Functions_FUNC_GET_DB_TABLES, [](const Request &r, uint8_t *out, size_t *len) { + return db::rpc_get_db_tables(r.msg.str, out, len); + }}, + { Functions_FUNC_EXEC_DB_QUERY, [](const Request &r, uint8_t *out, size_t *len) { + return db::rpc_exec_db_query(r.msg.query, out, len); + }}, + { Functions_FUNC_GET_DB_KEY, [](const Request &r, uint8_t *out, size_t *len) { + return db::rpc_get_db_key(out, len); + }}, + + // Chatroom Manager 模块 + { Functions_FUNC_ADD_ROOM_MEMBERS, [](const Request &r, uint8_t *out, size_t *len) { + return chatroom::rpc_add_chatroom_member(r.msg.m, out, len); + }}, + { Functions_FUNC_DEL_ROOM_MEMBERS, [](const Request &r, uint8_t *out, size_t *len) { + return chatroom::rpc_delete_chatroom_member(r.msg.m, out, len); + }}, + { Functions_FUNC_INV_ROOM_MEMBERS, [](const Request &r, uint8_t *out, size_t *len) { + return chatroom::rpc_invite_chatroom_member(r.msg.m, out, len); + }}, + + // Misc Manager 模块 + { Functions_FUNC_GET_AUDIO_MSG, [](const Request &r, uint8_t *out, size_t *len) { + return misc::rpc_get_audio(r.msg.am, out, len); + }}, + { Functions_FUNC_RECV_TRANSFER, [](const Request &r, uint8_t *out, size_t *len) { + return misc::rpc_receive_transfer(r.msg.tf, out, len); + }}, + { Functions_FUNC_REFRESH_PYQ, [](const Request &r, uint8_t *out, size_t *len) { + return misc::rpc_refresh_pyq(r.msg.ui64, out, len); + }}, + { Functions_FUNC_DOWNLOAD_ATTACH, [](const Request &r, uint8_t *out, size_t *len) { + return misc::rpc_download_attachment(r.msg.att, out, len); + }}, + { Functions_FUNC_REVOKE_MSG, [](const Request &r, uint8_t *out, size_t *len) { + return misc::rpc_revoke_message(r.msg.ui64, out, len); + }}, + { Functions_FUNC_REFRESH_QRCODE, [](const Request &r, uint8_t *out, size_t *len) { + return misc::rpc_get_login_url(out, len); + }}, + { Functions_FUNC_DECRYPT_IMAGE, [](const Request &r, uint8_t *out, size_t *len) { + return misc::rpc_decrypt_image(r.msg.dec, out, len); + }}, + { Functions_FUNC_EXEC_OCR, [](const Request &r, uint8_t *out, size_t *len) { + return misc::rpc_get_ocr_result(r.msg.str, out, len); + }} // clang-format on }; -bool RpcServer::dispatcher(uint8_t *in, size_t in_len, uint8_t *out, size_t *out_len) +static bool dispatcher(uint8_t *in, size_t in_len, uint8_t *out, size_t *out_len) { bool ret = false; Request req = Request_init_default; @@ -285,78 +191,110 @@ bool RpcServer::dispatcher(uint8_t *in, size_t in_len, uint8_t *out, size_t *out LOG_DEBUG("{:#04x}[{}] length: {}", (uint8_t)req.func, magic_enum::enum_name(req.func), in_len); - auto it = RpcServer::rpcFunctionMap.find(req.func); - if (it != RpcServer::rpcFunctionMap.end()) { + auto it = rpcFunctionMap.find(req.func); + if (it != rpcFunctionMap.end()) { ret = it->second(req, out, out_len); } else { - LOG_ERROR("[未知方法]"); + LOG_ERROR("[UNKNOWN FUNCTION]"); } pb_release(Request_fields, &req); return ret; } -void RpcServer::run_rpc_server() +static int RunServer() { - int rv = 0; - nng_socket cmdSock = NNG_SOCKET_INITIALIZER; - std::string url = build_url(port_); - + int rv = 0; + char url[URL_SIZE + 1] = { 0 }; + sprintf_s(url, URL_SIZE, "%s:%d", BASE_URL, lport); if ((rv = nng_pair1_open(&cmdSock)) != 0) { - LOG_ERROR("nng_pair1_open error: {}", nng_strerror(rv)); - return; + LOG_ERROR("nng_pair0_open error {}", nng_strerror(rv)); + return rv; } - if ((rv = nng_listen(cmdSock, url.c_str(), nullptr, 0)) != 0) { - LOG_ERROR("nng_listen error: {}", nng_strerror(rv)); - nng_close(cmdSock); - nng_fini(); - return; + if ((rv = nng_listen(cmdSock, (char *)url, NULL, 0)) != 0) { + LOG_ERROR("nng_listen error {}", nng_strerror(rv)); + return rv; } + LOG_INFO("CMD Server listening on {}", (char *)url); if ((rv = nng_setopt_ms(cmdSock, NNG_OPT_SENDTIMEO, 1000)) != 0) { LOG_ERROR("nng_setopt_ms error: {}", nng_strerror(rv)); - nng_close(cmdSock); - nng_fini(); - return; + return rv; } - LOG_INFO("CMD Server listening on {}", url); - std::vector cmdBuffer(DEFAULT_BUF_SIZE); - - while (isRunning_.load()) { - uint8_t *in = nullptr; - size_t in_len, out_len = cmdBuffer.size(); - - rv = nng_recv(cmdSock, &in, &in_len, NNG_FLAG_ALLOC); - if (rv != 0) { + lIsRunning = true; + while (lIsRunning) { + uint8_t *in = NULL; + size_t in_len, out_len = G_BUF_SIZE; + if ((rv = nng_recv(cmdSock, &in, &in_len, NNG_FLAG_ALLOC)) != 0) { LOG_ERROR("cmdSock-nng_recv error: {}", nng_strerror(rv)); break; } - try { - if (dispatcher(in, in_len, cmdBuffer.data(), &out_len)) { + // LOG_BUFFER(in, in_len); + if (dispatcher(in, in_len, gBuffer, &out_len)) { LOG_DEBUG("Send data length {}", out_len); - - rv = nng_send(cmdSock, cmdBuffer.data(), out_len, 0); + // LOG_BUFFER(gBuffer, out_len); + rv = nng_send(cmdSock, gBuffer, out_len, 0); if (rv != 0) { LOG_ERROR("cmdSock-nng_send: {}", nng_strerror(rv)); } - } else { // 处理失败情况 + + } else { // Error LOG_ERROR("Dispatcher failed..."); - rv = nng_send(cmdSock, cmdBuffer.data(), 0, 0); + rv = nng_send(cmdSock, gBuffer, 0, 0); if (rv != 0) { LOG_ERROR("cmdSock-nng_send: {}", nng_strerror(rv)); } + // break; } } catch (const std::exception &e) { LOG_ERROR(util::gb2312_to_utf8(e.what())); } catch (...) { - LOG_ERROR("Unknown exception."); + LOG_ERROR("Unknow exception."); } - nng_free(in, in_len); } + LOG_DEBUG("Leave RunServer"); + return rv; +} + +int RpcStartServer(int port) +{ + if (lIsRunning) { + return 0; + } + + lport = port; + + lRpcThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)RunServer, NULL, NULL, NULL); + if (lRpcThread == NULL) { + LOG_ERROR("Failed to create RPC thread"); + return -1; + } + + return 0; +} + +int RpcStopServer() +{ + if (!lIsRunning) { + return 0; + } + + lIsRunning = false; nng_close(cmdSock); - LOG_DEBUG("Leave RunRpcServer"); + nng_close(msgSock); + message::stop_receiving(); + + // 等待线程退出 + if (lRpcThread != NULL) { + WaitForSingleObject(lRpcThread, 5000); // 最多等待 5 秒 + CloseHandle(lRpcThread); + lRpcThread = NULL; + } + + LOG_INFO("Server stopped."); + return 0; } diff --git a/WeChatFerry/spy/rpc_server.h b/WeChatFerry/spy/rpc_server.h index 76dbc1b4..3bc31e19 100644 --- a/WeChatFerry/spy/rpc_server.h +++ b/WeChatFerry/spy/rpc_server.h @@ -1,58 +1,10 @@ #pragma once -#include -#include -#include -#include - -#include - -#include "wcf.pb.h" - -#include "message_handler.h" -#include "message_sender.h" - -class RpcServer -{ -public: - static RpcServer &getInstance(); - static void destroyInstance(); - - int start(int port = RPC_DEFAULT_PORT); - int stop(); - -private: - RpcServer(int port = RPC_DEFAULT_PORT); - ~RpcServer(); - RpcServer(const RpcServer &) = delete; - RpcServer &operator=(const RpcServer &) = delete; - - void run_rpc_server(); - void on_message_callback(); - bool start_message_listener(bool pyq, uint8_t *out, size_t *len); - bool stop_message_listener(uint8_t *out, size_t *len); - bool dispatcher(uint8_t *in, size_t in_len, uint8_t *out, size_t *out_len); - - static std::string build_url(int port); - - using FunctionHandler = std::function; - - // 服务器默认端口号和绑定地址 - static constexpr int RPC_DEFAULT_PORT = 10086; - static constexpr const char *RPC_SERVER_ADDRESS = "tcp://0.0.0.0"; - - int port_ = RPC_DEFAULT_PORT; - std::atomic isRunning_ { false }; - std::thread cmdThread_; - std::thread msgThread_; - - message::Handler &handler_; - message::Sender &sender_; - - struct Deleter { - void operator()(RpcServer *server) const { delete server; } - }; - - static std::unique_ptr instance_; - static const std::unordered_map rpcFunctionMap; -}; +#ifdef SPY_EXPORTS +#define SPY_API __declspec(dllexport) +#else +#define SPY_API __declspec(dllimport) +#endif + +int RpcStartServer(int port); +int RpcStopServer(); diff --git a/WeChatFerry/spy/spy.aps b/WeChatFerry/spy/spy.aps index 19965705..5fd638a3 100644 Binary files a/WeChatFerry/spy/spy.aps and b/WeChatFerry/spy/spy.aps differ diff --git a/WeChatFerry/spy/spy.cpp b/WeChatFerry/spy/spy.cpp index cc7e6b0b..1d1b4d11 100644 --- a/WeChatFerry/spy/spy.cpp +++ b/WeChatFerry/spy/spy.cpp @@ -3,46 +3,73 @@ #include #include "log.hpp" -#include "rpc_server.h" -#include "spy.h" #include "util.h" +#include "offsets.h" +#include "rpc_server.h" + +uint32_t g_WeChatWinDllAddr = 0; namespace Spy { -int Init(void *args) + +void init(void *args) { - auto *pp = static_cast(args); - - Log::InitLogger(pp->path); - if (auto dll_addr = GetModuleHandleW(L"WeChatWin.dll")) { - WeChatDll.store(reinterpret_cast(dll_addr)); - } else { - LOG_ERROR("获取 WeChatWin.dll 模块地址失败"); - return -1; + wchar_t version[16] = { 0 }; + PortPath_t *pp = (PortPath_t *)args; + int port = pp->port; + std::string path(pp->path); + + init_logger(path); + g_WeChatWinDllAddr = (uint32_t)GetModuleHandle(L"WeChatWin.dll"); + if (g_WeChatWinDllAddr == 0) { + LOG_ERROR("获取wechatWin.dll模块地址失败"); + return; } - std::string version = util::get_wechat_version(); - std::string msg = fmt::format("WCF 支持版本: {},当前版本: {}", SUPPORT_VERSION, version); - if (version != SUPPORT_VERSION) { - LOG_ERROR(msg); - util::MsgBox(NULL, msg.c_str(), "微信版本错误", MB_ICONERROR); - return -2; + if (!util::get_wechat_version(version)) { + LOG_ERROR("获取微信版本失败"); + return; } - LOG_INFO(msg); - RpcServer::getInstance().start(pp->port); + std::wstring ws_version(version); + LOG_DEBUG("WeChat version: {}", util::w2s(ws_version).c_str()); - return 0; + if (ws_version != std::wstring(SUPPORT_VERSION.begin(), SUPPORT_VERSION.end())) { + LOG_ERROR("不支持当前版本"); + MessageBox(NULL, L"不支持当前版本", L"错误", 0); + return; + } + + RpcStartServer(port); } -void Cleanup() +void cleanup() { - LOG_DEBUG("CleanupSpy"); - RpcServer::destroyInstance(); + RpcStopServer(); } + +bool is_logged_in() +{ + return g_WeChatWinDllAddr && util::get_dword(g_WeChatWinDllAddr + Offsets::Account::SERVICE) != 0; } +} // namespace Spy + extern "C" { -__declspec(dllexport) int InitSpy(void *args) { return Spy::Init(args); } -__declspec(dllexport) int CleanupSpy() { Spy::Cleanup(); return 0;} + +void InitSpy(void *args) +{ + Spy::init(args); +} + +void CleanupSpy() +{ + Spy::cleanup(); +} + +int IsLogin(void) +{ + return Spy::is_logged_in() ? 1 : 0; +} + } diff --git a/WeChatFerry/spy/spy.def b/WeChatFerry/spy/spy.def new file mode 100644 index 00000000..7632d5cd --- /dev/null +++ b/WeChatFerry/spy/spy.def @@ -0,0 +1,3 @@ +EXPORTS + InitSpy + CleanupSpy diff --git a/WeChatFerry/spy/spy.h b/WeChatFerry/spy/spy.h index e460bc99..9debc7a8 100644 --- a/WeChatFerry/spy/spy.h +++ b/WeChatFerry/spy/spy.h @@ -4,17 +4,25 @@ #include #include +#include "framework.h" + namespace Spy { -constexpr std::string_view SUPPORT_VERSION = "3.9.12.51"; -inline std::atomic WeChatDll { 0 }; -template inline T getFunction(std::uintptr_t offset) { return reinterpret_cast(WeChatDll + offset); } -template inline T getFunction(std::uintptr_t base, std::uintptr_t offset) -{ - return reinterpret_cast(base + offset); -} +constexpr std::string_view SUPPORT_VERSION = "3.9.12.56"; + +void init(void *args); +void cleanup(); +bool is_logged_in(); + +} // namespace Spy + +// WeChatWin.dll 基址(运行时由 InitSpy 填入) +extern uint32_t g_WeChatWinDllAddr; -int Init(void *args); -void Cleanup(); +// 供 sdk.dll 通过 CallDllFuncEx 调用的 C 接口 +extern "C" { +void InitSpy(void *args); +void CleanupSpy(); +int IsLogin(void); } diff --git a/WeChatFerry/spy/spy.rc b/WeChatFerry/spy/spy.rc index 346545c1..c8d451b2 100644 --- a/WeChatFerry/spy/spy.rc +++ b/WeChatFerry/spy/spy.rc @@ -13,7 +13,7 @@ #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// -// 中文(简体,中国) resources +// (壬й) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_CHS) LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED @@ -51,8 +51,8 @@ END // VS_VERSION_INFO VERSIONINFO - FILEVERSION 39,5,2,0 - PRODUCTVERSION 3,9,12,51 + FILEVERSION 39,0,13,0 + PRODUCTVERSION 3,9,12,56 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -69,12 +69,12 @@ BEGIN BEGIN VALUE "CompanyName", "WeChatFerry" VALUE "FileDescription", "WeChatFerry" - VALUE "FileVersion", "39.5.2.0" + VALUE "FileVersion", "39.0.13.0" VALUE "InternalName", "spy.dll" VALUE "LegalCopyright", "Copyright (C) 2023" VALUE "OriginalFilename", "spy.dll" VALUE "ProductName", "WeChatFerry" - VALUE "ProductVersion", "3.9.12.51" + VALUE "ProductVersion", "3.9.12.56" END END BLOCK "VarFileInfo" @@ -83,7 +83,7 @@ BEGIN END END -#endif // 中文(简体,中国) resources +#endif // (壬й) resources ///////////////////////////////////////////////////////////////////////////// diff --git a/WeChatFerry/spy/spy_types.h b/WeChatFerry/spy/spy_types.h index 4e0f764e..b55ca3be 100644 --- a/WeChatFerry/spy/spy_types.h +++ b/WeChatFerry/spy/spy_types.h @@ -3,53 +3,32 @@ #include "framework.h" #include -typedef uint64_t QWORD; +// 注:所有内部地址/字段偏移都统一收敛到 offsets.h 的命名空间 +// (Message::Receive/Send、Moments、Database、Chatroom 等),并以带类型的函数指针建模调用。 -class WxString -{ -public: +// 微信内部字符串对象视图:wptr 指向宽字符缓冲(通常是本进程 std::wstring::c_str()), +// 为非拥有视图;ptr/clen 为可选窄字符侧,默认空。 +struct WxString { const wchar_t *wptr; - DWORD size; - DWORD length; + uint32_t size; + uint32_t capacity; const char *ptr; - DWORD clen; - - WxString() : wptr(nullptr), size(0), length(0), ptr(nullptr), clen(0) { } - - explicit WxString(const std::wstring &ws) - : wptr(ws.c_str()), size(static_cast(ws.size())), length(static_cast(ws.length())), ptr(nullptr), - clen(0) + uint32_t clen; + WxString() { + wptr = NULL; + size = 0; + capacity = 0; + ptr = NULL; + clen = 0; } - WxString(const WxString &) = delete; - WxString &operator=(const WxString &) = delete; - - WxString(WxString &&other) noexcept - : wptr(other.wptr), size(other.size), length(other.length), ptr(other.ptr), clen(other.clen) + WxString(std::wstring &ws) { - other.wptr = nullptr; - other.size = 0; - other.length = 0; - other.ptr = nullptr; - other.clen = 0; - } - - WxString &operator=(WxString &&other) noexcept - { - if (this != &other) { - wptr = other.wptr; - size = other.size; - length = other.length; - ptr = other.ptr; - clen = other.clen; - - other.wptr = nullptr; - other.size = 0; - other.length = 0; - other.ptr = nullptr; - other.clen = 0; - } - return *this; + wptr = ws.c_str(); + size = ws.size(); + capacity = ws.capacity(); + ptr = NULL; + clen = 0; } }; diff --git a/WeChatFerry/spy/sqlite3.h b/WeChatFerry/spy/sqlite3.h deleted file mode 100644 index a680be7b..00000000 --- a/WeChatFerry/spy/sqlite3.h +++ /dev/null @@ -1,175 +0,0 @@ -#pragma once - -#include "Windows.h" - -#include "spy_types.h" - -#define SQLITE_OK 0 /* Successful result */ - -/* beginning-of-error-codes */ -#define SQLITE_ERROR 1 /* Generic error */ -#define SQLITE_INTERNAL 2 /* Internal logic error in SQLite */ -#define SQLITE_PERM 3 /* Access permission denied */ -#define SQLITE_ABORT 4 /* Callback routine requested an abort */ -#define SQLITE_BUSY 5 /* The database file is locked */ -#define SQLITE_LOCKED 6 /* A table in the database is locked */ -#define SQLITE_NOMEM 7 /* A malloc() failed */ -#define SQLITE_READONLY 8 /* Attempt to write a readonly database */ -#define SQLITE_INTERRUPT 9 /* Operation terminated by sqlite3_interrupt()*/ -#define SQLITE_IOERR 10 /* Some kind of disk I/O error occurred */ -#define SQLITE_CORRUPT 11 /* The database disk image is malformed */ -#define SQLITE_NOTFOUND 12 /* Unknown opcode in sqlite3_file_control() */ -#define SQLITE_FULL 13 /* Insertion failed because database is full */ -#define SQLITE_CANTOPEN 14 /* Unable to open the database file */ -#define SQLITE_PROTOCOL 15 /* Database lock protocol error */ -#define SQLITE_EMPTY 16 /* Internal use only */ -#define SQLITE_SCHEMA 17 /* The database schema changed */ -#define SQLITE_TOOBIG 18 /* String or BLOB exceeds size limit */ -#define SQLITE_CONSTRAINT 19 /* Abort due to constraint violation */ -#define SQLITE_MISMATCH 20 /* Data type mismatch */ -#define SQLITE_MISUSE 21 /* Library used incorrectly */ -#define SQLITE_NOLFS 22 /* Uses OS features not supported on host */ -#define SQLITE_AUTH 23 /* Authorization denied */ -#define SQLITE_FORMAT 24 /* Not used */ -#define SQLITE_RANGE 25 /* 2nd parameter to sqlite3_bind out of range */ -#define SQLITE_NOTADB 26 /* File opened that is not a database file */ -#define SQLITE_NOTICE 27 /* Notifications from sqlite3_log() */ -#define SQLITE_WARNING 28 /* Warnings from sqlite3_log() */ -#define SQLITE_ROW 100 /* sqlite3_step() has another row ready */ -#define SQLITE_DONE 101 /* sqlite3_step() has finished executing */ -/* end-of-error-codes */ - -/* -** CAPI3REF: Extended Result Codes -** KEYWORDS: {extended result code definitions} -** -** In its default configuration, SQLite API routines return one of 30 integer -** [result codes]. However, experience has shown that many of -** these result codes are too coarse-grained. They do not provide as -** much information about problems as programmers might like. In an effort to -** address this, newer versions of SQLite (version 3.3.8 [dateof:3.3.8] -** and later) include -** support for additional result codes that provide more detailed information -** about errors. These [extended result codes] are enabled or disabled -** on a per database connection basis using the -** [sqlite3_extended_result_codes()] API. Or, the extended code for -** the most recent error can be obtained using -** [sqlite3_extended_errcode()]. -*/ -#define SQLITE_ERROR_MISSING_COLLSEQ (SQLITE_ERROR | (1 << 8)) -#define SQLITE_ERROR_RETRY (SQLITE_ERROR | (2 << 8)) -#define SQLITE_ERROR_SNAPSHOT (SQLITE_ERROR | (3 << 8)) -#define SQLITE_IOERR_READ (SQLITE_IOERR | (1 << 8)) -#define SQLITE_IOERR_SHORT_READ (SQLITE_IOERR | (2 << 8)) -#define SQLITE_IOERR_WRITE (SQLITE_IOERR | (3 << 8)) -#define SQLITE_IOERR_FSYNC (SQLITE_IOERR | (4 << 8)) -#define SQLITE_IOERR_DIR_FSYNC (SQLITE_IOERR | (5 << 8)) -#define SQLITE_IOERR_TRUNCATE (SQLITE_IOERR | (6 << 8)) -#define SQLITE_IOERR_FSTAT (SQLITE_IOERR | (7 << 8)) -#define SQLITE_IOERR_UNLOCK (SQLITE_IOERR | (8 << 8)) -#define SQLITE_IOERR_RDLOCK (SQLITE_IOERR | (9 << 8)) -#define SQLITE_IOERR_DELETE (SQLITE_IOERR | (10 << 8)) -#define SQLITE_IOERR_BLOCKED (SQLITE_IOERR | (11 << 8)) -#define SQLITE_IOERR_NOMEM (SQLITE_IOERR | (12 << 8)) -#define SQLITE_IOERR_ACCESS (SQLITE_IOERR | (13 << 8)) -#define SQLITE_IOERR_CHECKRESERVEDLOCK (SQLITE_IOERR | (14 << 8)) -#define SQLITE_IOERR_LOCK (SQLITE_IOERR | (15 << 8)) -#define SQLITE_IOERR_CLOSE (SQLITE_IOERR | (16 << 8)) -#define SQLITE_IOERR_DIR_CLOSE (SQLITE_IOERR | (17 << 8)) -#define SQLITE_IOERR_SHMOPEN (SQLITE_IOERR | (18 << 8)) -#define SQLITE_IOERR_SHMSIZE (SQLITE_IOERR | (19 << 8)) -#define SQLITE_IOERR_SHMLOCK (SQLITE_IOERR | (20 << 8)) -#define SQLITE_IOERR_SHMMAP (SQLITE_IOERR | (21 << 8)) -#define SQLITE_IOERR_SEEK (SQLITE_IOERR | (22 << 8)) -#define SQLITE_IOERR_DELETE_NOENT (SQLITE_IOERR | (23 << 8)) -#define SQLITE_IOERR_MMAP (SQLITE_IOERR | (24 << 8)) -#define SQLITE_IOERR_GETTEMPPATH (SQLITE_IOERR | (25 << 8)) -#define SQLITE_IOERR_CONVPATH (SQLITE_IOERR | (26 << 8)) -#define SQLITE_IOERR_VNODE (SQLITE_IOERR | (27 << 8)) -#define SQLITE_IOERR_AUTH (SQLITE_IOERR | (28 << 8)) -#define SQLITE_IOERR_BEGIN_ATOMIC (SQLITE_IOERR | (29 << 8)) -#define SQLITE_IOERR_COMMIT_ATOMIC (SQLITE_IOERR | (30 << 8)) -#define SQLITE_IOERR_ROLLBACK_ATOMIC (SQLITE_IOERR | (31 << 8)) -#define SQLITE_IOERR_DATA (SQLITE_IOERR | (32 << 8)) -#define SQLITE_IOERR_CORRUPTFS (SQLITE_IOERR | (33 << 8)) -#define SQLITE_LOCKED_SHAREDCACHE (SQLITE_LOCKED | (1 << 8)) -#define SQLITE_LOCKED_VTAB (SQLITE_LOCKED | (2 << 8)) -#define SQLITE_BUSY_RECOVERY (SQLITE_BUSY | (1 << 8)) -#define SQLITE_BUSY_SNAPSHOT (SQLITE_BUSY | (2 << 8)) -#define SQLITE_BUSY_TIMEOUT (SQLITE_BUSY | (3 << 8)) -#define SQLITE_CANTOPEN_NOTEMPDIR (SQLITE_CANTOPEN | (1 << 8)) -#define SQLITE_CANTOPEN_ISDIR (SQLITE_CANTOPEN | (2 << 8)) -#define SQLITE_CANTOPEN_FULLPATH (SQLITE_CANTOPEN | (3 << 8)) -#define SQLITE_CANTOPEN_CONVPATH (SQLITE_CANTOPEN | (4 << 8)) -#define SQLITE_CANTOPEN_DIRTYWAL (SQLITE_CANTOPEN | (5 << 8)) /* Not Used */ -#define SQLITE_CANTOPEN_SYMLINK (SQLITE_CANTOPEN | (6 << 8)) -#define SQLITE_CORRUPT_VTAB (SQLITE_CORRUPT | (1 << 8)) -#define SQLITE_CORRUPT_SEQUENCE (SQLITE_CORRUPT | (2 << 8)) -#define SQLITE_CORRUPT_INDEX (SQLITE_CORRUPT | (3 << 8)) -#define SQLITE_READONLY_RECOVERY (SQLITE_READONLY | (1 << 8)) -#define SQLITE_READONLY_CANTLOCK (SQLITE_READONLY | (2 << 8)) -#define SQLITE_READONLY_ROLLBACK (SQLITE_READONLY | (3 << 8)) -#define SQLITE_READONLY_DBMOVED (SQLITE_READONLY | (4 << 8)) -#define SQLITE_READONLY_CANTINIT (SQLITE_READONLY | (5 << 8)) -#define SQLITE_READONLY_DIRECTORY (SQLITE_READONLY | (6 << 8)) -#define SQLITE_ABORT_ROLLBACK (SQLITE_ABORT | (2 << 8)) -#define SQLITE_CONSTRAINT_CHECK (SQLITE_CONSTRAINT | (1 << 8)) -#define SQLITE_CONSTRAINT_COMMITHOOK (SQLITE_CONSTRAINT | (2 << 8)) -#define SQLITE_CONSTRAINT_FOREIGNKEY (SQLITE_CONSTRAINT | (3 << 8)) -#define SQLITE_CONSTRAINT_FUNCTION (SQLITE_CONSTRAINT | (4 << 8)) -#define SQLITE_CONSTRAINT_NOTNULL (SQLITE_CONSTRAINT | (5 << 8)) -#define SQLITE_CONSTRAINT_PRIMARYKEY (SQLITE_CONSTRAINT | (6 << 8)) -#define SQLITE_CONSTRAINT_TRIGGER (SQLITE_CONSTRAINT | (7 << 8)) -#define SQLITE_CONSTRAINT_UNIQUE (SQLITE_CONSTRAINT | (8 << 8)) -#define SQLITE_CONSTRAINT_VTAB (SQLITE_CONSTRAINT | (9 << 8)) -#define SQLITE_CONSTRAINT_ROWID (SQLITE_CONSTRAINT | (10 << 8)) -#define SQLITE_CONSTRAINT_PINNED (SQLITE_CONSTRAINT | (11 << 8)) -#define SQLITE_CONSTRAINT_DATATYPE (SQLITE_CONSTRAINT | (12 << 8)) -#define SQLITE_NOTICE_RECOVER_WAL (SQLITE_NOTICE | (1 << 8)) -#define SQLITE_NOTICE_RECOVER_ROLLBACK (SQLITE_NOTICE | (2 << 8)) -#define SQLITE_WARNING_AUTOINDEX (SQLITE_WARNING | (1 << 8)) -#define SQLITE_AUTH_USER (SQLITE_AUTH | (1 << 8)) -#define SQLITE_OK_LOAD_PERMANENTLY (SQLITE_OK | (1 << 8)) -#define SQLITE_OK_SYMLINK (SQLITE_OK | (2 << 8)) /* internal use only */ - -#define SQLITE_INTEGER 1 -#define SQLITE_FLOAT 2 -#define SQLITE_BLOB 4 -#define SQLITE_NULL 5 -#define SQLITE_TEXT 3 - -typedef int (*Sqlite3_callback)(void *, int, char **, char **); - -typedef int(__cdecl *Sqlite3_exec)(QWORD, /* An open database */ - const char *sql, /* SQL to be evaluated */ - Sqlite3_callback, /* Callback function */ - void *, /* 1st argument to callback */ - char **errmsg /* Error msg written here */ -); -typedef QWORD(__cdecl *Sqlite3_backup_init)(QWORD *pDest, /* Destination database handle */ - const char *zDestName, /* Destination database name */ - QWORD *pSource, /* Source database handle */ - const char *zSourceName /* Source database name */ -); -typedef int(__cdecl *Sqlite3_prepare)(QWORD db, /* Database handle */ - const char *zSql, /* SQL statement, UTF-8 encoded */ - int nByte, /* Maximum length of zSql in bytes. */ - QWORD **ppStmt, /* OUT: Statement handle */ - const char **pzTail /* OUT: Pointer to unused portion of zSql */ -); -typedef int(__cdecl *Sqlite3_open)(const char *filename, QWORD **ppDb); -typedef int(__cdecl *Sqlite3_backup_step)(QWORD *p, int nPage); -typedef int(__cdecl *Sqlite3_backup_remaining)(QWORD *p); -typedef int(__cdecl *Sqlite3_backup_pagecount)(QWORD *p); -typedef int(__cdecl *Sqlite3_backup_finish)(QWORD *p); -typedef int(__cdecl *Sqlite3_sleep)(int); -typedef int(__cdecl *Sqlite3_errcode)(QWORD *db); -typedef int(__cdecl *Sqlite3_close)(QWORD *); - -typedef int(__cdecl *Sqlite3_step)(QWORD *); -typedef int(__cdecl *Sqlite3_column_count)(QWORD *pStmt); -typedef const char *(__cdecl *Sqlite3_column_name)(QWORD *, int N); -typedef int(__cdecl *Sqlite3_column_type)(QWORD *, int iCol); -typedef const void *(__cdecl *Sqlite3_column_blob)(QWORD *, int iCol); -typedef int(__cdecl *Sqlite3_column_bytes)(QWORD *, int iCol); -typedef int(__cdecl *Sqlite3_finalize)(QWORD *pStmt); diff --git a/WeChatFerry/wcf/main.cpp b/WeChatFerry/wcf/main.cpp new file mode 100644 index 00000000..d56b53c2 --- /dev/null +++ b/WeChatFerry/wcf/main.cpp @@ -0,0 +1,69 @@ +#include +#include +#include + +#include "sdk.h" + +void help() +{ + fprintf(stderr, + "\nUsage: \n" + "启动: wcf.exe start port [debug] [--gui]\n" + "关闭: wcf.exe stop [--gui]\n" + "port : 命令端口(1-65534),消息端口为命令端口+1\n" + "debug: 注入 debug 版 spy\n" + "--gui: 出错时弹模态框(默认写 stderr,适合自动化)\n" + "\n退出码: \n" + " 0 成功\n" + " 2 命令行参数错误\n" + " 3 spy DLL 不存在\n" + " 4 打开/启动微信失败\n" + " 5 注入 spy 失败\n" + " 6 初始化 spy(RPC 服务)失败\n" + " 7 .wcf.lock 读写失败\n" + " 8 停止时微信未运行\n" + " 9 卸载 spy 失败\n" + " 10 spy 已注入(重复 start)\n"); +} + +int main(int argc, char *argv[]) +{ + int ret = -1; + bool debug = false; + + if (argc < 2) { + help(); + return WX_ERR_USAGE; + } + + // 先扫描可选开关,剩下的按位置解析 + for (int i = 2; i < argc; i++) { + if (strcmp(argv[i], "debug") == 0) { + debug = true; + } else if (strcmp(argv[i], "--gui") == 0) { + WxSetGuiMode(true); + } + } + + if (strcmp(argv[1], "start") == 0) { + if (argc < 3) { + fprintf(stderr, "缺少端口参数\n"); + help(); + return WX_ERR_USAGE; + } + char *end = NULL; + long port = strtol(argv[2], &end, 10); + if (end == argv[2] || *end != '\0' || port < 1 || port > 65534) { + fprintf(stderr, "端口非法: %s(应为 1-65534 的整数)\n", argv[2]); + return WX_ERR_USAGE; + } + ret = WxInitSDK(debug, (int)port); + } else if (strcmp(argv[1], "stop") == 0) { + ret = WxDestroySDK(); + } else { + help(); + return WX_ERR_USAGE; + } + + return ret; +} diff --git a/WeChatFerry/wcf/wcf.vcxproj b/WeChatFerry/wcf/wcf.vcxproj new file mode 100644 index 00000000..5363e60b --- /dev/null +++ b/WeChatFerry/wcf/wcf.vcxproj @@ -0,0 +1,185 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 16.0 + Win32Proj + {02747ce0-ad9f-4812-b019-fcf9867f7514} + wcf + 10.0 + + + + Application + true + v142 + Unicode + + + Application + false + v142 + true + Unicode + + + Application + true + v142 + Unicode + + + Application + false + v142 + true + Unicode + + + + + + + + + + + + + + + + + + + + + true + $(ProjectDir)..\_build\$(ProjectName)\$(Configuration)\ + + + false + $(ProjectDir)..\_build\$(ProjectName)\$(Configuration)\ + + + true + + + false + + + true + x86-windows-static + + + true + x86-windows-static + + + + Level3 + true + WIN32;_DEBUG;_CONSOLE;WCF;%(PreprocessorDefinitions) + true + $(ProjectDir)..\sdk;$(ProjectDir)..\spy\com;%(AdditionalIncludeDirectories) + MultiThreadedDebug + stdcpp17 + 4996 + + + Console + true + + + + + Level3 + true + true + true + WIN32;NDEBUG;_CONSOLE;WCF;%(PreprocessorDefinitions) + true + $(ProjectDir)..\sdk;$(ProjectDir)..\spy\com;%(AdditionalIncludeDirectories) + MultiThreaded + stdcpp17 + 4996 + + + Console + true + true + true + + + if not exist "$(ProjectDir)..\Out" md "$(ProjectDir)..\Out" +xcopy /y "$(OutDir)$(TargetFileName)" "$(ProjectDir)..\Out\" +xcopy /y "$(OutDir)$(TargetFileName)" "$(ProjectDir)..\..\clients\python\wcferry\" + + + Copy files + + + + + Level3 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + Level3 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + true + true + + + + + + + + + + + + + + + + + + + + + diff --git a/WeChatFerry/wcf/wcf.vcxproj.filters b/WeChatFerry/wcf/wcf.vcxproj.filters new file mode 100644 index 00000000..e90939a5 --- /dev/null +++ b/WeChatFerry/wcf/wcf.vcxproj.filters @@ -0,0 +1,45 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + 头文件 + + + 头文件 + + + 头文件 + + + 头文件 + + + + + 源文件 + + + 源文件 + + + 源文件 + + + 源文件 + + + \ No newline at end of file diff --git a/WeChatFerry/wcf/wcf.vcxproj.user b/WeChatFerry/wcf/wcf.vcxproj.user new file mode 100644 index 00000000..88a55094 --- /dev/null +++ b/WeChatFerry/wcf/wcf.vcxproj.user @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/TEQuant.jpg b/assets/TEQuant.jpg new file mode 100644 index 00000000..02b41aca Binary files /dev/null and b/assets/TEQuant.jpg differ diff --git a/clients/python/MANIFEST.in b/clients/python/MANIFEST.in index bc25aee3..efd2d375 100644 --- a/clients/python/MANIFEST.in +++ b/clients/python/MANIFEST.in @@ -1,2 +1,3 @@ include wcferry/*.dll +include wcferry/*.exe include wcferry/*.md diff --git a/clients/python/setup.py b/clients/python/setup.py index 2f1fa75f..c6a3bdde 100644 --- a/clients/python/setup.py +++ b/clients/python/setup.py @@ -26,10 +26,13 @@ include_package_data=True, install_requires=[ "setuptools", - "grpcio-tools", + "protobuf>=4.25.0", "pynng", "requests", ], + extras_require={ + "dev": ["grpcio-tools>=1.62"], + }, classifiers=[ "Environment :: Win32 (MS Windows)", "Intended Audience :: Developers", diff --git a/clients/python/wcferry/client.py b/clients/python/wcferry/client.py index 71675aef..4c6a8713 100644 --- a/clients/python/wcferry/client.py +++ b/clients/python/wcferry/client.py @@ -1,13 +1,10 @@ #! /usr/bin/env python3 # -*- coding: utf-8 -*- -__version__ = "39.5.2.0" +__version__ = "39.6.0.0" import atexit import base64 -import ctypes -import ctypes.wintypes -import gc import logging import mimetypes import os @@ -78,7 +75,6 @@ def __init__(self, host: str = None, port: int = 10086, debug: bool = True, bloc self.LOG.info(f"wcferry version: {__version__}") self.port = port self.host = host - self.sdk = None if host is None: self._local_mode = True self.host = "127.0.0.1" @@ -125,25 +121,18 @@ def _set_console_utf8(self): self.LOG.error(f"修改控制台代码页失败: {e}") def _sdk_init(self, debug, port): - sdk = ctypes.cdll.LoadLibrary(f"{self._wcf_root}/sdk.dll") - if sdk.WxInitSDK(debug, port) != 0: - self.LOG.error("初始化失败!") + # 经 wcf.exe 子进程注入:spy 为 32 位,用独立进程驱动可兼容 32/64 位 Python + cmd = [os.path.join(self._wcf_root, "wcf.exe"), "start", str(port)] + if debug: + cmd.append("debug") + ret = subprocess.run(cmd, cwd=self._wcf_root).returncode + if ret not in (0, 10): # 0=成功,10=spy 已注入(视为已就绪) + self.LOG.error(f"初始化失败,wcf.exe 退出码: {ret}") os._exit(-1) - # 主动卸载 - ctypes.windll.kernel32.FreeLibrary.argtypes = [ctypes.wintypes.HMODULE] - ctypes.windll.kernel32.FreeLibrary(sdk._handle) - del sdk # 删除 Python 对象、触发垃圾回收 - gc.collect() - - def _sdk_destroy(self): - sdk = ctypes.cdll.LoadLibrary(f"{self._wcf_root}/sdk.dll") - sdk.WxDestroySDK() - # 主动卸载 - ctypes.windll.kernel32.FreeLibrary.argtypes = [ctypes.wintypes.HMODULE] - ctypes.windll.kernel32.FreeLibrary(sdk._handle) - del sdk # 删除 Python 对象、触发垃圾回收 - gc.collect() + def _sdk_destroy(self) -> int: + return subprocess.run( + [os.path.join(self._wcf_root, "wcf.exe"), "stop"], cwd=self._wcf_root).returncode def cleanup(self) -> None: """关闭连接,回收资源""" @@ -152,14 +141,10 @@ def cleanup(self) -> None: self.disable_recv_msg() - req = wcf_pb2.Request() - req.func = wcf_pb2.FUNC_SHUTDOWN - _ = self._send_request(req) - self.cmd_socket.close() self.msg_socket.close() - if self._local_mode and self.sdk and self._sdk_destroy() != 0: + if self._local_mode and self._sdk_destroy() != 0: self.LOG.error("退出失败!") self._is_running = False @@ -274,6 +259,14 @@ def get_tables(self, db: str) -> List[Dict]: return tables + def get_db_key(self) -> str: + """获取数据库密钥(32 字节,返回小写十六进制串,供外部离线解密 .db)""" + req = wcf_pb2.Request() + req.func = wcf_pb2.FUNC_GET_DB_KEY # FUNC_GET_DB_KEY + rsp = self._send_request(req) + + return rsp.str + def get_user_info(self) -> Dict: """获取登录账号个人信息""" req = wcf_pb2.Request() diff --git a/clients/python/wcferry/wcf_pb2.py b/clients/python/wcferry/wcf_pb2.py index 36cf8574..79ecfd30 100644 --- a/clients/python/wcferry/wcf_pb2.py +++ b/clients/python/wcferry/wcf_pb2.py @@ -1,22 +1,12 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE # source: wcf.proto -# Protobuf Python Version: 5.29.0 +# Protobuf Python Version: 4.25.1 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 5, - 29, - 0, - '', - 'wcf.proto' -) # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() @@ -24,28 +14,28 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\twcf.proto\x12\x03wcf\"\xff\x03\n\x07Request\x12\x1c\n\x04\x66unc\x18\x01 \x01(\x0e\x32\x0e.wcf.Functions\x12\x1b\n\x05\x65mpty\x18\x02 \x01(\x0b\x32\n.wcf.EmptyH\x00\x12\r\n\x03str\x18\x03 \x01(\tH\x00\x12\x1b\n\x03txt\x18\x04 \x01(\x0b\x32\x0c.wcf.TextMsgH\x00\x12\x1c\n\x04\x66ile\x18\x05 \x01(\x0b\x32\x0c.wcf.PathMsgH\x00\x12\x1d\n\x05query\x18\x06 \x01(\x0b\x32\x0c.wcf.DbQueryH\x00\x12\x1e\n\x01v\x18\x07 \x01(\x0b\x32\x11.wcf.VerificationH\x00\x12\x1c\n\x01m\x18\x08 \x01(\x0b\x32\x0f.wcf.MemberMgmtH\x00\x12\x1a\n\x03xml\x18\t \x01(\x0b\x32\x0b.wcf.XmlMsgH\x00\x12\x1b\n\x03\x64\x65\x63\x18\n \x01(\x0b\x32\x0c.wcf.DecPathH\x00\x12\x1b\n\x02tf\x18\x0b \x01(\x0b\x32\r.wcf.TransferH\x00\x12\x12\n\x04ui64\x18\x0c \x01(\x04\x42\x02\x30\x01H\x00\x12\x0e\n\x04\x66lag\x18\r \x01(\x08H\x00\x12\x1d\n\x03\x61tt\x18\x0e \x01(\x0b\x32\x0e.wcf.AttachMsgH\x00\x12\x1b\n\x02\x61m\x18\x0f \x01(\x0b\x32\r.wcf.AudioMsgH\x00\x12\x1b\n\x02rt\x18\x10 \x01(\x0b\x32\r.wcf.RichTextH\x00\x12\x19\n\x02pm\x18\x11 \x01(\x0b\x32\x0b.wcf.PatMsgH\x00\x12\x1d\n\x02\x66m\x18\x12 \x01(\x0b\x32\x0f.wcf.ForwardMsgH\x00\x42\x05\n\x03msg\"\xc7\x02\n\x08Response\x12\x1c\n\x04\x66unc\x18\x01 \x01(\x0e\x32\x0e.wcf.Functions\x12\x10\n\x06status\x18\x02 \x01(\x05H\x00\x12\r\n\x03str\x18\x03 \x01(\tH\x00\x12\x1b\n\x05wxmsg\x18\x04 \x01(\x0b\x32\n.wcf.WxMsgH\x00\x12\x1e\n\x05types\x18\x05 \x01(\x0b\x32\r.wcf.MsgTypesH\x00\x12$\n\x08\x63ontacts\x18\x06 \x01(\x0b\x32\x10.wcf.RpcContactsH\x00\x12\x1b\n\x03\x64\x62s\x18\x07 \x01(\x0b\x32\x0c.wcf.DbNamesH\x00\x12\x1f\n\x06tables\x18\x08 \x01(\x0b\x32\r.wcf.DbTablesH\x00\x12\x1b\n\x04rows\x18\t \x01(\x0b\x32\x0b.wcf.DbRowsH\x00\x12\x1b\n\x02ui\x18\n \x01(\x0b\x32\r.wcf.UserInfoH\x00\x12\x1a\n\x03ocr\x18\x0b \x01(\x0b\x32\x0b.wcf.OcrMsgH\x00\x42\x05\n\x03msg\"\x07\n\x05\x45mpty\"\xbe\x01\n\x05WxMsg\x12\x0f\n\x07is_self\x18\x01 \x01(\x08\x12\x10\n\x08is_group\x18\x02 \x01(\x08\x12\x0e\n\x02id\x18\x03 \x01(\x04\x42\x02\x30\x01\x12\x0c\n\x04type\x18\x04 \x01(\r\x12\n\n\x02ts\x18\x05 \x01(\r\x12\x0e\n\x06roomid\x18\x06 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x07 \x01(\t\x12\x0e\n\x06sender\x18\x08 \x01(\t\x12\x0c\n\x04sign\x18\t \x01(\t\x12\r\n\x05thumb\x18\n \x01(\t\x12\r\n\x05\x65xtra\x18\x0b \x01(\t\x12\x0b\n\x03xml\x18\x0c \x01(\t\"7\n\x07TextMsg\x12\x0b\n\x03msg\x18\x01 \x01(\t\x12\x10\n\x08receiver\x18\x02 \x01(\t\x12\r\n\x05\x61ters\x18\x03 \x01(\t\")\n\x07PathMsg\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x10\n\x08receiver\x18\x02 \x01(\t\"G\n\x06XmlMsg\x12\x10\n\x08receiver\x18\x01 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\t\x12\x0c\n\x04path\x18\x03 \x01(\t\x12\x0c\n\x04type\x18\x04 \x01(\x04\"a\n\x08MsgTypes\x12\'\n\x05types\x18\x01 \x03(\x0b\x32\x18.wcf.MsgTypes.TypesEntry\x1a,\n\nTypesEntry\x12\x0b\n\x03key\x18\x01 \x01(\x05\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x87\x01\n\nRpcContact\x12\x0c\n\x04wxid\x18\x01 \x01(\t\x12\x0c\n\x04\x63ode\x18\x02 \x01(\t\x12\x0e\n\x06remark\x18\x03 \x01(\t\x12\x0c\n\x04name\x18\x04 \x01(\t\x12\x0f\n\x07\x63ountry\x18\x05 \x01(\t\x12\x10\n\x08province\x18\x06 \x01(\t\x12\x0c\n\x04\x63ity\x18\x07 \x01(\t\x12\x0e\n\x06gender\x18\x08 \x01(\x05\"0\n\x0bRpcContacts\x12!\n\x08\x63ontacts\x18\x01 \x03(\x0b\x32\x0f.wcf.RpcContact\"\x18\n\x07\x44\x62Names\x12\r\n\x05names\x18\x01 \x03(\t\"$\n\x07\x44\x62Table\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0b\n\x03sql\x18\x02 \x01(\t\"(\n\x08\x44\x62Tables\x12\x1c\n\x06tables\x18\x01 \x03(\x0b\x32\x0c.wcf.DbTable\"\"\n\x07\x44\x62Query\x12\n\n\x02\x64\x62\x18\x01 \x01(\t\x12\x0b\n\x03sql\x18\x02 \x01(\t\"8\n\x07\x44\x62\x46ield\x12\x0c\n\x04type\x18\x01 \x01(\x05\x12\x0e\n\x06\x63olumn\x18\x02 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x03 \x01(\x0c\"%\n\x05\x44\x62Row\x12\x1c\n\x06\x66ields\x18\x01 \x03(\x0b\x32\x0c.wcf.DbField\"\"\n\x06\x44\x62Rows\x12\x18\n\x04rows\x18\x01 \x03(\x0b\x32\n.wcf.DbRow\"5\n\x0cVerification\x12\n\n\x02v3\x18\x01 \x01(\t\x12\n\n\x02v4\x18\x02 \x01(\t\x12\r\n\x05scene\x18\x03 \x01(\x05\"+\n\nMemberMgmt\x12\x0e\n\x06roomid\x18\x01 \x01(\t\x12\r\n\x05wxids\x18\x02 \x01(\t\"S\n\x08UserInfo\x12\x0c\n\x04wxid\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0e\n\x06mobile\x18\x03 \x01(\t\x12\x0c\n\x04home\x18\x04 \x01(\t\x12\r\n\x05\x61lias\x18\x05 \x01(\t\"#\n\x07\x44\x65\x63Path\x12\x0b\n\x03src\x18\x01 \x01(\t\x12\x0b\n\x03\x64st\x18\x02 \x01(\t\"4\n\x08Transfer\x12\x0c\n\x04wxid\x18\x01 \x01(\t\x12\x0c\n\x04tfid\x18\x02 \x01(\t\x12\x0c\n\x04taid\x18\x03 \x01(\t\"9\n\tAttachMsg\x12\x0e\n\x02id\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05thumb\x18\x02 \x01(\t\x12\r\n\x05\x65xtra\x18\x03 \x01(\t\"\'\n\x08\x41udioMsg\x12\x0e\n\x02id\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x0b\n\x03\x64ir\x18\x02 \x01(\t\"y\n\x08RichText\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07\x61\x63\x63ount\x18\x02 \x01(\t\x12\r\n\x05title\x18\x03 \x01(\t\x12\x0e\n\x06\x64igest\x18\x04 \x01(\t\x12\x0b\n\x03url\x18\x05 \x01(\t\x12\x10\n\x08thumburl\x18\x06 \x01(\t\x12\x10\n\x08receiver\x18\x07 \x01(\t\"&\n\x06PatMsg\x12\x0e\n\x06roomid\x18\x01 \x01(\t\x12\x0c\n\x04wxid\x18\x02 \x01(\t\"(\n\x06OcrMsg\x12\x0e\n\x06status\x18\x01 \x01(\x05\x12\x0e\n\x06result\x18\x02 \x01(\t\".\n\nForwardMsg\x12\x0e\n\x02id\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x10\n\x08receiver\x18\x02 \x01(\t\"\xb7\x02\n\x08RoomData\x12)\n\x07members\x18\x01 \x03(\x0b\x32\x18.wcf.RoomData.RoomMember\x12\x14\n\x07\x66ield_2\x18\x02 \x01(\x05H\x00\x88\x01\x01\x12\x0f\n\x07\x66ield_3\x18\x03 \x01(\x05\x12\x14\n\x07\x66ield_4\x18\x04 \x01(\x05H\x01\x88\x01\x01\x12\x10\n\x08\x63\x61pacity\x18\x05 \x01(\x05\x12\x14\n\x07\x66ield_6\x18\x06 \x01(\tH\x02\x88\x01\x01\x12\x0f\n\x07\x66ield_7\x18\x07 \x01(\x05\x12\x0f\n\x07\x66ield_8\x18\x08 \x01(\x05\x12\x0e\n\x06\x61\x64mins\x18\t \x03(\t\x1a\x45\n\nRoomMember\x12\x0c\n\x04wxid\x18\x01 \x01(\t\x12\x11\n\x04name\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\r\n\x05state\x18\x03 \x01(\x05\x42\x07\n\x05_nameB\n\n\x08_field_2B\n\n\x08_field_4B\n\n\x08_field_6*\x86\x06\n\tFunctions\x12\x11\n\rFUNC_RESERVED\x10\x00\x12\x11\n\rFUNC_IS_LOGIN\x10\x01\x12\x16\n\x12\x46UNC_GET_SELF_WXID\x10\x10\x12\x16\n\x12\x46UNC_GET_MSG_TYPES\x10\x11\x12\x15\n\x11\x46UNC_GET_CONTACTS\x10\x12\x12\x15\n\x11\x46UNC_GET_DB_NAMES\x10\x13\x12\x16\n\x12\x46UNC_GET_DB_TABLES\x10\x14\x12\x16\n\x12\x46UNC_GET_USER_INFO\x10\x15\x12\x16\n\x12\x46UNC_GET_AUDIO_MSG\x10\x16\x12\x11\n\rFUNC_SEND_TXT\x10 \x12\x11\n\rFUNC_SEND_IMG\x10!\x12\x12\n\x0e\x46UNC_SEND_FILE\x10\"\x12\x11\n\rFUNC_SEND_XML\x10#\x12\x15\n\x11\x46UNC_SEND_EMOTION\x10$\x12\x16\n\x12\x46UNC_SEND_RICH_TXT\x10%\x12\x15\n\x11\x46UNC_SEND_PAT_MSG\x10&\x12\x14\n\x10\x46UNC_FORWARD_MSG\x10\'\x12\x18\n\x14\x46UNC_ENABLE_RECV_TXT\x10\x30\x12\x19\n\x15\x46UNC_DISABLE_RECV_TXT\x10@\x12\x16\n\x12\x46UNC_EXEC_DB_QUERY\x10P\x12\x16\n\x12\x46UNC_ACCEPT_FRIEND\x10Q\x12\x16\n\x12\x46UNC_RECV_TRANSFER\x10R\x12\x14\n\x10\x46UNC_REFRESH_PYQ\x10S\x12\x18\n\x14\x46UNC_DOWNLOAD_ATTACH\x10T\x12\x19\n\x15\x46UNC_GET_CONTACT_INFO\x10U\x12\x13\n\x0f\x46UNC_REVOKE_MSG\x10V\x12\x17\n\x13\x46UNC_REFRESH_QRCODE\x10W\x12\x16\n\x12\x46UNC_DECRYPT_IMAGE\x10`\x12\x11\n\rFUNC_EXEC_OCR\x10\x61\x12\x19\n\x15\x46UNC_ADD_ROOM_MEMBERS\x10p\x12\x19\n\x15\x46UNC_DEL_ROOM_MEMBERS\x10q\x12\x19\n\x15\x46UNC_INV_ROOM_MEMBERS\x10r\x12\x12\n\rFUNC_SHUTDOWN\x10\xff\x01\x42\r\n\x0b\x63om.iamteerb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\twcf.proto\x12\x03wcf\"\xff\x03\n\x07Request\x12\x1c\n\x04\x66unc\x18\x01 \x01(\x0e\x32\x0e.wcf.Functions\x12\x1b\n\x05\x65mpty\x18\x02 \x01(\x0b\x32\n.wcf.EmptyH\x00\x12\r\n\x03str\x18\x03 \x01(\tH\x00\x12\x1b\n\x03txt\x18\x04 \x01(\x0b\x32\x0c.wcf.TextMsgH\x00\x12\x1c\n\x04\x66ile\x18\x05 \x01(\x0b\x32\x0c.wcf.PathMsgH\x00\x12\x1d\n\x05query\x18\x06 \x01(\x0b\x32\x0c.wcf.DbQueryH\x00\x12\x1e\n\x01v\x18\x07 \x01(\x0b\x32\x11.wcf.VerificationH\x00\x12\x1c\n\x01m\x18\x08 \x01(\x0b\x32\x0f.wcf.MemberMgmtH\x00\x12\x1a\n\x03xml\x18\t \x01(\x0b\x32\x0b.wcf.XmlMsgH\x00\x12\x1b\n\x03\x64\x65\x63\x18\n \x01(\x0b\x32\x0c.wcf.DecPathH\x00\x12\x1b\n\x02tf\x18\x0b \x01(\x0b\x32\r.wcf.TransferH\x00\x12\x12\n\x04ui64\x18\x0c \x01(\x04\x42\x02\x30\x01H\x00\x12\x0e\n\x04\x66lag\x18\r \x01(\x08H\x00\x12\x1d\n\x03\x61tt\x18\x0e \x01(\x0b\x32\x0e.wcf.AttachMsgH\x00\x12\x1b\n\x02\x61m\x18\x0f \x01(\x0b\x32\r.wcf.AudioMsgH\x00\x12\x1b\n\x02rt\x18\x10 \x01(\x0b\x32\r.wcf.RichTextH\x00\x12\x19\n\x02pm\x18\x11 \x01(\x0b\x32\x0b.wcf.PatMsgH\x00\x12\x1d\n\x02\x66m\x18\x12 \x01(\x0b\x32\x0f.wcf.ForwardMsgH\x00\x42\x05\n\x03msg\"\xc7\x02\n\x08Response\x12\x1c\n\x04\x66unc\x18\x01 \x01(\x0e\x32\x0e.wcf.Functions\x12\x10\n\x06status\x18\x02 \x01(\x05H\x00\x12\r\n\x03str\x18\x03 \x01(\tH\x00\x12\x1b\n\x05wxmsg\x18\x04 \x01(\x0b\x32\n.wcf.WxMsgH\x00\x12\x1e\n\x05types\x18\x05 \x01(\x0b\x32\r.wcf.MsgTypesH\x00\x12$\n\x08\x63ontacts\x18\x06 \x01(\x0b\x32\x10.wcf.RpcContactsH\x00\x12\x1b\n\x03\x64\x62s\x18\x07 \x01(\x0b\x32\x0c.wcf.DbNamesH\x00\x12\x1f\n\x06tables\x18\x08 \x01(\x0b\x32\r.wcf.DbTablesH\x00\x12\x1b\n\x04rows\x18\t \x01(\x0b\x32\x0b.wcf.DbRowsH\x00\x12\x1b\n\x02ui\x18\n \x01(\x0b\x32\r.wcf.UserInfoH\x00\x12\x1a\n\x03ocr\x18\x0b \x01(\x0b\x32\x0b.wcf.OcrMsgH\x00\x42\x05\n\x03msg\"\x07\n\x05\x45mpty\"\xbe\x01\n\x05WxMsg\x12\x0f\n\x07is_self\x18\x01 \x01(\x08\x12\x10\n\x08is_group\x18\x02 \x01(\x08\x12\x0e\n\x02id\x18\x03 \x01(\x04\x42\x02\x30\x01\x12\x0c\n\x04type\x18\x04 \x01(\r\x12\n\n\x02ts\x18\x05 \x01(\r\x12\x0e\n\x06roomid\x18\x06 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x07 \x01(\t\x12\x0e\n\x06sender\x18\x08 \x01(\t\x12\x0c\n\x04sign\x18\t \x01(\t\x12\r\n\x05thumb\x18\n \x01(\t\x12\r\n\x05\x65xtra\x18\x0b \x01(\t\x12\x0b\n\x03xml\x18\x0c \x01(\t\"7\n\x07TextMsg\x12\x0b\n\x03msg\x18\x01 \x01(\t\x12\x10\n\x08receiver\x18\x02 \x01(\t\x12\r\n\x05\x61ters\x18\x03 \x01(\t\")\n\x07PathMsg\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x10\n\x08receiver\x18\x02 \x01(\t\"G\n\x06XmlMsg\x12\x10\n\x08receiver\x18\x01 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\t\x12\x0c\n\x04path\x18\x03 \x01(\t\x12\x0c\n\x04type\x18\x04 \x01(\x05\"a\n\x08MsgTypes\x12\'\n\x05types\x18\x01 \x03(\x0b\x32\x18.wcf.MsgTypes.TypesEntry\x1a,\n\nTypesEntry\x12\x0b\n\x03key\x18\x01 \x01(\x05\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x87\x01\n\nRpcContact\x12\x0c\n\x04wxid\x18\x01 \x01(\t\x12\x0c\n\x04\x63ode\x18\x02 \x01(\t\x12\x0e\n\x06remark\x18\x03 \x01(\t\x12\x0c\n\x04name\x18\x04 \x01(\t\x12\x0f\n\x07\x63ountry\x18\x05 \x01(\t\x12\x10\n\x08province\x18\x06 \x01(\t\x12\x0c\n\x04\x63ity\x18\x07 \x01(\t\x12\x0e\n\x06gender\x18\x08 \x01(\x05\"0\n\x0bRpcContacts\x12!\n\x08\x63ontacts\x18\x01 \x03(\x0b\x32\x0f.wcf.RpcContact\"\x18\n\x07\x44\x62Names\x12\r\n\x05names\x18\x01 \x03(\t\"$\n\x07\x44\x62Table\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0b\n\x03sql\x18\x02 \x01(\t\"(\n\x08\x44\x62Tables\x12\x1c\n\x06tables\x18\x01 \x03(\x0b\x32\x0c.wcf.DbTable\"\"\n\x07\x44\x62Query\x12\n\n\x02\x64\x62\x18\x01 \x01(\t\x12\x0b\n\x03sql\x18\x02 \x01(\t\"8\n\x07\x44\x62\x46ield\x12\x0c\n\x04type\x18\x01 \x01(\x05\x12\x0e\n\x06\x63olumn\x18\x02 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x03 \x01(\x0c\"%\n\x05\x44\x62Row\x12\x1c\n\x06\x66ields\x18\x01 \x03(\x0b\x32\x0c.wcf.DbField\"\"\n\x06\x44\x62Rows\x12\x18\n\x04rows\x18\x01 \x03(\x0b\x32\n.wcf.DbRow\"5\n\x0cVerification\x12\n\n\x02v3\x18\x01 \x01(\t\x12\n\n\x02v4\x18\x02 \x01(\t\x12\r\n\x05scene\x18\x03 \x01(\x05\"+\n\nMemberMgmt\x12\x0e\n\x06roomid\x18\x01 \x01(\t\x12\r\n\x05wxids\x18\x02 \x01(\t\"D\n\x08UserInfo\x12\x0c\n\x04wxid\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0e\n\x06mobile\x18\x03 \x01(\t\x12\x0c\n\x04home\x18\x04 \x01(\t\"#\n\x07\x44\x65\x63Path\x12\x0b\n\x03src\x18\x01 \x01(\t\x12\x0b\n\x03\x64st\x18\x02 \x01(\t\"4\n\x08Transfer\x12\x0c\n\x04wxid\x18\x01 \x01(\t\x12\x0c\n\x04tfid\x18\x02 \x01(\t\x12\x0c\n\x04taid\x18\x03 \x01(\t\"9\n\tAttachMsg\x12\x0e\n\x02id\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05thumb\x18\x02 \x01(\t\x12\r\n\x05\x65xtra\x18\x03 \x01(\t\"\'\n\x08\x41udioMsg\x12\x0e\n\x02id\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x0b\n\x03\x64ir\x18\x02 \x01(\t\"y\n\x08RichText\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07\x61\x63\x63ount\x18\x02 \x01(\t\x12\r\n\x05title\x18\x03 \x01(\t\x12\x0e\n\x06\x64igest\x18\x04 \x01(\t\x12\x0b\n\x03url\x18\x05 \x01(\t\x12\x10\n\x08thumburl\x18\x06 \x01(\t\x12\x10\n\x08receiver\x18\x07 \x01(\t\"&\n\x06PatMsg\x12\x0e\n\x06roomid\x18\x01 \x01(\t\x12\x0c\n\x04wxid\x18\x02 \x01(\t\"(\n\x06OcrMsg\x12\x0e\n\x06status\x18\x01 \x01(\x05\x12\x0e\n\x06result\x18\x02 \x01(\t\".\n\nForwardMsg\x12\x0e\n\x02id\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x10\n\x08receiver\x18\x02 \x01(\t\"\xb7\x02\n\x08RoomData\x12)\n\x07members\x18\x01 \x03(\x0b\x32\x18.wcf.RoomData.RoomMember\x12\x14\n\x07\x66ield_2\x18\x02 \x01(\x05H\x00\x88\x01\x01\x12\x0f\n\x07\x66ield_3\x18\x03 \x01(\x05\x12\x14\n\x07\x66ield_4\x18\x04 \x01(\x05H\x01\x88\x01\x01\x12\x10\n\x08\x63\x61pacity\x18\x05 \x01(\x05\x12\x14\n\x07\x66ield_6\x18\x06 \x01(\tH\x02\x88\x01\x01\x12\x0f\n\x07\x66ield_7\x18\x07 \x01(\x05\x12\x0f\n\x07\x66ield_8\x18\x08 \x01(\x05\x12\x0e\n\x06\x61\x64mins\x18\t \x03(\t\x1a\x45\n\nRoomMember\x12\x0c\n\x04wxid\x18\x01 \x01(\t\x12\x11\n\x04name\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\r\n\x05state\x18\x03 \x01(\x05\x42\x07\n\x05_nameB\n\n\x08_field_2B\n\n\x08_field_4B\n\n\x08_field_6*\x87\x06\n\tFunctions\x12\x11\n\rFUNC_RESERVED\x10\x00\x12\x11\n\rFUNC_IS_LOGIN\x10\x01\x12\x16\n\x12\x46UNC_GET_SELF_WXID\x10\x10\x12\x16\n\x12\x46UNC_GET_MSG_TYPES\x10\x11\x12\x15\n\x11\x46UNC_GET_CONTACTS\x10\x12\x12\x15\n\x11\x46UNC_GET_DB_NAMES\x10\x13\x12\x16\n\x12\x46UNC_GET_DB_TABLES\x10\x14\x12\x16\n\x12\x46UNC_GET_USER_INFO\x10\x15\x12\x16\n\x12\x46UNC_GET_AUDIO_MSG\x10\x16\x12\x13\n\x0f\x46UNC_GET_DB_KEY\x10\x17\x12\x11\n\rFUNC_SEND_TXT\x10 \x12\x11\n\rFUNC_SEND_IMG\x10!\x12\x12\n\x0e\x46UNC_SEND_FILE\x10\"\x12\x11\n\rFUNC_SEND_XML\x10#\x12\x15\n\x11\x46UNC_SEND_EMOTION\x10$\x12\x16\n\x12\x46UNC_SEND_RICH_TXT\x10%\x12\x15\n\x11\x46UNC_SEND_PAT_MSG\x10&\x12\x14\n\x10\x46UNC_FORWARD_MSG\x10\'\x12\x18\n\x14\x46UNC_ENABLE_RECV_TXT\x10\x30\x12\x19\n\x15\x46UNC_DISABLE_RECV_TXT\x10@\x12\x16\n\x12\x46UNC_EXEC_DB_QUERY\x10P\x12\x16\n\x12\x46UNC_ACCEPT_FRIEND\x10Q\x12\x16\n\x12\x46UNC_RECV_TRANSFER\x10R\x12\x14\n\x10\x46UNC_REFRESH_PYQ\x10S\x12\x18\n\x14\x46UNC_DOWNLOAD_ATTACH\x10T\x12\x19\n\x15\x46UNC_GET_CONTACT_INFO\x10U\x12\x13\n\x0f\x46UNC_REVOKE_MSG\x10V\x12\x17\n\x13\x46UNC_REFRESH_QRCODE\x10W\x12\x16\n\x12\x46UNC_DECRYPT_IMAGE\x10`\x12\x11\n\rFUNC_EXEC_OCR\x10\x61\x12\x19\n\x15\x46UNC_ADD_ROOM_MEMBERS\x10p\x12\x19\n\x15\x46UNC_DEL_ROOM_MEMBERS\x10q\x12\x19\n\x15\x46UNC_INV_ROOM_MEMBERS\x10rB\r\n\x0b\x63om.iamteerb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'wcf_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - _globals['DESCRIPTOR']._loaded_options = None +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None _globals['DESCRIPTOR']._serialized_options = b'\n\013com.iamteer' - _globals['_REQUEST'].fields_by_name['ui64']._loaded_options = None + _globals['_REQUEST'].fields_by_name['ui64']._options = None _globals['_REQUEST'].fields_by_name['ui64']._serialized_options = b'0\001' - _globals['_WXMSG'].fields_by_name['id']._loaded_options = None + _globals['_WXMSG'].fields_by_name['id']._options = None _globals['_WXMSG'].fields_by_name['id']._serialized_options = b'0\001' - _globals['_MSGTYPES_TYPESENTRY']._loaded_options = None + _globals['_MSGTYPES_TYPESENTRY']._options = None _globals['_MSGTYPES_TYPESENTRY']._serialized_options = b'8\001' - _globals['_ATTACHMSG'].fields_by_name['id']._loaded_options = None + _globals['_ATTACHMSG'].fields_by_name['id']._options = None _globals['_ATTACHMSG'].fields_by_name['id']._serialized_options = b'0\001' - _globals['_AUDIOMSG'].fields_by_name['id']._loaded_options = None + _globals['_AUDIOMSG'].fields_by_name['id']._options = None _globals['_AUDIOMSG'].fields_by_name['id']._serialized_options = b'0\001' - _globals['_FORWARDMSG'].fields_by_name['id']._loaded_options = None + _globals['_FORWARDMSG'].fields_by_name['id']._options = None _globals['_FORWARDMSG'].fields_by_name['id']._serialized_options = b'0\001' - _globals['_FUNCTIONS']._serialized_start=2743 - _globals['_FUNCTIONS']._serialized_end=3517 + _globals['_FUNCTIONS']._serialized_start=2728 + _globals['_FUNCTIONS']._serialized_end=3503 _globals['_REQUEST']._serialized_start=19 _globals['_REQUEST']._serialized_end=530 _globals['_RESPONSE']._serialized_start=533 @@ -87,25 +77,25 @@ _globals['_MEMBERMGMT']._serialized_start=1854 _globals['_MEMBERMGMT']._serialized_end=1897 _globals['_USERINFO']._serialized_start=1899 - _globals['_USERINFO']._serialized_end=1982 - _globals['_DECPATH']._serialized_start=1984 - _globals['_DECPATH']._serialized_end=2019 - _globals['_TRANSFER']._serialized_start=2021 - _globals['_TRANSFER']._serialized_end=2073 - _globals['_ATTACHMSG']._serialized_start=2075 - _globals['_ATTACHMSG']._serialized_end=2132 - _globals['_AUDIOMSG']._serialized_start=2134 - _globals['_AUDIOMSG']._serialized_end=2173 - _globals['_RICHTEXT']._serialized_start=2175 - _globals['_RICHTEXT']._serialized_end=2296 - _globals['_PATMSG']._serialized_start=2298 - _globals['_PATMSG']._serialized_end=2336 - _globals['_OCRMSG']._serialized_start=2338 - _globals['_OCRMSG']._serialized_end=2378 - _globals['_FORWARDMSG']._serialized_start=2380 - _globals['_FORWARDMSG']._serialized_end=2426 - _globals['_ROOMDATA']._serialized_start=2429 - _globals['_ROOMDATA']._serialized_end=2740 - _globals['_ROOMDATA_ROOMMEMBER']._serialized_start=2635 - _globals['_ROOMDATA_ROOMMEMBER']._serialized_end=2704 + _globals['_USERINFO']._serialized_end=1967 + _globals['_DECPATH']._serialized_start=1969 + _globals['_DECPATH']._serialized_end=2004 + _globals['_TRANSFER']._serialized_start=2006 + _globals['_TRANSFER']._serialized_end=2058 + _globals['_ATTACHMSG']._serialized_start=2060 + _globals['_ATTACHMSG']._serialized_end=2117 + _globals['_AUDIOMSG']._serialized_start=2119 + _globals['_AUDIOMSG']._serialized_end=2158 + _globals['_RICHTEXT']._serialized_start=2160 + _globals['_RICHTEXT']._serialized_end=2281 + _globals['_PATMSG']._serialized_start=2283 + _globals['_PATMSG']._serialized_end=2321 + _globals['_OCRMSG']._serialized_start=2323 + _globals['_OCRMSG']._serialized_end=2363 + _globals['_FORWARDMSG']._serialized_start=2365 + _globals['_FORWARDMSG']._serialized_end=2411 + _globals['_ROOMDATA']._serialized_start=2414 + _globals['_ROOMDATA']._serialized_end=2725 + _globals['_ROOMDATA_ROOMMEMBER']._serialized_start=2620 + _globals['_ROOMDATA_ROOMMEMBER']._serialized_end=2689 # @@protoc_insertion_point(module_scope)