重构文件结构,增加合并数据库功能,修复部分bug
This commit is contained in:
commit
ca0b442d81
330
README.md
330
README.md
@ -13,48 +13,6 @@
|
||||
[](https://github.com/xaoyaoo/PyWxDump/blob/master/LICENSE)
|
||||
[](https://github.com/xaoyaoo/PyWxDump/actions/workflows/publish.yml)
|
||||
|
||||
<details>
|
||||
<summary><strong>更新日志(点击展开):</strong></summary>
|
||||
|
||||
* 2023.12.03 增加分析聊天记录的功能,生成词云、绘制折线图等
|
||||
* 2023.12.03 修复部分bug,更改获取wx文件夹方式 [#34](https://github.com/xaoyaoo/PyWxDump/issues/34)
|
||||
* 2023.12.01 为exe添加图标
|
||||
* 2023.11.30 优化命令行界面
|
||||
* 2023.11.29 添加异形wxid获取方式,添加用户路径自动获取,重建说明文档,对新手更友好
|
||||
* 2023.11.28 修改wxid获取方式,修复部分bug
|
||||
* 2023.11.27 解决相对导入包的问题,完善错误提示
|
||||
* 2023.11.25 聊天记录查看工具bootstrap更换国内cdn
|
||||
* 2023.11.22 添加all命令中解密错误数据日志写入文件,修复部分bug
|
||||
* 2023.11.16 增加聊天记录导出为html
|
||||
* 2023.11.15 添加test文件,添加自动构建可执行文件的脚本,添加版本描述
|
||||
* 2023.11.15 [v2.2.5变化较大]重构解密脚本的返回值,重构命令行参数
|
||||
* 2023.11.15 修复无法获取wxid的bug
|
||||
* 2023.11.14 修复部分bug
|
||||
* 2023.11.11 添加聊天记录解析,查看工具,修复部分bug
|
||||
* 2023.11.10 修复wxdump wx_db命令行参数错误 [#19](https://github.com/xaoyaoo/PyWxDump/issues/19)
|
||||
* 2023.11.08 增加3.9.8.15版本支持
|
||||
* 2023.10.31 修复3.9.2.*版本无法正常运行
|
||||
* 2023.10.28 添加自动发布到pypi的github action
|
||||
* 2023.10.28 修复3.9.5.91版本的偏移
|
||||
* 2023.10.24 add auto get bias addr ,not need input key or wx folder path.
|
||||
* 2023.10.17 add LICENSE
|
||||
* 2023.10.16 添加"3.9.7.15"版本的偏移[#12](https://github.com/xaoyaoo/PyWxDump/issues/12)
|
||||
,感谢@[GentlemanII](https://github.com/GentlemanII)
|
||||
* 2023.10.15 将整个项目作为包安装,增加命令行统一操作
|
||||
* 2023.10.14 整体重构项目,优化代码,增加命令行统一操作
|
||||
* 2023.10.11 添加"3.9.5.81"版本的偏移[#10](https://github.com/xaoyaoo/PyWxDump/issues/10)
|
||||
,感谢@[sv3nbeast](https://github.com/sv3nbeast)
|
||||
* 2023.10.09 获取key基址偏移可以根据微信文件夹获取,不需要输入key
|
||||
* 2023.10.09 优化代码,删减没必要代码,重新修改获取基址代码,加快运行速度(需要安装新的库 pymem)
|
||||
* 2023.10.07 修改获取基址内存搜索方式,防止进入死循环
|
||||
* 2023.10.07 增加了3.9.7.29版本的偏移地址
|
||||
* 2023.10.06 增加命令行解密数据库
|
||||
* 2023.09.28 增加了数据库部分解析
|
||||
* 2023.09.15 增加了3.9.7.25版本的偏移地址
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
**更新计划【由于家里有事,这些计划(除8、9、10)将会在12.30号前统一更新】**
|
||||
|
||||
* 1.每个人聊天记录分析,生成词云。
|
||||
@ -63,38 +21,13 @@
|
||||
* 4.生成年度可视化报告
|
||||
* 5.创建GUI图形界面,方便使用
|
||||
* 6.查看群聊中具体发言成员的ID [#31](https://github.com/xaoyaoo/PyWxDump/issues/31)
|
||||
* 7.增加数据库合并功能,方便查看
|
||||
* 7.增加数据库合并功能,方便查看(已完成,待测试)
|
||||
* 8.增加企业微信的支持
|
||||
* 9.增加获取实时聊天记录的功能
|
||||
* 10.聊天记录关键字搜索 或者按时间点搜索列出所有的联系人记录就nice了
|
||||
|
||||
注: 欢迎大家提供更多的想法,或者提供代码,一起完善这个项目。
|
||||
|
||||
<details>
|
||||
<summary>贡献代码方法(点击展开):</summary>
|
||||
|
||||
|
||||
提交拉取请求(Pull Request),请按照以下步骤进行操作:
|
||||
|
||||
1. Fork 仓库:首先,在项目的 GitHub 页面上点击 "Fork" 按钮,将项目的代码仓库 fork 到你自己的 GitHub 账号下。
|
||||
2. 克隆仓库:在你自己的 GitHub 账号下找到 fork 后的项目,点击 `Clone or download`按钮,获取仓库的 URL。然后在本地使用 Git
|
||||
命令克隆仓库到你的电脑上:`git clone 仓库的URL`
|
||||
3. 创建分支:在本地仓库中创建一个新的分支,用于进行你的修改:`git checkout -b 你的分支名`
|
||||
4. 进行修改:在新创建的分支中进行你需要的修改,包括修复错误、改进现有功能或添加新功能。
|
||||
5. 提交修改:使用 `git add` 和 `git commit` 命令将修改提交到本地仓库中:
|
||||
```
|
||||
git add .
|
||||
git commit -m "提交信息"
|
||||
```
|
||||
6. 推送分支:使用 `git push` 命令将你的本地分支推送到你的 GitHub 仓库中:`git push origin 你的分支名`
|
||||
7. 提交拉取请求:在你的 GitHub 仓库页面上切换到你刚刚推送的分支,点击 "New pull request"
|
||||
按钮,填写一些说明信息,然后点击 `Create pull request`
|
||||
按钮,即可提交拉取请求。
|
||||
8. 等待审核:等待项目维护者审核你的拉取请求,如果通过审核,你的修改将会被合并到项目的主分支中
|
||||
9. 接着你就可以在右边的`contributors`中看到你的名字了。
|
||||
|
||||
</details>
|
||||
|
||||
欢迎加入交流qq群:577704006 or
|
||||
点击链接加入群聊[pywxdump功能交流](https://s.xaoyo.top/gOLUDl)。
|
||||
|
||||
@ -122,11 +55,7 @@
|
||||
* (7)支持微信多开场景,获取多用户信息等
|
||||
* (8)微信需要登录状态才能获取数据库密钥
|
||||
* (9)支持导出聊天记录为html,备份微信聊天记录,方便查看
|
||||
|
||||
**版本差异**
|
||||
|
||||
1. 版本 < 3.7.0.30 只运行不登录能获取个人信息,登录后可以获取数据库密钥
|
||||
2. 版本 > 3.7.0.30 只运行不登录不能获取个人信息,登录后都能获取
|
||||
* (10)合并多个数据库,方便查看
|
||||
|
||||
**利用场景**
|
||||
|
||||
@ -136,41 +65,7 @@
|
||||
4. 自行备份(日常备份自己留存)
|
||||
5. 等等...............
|
||||
|
||||
## 3. 项目结构
|
||||
|
||||
<details>
|
||||
<summary>点击展开</summary>
|
||||
|
||||
```
|
||||
PyWxDump
|
||||
├─ pywxdump # 项目代码,存放各个模块
|
||||
│ ├─ analyse # 解析数据库
|
||||
│ │ └─ parse.py # 解析数据库脚本,可以解析语音、图片、聊天记录等
|
||||
│ ├─ bias_addr # 获取偏移地址
|
||||
│ │ └─ get_bias_addr.py # 获取偏移地址脚本
|
||||
│ ├─ decrypted # 解密数据库
|
||||
│ │ ├─ decrypt.py # 解密数据库脚本
|
||||
│ │ └─ get_wx_decrypted_db.py # 直接读取当前登录微信的数据库,解密后保存到当前目录下的decrypted文件夹中
|
||||
│ ├─ wx_info # 获取微信基本信息
|
||||
│ │ ├─ get_wx_info.py # 获取微信基本信息脚本
|
||||
│ │ └─ get_wx_db.py # 获取本地所有的微信相关数据库
|
||||
│ ├─ show_records # 显示聊天记录
|
||||
│ │ ├─ main_window.py # 显示聊天记录的窗口
|
||||
│ │ └─ templates # 显示聊天记录的html模板
|
||||
│ ├─ command.py # 命令行入口
|
||||
│ └─ version_list.json # 微信版本列表 (十进制)按顺序代表:微信昵称、微信账号、微信手机号、微信邮箱(默认0)、微信KEY、微信原始ID(wxid_******)
|
||||
├─ doc # 项目文档
|
||||
│ ├─ python1.0_README.md # python1.0版本的README
|
||||
│ ├─ wx数据库简述.md # wx数据库简述
|
||||
│ └─ CE获取基址.md # CE获取基址
|
||||
├─ README.md
|
||||
├─ setup.py # 安装脚本
|
||||
└─ requirements.txt
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## 4. 其他
|
||||
## 3. 其他
|
||||
|
||||
[PyWxDump](https://github.com/xaoyaoo/PyWxDump)是[SharpWxDump](https://github.com/AdminTest0/SharpWxDump)
|
||||
的经过重构python语言版本,同时添加了一些新的功能。
|
||||
@ -182,15 +77,7 @@ PyWxDump
|
||||
* 如发现bug或有改进意见, 请提交[issues](https://github.com/xaoyaoo/PyWxDump/issues).
|
||||
* 如有其他想要的功能, 请提交[issues](https://github.com/xaoyaoo/PyWxDump/issues).
|
||||
* 常见问题请参考[FAQ](./doc/FAQ.md)
|
||||
|
||||
<details>
|
||||
<summary>提交issues方法(点击展开)</summary>
|
||||
|
||||
[](https://github.com/xaoyaoo/PyWxDump/issues)
|
||||
[](https://github.com/xaoyaoo/PyWxDump/issues)
|
||||
[](https://github.com/xaoyaoo/PyWxDump/issues)
|
||||
|
||||
</details>
|
||||
* 更新日志请参考[CHANGELOG](./doc/CHANGELOG.md)
|
||||
|
||||
<details>
|
||||
<summary><strong>Star History(click to expand)</strong></summary>
|
||||
@ -201,214 +88,7 @@ PyWxDump
|
||||
|
||||
# 二、使用说明
|
||||
|
||||
## 1. 安装
|
||||
|
||||
### 1.1 从pypi安装(安装稳定版)
|
||||
|
||||
```shell script
|
||||
pip install -U pywxdump
|
||||
```
|
||||
|
||||
### 1.2 从源码安装(安装最新版)
|
||||
|
||||
<details>
|
||||
<summary>点击展开</summary>
|
||||
|
||||
```shell script
|
||||
pip install -U git+git://github.com/xaoyaoo/PyWxDump.git
|
||||
```
|
||||
|
||||
或
|
||||
|
||||
```shell script
|
||||
git clone https://github.com/xaoyaoo/PyWxDump.git
|
||||
cd PyWxDump
|
||||
python -m pip install -U .
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### 1.3 使用可执行文件exe
|
||||
|
||||
<details>
|
||||
<summary>点击展开</summary>
|
||||
|
||||
* 1.下载[release](https://github.com/xaoyaoo/PyWxDump/releases)中的exe文件
|
||||
* 2.或者自行打包,打包脚本见: [build_exe.py](./tests/build_exe.py)
|
||||
|
||||
```shell
|
||||
cd tests
|
||||
python build_exe.py
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## 2. 使用
|
||||
|
||||
### 2.1 命令行
|
||||
|
||||
激活虚拟环境后(如果有的话),在项目根目录下运行:
|
||||
|
||||
```shell script
|
||||
wxdump 模式 [参数]
|
||||
# 运行模式(mode):
|
||||
# bias 获取微信基址偏移
|
||||
# info 获取微信信息
|
||||
# db_path 获取微信文件夹路径
|
||||
# decrypt 解密微信数据库
|
||||
# dbshow 聊天记录查看
|
||||
# export 聊天记录导出为html
|
||||
# all 获取微信信息,解密微信数据库,查看聊天记录
|
||||
```
|
||||
|
||||
*示例*
|
||||
|
||||
<details>
|
||||
<summary>点击展开示例</summary>
|
||||
|
||||
以下是示例命令:
|
||||
|
||||
##### 获取微信基址偏移
|
||||
|
||||
```bash
|
||||
pywxdump bias --mobile <手机号> --name <微信昵称> --account <微信账号> [--key <密钥>] [--db_path <已登录账号的微信文件夹路径>] [--version_list_path <微信版本偏移文件路径>]
|
||||
```
|
||||
|
||||
##### 获取微信信息
|
||||
|
||||
```bash
|
||||
pywxdump info [--version_list_path <微信版本偏移文件路径>]
|
||||
```
|
||||
|
||||
##### 获取微信文件夹路径
|
||||
|
||||
```bash
|
||||
pywxdump db_path [-r <需要的数据库名称>] [-wf <WeChat Files 路径>] [-id <wxid_>]
|
||||
```
|
||||
|
||||
##### 解密微信数据库
|
||||
|
||||
```bash
|
||||
pywxdump decrypt -k <密钥> -i <数据库路径(目录or文件)> [-o <输出路径>]
|
||||
```
|
||||
|
||||
##### 查看聊天记录
|
||||
|
||||
```bash
|
||||
pywxdump dbshow -msg <解密后的 MSG.db 的路径> -micro <解密后的 MicroMsg.db 的路径> -media <解密后的 MediaMSG.db 的路径> [-fs <FileStorage 路径>]
|
||||
```
|
||||
|
||||
##### 导出聊天记录为 HTML
|
||||
|
||||
```bash
|
||||
pywxdump export -u <微信账号> -o <导出路径> -msg <解密后的 MSG.db 的路径> -micro <解密后的 MicroMsg.db 的路径> -media <解密后的 MediaMSG.db 的路径> [-fs <FileStorage 路径>]
|
||||
```
|
||||
|
||||
##### 获取微信信息、解密数据库、查看聊天记录,一条命令搞定,开放端口5000,浏览器访问查看聊天记录(支持局域网其他机器访问)
|
||||
|
||||
```bash
|
||||
pywxdump all
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### 2.2 python API
|
||||
|
||||
*import调用示例*
|
||||
|
||||
<details>
|
||||
<summary>点击展开示例</summary>
|
||||
|
||||
```python
|
||||
# 单独使用各模块,返回值一般为字典,参数参考命令行
|
||||
from pywxdump import *
|
||||
|
||||
# ************************************************************************************************ #
|
||||
# 获取微信基址偏移
|
||||
args = {
|
||||
"mode": "bias",
|
||||
"mobile": "13800138000", # 手机号
|
||||
"name": "微信昵称", # 微信昵称
|
||||
"account": "微信账号", # 微信账号
|
||||
"key": "密钥", # 密钥(可选)
|
||||
"db_path": "已登录账号的微信文件夹路径", # 微信文件夹路径(可选)
|
||||
"version_list_path": "微信版本偏移文件路径" # 微信版本偏移文件路径(可选)
|
||||
}
|
||||
bias_addr = BiasAddr(args["account"], args["mobile"], args["name"], args["key"], args["db_path"])
|
||||
result = bias_addr.run(True, args["version_list_path"])
|
||||
# ************************************************************************************************ #
|
||||
# 获取微信信息
|
||||
wx_info = read_info(VERSION_LIST, True)
|
||||
|
||||
# 获取微信文件夹路径
|
||||
args = {
|
||||
"mode": "db_path",
|
||||
"require_list": "all", # 需要的数据库名称(可选)
|
||||
"wx_files": "WeChat Files", # 'WeChat Files'路径(可选)
|
||||
"wxid": "wxid_", # wxid_,用于确认用户文件夹(可选)
|
||||
}
|
||||
user_dirs = get_wechat_db(args["require_list"], args["wx_files"], args["wxid"], True)
|
||||
# ************************************************************************************************ #
|
||||
# 解密微信数据库
|
||||
args = {
|
||||
"mode": "decrypt",
|
||||
"key": "密钥", # 密钥
|
||||
"db_path": "数据库路径(目录or文件)", # 数据库路径
|
||||
"out_path": "/path/to/decrypted" # 输出路径(必须是目录)[默认为当前路径下decrypted文件夹]
|
||||
}
|
||||
result = batch_decrypt(args["key"], args["db_path"], args["out_path"], True)
|
||||
# ************************************************************************************************ #
|
||||
# 查看聊天记录
|
||||
args = {
|
||||
"mode": "dbshow",
|
||||
"msg_path": "解密后的 MSG.db 的路径", # 解密后的 MSG.db 的路径
|
||||
"micro_path": "解密后的 MicroMsg.db 的路径", # 解密后的 MicroMsg.db 的路径
|
||||
"media_path": "解密后的 MediaMSG.db 的路径", # 解密后的 MediaMSG.db 的路径
|
||||
"filestorage_path": "文件夹FileStorage的路径" # 文件夹 FileStorage 的路径(用于显示图片)
|
||||
}
|
||||
from flask import Flask, request, jsonify, render_template, g
|
||||
import logging
|
||||
|
||||
app = Flask(__name__, template_folder='./show_chat/templates')
|
||||
app.logger.setLevel(logging.ERROR)
|
||||
|
||||
|
||||
@app.before_request
|
||||
def before_request():
|
||||
g.MSG_ALL_db_path = args["msg_path"]
|
||||
g.MicroMsg_db_path = args["micro_path"]
|
||||
g.MediaMSG_all_db_path = args["media_path"]
|
||||
g.FileStorage_path = args["filestorage_path"]
|
||||
g.USER_LIST = get_user_list(args["msg_path"], args["micro_path"])
|
||||
|
||||
|
||||
app.register_blueprint(app_show_chat)
|
||||
print("[+] 请使用浏览器访问 http://127.0.0.1:5000/ 查看聊天记录")
|
||||
app.run(debug=False)
|
||||
# ************************************************************************************************ #
|
||||
# 导出聊天记录为 HTML
|
||||
args = {
|
||||
"mode": "export",
|
||||
"username": "微信账号", # 微信账号(聊天对象账号)
|
||||
"outpath": "/path/to/export", # 导出路径
|
||||
"msg_path": "解密后的 MSG.db 的路径", # 解密后的 MSG.db 的路径
|
||||
"micro_path": "解密后的 MicroMsg.db 的路径", # 解密后的 MicroMsg.db 的路径
|
||||
"media_path": "解密后的 MediaMSG.db 的路径", # 解密后的 MediaMSG.db 的路径
|
||||
"filestorage_path": "文件夹FileStorage的路径" # 文件夹 FileStorage 的路径(用于显示图片)
|
||||
}
|
||||
export(args["username"], args["outpath"], args["msg_path"], args["micro_path"], args["media_path"],
|
||||
args["filestorage_path"])
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
更多使用方法参考[tests](./tests)文件夹下的[test_*.py](./tests/)文件
|
||||
|
||||
### 2.3 可执行文件exe
|
||||
|
||||
进入exe文件所在目录,运行: `wxdump.exe 模式 [参数]`,方法同[命令行](#21-命令行)
|
||||
|
||||
### 2.4 其他说明
|
||||
详细使用说明见[UserGuide.md](./doc/UserGuide.md)
|
||||
|
||||
【注】:
|
||||
|
||||
|
53
doc/CHANGELOG.md
Normal file
53
doc/CHANGELOG.md
Normal file
@ -0,0 +1,53 @@
|
||||
# 更新日志
|
||||
|
||||
## v2.3.0 (2023-12-06)
|
||||
|
||||
### 新功能
|
||||
|
||||
- 增加数据库合并功能,方便查看
|
||||
- 重新整合代码,优化代码结构
|
||||
- 优化命令行参数
|
||||
|
||||
### 优化
|
||||
|
||||
- 修复部分bug
|
||||
|
||||
## version < v2.2.18 (2023-12-06)
|
||||
|
||||
### 优化/新功能
|
||||
|
||||
* 2023.12.03 增加分析聊天记录的功能,生成词云、绘制折线图等
|
||||
* 2023.12.03 修复部分bug,更改获取wx文件夹方式 [#34](https://github.com/xaoyaoo/PyWxDump/issues/34)
|
||||
* 2023.12.01 为exe添加图标
|
||||
* 2023.11.30 优化命令行界面
|
||||
* 2023.11.29 添加异形wxid获取方式,添加用户路径自动获取,重建说明文档,对新手更友好
|
||||
* 2023.11.28 修改wxid获取方式,修复部分bug
|
||||
* 2023.11.27 解决相对导入包的问题,完善错误提示
|
||||
* 2023.11.25 聊天记录查看工具bootstrap更换国内cdn
|
||||
* 2023.11.22 添加all命令中解密错误数据日志写入文件,修复部分bug
|
||||
* 2023.11.16 增加聊天记录导出为html
|
||||
* 2023.11.15 添加test文件,添加自动构建可执行文件的脚本,添加版本描述
|
||||
* 2023.11.15 [v2.2.5变化较大]重构解密脚本的返回值,重构命令行参数
|
||||
* 2023.11.15 修复无法获取wxid的bug
|
||||
* 2023.11.14 修复部分bug
|
||||
* 2023.11.11 添加聊天记录解析,查看工具,修复部分bug
|
||||
* 2023.11.10 修复wxdump wx_db命令行参数错误 [#19](https://github.com/xaoyaoo/PyWxDump/issues/19)
|
||||
* 2023.11.08 增加3.9.8.15版本支持
|
||||
* 2023.10.31 修复3.9.2.*版本无法正常运行
|
||||
* 2023.10.28 添加自动发布到pypi的github action
|
||||
* 2023.10.28 修复3.9.5.91版本的偏移
|
||||
* 2023.10.24 add auto get bias addr ,not need input key or wx folder path.
|
||||
* 2023.10.17 add LICENSE
|
||||
* 2023.10.16 添加"3.9.7.15"版本的偏移[#12](https://github.com/xaoyaoo/PyWxDump/issues/12)
|
||||
,感谢@[GentlemanII](https://github.com/GentlemanII)
|
||||
* 2023.10.15 将整个项目作为包安装,增加命令行统一操作
|
||||
* 2023.10.14 整体重构项目,优化代码,增加命令行统一操作
|
||||
* 2023.10.11 添加"3.9.5.81"版本的偏移[#10](https://github.com/xaoyaoo/PyWxDump/issues/10)
|
||||
,感谢@[sv3nbeast](https://github.com/sv3nbeast)
|
||||
* 2023.10.09 获取key基址偏移可以根据微信文件夹获取,不需要输入key
|
||||
* 2023.10.09 优化代码,删减没必要代码,重新修改获取基址代码,加快运行速度(需要安装新的库 pymem)
|
||||
* 2023.10.07 修改获取基址内存搜索方式,防止进入死循环
|
||||
* 2023.10.07 增加了3.9.7.29版本的偏移地址
|
||||
* 2023.10.06 增加命令行解密数据库
|
||||
* 2023.09.28 增加了数据库部分解析
|
||||
* 2023.09.15 增加了3.9.7.25版本的偏移地址
|
67
doc/FAQ.md
67
doc/FAQ.md
@ -1,13 +1,16 @@
|
||||
## 怎么下载
|
||||
# FAQ
|
||||
|
||||
方法一:进入链接[releases](https://github.com/xaoyaoo/PyWxDump/releases)下载最新版本exe文件
|
||||
- ### 一、怎么下载/怎么安装?
|
||||
|
||||
方法二:(本地安装有python环境)使用pip安装
|
||||
```
|
||||
pip install PyWxDump
|
||||
```
|
||||
方法一:进入链接[releases](https://github.com/xaoyaoo/PyWxDump/releases)下载最新版本exe文件
|
||||
|
||||
方法二:(本地安装有python环境)使用pip安装
|
||||
```
|
||||
pip install PyWxDump
|
||||
```
|
||||
|
||||
- ### 二、怎么使用
|
||||
|
||||
## 怎么使用
|
||||
|
||||
1. 打开微信电脑版,登录微信
|
||||
2. 进入下载的exe文件所在目录,使用pip安装,跳过此步
|
||||
@ -15,17 +18,55 @@ pip install PyWxDump
|
||||
4. 在命令窗口中输入`PyWxDump`,按回车键(pip安装输入`wxdump`)
|
||||
5. 接着根据提示输入参数,回车键确认
|
||||
|
||||
## 每台电脑上微信账户的key是不是永远不会变?
|
||||
- ### 三、每台电脑上微信账户的key是不是永远不会变?
|
||||
|
||||
同一设备,同一微信,不删除数据情况下,key(密钥)相同
|
||||
1. 同一设备,同一微信,不删除数据情况下,key(密钥)相同
|
||||
|
||||
- ### 四、刚打开就闪退的问题
|
||||
|
||||
## 刚打开就闪退的问题
|
||||
|
||||
1. 请检查是否由cmd或powershell打开,不要直接双击exe文件
|
||||
2. 如果使用方法二安装,请检查是否已经安装了python环境(如果使用pip安装,命令行直接输入wxdump即可)
|
||||
3. 如果使用方法二安装,检查是否将python安装目录添加到了环境变量中,如果没有,请添加
|
||||
|
||||
## 如果遇到其他问题
|
||||
- ### 五、如果遇到其他问题
|
||||
|
||||
1. 截图或复制错误信息,请全截图或全复制,不要只截一部分或复制部分信息。
|
||||
2. 通过issue反馈问题,或者加入QQ群:[加入QQ群](https://s.xaoyo.top/gOLUDl)
|
||||
|
||||
- ### 六、如何为PyWxDump贡献代码(提交pr)
|
||||
|
||||
提交拉取请求(Pull Request),请按照以下步骤进行操作:
|
||||
|
||||
1. Fork 仓库:首先,在项目的 GitHub 页面上点击 "Fork" 按钮,将项目的代码仓库 fork 到你自己的 GitHub 账号下。
|
||||
2. 克隆仓库:在你自己的 GitHub 账号下找到 fork 后的项目,点击 `Clone or download`按钮,获取仓库的 URL。然后在本地使用 Git
|
||||
命令克隆仓库到你的电脑上:`git clone 仓库的URL`
|
||||
3. 创建分支:在本地仓库中创建一个新的分支,用于进行你的修改:`git checkout -b 你的分支名`
|
||||
4. 进行修改:在新创建的分支中进行你需要的修改,包括修复错误、改进现有功能或添加新功能。
|
||||
5. 提交修改:使用 `git add` 和 `git commit` 命令将修改提交到本地仓库中:
|
||||
```
|
||||
git add .
|
||||
git commit -m "提交信息"
|
||||
```
|
||||
6. 推送分支:使用 `git push` 命令将你的本地分支推送到你的 GitHub 仓库中:`git push origin 你的分支名`
|
||||
7. 提交拉取请求:在你的 GitHub 仓库页面上切换到你刚刚推送的分支,点击 "New pull request"
|
||||
按钮,填写一些说明信息,然后点击 `Create pull request`
|
||||
按钮,即可提交拉取请求。
|
||||
8. 等待审核:等待项目维护者审核你的拉取请求,如果通过审核,你的修改将会被合并到项目的主分支中
|
||||
9. 接着你就可以在右边的`contributors`中看到你的名字了。
|
||||
|
||||
- ### 七、为什么要提交issues
|
||||
|
||||
1. 提交issues可以帮助我们更好的改进项目,提高项目的质量
|
||||
|
||||
- ### 八、提交issues方法
|
||||
|
||||
[](https://github.com/xaoyaoo/PyWxDump/issues)
|
||||
[](https://github.com/xaoyaoo/PyWxDump/issues)
|
||||
[](https://github.com/xaoyaoo/PyWxDump/issues)
|
||||
|
||||
- ### 九、版本差异
|
||||
|
||||
1. 版本 < 3.7.0.30 只运行不登录能获取个人信息,登录后可以获取数据库密钥
|
||||
2. 版本 > 3.7.0.30 只运行不登录不能获取个人信息,登录后都能获取
|
||||
|
||||
截图或复制错误信息,请全截图或全复制,不要只截一部分或复制部分信息。
|
||||
通过issue反馈问题,或者加入QQ群:[加入QQ群](https://s.xaoyo.top/gOLUDl)
|
||||
|
237
doc/UserGuide.md
Normal file
237
doc/UserGuide.md
Normal file
@ -0,0 +1,237 @@
|
||||
# 用户指南
|
||||
|
||||
## 小白教程
|
||||
|
||||
### 1. 安装
|
||||
|
||||
下载[release](https://github.com/xaoyaoo/PyWxDump/releases)中的exe文件
|
||||
|
||||
### 2. 使用
|
||||
|
||||
* 1.打开微信电脑版,登录微信
|
||||
* 2.进入下载的exe文件所在目录
|
||||
* 3.按住shift键,同时鼠标右键,选择“在此处打开命令窗口”,或者“在此处打开powershell窗口”
|
||||
* 4.在命令窗口中输入`wxdump`,按回车键
|
||||
* 5.接着根据提示输入参数,回车键确认
|
||||
eg:
|
||||
|
||||
```shell script
|
||||
wxdump info # 获取微信信息
|
||||
wxdump decrypt -k "密钥" -i "数据库路径(目录or文件)" # 解密微信数据库,引号必须在英文状态下输入
|
||||
wxdump dbshow -msg "解密后的 MSG.db 的路径" -micro "解密后的 MicroMsg.db 的路径" -media "解密后的 MediaMSG.db 的路径" # 接着打开浏览器访问 http://127.0.0.1:5000/ 查看聊天记录
|
||||
wxdump export -u "微信账号" -o "导出路径" -msg "解密后的 MSG.db 的路径" -micro "解密后的 MicroMsg.db 的路径" -media "解密后的 MediaMSG.db 的路径" # 导出聊天记录为html
|
||||
wxdump all # 获取微信信息,解密微信数据库,查看聊天记录
|
||||
```
|
||||
|
||||
* 6.查看聊天记录后,按`ctrl+c`退出
|
||||
|
||||
## 详细教程(小白请看上面)
|
||||
|
||||
### 1. 安装
|
||||
|
||||
#### 1.1 从pypi安装(安装稳定版)
|
||||
|
||||
```shell script
|
||||
pip install -U pywxdump
|
||||
```
|
||||
|
||||
#### 1.2 从源码安装(安装最新版)
|
||||
|
||||
```shell script
|
||||
pip install -U git+git://github.com/xaoyaoo/PyWxDump.git
|
||||
```
|
||||
|
||||
或
|
||||
|
||||
```shell script
|
||||
git clone https://github.com/xaoyaoo/PyWxDump.git
|
||||
cd PyWxDump
|
||||
python -m pip install -U .
|
||||
```
|
||||
|
||||
#### 1.3 打包可执行文件exe
|
||||
|
||||
* 自行打包,打包脚本见: [build_exe.py](./tests/build_exe.py)
|
||||
|
||||
```shell
|
||||
cd tests
|
||||
python build_exe.py
|
||||
# 接着执行输出的打包脚本
|
||||
```
|
||||
|
||||
* 直接下载打包好的exe文件:[release](https://github.com/xaoyaoo/PyWxDump/releases)
|
||||
|
||||
### 2. 使用
|
||||
|
||||
#### 2.1 命令行
|
||||
|
||||
激活虚拟环境后(如果有的话),在项目根目录下运行:
|
||||
|
||||
```shell script
|
||||
wxdump 模式 [参数]
|
||||
# 运行模式(mode):
|
||||
# bias 获取微信基址偏移
|
||||
# info 获取微信信息
|
||||
# db_path 获取微信文件夹路径
|
||||
# decrypt 解密微信数据库
|
||||
# dbshow 聊天记录查看
|
||||
# export 聊天记录导出为html
|
||||
# all 获取微信信息,解密微信数据库,查看聊天记录
|
||||
```
|
||||
|
||||
*示例*
|
||||
|
||||
<details>
|
||||
<summary>点击展开示例</summary>
|
||||
|
||||
以下是示例命令:
|
||||
|
||||
##### 获取微信基址偏移
|
||||
|
||||
```bash
|
||||
pywxdump bias --mobile <手机号> --name <微信昵称> --account <微信账号> [--key <密钥>] [--db_path <已登录账号的微信文件夹路径>] [--version_list_path <微信版本偏移文件路径>]
|
||||
```
|
||||
|
||||
##### 获取微信信息
|
||||
|
||||
```bash
|
||||
pywxdump info [--version_list_path <微信版本偏移文件路径>]
|
||||
```
|
||||
|
||||
##### 获取微信文件夹路径
|
||||
|
||||
```bash
|
||||
pywxdump db_path [-r <需要的数据库名称>] [-wf <WeChat Files 路径>] [-id <wxid_>]
|
||||
```
|
||||
|
||||
##### 解密微信数据库
|
||||
|
||||
```bash
|
||||
pywxdump decrypt -k <密钥> -i <数据库路径(目录or文件)> [-o <输出路径>]
|
||||
```
|
||||
|
||||
##### 查看聊天记录
|
||||
|
||||
```bash
|
||||
pywxdump dbshow -msg <解密后的 MSG.db 的路径> -micro <解密后的 MicroMsg.db 的路径> -media <解密后的 MediaMSG.db 的路径> [-fs <FileStorage 路径>]
|
||||
```
|
||||
|
||||
##### 导出聊天记录为 HTML
|
||||
|
||||
```bash
|
||||
pywxdump export -u <微信账号> -o <导出路径> -msg <解密后的 MSG.db 的路径> -micro <解密后的 MicroMsg.db 的路径> -media <解密后的 MediaMSG.db 的路径> [-fs <FileStorage 路径>]
|
||||
```
|
||||
|
||||
##### 获取微信信息、解密数据库、查看聊天记录,一条命令搞定,开放端口5000,浏览器访问查看聊天记录(支持局域网其他机器访问)
|
||||
|
||||
```bash
|
||||
pywxdump all
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### 2.2 python API
|
||||
|
||||
*import调用示例*
|
||||
|
||||
<details>
|
||||
<summary>点击展开示例</summary>
|
||||
|
||||
```python
|
||||
# 单独使用各模块,返回值一般为字典,参数参考命令行
|
||||
from pywxdump import *
|
||||
|
||||
# ************************************************************************************************ #
|
||||
# 获取微信基址偏移
|
||||
args = {
|
||||
"mode": "bias",
|
||||
"mobile": "13800138000", # 手机号
|
||||
"name": "微信昵称", # 微信昵称
|
||||
"account": "微信账号", # 微信账号
|
||||
"key": "密钥", # 密钥(可选)
|
||||
"db_path": "已登录账号的微信文件夹路径", # 微信文件夹路径(可选)
|
||||
"version_list_path": "微信版本偏移文件路径" # 微信版本偏移文件路径(可选)
|
||||
}
|
||||
bias_addr = BiasAddr(args["account"], args["mobile"], args["name"], args["key"], args["db_path"])
|
||||
result = bias_addr.run(True, args["version_list_path"])
|
||||
# ************************************************************************************************ #
|
||||
# 获取微信信息
|
||||
wx_info = read_info(VERSION_LIST, True)
|
||||
|
||||
# 获取微信文件夹路径
|
||||
args = {
|
||||
"mode": "db_path",
|
||||
"require_list": "all", # 需要的数据库名称(可选)
|
||||
"wx_files": "WeChat Files", # 'WeChat Files'路径(可选)
|
||||
"wxid": "wxid_", # wxid_,用于确认用户文件夹(可选)
|
||||
}
|
||||
user_dirs = get_wechat_db(args["require_list"], args["wx_files"], args["wxid"], True)
|
||||
# ************************************************************************************************ #
|
||||
# 解密微信数据库
|
||||
args = {
|
||||
"mode": "decrypt",
|
||||
"key": "密钥", # 密钥
|
||||
"db_path": "数据库路径(目录or文件)", # 数据库路径
|
||||
"out_path": "/path/to/decrypted" # 输出路径(必须是目录)[默认为当前路径下decrypted文件夹]
|
||||
}
|
||||
result = batch_decrypt(args["key"], args["db_path"], args["out_path"], True)
|
||||
# ************************************************************************************************ #
|
||||
# 查看聊天记录
|
||||
args = {
|
||||
"mode": "dbshow",
|
||||
"msg_path": "解密后的 MSG.db 的路径", # 解密后的 MSG.db 的路径
|
||||
"micro_path": "解密后的 MicroMsg.db 的路径", # 解密后的 MicroMsg.db 的路径
|
||||
"media_path": "解密后的 MediaMSG.db 的路径", # 解密后的 MediaMSG.db 的路径
|
||||
"filestorage_path": "文件夹FileStorage的路径" # 文件夹 FileStorage 的路径(用于显示图片)
|
||||
}
|
||||
from flask import Flask, request, jsonify, render_template, g
|
||||
import logging
|
||||
|
||||
app = Flask(__name__, template_folder='./show_chat/templates')
|
||||
app.logger.setLevel(logging.ERROR)
|
||||
|
||||
|
||||
@app.before_request
|
||||
def before_request():
|
||||
g.MSG_ALL_db_path = args["msg_path"]
|
||||
g.MicroMsg_db_path = args["micro_path"]
|
||||
g.MediaMSG_all_db_path = args["media_path"]
|
||||
g.FileStorage_path = args["filestorage_path"]
|
||||
g.USER_LIST = get_user_list(args["msg_path"], args["micro_path"])
|
||||
|
||||
|
||||
app.register_blueprint(app_show_chat)
|
||||
print("[+] 请使用浏览器访问 http://127.0.0.1:5000/ 查看聊天记录")
|
||||
app.run(debug=False)
|
||||
# ************************************************************************************************ #
|
||||
# 导出聊天记录为 HTML
|
||||
args = {
|
||||
"mode": "export",
|
||||
"username": "微信账号", # 微信账号(聊天对象账号)
|
||||
"outpath": "/path/to/export", # 导出路径
|
||||
"msg_path": "解密后的 MSG.db 的路径", # 解密后的 MSG.db 的路径
|
||||
"micro_path": "解密后的 MicroMsg.db 的路径", # 解密后的 MicroMsg.db 的路径
|
||||
"media_path": "解密后的 MediaMSG.db 的路径", # 解密后的 MediaMSG.db 的路径
|
||||
"filestorage_path": "文件夹FileStorage的路径" # 文件夹 FileStorage 的路径(用于显示图片)
|
||||
}
|
||||
export(args["username"], args["outpath"], args["msg_path"], args["micro_path"], args["media_path"],
|
||||
args["filestorage_path"])
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
更多使用方法参考[tests](../tests)文件夹下的[test_*.py](../tests/)文件
|
||||
|
||||
#### 2.3 可执行文件exe
|
||||
|
||||
进入exe文件所在目录,运行: `wxdump.exe 模式 [参数]`,方法同[命令行](#21-命令行)
|
||||
|
||||
### 3. FAQ
|
||||
|
||||
详见[FAQ](./FAQ.md)
|
||||
|
||||
### 4. 更新日志
|
||||
|
||||
详见[更新日志](./CHANGELOG.md)
|
||||
|
||||
|
@ -189,7 +189,7 @@ python get_wx_decrypted_db.py --key ********
|
||||
|
||||
## 四、解析数据库
|
||||
|
||||
* [parse.py](../pywxdump/analyse/parse.py) : 数据库解析脚本,可以解析语音、图片、聊天记录等
|
||||
* [parse.py](../pywxdump/analyzer/parse.py) : 数据库解析脚本,可以解析语音、图片、聊天记录等
|
||||
* 关于各个数据库的说明文档,请查看[wx数据库简述.md](./wx数据库简述.md)
|
||||
|
||||
未完待续...
|
||||
|
@ -5,13 +5,10 @@
|
||||
# Author: xaoyaoo
|
||||
# Date: 2023/10/14
|
||||
# -------------------------------------------------------------------------------
|
||||
from .bias_addr.get_bias_addr import BiasAddr
|
||||
from .wx_info.get_wx_info import read_info
|
||||
from .wx_info.get_wx_db import get_wechat_db
|
||||
from .decrypted.decrypt import batch_decrypt, decrypt,encrypt
|
||||
from .decrypted.get_wx_decrypted_db import all_decrypt, merge_copy_msg_db, merge_msg_db, merge_media_msg_db
|
||||
from .analyse.parse import read_img_dat, read_emoji, decompress_CompressContent, read_audio_buf, read_audio, parse_xml_string
|
||||
from .show_chat import app_show_chat, get_user_list, export
|
||||
from .wx_info import BiasAddr,read_info, get_wechat_db,encrypt,batch_decrypt,decrypt
|
||||
from .wx_info import merge_copy_db, merge_msg_db, merge_media_msg_db
|
||||
from .analyzer.db_parsing import read_img_dat, read_emoji, decompress_CompressContent, read_audio_buf, read_audio, parse_xml_string
|
||||
from .ui import app_show_chat, get_user_list, export
|
||||
|
||||
import os,json
|
||||
|
||||
|
@ -5,4 +5,4 @@
|
||||
# Author: xaoyaoo
|
||||
# Date: 2023/09/27
|
||||
# -------------------------------------------------------------------------------
|
||||
from .parse import read_img_dat, read_emoji, decompress_CompressContent, read_audio_buf, read_audio, parse_xml_string
|
||||
from .db_parsing import read_img_dat, read_emoji, decompress_CompressContent, read_audio_buf, read_audio, parse_xml_string
|
@ -153,9 +153,10 @@ def decompress_CompressContent(data):
|
||||
"""
|
||||
if data is None or not isinstance(data, bytes):
|
||||
return None
|
||||
|
||||
dst = lz4.block.decompress(data, uncompressed_size=len(data) << 8)
|
||||
dst.decode().replace('\x00', '') # 已经解码完成后,还含有0x00的部分,要删掉,要不后面ET识别的时候会报错
|
||||
uncompressed_data = dst.encode()
|
||||
uncompressed_data = dst.encode('utf-8', errors='ignore')
|
||||
return uncompressed_data
|
||||
|
||||
|
||||
@ -244,19 +245,25 @@ def wordcloud_generator(text, out_path="", is_show=False, img_path="", font="C:\
|
||||
wordcloud_img = wordcloud1.to_image()
|
||||
wordcloud_img.show()
|
||||
|
||||
def read_BytesExtra(bytes_extra):
|
||||
if bytes_extra is None:
|
||||
|
||||
def read_BytesExtra(BytesExtra):
|
||||
if BytesExtra is None or not isinstance(BytesExtra, bytes):
|
||||
return None
|
||||
try:
|
||||
deserialize_data, message_type = blackboxprotobuf.decode_message(bytes_extra)
|
||||
deserialize_data, message_type = blackboxprotobuf.decode_message(BytesExtra)
|
||||
return deserialize_data
|
||||
except Exception as e:
|
||||
# print(f"can not decode bytes_extra:{e}")
|
||||
return None
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
data = ''
|
||||
read_BytesExtra(data)
|
||||
print('*' * 50)
|
||||
data2 = ''
|
||||
read_BytesExtra(data2)
|
||||
DB = sqlite3.connect(r"D:\_code\py_code\test\a2023\b0821wxdb\merge_wfwx_db\kkWxMsg\MSG_all.db")
|
||||
cursor = DB.cursor()
|
||||
sql = "select MsgSvrID,BytesExtra from MSG where BytesExtra is not null and StrTalker='24724392255@chatroom' order by CreateTime desc limit 10"
|
||||
DBdata = cursor.execute(sql).fetchall()
|
||||
for i in DBdata:
|
||||
MsgSvrID, BytesExtra = i
|
||||
data = read_BytesExtra(BytesExtra)
|
||||
# 提取特定键的信息
|
||||
print(MsgSvrID,"\n",data)
|
||||
print("-" * 64)
|
324
pywxdump/analyzer/export_chat.py
Normal file
324
pywxdump/analyzer/export_chat.py
Normal file
@ -0,0 +1,324 @@
|
||||
# -*- coding: utf-8 -*-#
|
||||
# -------------------------------------------------------------------------------
|
||||
# Name: export_chat.py
|
||||
# Description:
|
||||
# Author: xaoyaoo
|
||||
# Date: 2023/12/03
|
||||
# -------------------------------------------------------------------------------
|
||||
# -*- coding: utf-8 -*-#
|
||||
# -------------------------------------------------------------------------------
|
||||
# Name: GUI.py
|
||||
# Description:
|
||||
# Author: xaoyaoo
|
||||
# Date: 2023/11/10
|
||||
# -------------------------------------------------------------------------------
|
||||
import base64
|
||||
import sqlite3
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
from functools import wraps
|
||||
|
||||
from .utils import get_md5, detach_databases, attach_databases, execute_sql
|
||||
from .db_parsing import read_img_dat, decompress_CompressContent, read_audio, parse_xml_string
|
||||
|
||||
from flask import Flask, request, render_template, g, Blueprint
|
||||
|
||||
|
||||
def get_contact_list(MicroMsg_db_path):
|
||||
"""
|
||||
获取联系人列表
|
||||
:param MicroMsg_db_path: MicroMsg.db 文件路径
|
||||
:return: 联系人列表
|
||||
"""
|
||||
users = []
|
||||
# 连接 MicroMsg.db 数据库,并执行查询
|
||||
db = sqlite3.connect(MicroMsg_db_path)
|
||||
cursor = db.cursor()
|
||||
cursor.execute(
|
||||
"SELECT A.UserName, A.NickName, A.Remark,B.bigHeadImgUrl FROM Contact A,ContactHeadImgUrl B ORDER BY NickName ASC")
|
||||
result = cursor.fetchall()
|
||||
|
||||
for row in result:
|
||||
# 获取用户名、昵称、备注和聊天记录数量
|
||||
username, nickname, remark, headImgUrl = row
|
||||
users.append({"username": username, "nickname": nickname, "remark": remark, "headImgUrl": headImgUrl})
|
||||
cursor.close()
|
||||
db.close()
|
||||
return users
|
||||
|
||||
|
||||
def msg_db_connect(func):
|
||||
@wraps(func)
|
||||
def wrapper(MSG_db_path, *args, **kwargs):
|
||||
# 连接 MSG.db 数据库,并执行查询
|
||||
if isinstance(MSG_db_path, list):
|
||||
# alias, file_path
|
||||
databases = {f"MSG{i}": db_path for i, db_path in enumerate(MSG_db_path)}
|
||||
elif isinstance(MSG_db_path, str):
|
||||
databases = {"MSG": MSG_db_path}
|
||||
else:
|
||||
raise TypeError("MSG_db_path 类型错误")
|
||||
|
||||
# 连接 MSG_ALL.db 数据库,并执行查询
|
||||
if len(databases) > 1:
|
||||
db = sqlite3.connect(":memory:")
|
||||
attach_databases(db, databases)
|
||||
else:
|
||||
db = sqlite3.connect(list(databases.values())[0])
|
||||
|
||||
result = func("", db=db, databases=databases, *args, **kwargs)
|
||||
|
||||
# 断开数据库连接
|
||||
if len(databases) > 1:
|
||||
for alias in databases:
|
||||
db.execute(f"DETACH DATABASE {alias}")
|
||||
db.close()
|
||||
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@msg_db_connect
|
||||
def get_chat_count(MSG_db_path: [str, list], db=None, databases=None):
|
||||
"""
|
||||
获取聊天记录数量
|
||||
:param MSG_db_path: MSG.db 文件路径
|
||||
:return: 聊天记录数量列表
|
||||
"""
|
||||
# 构造 SQL 查询,使用 UNION ALL 联合不同数据库的 MSG 表
|
||||
union_sql = " UNION ALL ".join(
|
||||
f"SELECT StrTalker, COUNT(*) AS ChatCount FROM {alias}.MSG GROUP BY StrTalker" for alias in databases)
|
||||
|
||||
sql = f"SELECT StrTalker, SUM(ChatCount) AS TotalChatCount FROM ({union_sql}) GROUP BY StrTalker ORDER BY TotalChatCount DESC"
|
||||
|
||||
chat_counts = []
|
||||
result = execute_sql(db, sql)
|
||||
for row in result:
|
||||
username, chat_count = row
|
||||
row_data = {"username": username, "chat_count": chat_count}
|
||||
chat_counts.append(row_data)
|
||||
return chat_counts
|
||||
|
||||
|
||||
def load_base64_audio_data(MsgSvrID, MediaMSG_all_db_path):
|
||||
wave_data = read_audio(MsgSvrID, is_wave=True, DB_PATH=MediaMSG_all_db_path)
|
||||
if not wave_data:
|
||||
return ""
|
||||
video_base64 = base64.b64encode(wave_data).decode("utf-8")
|
||||
video_data = f"data:audio/wav;base64,{video_base64}"
|
||||
return video_data
|
||||
|
||||
|
||||
def load_base64_img_data(start_time, end_time, username_md5, FileStorage_path):
|
||||
"""
|
||||
获取图片的base64数据
|
||||
:param start_time: 开始时间戳
|
||||
:param end_time: 结束时间戳
|
||||
:param username_md5: 用户名的md5值
|
||||
:return:
|
||||
"""
|
||||
# 获取CreateTime的最大值日期
|
||||
min_time = time.strftime("%Y-%m", time.localtime(start_time))
|
||||
max_time = time.strftime("%Y-%m", time.localtime(end_time))
|
||||
img_path = os.path.join(FileStorage_path, "MsgAttach", username_md5, "Image")
|
||||
if not os.path.exists(img_path):
|
||||
return {}
|
||||
# print(min_time, max_time, img_path)
|
||||
paths = []
|
||||
for root, path, files in os.walk(img_path):
|
||||
for p in path:
|
||||
if p >= min_time and p <= max_time:
|
||||
paths.append(os.path.join(root, p))
|
||||
# print(paths)
|
||||
img_md5_data = {}
|
||||
for path in paths:
|
||||
for root, path, files in os.walk(path):
|
||||
for file in files:
|
||||
if file.endswith(".dat"):
|
||||
file_path = os.path.join(root, file)
|
||||
fomt, md5, out_bytes = read_img_dat(file_path)
|
||||
out_bytes = base64.b64encode(out_bytes).decode("utf-8")
|
||||
img_md5_data[md5] = f"data:{fomt};base64,{out_bytes}"
|
||||
return img_md5_data
|
||||
|
||||
|
||||
def load_chat_records(selected_talker, start_index, page_size, user_list, MSG_ALL_db_path, MediaMSG_all_db_path,
|
||||
FileStorage_path):
|
||||
username = user_list.get("username", "")
|
||||
username_md5 = get_md5(username)
|
||||
type_name_dict = {
|
||||
1: {0: "文本"},
|
||||
3: {0: "图片"},
|
||||
34: {0: "语音"},
|
||||
43: {0: "视频"},
|
||||
47: {0: "动画表情"},
|
||||
49: {0: "文本", 1: "类似文字消息而不一样的消息", 5: "卡片式链接", 6: "文件", 8: "用户上传的 GIF 表情",
|
||||
19: "合并转发的聊天记录", 33: "分享的小程序", 36: "分享的小程序", 57: "带有引用的文本消息",
|
||||
63: "视频号直播或直播回放等",
|
||||
87: "群公告", 88: "视频号直播或直播回放等", 2000: "转账消息", 2003: "赠送红包封面"},
|
||||
50: {0: "语音通话"},
|
||||
10000: {0: "系统通知", 4: "拍一拍", 8000: "系统通知"}
|
||||
}
|
||||
|
||||
# 连接 MSG_ALL.db 数据库,并执行查询
|
||||
db1 = sqlite3.connect(MSG_ALL_db_path)
|
||||
cursor1 = db1.cursor()
|
||||
|
||||
cursor1.execute(
|
||||
"SELECT localId, IsSender, StrContent, StrTalker, Sequence, Type, SubType,CreateTime,MsgSvrID,DisplayContent,CompressContent FROM MSG WHERE StrTalker=? ORDER BY CreateTime ASC LIMIT ?,?",
|
||||
(selected_talker, start_index, page_size))
|
||||
result1 = cursor1.fetchall()
|
||||
|
||||
cursor1.close()
|
||||
db1.close()
|
||||
|
||||
img_md5_data = load_base64_img_data(result1[0][7], result1[-1][7], username_md5, FileStorage_path) # 获取图片的base64数据
|
||||
|
||||
data = []
|
||||
for row in result1:
|
||||
localId, IsSender, StrContent, StrTalker, Sequence, Type, SubType, CreateTime, MsgSvrID, DisplayContent, CompressContent = row
|
||||
CreateTime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(CreateTime))
|
||||
|
||||
type_name = type_name_dict.get(Type, {}).get(SubType, "未知")
|
||||
|
||||
content = {"src": "", "msg": "", "style": ""}
|
||||
|
||||
if Type == 47 and SubType == 0: # 动画表情
|
||||
content_tmp = parse_xml_string(StrContent)
|
||||
cdnurl = content_tmp.get("emoji", {}).get("cdnurl", "")
|
||||
# md5 = content_tmp.get("emoji", {}).get("md5", "")
|
||||
if cdnurl:
|
||||
content = {"src": cdnurl, "msg": "表情", "style": "width: 100px; height: 100px;"}
|
||||
|
||||
elif Type == 49 and SubType == 57: # 带有引用的文本消息
|
||||
CompressContent = CompressContent.rsplit(b'\x00', 1)[0]
|
||||
content["msg"] = decompress_CompressContent(CompressContent)
|
||||
try:
|
||||
content["msg"] = content["msg"].decode("utf-8")
|
||||
content["msg"] = parse_xml_string(content["msg"])
|
||||
content["msg"] = json.dumps(content["msg"], ensure_ascii=False)
|
||||
except Exception as e:
|
||||
content["msg"] = "[带有引用的文本消息]解析失败"
|
||||
elif Type == 34 and SubType == 0: # 语音
|
||||
tmp_c = parse_xml_string(StrContent)
|
||||
voicelength = tmp_c.get("voicemsg", {}).get("voicelength", "")
|
||||
transtext = tmp_c.get("voicetrans", {}).get("transtext", "")
|
||||
if voicelength.isdigit():
|
||||
voicelength = int(voicelength) / 1000
|
||||
voicelength = f"{voicelength:.2f}"
|
||||
content["msg"] = f"语音时长:{voicelength}秒\n翻译结果:{transtext}"
|
||||
|
||||
src = load_base64_audio_data(MsgSvrID, MediaMSG_all_db_path=MediaMSG_all_db_path)
|
||||
content["src"] = src
|
||||
elif Type == 3 and SubType == 0: # 图片
|
||||
xml_content = parse_xml_string(StrContent)
|
||||
md5 = xml_content.get("img", {}).get("md5", "")
|
||||
if md5:
|
||||
content["src"] = img_md5_data.get(md5, "")
|
||||
else:
|
||||
content["src"] = ""
|
||||
content["msg"] = "图片"
|
||||
|
||||
else:
|
||||
content["msg"] = StrContent
|
||||
|
||||
row_data = {"MsgSvrID": MsgSvrID, "type_name": type_name, "is_sender": IsSender,
|
||||
"content": content, "CreateTime": CreateTime}
|
||||
data.append(row_data)
|
||||
return data
|
||||
|
||||
|
||||
def export_html(user, outpath, MSG_ALL_db_path, MediaMSG_all_db_path, FileStorage_path, page_size=500):
|
||||
name_save = user.get("remark", user.get("nickname", user.get("username", "")))
|
||||
username = user.get("username", "")
|
||||
|
||||
chatCount = user.get("chat_count", 0)
|
||||
if chatCount == 0:
|
||||
return False, "没有聊天记录"
|
||||
|
||||
for i in range(0, chatCount, page_size):
|
||||
start_index = i
|
||||
data = load_chat_records(username, start_index, page_size, user, MSG_ALL_db_path, MediaMSG_all_db_path,
|
||||
FileStorage_path)
|
||||
if len(data) == 0:
|
||||
break
|
||||
save_path = os.path.join(outpath, f"{name_save}_{int(i / page_size)}.html")
|
||||
with open(save_path, "w", encoding="utf-8") as f:
|
||||
f.write(render_template("chat.html", msgs=data))
|
||||
return True, f"导出成功{outpath}"
|
||||
|
||||
|
||||
def export(username, outpath, MSG_ALL_db_path, MicroMsg_db_path, MediaMSG_all_db_path, FileStorage_path):
|
||||
if not os.path.exists(outpath):
|
||||
outpath = os.path.join(os.getcwd(), "export" + os.sep + username)
|
||||
if not os.path.exists(outpath):
|
||||
os.makedirs(outpath)
|
||||
|
||||
USER_LIST = get_user_list(MSG_ALL_db_path, MicroMsg_db_path)
|
||||
user = list(filter(lambda x: x["username"] == username, USER_LIST))
|
||||
|
||||
if username and len(user) > 0:
|
||||
user = user[0]
|
||||
return export_html(user, outpath, MSG_ALL_db_path, MediaMSG_all_db_path, FileStorage_path)
|
||||
|
||||
|
||||
app_show_chat = Blueprint('show_chat_main', __name__, template_folder='templates')
|
||||
app_show_chat.debug = False
|
||||
|
||||
|
||||
# 主页 - 显示用户列表
|
||||
@app_show_chat.route('/')
|
||||
def index():
|
||||
g.USER_LIST = get_user_list(g.MSG_ALL_db_path, g.MicroMsg_db_path)
|
||||
return render_template("index.html", users=g.USER_LIST)
|
||||
|
||||
|
||||
# 获取聊天记录
|
||||
@app_show_chat.route('/get_chat_data', methods=["GET", 'POST'])
|
||||
def get_chat_data():
|
||||
username = request.args.get("username", "")
|
||||
user = list(filter(lambda x: x["username"] == username, g.USER_LIST))
|
||||
|
||||
if username and len(user) > 0:
|
||||
user = user[0]
|
||||
|
||||
limit = int(request.args.get("limit", 100)) # 每页显示的条数
|
||||
page = int(request.args.get("page", user.get("chat_count", limit) / limit)) # 当前页数
|
||||
|
||||
start_index = (page - 1) * limit
|
||||
page_size = limit
|
||||
|
||||
data = load_chat_records(username, start_index, page_size, user, g.MSG_ALL_db_path, g.MediaMSG_all_db_path,
|
||||
g.FileStorage_path)
|
||||
return render_template("chat.html", msgs=data)
|
||||
else:
|
||||
return "error"
|
||||
|
||||
|
||||
# 聊天记录导出为html
|
||||
@app_show_chat.route('/export_chat_data', methods=["GET", 'POST'])
|
||||
def get_export():
|
||||
username = request.args.get("username", "")
|
||||
|
||||
user = list(filter(lambda x: x["username"] == username, g.USER_LIST))
|
||||
|
||||
if username and len(user) > 0:
|
||||
user = user[0]
|
||||
n = f"{user.get('username', '')}_{user.get('nickname', '')}_{user.get('remark', '')}"
|
||||
outpath = os.path.join(os.getcwd(), "export" + os.sep + n)
|
||||
if not os.path.exists(outpath):
|
||||
os.makedirs(outpath)
|
||||
|
||||
ret = export_html(user, outpath, g.MSG_ALL_db_path, g.MediaMSG_all_db_path, g.FileStorage_path, page_size=200)
|
||||
if ret[0]:
|
||||
return ret[1]
|
||||
else:
|
||||
return ret[1]
|
||||
else:
|
||||
return "error"
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pass
|
BIN
pywxdump/analyzer/img.png
Normal file
BIN
pywxdump/analyzer/img.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 331 KiB |
67
pywxdump/analyzer/utils.py
Normal file
67
pywxdump/analyzer/utils.py
Normal file
@ -0,0 +1,67 @@
|
||||
# -*- coding: utf-8 -*-#
|
||||
# -------------------------------------------------------------------------------
|
||||
# Name: utils.py
|
||||
# Description:
|
||||
# Author: xaoyaoo
|
||||
# Date: 2023/12/03
|
||||
# -------------------------------------------------------------------------------
|
||||
import hashlib
|
||||
|
||||
|
||||
def get_md5(data):
|
||||
"""
|
||||
获取数据的 MD5 值
|
||||
:param data: 数据(bytes)
|
||||
:return:
|
||||
"""
|
||||
md5 = hashlib.md5()
|
||||
md5.update(data)
|
||||
return md5.hexdigest()
|
||||
|
||||
|
||||
def attach_databases(connection, databases):
|
||||
"""
|
||||
将多个数据库附加到给定的SQLite连接。
|
||||
参数:
|
||||
-连接:SQLite连接
|
||||
-数据库:包含数据库别名和文件路径的词典
|
||||
"""
|
||||
cursor = connection.cursor()
|
||||
for alias, file_path in databases.items():
|
||||
attach_command = f"ATTACH DATABASE '{file_path}' AS {alias};"
|
||||
cursor.execute(attach_command)
|
||||
connection.commit()
|
||||
|
||||
|
||||
def detach_databases(connection, aliases):
|
||||
"""
|
||||
从给定的 SQLite 连接中分离多个数据库。
|
||||
|
||||
参数:
|
||||
- connection: SQLite连接
|
||||
- aliases:要分离的数据库别名列表
|
||||
"""
|
||||
cursor = connection.cursor()
|
||||
for alias in aliases:
|
||||
detach_command = f"DETACH DATABASE {alias};"
|
||||
cursor.execute(detach_command)
|
||||
connection.commit()
|
||||
|
||||
|
||||
def execute_sql(connection, sql, params=None):
|
||||
"""
|
||||
执行给定的SQL语句,返回结果。
|
||||
参数:
|
||||
- connection: SQLite连接
|
||||
- sql:要执行的SQL语句
|
||||
- params:SQL语句中的参数
|
||||
"""
|
||||
cursor = connection.cursor()
|
||||
if params:
|
||||
cursor.execute(sql, params)
|
||||
else:
|
||||
cursor.execute(sql)
|
||||
return cursor.fetchall()
|
||||
|
||||
if __name__ == '__main__':
|
||||
pass
|
@ -1,8 +0,0 @@
|
||||
# -*- coding: utf-8 -*-#
|
||||
# -------------------------------------------------------------------------------
|
||||
# Name: __init__.py.py
|
||||
# Description:
|
||||
# Author: xaoyaoo
|
||||
# Date: 2023/10/14
|
||||
# -------------------------------------------------------------------------------
|
||||
from .get_bias_addr import BiasAddr
|
@ -126,6 +126,45 @@ class MainDecrypt():
|
||||
return result
|
||||
|
||||
|
||||
class MainMerge():
|
||||
def init_parses(self, parser):
|
||||
self.mode = "merge"
|
||||
# 添加 'decrypt' 子命令解析器
|
||||
sb_merge = parser.add_parser(self.mode, help="合并微信数据库(MSG.db or MediaMSG.db)")
|
||||
sb_merge.add_argument("-t", "--dbtype", type=str, help="数据库类型(可选值):[msg,media]", required=True, metavar="")
|
||||
sb_merge.add_argument("-i", "--db_path", type=str, help="数据库路径(文件路径,使用英文[,]分割)", required=True, metavar="")
|
||||
sb_merge.add_argument("-o", "--out_path", type=str, default=os.path.join(os.getcwd(), "decrypted"),
|
||||
help="输出路径(必须是目录)[默认为当前路径下decrypted文件夹]", required=False,
|
||||
metavar="")
|
||||
return sb_merge
|
||||
|
||||
def run(self, args):
|
||||
# 从命令行参数获取值
|
||||
dbtype = args.dbtype
|
||||
db_path = args.db_path
|
||||
out_path = args.out_path
|
||||
|
||||
db_path = db_path.split(",")
|
||||
for i in db_path:
|
||||
if not os.path.exists(i):
|
||||
print(f"[-] 数据库路径不存在:{i}")
|
||||
return
|
||||
|
||||
if not os.path.exists(out_path):
|
||||
os.makedirs(out_path)
|
||||
print(f"[+] 创建输出文件夹:{out_path}")
|
||||
|
||||
if dbtype == "msg":
|
||||
result = merge_msg_db(db_path, out_path)
|
||||
elif dbtype == "media":
|
||||
result = merge_media_msg_db(db_path, out_path)
|
||||
else:
|
||||
print(f"[-] 未知数据库类型:{dbtype}")
|
||||
return
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class MainShowChatRecords():
|
||||
def init_parses(self, parser):
|
||||
self.mode = "dbshow"
|
||||
@ -363,6 +402,11 @@ def console_run():
|
||||
sb_decrypt = main_decrypt.init_parses(subparsers)
|
||||
modes[main_decrypt.mode] = main_decrypt
|
||||
|
||||
# 添加 'merge' 子命令解析器
|
||||
main_merge = MainMerge()
|
||||
sb_merge = main_merge.init_parses(subparsers)
|
||||
modes[main_merge.mode] = main_merge
|
||||
|
||||
# 添加 '' 子命令解析器
|
||||
main_show_chat_records = MainShowChatRecords()
|
||||
sb_dbshow = main_show_chat_records.init_parses(subparsers)
|
@ -1,9 +0,0 @@
|
||||
# -*- coding: utf-8 -*-#
|
||||
# -------------------------------------------------------------------------------
|
||||
# Name: __init__.py.py
|
||||
# Description:
|
||||
# Author: xaoyaoo
|
||||
# Date: 2023/08/21
|
||||
# -------------------------------------------------------------------------------
|
||||
from .decrypt import batch_decrypt, encrypt
|
||||
from .get_wx_decrypted_db import all_decrypt, merge_copy_msg_db, merge_msg_db, merge_media_msg_db
|
@ -3,6 +3,9 @@
|
||||
# Name: __init__.py.py
|
||||
# Description:
|
||||
# Author: xaoyaoo
|
||||
# Date: 2023/11/10
|
||||
# Date: 2023/12/03
|
||||
# -------------------------------------------------------------------------------
|
||||
from .main_window import app_show_chat, get_user_list, export
|
||||
from .view_chat import app_show_chat, get_user_list, export
|
||||
|
||||
if __name__ == '__main__':
|
||||
pass
|
@ -11,7 +11,7 @@ import os
|
||||
import json
|
||||
import time
|
||||
import hashlib
|
||||
from pywxdump.analyse import read_img_dat, decompress_CompressContent, read_audio, parse_xml_string
|
||||
from pywxdump.analyzer import read_img_dat, decompress_CompressContent, read_audio, parse_xml_string
|
||||
|
||||
from flask import Flask, request, render_template, g, Blueprint
|
||||
|
||||
@ -71,7 +71,7 @@ def load_base64_img_data(start_time, end_time, username_md5, FileStorage_path):
|
||||
# 获取CreateTime的最大值日期
|
||||
min_time = time.strftime("%Y-%m", time.localtime(start_time))
|
||||
max_time = time.strftime("%Y-%m", time.localtime(end_time))
|
||||
img_path = os.path.join(FileStorage_path, "MsgAttach", username_md5, "Image")
|
||||
img_path = os.path.join(FileStorage_path, "MsgAttach", username_md5, "Image") if FileStorage_path else ""
|
||||
if not os.path.exists(img_path):
|
||||
return {}
|
||||
# print(min_time, max_time, img_path)
|
@ -5,5 +5,7 @@
|
||||
# Author: xaoyaoo
|
||||
# Date: 2023/08/21
|
||||
# -------------------------------------------------------------------------------
|
||||
from .get_wx_info import read_info
|
||||
from .get_wx_db import get_wechat_db
|
||||
from .get_wx_info import read_info, get_wechat_db
|
||||
from .get_bias_addr import BiasAddr
|
||||
from .decryption import batch_decrypt, encrypt, decrypt
|
||||
from .merge_db import merge_msg_db, merge_copy_db, merge_media_msg_db
|
||||
|
@ -12,7 +12,6 @@
|
||||
# 为了保证数据部分长度是16字节即AES块大小的整倍数,每一页的末尾将填充一段空字节,使得保留字段的长度为48字节。
|
||||
# 综上,加密文件结构为第一页4KB数据前16字节为盐值,紧接着4032字节数据,再加上16字节IV和20字节HMAC以及12字节空字节;而后的页均是4048字节长度的加密数据段和48字节的保留段。
|
||||
# -------------------------------------------------------------------------------
|
||||
|
||||
import argparse
|
||||
import hmac
|
||||
import hashlib
|
||||
@ -206,23 +205,3 @@ def encrypt(key: str, db_path, out_path):
|
||||
enFile.write(encrypted)
|
||||
|
||||
return True, [db_path, out_path, key]
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 创建命令行参数解析器
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("-k", "--key", type=str, help="密钥", required=True)
|
||||
parser.add_argument("-i", "--db_path", type=str, help="数据库路径(目录or文件)", required=True)
|
||||
parser.add_argument("-o", "--out_path", type=str,
|
||||
help="输出路径(必须是目录),输出文件为 out_path/de_{original_name}", required=True)
|
||||
|
||||
# 解析命令行参数
|
||||
args = parser.parse_args()
|
||||
|
||||
# 从命令行参数获取值
|
||||
key = args.key
|
||||
db_path = args.db_path
|
||||
out_path = args.out_path
|
||||
|
||||
# 调用 decrypt 函数,并传入参数
|
||||
result = batch_decrypt(key, db_path, out_path, is_logging=True)
|
@ -99,7 +99,7 @@ class BiasAddr:
|
||||
self.mobile = mobile.encode("utf-8")
|
||||
self.name = name.encode("utf-8")
|
||||
self.key = bytes.fromhex(key) if key else b""
|
||||
self.db_path = db_path if os.path.exists(db_path) else ""
|
||||
self.db_path = db_path if db_path and os.path.exists(db_path) else ""
|
||||
|
||||
self.process_name = "WeChat.exe"
|
||||
self.module_name = "WeChatWin.dll"
|
||||
@ -251,6 +251,23 @@ class BiasAddr:
|
||||
key, bais = verify_key(maybe_key, wx_db_path)
|
||||
return bais
|
||||
|
||||
def test(self):
|
||||
phone_type1 = "iphone\x00"
|
||||
phone_type2 = "android\x00"
|
||||
Regex = re.compile(r"^[a-zA-Z0-9_]+$")
|
||||
# 内存搜索
|
||||
module = pymem.process.module_from_name(self.pm.process_handle, self.module_name)
|
||||
print(hex(module.lpBaseOfDll))
|
||||
phone_type1_bias = self.pm.pattern_scan_module(phone_type1.encode(), self.module_name, return_multiple=True)
|
||||
phone_type2_bias = self.pm.pattern_scan_module(phone_type2.encode(), self.module_name, return_multiple=True)
|
||||
phone_type_bias = phone_type1_bias + phone_type2_bias
|
||||
print(len(phone_type1_bias))
|
||||
for i in phone_type_bias[::-1]:
|
||||
for j in range(i, i - 1000, -16):
|
||||
a = get_info_without_key(self.process_handle, j, 32)
|
||||
if Regex.match(a) and len(a) >= 6:
|
||||
print(a)
|
||||
|
||||
def run(self, logging_path=False, version_list_path=None):
|
||||
if not self.get_process_handle()[0]:
|
||||
return None
|
||||
@ -263,6 +280,8 @@ class BiasAddr:
|
||||
key_bias = self.get_key_bias2(self.db_path, account_bias) if key_bias <= 0 and self.db_path else key_bias
|
||||
|
||||
rdata = {self.version: [name_bias, account_bias, mobile_bias, 0, key_bias]}
|
||||
# print(rdata)
|
||||
# self.test()
|
||||
if version_list_path and os.path.exists(version_list_path):
|
||||
with open(version_list_path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
@ -279,235 +298,15 @@ class BiasAddr:
|
||||
return rdata
|
||||
|
||||
|
||||
# class BiasAddr:
|
||||
# def __init__(self, account, mobile, name, key, db_path):
|
||||
# self.account = account.encode("utf-8")
|
||||
# self.mobile = mobile.encode("utf-8")
|
||||
# self.name = name.encode("utf-8")
|
||||
# self.key = bytes.fromhex(key) if key else b""
|
||||
# self.db_path = db_path if db_path else ""
|
||||
#
|
||||
# self.process_name = "WeChat.exe"
|
||||
# self.module_name = "WeChatWin.dll"
|
||||
#
|
||||
# self.pm = Pymem("WeChat.exe")
|
||||
#
|
||||
# self.bits = self.get_osbits()
|
||||
# self.version = self.get_file_version(self.process_name)
|
||||
# self.address_len = self.get_addr_len()
|
||||
#
|
||||
# self.islogin = True
|
||||
#
|
||||
# def get_addr_len(self):
|
||||
# version_nums = list(map(int, self.version.split("."))) # 将版本号拆分为数字列表
|
||||
# if version_nums[0] <= 3 and version_nums[1] <= 9 and version_nums[2] <= 2:
|
||||
# return 4
|
||||
# else:
|
||||
# return 8
|
||||
#
|
||||
# def find_all(self, c: bytes, string: bytes, base_addr=0):
|
||||
# """
|
||||
# 查找字符串中所有子串的位置
|
||||
# :param c: 子串 b'123'
|
||||
# :param string: 字符串 b'123456789123'
|
||||
# :return:
|
||||
# """
|
||||
# return [base_addr + m.start() for m in re.finditer(re.escape(c), string)]
|
||||
#
|
||||
# def get_file_version(self, process_name):
|
||||
# for process in psutil.process_iter(['pid', 'name', 'exe']):
|
||||
# if process.name() == process_name:
|
||||
# file_version = Dispatch("Scripting.FileSystemObject").GetFileVersion(process.exe())
|
||||
# return file_version
|
||||
# self.islogin = False
|
||||
#
|
||||
# def get_osbits(self):
|
||||
# return int(platform.architecture()[0][:-3])
|
||||
#
|
||||
# def search_memory_value(self, value: bytes, module_name="WeChatWin.dll"):
|
||||
# # 创建 Pymem 对象
|
||||
# pm = self.pm
|
||||
# module = pymem.process.module_from_name(pm.process_handle, module_name)
|
||||
#
|
||||
# # result = pymem.pattern.pattern_scan_module(pm.process_handle, module, value, return_multiple=True)
|
||||
# # result = result[-1]-module.lpBaseOfDll if len(result) > 0 else 0
|
||||
# mem_data = pm.read_bytes(module.lpBaseOfDll, module.SizeOfImage)
|
||||
# result = self.find_all(value, mem_data)
|
||||
# result = result[-1] if len(result) > 0 else 0
|
||||
# return result
|
||||
#
|
||||
# def search_key(self, key: bytes):
|
||||
# byteLen = self.address_len # if self.bits == 32 else 8 # 4字节或8字节
|
||||
# key = re.escape(key) # 转义特殊字符
|
||||
# key_addr = self.pm.pattern_scan_all(key, return_multiple=True)[-1] if len(key) > 0 else 0
|
||||
# key = key_addr.to_bytes(byteLen, byteorder='little', signed=True)
|
||||
# result = self.search_memory_value(key, self.module_name)
|
||||
# return result
|
||||
#
|
||||
# def get_key_bias_test(self):
|
||||
# byteLen = self.address_len # 4 if self.bits == 32 else 8 # 4字节或8字节
|
||||
# keyLenOffset = 0x8c if self.bits == 32 else 0xd0
|
||||
# keyWindllOffset = 0x90 if self.bits == 32 else 0xd8
|
||||
#
|
||||
# pm = self.pm
|
||||
#
|
||||
# module = pymem.process.module_from_name(pm.process_handle, "WeChatWin.dll")
|
||||
# keyBytes = b'-----BEGIN PUBLIC KEY-----\n...'
|
||||
# publicKeyList = pymem.pattern.pattern_scan_all(self.pm.process_handle, keyBytes, return_multiple=True)
|
||||
#
|
||||
# keyaddrs = []
|
||||
# for addr in publicKeyList:
|
||||
# keyBytes = addr.to_bytes(byteLen, byteorder="little", signed=True) # 低位在前
|
||||
# addrs = pymem.pattern.pattern_scan_module(pm.process_handle, module, keyBytes, return_multiple=True)
|
||||
# if addrs != 0:
|
||||
# keyaddrs += addrs
|
||||
#
|
||||
# keyWinAddr = 0
|
||||
# for addr in keyaddrs:
|
||||
# keyLen = pm.read_uchar(addr - keyLenOffset)
|
||||
# if keyLen != 32:
|
||||
# continue
|
||||
# keyWinAddr = addr - keyWindllOffset
|
||||
# # keyaddr = int.from_bytes(pm.read_bytes(keyWinAddr, byteLen), byteorder='little')
|
||||
# # key = pm.read_bytes(keyaddr, 32)
|
||||
# # print("key", key.hex())
|
||||
#
|
||||
# return keyWinAddr - module.lpBaseOfDll
|
||||
#
|
||||
# def get_key_bias(self, wx_db_path, account_bias=0):
|
||||
# wx_db_path = os.path.join(wx_db_path, "Msg", "MicroMsg.db")
|
||||
# if not os.path.exists(wx_db_path):
|
||||
# return 0
|
||||
#
|
||||
# def get_maybe_key(mem_data):
|
||||
# maybe_key = []
|
||||
# for i in range(0, len(mem_data), self.address_len):
|
||||
# addr = mem_data[i:i + self.address_len]
|
||||
# addr = int.from_bytes(addr, byteorder='little')
|
||||
# # 去掉不可能的地址
|
||||
# if min_addr < addr < max_addr:
|
||||
# key = read_key(addr)
|
||||
# if key == b"":
|
||||
# continue
|
||||
# maybe_key.append([key, i])
|
||||
# return maybe_key
|
||||
#
|
||||
# def read_key(addr):
|
||||
# key = ctypes.create_string_buffer(35)
|
||||
# if ReadProcessMemory(pm.process_handle, void_p(addr - 1), key, 35, 0) == 0:
|
||||
# return b""
|
||||
#
|
||||
# if b"\x00\x00" in key.raw[1:33]:
|
||||
# return b""
|
||||
#
|
||||
# if b"\x00\x00" == key.raw[33:35] and b"\x90" == key.raw[0:1]:
|
||||
# return key.raw[1:33]
|
||||
# return b""
|
||||
#
|
||||
# def verify_key(keys, wx_db_path):
|
||||
# with open(wx_db_path, "rb") as file:
|
||||
# blist = file.read(5000)
|
||||
# salt = blist[:16]
|
||||
# first = blist[16:DEFAULT_PAGESIZE]
|
||||
# mac_salt = bytes([(salt[i] ^ 58) for i in range(16)])
|
||||
#
|
||||
# with multiprocessing.Pool(processes=8) as pool:
|
||||
# results = [pool.apply_async(validate_key, args=(key, salt, first, mac_salt)) for key, i in keys[-1::-1]]
|
||||
# results = [p.get() for p in results]
|
||||
# for i, result in enumerate(results[-1::-1]):
|
||||
# if result:
|
||||
# return keys[i]
|
||||
# return b"", 0
|
||||
#
|
||||
# module_name = "WeChatWin.dll"
|
||||
# pm = self.pm
|
||||
# module = pymem.process.module_from_name(pm.process_handle, module_name)
|
||||
# start_addr = module.lpBaseOfDll
|
||||
# size = module.SizeOfImage
|
||||
#
|
||||
# if account_bias > 1:
|
||||
# maybe_key = []
|
||||
# for i in [0x24, 0x40]:
|
||||
# addr = start_addr + account_bias - i
|
||||
# mem_data = pm.read_bytes(addr, self.address_len)
|
||||
# key = read_key(int.from_bytes(mem_data, byteorder='little'))
|
||||
# if key != b"":
|
||||
# maybe_key.append([key, addr - start_addr])
|
||||
# key, bais = verify_key(maybe_key, wx_db_path)
|
||||
# if bais != 0:
|
||||
# return bais
|
||||
#
|
||||
# min_addr = 0xffffffffffffffffffffffff
|
||||
# max_addr = 0
|
||||
# for module1 in pm.list_modules():
|
||||
# if module1.lpBaseOfDll < min_addr:
|
||||
# min_addr = module1.lpBaseOfDll
|
||||
# if module1.lpBaseOfDll > max_addr:
|
||||
# max_addr = module1.lpBaseOfDll + module1.SizeOfImage
|
||||
#
|
||||
# mem_data = pm.read_bytes(start_addr, size)
|
||||
# maybe_key = get_maybe_key(mem_data)
|
||||
# key, bais = verify_key(maybe_key, wx_db_path)
|
||||
# return bais
|
||||
#
|
||||
# def run(self, is_logging=False, version_list_path=None):
|
||||
# self.version = self.get_file_version(self.process_name)
|
||||
# if not self.islogin:
|
||||
# error = "[-] WeChat No Run"
|
||||
# if is_logging: print(error)
|
||||
# return error
|
||||
# mobile_bias = self.search_memory_value(self.mobile)
|
||||
# name_bias = self.search_memory_value(self.name)
|
||||
# account_bias = self.search_memory_value(self.account)
|
||||
# # version_bias = self.search_memory_value(self.version.encode("utf-8"))
|
||||
#
|
||||
# try:
|
||||
# key_bias = self.get_key_bias_test()
|
||||
# except:
|
||||
# key_bias = 0
|
||||
#
|
||||
# if key_bias <= 0:
|
||||
# if self.key:
|
||||
# key_bias = self.search_key(self.key)
|
||||
# elif self.db_path:
|
||||
# key_bias = self.get_key_bias(self.db_path, account_bias)
|
||||
# else:
|
||||
# key_bias = 0
|
||||
# rdata = {self.version: [name_bias, account_bias, mobile_bias, 0, key_bias]}
|
||||
# if version_list_path and os.path.exists(version_list_path):
|
||||
# with open(version_list_path, "r", encoding="utf-8") as f:
|
||||
# data = json.load(f)
|
||||
# data.update(rdata)
|
||||
# with open(version_list_path, "w", encoding="utf-8") as f:
|
||||
# json.dump(data, f, ensure_ascii=False, indent=4)
|
||||
# if is_logging:
|
||||
# print("{版本号:昵称,账号,手机号,邮箱,KEY}")
|
||||
# print(rdata)
|
||||
# return rdata
|
||||
def get_info_without_key(h_process, address, n_size=64):
|
||||
array = ctypes.create_string_buffer(n_size)
|
||||
if ReadProcessMemory(h_process, void_p(address), array, n_size, 0) == 0: return "None"
|
||||
array = bytes(array).split(b"\x00")[0] if b"\x00" in array else bytes(array)
|
||||
text = array.decode('utf-8', errors='ignore')
|
||||
return text.strip() if text.strip() != "" else "None"
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 创建命令行参数解析器
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--mobile", type=str, help="手机号", required=True)
|
||||
parser.add_argument("--name", type=str, help="微信昵称", required=True)
|
||||
parser.add_argument("--account", type=str, help="微信账号", required=True)
|
||||
parser.add_argument("--key", type=str, help="(可选)密钥")
|
||||
parser.add_argument("--db_path", type=str, help="(可选)已登录账号的微信文件夹路径")
|
||||
|
||||
# 解析命令行参数
|
||||
args = parser.parse_args()
|
||||
|
||||
# 检查是否缺少必要参数,并抛出错误
|
||||
if not args.mobile or not args.name or not args.account:
|
||||
raise ValueError("缺少必要的命令行参数!请提供手机号、微信昵称、微信账号。")
|
||||
|
||||
# 从命令行参数获取值
|
||||
mobile = args.mobile
|
||||
name = args.name
|
||||
account = args.account
|
||||
key = args.key
|
||||
db_path = args.db_path
|
||||
|
||||
# 调用 run 函数,并传入参数
|
||||
rdata = BiasAddr(account, mobile, name, key, db_path).run(True, "../version_list.json")
|
||||
account, mobile, name, key, db_path = "test", "test", "test", "0000", "test"
|
||||
bias_addr = BiasAddr(account, mobile, name, key, db_path)
|
||||
bias_addr.run()
|
@ -1,101 +0,0 @@
|
||||
# -*- coding: utf-8 -*-#
|
||||
# -------------------------------------------------------------------------------
|
||||
# Name: get_wx_db.py
|
||||
# Description:
|
||||
# Author: xaoyaoo
|
||||
# Date: 2023/10/14
|
||||
# -------------------------------------------------------------------------------
|
||||
import os
|
||||
import re
|
||||
import winreg
|
||||
from typing import List, Union
|
||||
|
||||
|
||||
def get_wechat_db(require_list: Union[List[str], str] = "all", msg_dir: str = None, wxid: Union[List[str], str] = None,
|
||||
is_logging: bool = False):
|
||||
if not msg_dir:
|
||||
try:
|
||||
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Tencent\WeChat", 0, winreg.KEY_READ)
|
||||
value, _ = winreg.QueryValueEx(key, "FileSavePath")
|
||||
winreg.CloseKey(key)
|
||||
w_dir = value
|
||||
except Exception as e:
|
||||
# 获取文档实际目录
|
||||
try:
|
||||
# 打开注册表路径
|
||||
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER,
|
||||
r"Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders")
|
||||
documents_path = winreg.QueryValueEx(key, "Personal")[0] # 读取文档实际目录路径
|
||||
winreg.CloseKey(key) # 关闭注册表
|
||||
documents_paths = os.path.split(documents_path)
|
||||
if "%" in documents_paths[0]:
|
||||
w_dir = os.environ.get(documents_paths[0].replace("%", ""))
|
||||
w_dir = os.path.join(w_dir, os.path.join(*documents_paths[1:]))
|
||||
else:
|
||||
w_dir = documents_path
|
||||
except Exception as e:
|
||||
profile = os.path.expanduser("~")
|
||||
w_dir = os.path.join(profile, "Documents")
|
||||
msg_dir = os.path.join(w_dir, "WeChat Files")
|
||||
|
||||
if not os.path.exists(msg_dir):
|
||||
error = f"[-] 目录不存在: {msg_dir}"
|
||||
if is_logging: print(error)
|
||||
return error
|
||||
|
||||
user_dirs = {} # wx用户目录
|
||||
files = os.listdir(msg_dir)
|
||||
if wxid: # 如果指定wxid
|
||||
if isinstance(wxid, str):
|
||||
wxid = wxid.split(";")
|
||||
for file_name in files:
|
||||
if file_name in wxid:
|
||||
user_dirs[os.path.join(msg_dir, file_name)] = os.path.join(msg_dir, file_name)
|
||||
else: # 如果未指定wxid
|
||||
for file_name in files:
|
||||
if file_name == "All Users" or file_name == "Applet" or file_name == "WMPF":
|
||||
continue
|
||||
user_dirs[os.path.join(msg_dir, file_name)] = os.path.join(msg_dir, file_name)
|
||||
|
||||
if isinstance(require_list, str):
|
||||
require_list = require_list.split(";")
|
||||
|
||||
# generate pattern
|
||||
if "all" in require_list:
|
||||
pattern = {"all": re.compile(r".*\.db$")}
|
||||
elif isinstance(require_list, list):
|
||||
pattern = {}
|
||||
for require in require_list:
|
||||
pattern[require] = re.compile(r"%s.*\.db$" % require)
|
||||
else:
|
||||
error = f"[-] 参数错误: {require_list}"
|
||||
if is_logging: print(error)
|
||||
return error
|
||||
|
||||
# 获取数据库路径
|
||||
for user, user_dir in user_dirs.items(): # 遍历用户目录
|
||||
user_dirs[user] = {n: [] for n in pattern.keys()}
|
||||
for root, dirs, files in os.walk(user_dir):
|
||||
for file_name in files:
|
||||
for n, p in pattern.items():
|
||||
if p.match(file_name):
|
||||
src_path = os.path.join(root, file_name)
|
||||
user_dirs[user][n].append(src_path)
|
||||
|
||||
if is_logging:
|
||||
for user, user_dir in user_dirs.items():
|
||||
print(f"[+] user_path: {user}")
|
||||
for n, paths in user_dir.items():
|
||||
print(f" {n}:")
|
||||
for path in paths:
|
||||
print(f" {path.replace(user, '')}")
|
||||
print("-" * 32)
|
||||
print(f"[+] 共 {len(user_dirs)} 个微信账号")
|
||||
|
||||
return user_dirs
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
require_list = ["MediaMSG", "MicroMsg", "FTSMSG", "MSG", "Sns", "Emotion"]
|
||||
# require_list = "all"
|
||||
user_dirs = get_wechat_db(require_list, is_logging=True)
|
@ -8,11 +8,13 @@
|
||||
import json
|
||||
import ctypes
|
||||
import os
|
||||
import re
|
||||
import winreg
|
||||
import pymem
|
||||
from win32com.client import Dispatch
|
||||
import psutil
|
||||
import sys
|
||||
from typing import List, Union
|
||||
|
||||
ReadProcessMemory = ctypes.windll.kernel32.ReadProcessMemory
|
||||
void_p = ctypes.c_void_p
|
||||
@ -89,24 +91,32 @@ def get_info_filePath(wxid="all"):
|
||||
value, _ = winreg.QueryValueEx(key, "FileSavePath")
|
||||
winreg.CloseKey(key)
|
||||
w_dir = value
|
||||
print(0, w_dir)
|
||||
except Exception as e:
|
||||
# 获取文档实际目录
|
||||
try:
|
||||
# 打开注册表路径
|
||||
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER,r"Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders")
|
||||
documents_path = winreg.QueryValueEx(key, "Personal")[0]# 读取文档实际目录路径
|
||||
winreg.CloseKey(key) # 关闭注册表
|
||||
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER,
|
||||
r"Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders")
|
||||
documents_path = winreg.QueryValueEx(key, "Personal")[0] # 读取文档实际目录路径
|
||||
winreg.CloseKey(key) # 关闭注册表
|
||||
documents_paths = os.path.split(documents_path)
|
||||
if "%" in documents_paths[0]:
|
||||
w_dir = os.environ.get(documents_paths[0].replace("%",""))
|
||||
w_dir = os.path.join(w_dir,os.path.join(*documents_paths[1:]))
|
||||
w_dir = os.environ.get(documents_paths[0].replace("%", ""))
|
||||
w_dir = os.path.join(w_dir, os.path.join(*documents_paths[1:]))
|
||||
print(1, w_dir)
|
||||
else:
|
||||
w_dir = documents_path
|
||||
print(2, w_dir)
|
||||
except Exception as e:
|
||||
profile = os.path.expanduser("~")
|
||||
profile = os.environ.get("USERPROFILE") # 获取用户目录
|
||||
w_dir = os.path.join(profile, "Documents")
|
||||
print(3, w_dir)
|
||||
if w_dir == "MyDocument:":
|
||||
profile = os.environ.get("USERPROFILE")
|
||||
w_dir = os.path.join(profile, "Documents")
|
||||
msg_dir = os.path.join(w_dir, "WeChat Files")
|
||||
|
||||
print(msg_dir)
|
||||
if wxid == "all" and os.path.exists(msg_dir):
|
||||
return msg_dir
|
||||
|
||||
@ -194,27 +204,69 @@ def read_info(version_list, is_logging=False):
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
def get_wechat_db(require_list: Union[List[str], str] = "all", msg_dir: str = None, wxid: Union[List[str], str] = None,
|
||||
is_logging: bool = False):
|
||||
if not msg_dir:
|
||||
msg_dir = get_info_filePath(wxid="all")
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--vlfile", type=str, help="手机号", required=False)
|
||||
parser.add_argument("--vldict", type=str, help="微信昵称", required=False)
|
||||
if not os.path.exists(msg_dir):
|
||||
error = f"[-] 目录不存在: {msg_dir}"
|
||||
if is_logging: print(error)
|
||||
return error
|
||||
|
||||
args = parser.parse_args()
|
||||
user_dirs = {} # wx用户目录
|
||||
files = os.listdir(msg_dir)
|
||||
if wxid: # 如果指定wxid
|
||||
if isinstance(wxid, str):
|
||||
wxid = wxid.split(";")
|
||||
for file_name in files:
|
||||
if file_name in wxid:
|
||||
user_dirs[os.path.join(msg_dir, file_name)] = os.path.join(msg_dir, file_name)
|
||||
else: # 如果未指定wxid
|
||||
for file_name in files:
|
||||
if file_name == "All Users" or file_name == "Applet" or file_name == "WMPF":
|
||||
continue
|
||||
user_dirs[os.path.join(msg_dir, file_name)] = os.path.join(msg_dir, file_name)
|
||||
|
||||
# 读取微信各版本偏移
|
||||
if args.vlfile:
|
||||
VERSION_LIST_PATH = args.vlfile
|
||||
with open(VERSION_LIST_PATH, "r", encoding="utf-8") as f:
|
||||
VERSION_LIST = json.load(f)
|
||||
if args.vldict:
|
||||
VERSION_LIST = json.loads(args.vldict)
|
||||
if isinstance(require_list, str):
|
||||
require_list = require_list.split(";")
|
||||
|
||||
if not args.vlfile and not args.vldict:
|
||||
VERSION_LIST_PATH = "../version_list.json"
|
||||
# generate pattern
|
||||
if "all" in require_list:
|
||||
pattern = {"all": re.compile(r".*\.db$")}
|
||||
elif isinstance(require_list, list):
|
||||
pattern = {}
|
||||
for require in require_list:
|
||||
pattern[require] = re.compile(r"%s.*\.db$" % require)
|
||||
else:
|
||||
error = f"[-] 参数错误: {require_list}"
|
||||
if is_logging: print(error)
|
||||
return error
|
||||
|
||||
with open(VERSION_LIST_PATH, "r", encoding="utf-8") as f:
|
||||
VERSION_LIST = json.load(f)
|
||||
# 获取数据库路径
|
||||
for user, user_dir in user_dirs.items(): # 遍历用户目录
|
||||
user_dirs[user] = {n: [] for n in pattern.keys()}
|
||||
for root, dirs, files in os.walk(user_dir):
|
||||
for file_name in files:
|
||||
for n, p in pattern.items():
|
||||
if p.match(file_name):
|
||||
src_path = os.path.join(root, file_name)
|
||||
user_dirs[user][n].append(src_path)
|
||||
|
||||
result = read_info(VERSION_LIST, True) # 读取微信信息
|
||||
if is_logging:
|
||||
for user, user_dir in user_dirs.items():
|
||||
print(f"[+] user_path: {user}")
|
||||
for n, paths in user_dir.items():
|
||||
print(f" {n}:")
|
||||
for path in paths:
|
||||
print(f" {path.replace(user, '')}")
|
||||
print("-" * 32)
|
||||
print(f"[+] 共 {len(user_dirs)} 个微信账号")
|
||||
|
||||
return user_dirs
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
with open("version_list.json", "r", encoding="utf-8") as f:
|
||||
version_list = json.load(f)
|
||||
read_info(version_list, is_logging=True)
|
||||
|
@ -1,110 +1,16 @@
|
||||
# -*- coding: utf-8 -*-#
|
||||
# -------------------------------------------------------------------------------
|
||||
# Name: get_wx_decrypted_db.py
|
||||
# Name: merge_db.py
|
||||
# Description:
|
||||
# Author: xaoyaoo
|
||||
# Date: 2023/08/25
|
||||
# Date: 2023/12/03
|
||||
# -------------------------------------------------------------------------------
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sqlite3
|
||||
# import sys
|
||||
import winreg
|
||||
|
||||
# sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
try:
|
||||
from decrypted.decrypt import decrypt
|
||||
except ImportError:
|
||||
from .decrypt import decrypt
|
||||
|
||||
|
||||
|
||||
# 开始获取微信数据库
|
||||
def get_wechat_db():
|
||||
try:
|
||||
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Tencent\WeChat", 0, winreg.KEY_READ)
|
||||
value, _ = winreg.QueryValueEx(key, "FileSavePath")
|
||||
winreg.CloseKey(key)
|
||||
w_dir = value
|
||||
except Exception as e:
|
||||
try:
|
||||
w_dir = "MyDocument:"
|
||||
except Exception as e:
|
||||
print("读取注册表错误:", str(e))
|
||||
return str(e)
|
||||
|
||||
if w_dir == "MyDocument:":
|
||||
profile = os.path.expanduser("~")
|
||||
msg_dir = os.path.join(profile, "Documents", "WeChat Files")
|
||||
else:
|
||||
msg_dir = os.path.join(w_dir, "WeChat Files")
|
||||
if not os.path.exists(msg_dir):
|
||||
return FileNotFoundError("目录不存在")
|
||||
user_dirs = {} # wx用户目录
|
||||
files = os.listdir(msg_dir)
|
||||
for file_name in files:
|
||||
if file_name == "All Users" or file_name == "Applet" or file_name == "WMPF":
|
||||
continue
|
||||
user_dirs[file_name] = os.path.join(msg_dir, file_name)
|
||||
|
||||
# 获取数据库路径
|
||||
for user, user_dir in user_dirs.items():
|
||||
Media_p = []
|
||||
Micro_p = []
|
||||
FTS_p = []
|
||||
Sns_p = []
|
||||
Msg = []
|
||||
Emotion_p = []
|
||||
for root, dirs, files in os.walk(user_dir):
|
||||
for file_name in files:
|
||||
if re.match(r".*MediaMSG.*\.db$", file_name):
|
||||
src_path = os.path.join(root, file_name)
|
||||
Media_p.append(src_path)
|
||||
elif re.match(r".*MicroMsg.*\.db$", file_name):
|
||||
src_path = os.path.join(root, file_name)
|
||||
Micro_p.append(src_path)
|
||||
elif re.match(r".*FTSMSG.*\.db$", file_name):
|
||||
src_path = os.path.join(root, file_name)
|
||||
FTS_p.append(src_path)
|
||||
elif re.match(r".*MSG.*\.db$", file_name):
|
||||
src_path = os.path.join(root, file_name)
|
||||
Msg.append(src_path)
|
||||
elif re.match(r".*Sns.*\.db$", file_name):
|
||||
src_path = os.path.join(root, file_name)
|
||||
Sns_p.append(src_path)
|
||||
elif re.match(r".*Emotion.*\.db$", file_name):
|
||||
src_path = os.path.join(root, file_name)
|
||||
Emotion_p.append(src_path)
|
||||
Media_p.sort()
|
||||
Msg.sort()
|
||||
Micro_p.sort()
|
||||
# FTS_p.sort()
|
||||
user_dirs[user] = {"MicroMsg": Micro_p, "Msg": Msg, "MediaMSG": Media_p, "Sns": Sns_p, "Emotion": Emotion_p}
|
||||
return user_dirs
|
||||
|
||||
|
||||
# 解密所有数据库 paths(文件) 到 decrypted_path(目录)
|
||||
def all_decrypt(keys, paths, decrypted_path):
|
||||
decrypted_paths = []
|
||||
|
||||
for key in keys:
|
||||
for path in paths:
|
||||
|
||||
name = os.path.basename(path) # 文件名
|
||||
dtp = os.path.join(decrypted_path, name) # 解密后的路径
|
||||
if not decrypt(key, path, dtp):
|
||||
break
|
||||
decrypted_paths.append(dtp)
|
||||
else: # for循环正常结束,没有break
|
||||
break # 跳出while循环
|
||||
else:
|
||||
return False # while循环正常结束,没有break 解密失败
|
||||
return decrypted_paths
|
||||
|
||||
|
||||
def merge_copy_msg_db(db_path, save_path):
|
||||
def merge_copy_db(db_path, save_path):
|
||||
if isinstance(db_path, list) and len(db_path) == 1:
|
||||
db_path = db_path[0]
|
||||
if not os.path.exists(db_path):
|
||||
@ -112,7 +18,7 @@ def merge_copy_msg_db(db_path, save_path):
|
||||
shutil.move(db_path, save_path)
|
||||
|
||||
|
||||
# 合并相同名称的数据库
|
||||
# 合并相同名称的数据库 MSG0-MSG9.db
|
||||
def merge_msg_db(db_path: list, save_path: str, CreateTime: int = 0): # CreateTime: 从这个时间开始的消息 10位时间戳
|
||||
|
||||
merged_conn = sqlite3.connect(save_path)
|
||||
@ -252,64 +158,3 @@ def merge_media_msg_db(db_path: list, save_path: str):
|
||||
|
||||
merged_conn.close()
|
||||
return save_path
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 创建命令行参数解析器
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("-k", "--key", help="解密密钥", nargs="+", required=True)
|
||||
|
||||
# 解析命令行参数
|
||||
args = parser.parse_args()
|
||||
|
||||
# 检查是否缺少必要参数,并抛出错误
|
||||
if not args.key:
|
||||
raise ValueError("缺少必要的命令行参数!请提供密钥。")
|
||||
|
||||
# 从命令行参数获取值
|
||||
keys = args.key
|
||||
|
||||
decrypted_ROOT = os.path.join(os.getcwd(), "decrypted")
|
||||
|
||||
if keys is None:
|
||||
print("keys is None")
|
||||
exit(0)
|
||||
if isinstance(keys, str):
|
||||
keys = [keys]
|
||||
|
||||
user_dirs = get_wechat_db()
|
||||
for user, db_path in user_dirs.items(): # 遍历用户
|
||||
MicroMsgPaths = db_path["MicroMsg"]
|
||||
MsgPaths = db_path["Msg"]
|
||||
MediaMSGPaths = db_path["MediaMSG"]
|
||||
# FTSMSGPaths = db_path["FTSMSG"]
|
||||
SnsPaths = db_path["Sns"]
|
||||
EmotionPaths = db_path["Emotion"]
|
||||
|
||||
decrypted_path_tmp = os.path.join(decrypted_ROOT, user, "tmp") # 解密后的目录
|
||||
if not os.path.exists(decrypted_path_tmp):
|
||||
os.makedirs(decrypted_path_tmp)
|
||||
|
||||
MicroMsgDecryptPaths = all_decrypt(keys, MicroMsgPaths, decrypted_path_tmp)
|
||||
MsgDecryptPaths = all_decrypt(keys, MsgPaths, decrypted_path_tmp)
|
||||
MediaMSGDecryptPaths = all_decrypt(keys, MediaMSGPaths, decrypted_path_tmp)
|
||||
SnsDecryptPaths = all_decrypt(keys, SnsPaths, decrypted_path_tmp)
|
||||
EmotionDecryptPaths = all_decrypt(keys, EmotionPaths, decrypted_path_tmp)
|
||||
|
||||
# 合并数据库
|
||||
decrypted_path = os.path.join(decrypted_ROOT, user) # 解密后的目录
|
||||
|
||||
MicroMsgDbPath = os.path.join(decrypted_path, "MicroMsg.db")
|
||||
MsgDbPath = os.path.join(decrypted_path, "MSG_all.db")
|
||||
MediaMSGDbPath = os.path.join(decrypted_path, "MediaMSG_all.db")
|
||||
SnsDbPath = os.path.join(decrypted_path, "Sns_all.db")
|
||||
EmmotionDbPath = os.path.join(decrypted_path, "Emotion_all.db")
|
||||
|
||||
merge_copy_msg_db(MicroMsgDecryptPaths, MicroMsgDbPath)
|
||||
merge_msg_db(MsgDecryptPaths, MsgDbPath, 0)
|
||||
merge_media_msg_db(MediaMSGDecryptPaths, MediaMSGDbPath)
|
||||
merge_copy_msg_db(SnsDecryptPaths, SnsDbPath)
|
||||
merge_copy_msg_db(EmotionDecryptPaths, EmmotionDbPath)
|
||||
|
||||
shutil.rmtree(decrypted_path_tmp) # 删除临时文件
|
||||
print(f"解密完成:{user}, {decrypted_path}")
|
15
setup.py
15
setup.py
@ -3,7 +3,7 @@ from setuptools import setup, find_packages
|
||||
with open("README.md", "r", encoding="utf-8") as fh:
|
||||
long_description = fh.read()
|
||||
|
||||
version = "2.2.18"
|
||||
version = "2.3.0"
|
||||
|
||||
install_requires = [
|
||||
"psutil",
|
||||
@ -29,18 +29,15 @@ setup(
|
||||
url="https://github.com/xaoyaoo/PyWxDump",
|
||||
license='MIT',
|
||||
|
||||
packages=['pywxdump', 'pywxdump.bias_addr', 'pywxdump.wx_info', 'pywxdump.decrypted', 'pywxdump.analyse',
|
||||
'pywxdump.show_chat'],
|
||||
packages=['pywxdump', 'pywxdump.ui', 'pywxdump.wx_info', 'pywxdump.analyzer'],
|
||||
package_dir={'pywxdump': 'pywxdump',
|
||||
'pywxdump.bias_addr': 'pywxdump/bias_addr',
|
||||
'pywxdump.wx_info': 'pywxdump/wx_info',
|
||||
'pywxdump.decrypted': 'pywxdump/decrypted',
|
||||
'pywxdump.analyse': 'pywxdump/analyse',
|
||||
'pywxdump.show_chat': 'pywxdump/show_chat'
|
||||
'pywxdump.analyzer': 'pywxdump/analyzer',
|
||||
'pywxdump.ui': 'pywxdump/ui',
|
||||
},
|
||||
|
||||
package_data={
|
||||
'pywxdump': ['version_list.json', 'show_chat/templates/*']
|
||||
'pywxdump': ['version_list.json', 'ui/templates/*']
|
||||
},
|
||||
classifiers=[
|
||||
"Programming Language :: Python :: 3",
|
||||
@ -50,7 +47,7 @@ setup(
|
||||
install_requires=install_requires,
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'wxdump = pywxdump.command:console_run',
|
||||
'wxdump = pywxdump.cli:console_run',
|
||||
],
|
||||
},
|
||||
setup_requires=['wheel']
|
||||
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user