diff --git a/README.md b/README.md index 514bfc3..3fd0bf7 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@
更新日志(点击展开): +* 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) @@ -97,7 +98,8 @@ PyWxDump ## 4. 其他 -PyWxDump是[SharpWxDump](https://github.com/AdminTest0/SharpWxDump)的经过重构python语言版本,同时添加了一些新的功能。 +[PyWxDump](https://github.com/xaoyaoo/PyWxDump)是[SharpWxDump](https://github.com/AdminTest0/SharpWxDump) +的经过重构python语言版本,同时添加了一些新的功能。 * 如发现[version_list.json](pywxdump/version_list.json)缺失或错误, 请提交[issues](https://github.com/xaoyaoo/PyWxDump/issues). @@ -146,13 +148,12 @@ python -m pip install -U . ```shell script wxdump 模式 [参数] # 运行模式(mode): -# bias_addr 获取微信基址偏移 -# wx_info 获取微信信息 -# wx_db 获取微信文件夹路径 -# decrypt 解密微信数据库 -# show_records 显示聊天记录[需要安装flask] -# analyse 解析微信数据库(未完成) -# all 执行所有操作(除获取基址偏移、解密所有已经登陆的数据库) +# bias 获取微信基址偏移 +# info 获取微信信息 +# db_path 获取微信文件夹路径 +# decrypt 解密微信数据库 +# dbshow 聊天记录查看[需要安装flask] +# all 获取微信信息,解密微信数据库,查看聊天记录 ``` *示例* @@ -160,55 +161,51 @@ wxdump 模式 [参数] 以下是示例命令: ```shell script -wxdump bias_addr -h +wxdump bias -h #usage: main.py bias_addr [-h] --mobile MOBILE --name NAME --account ACCOUNT [--key KEY] [--db_path DB_PATH] [-vlp VLP] #options: -# -h, --help show this help message and exit -# --mobile MOBILE 手机号 -# --name NAME 微信昵称 -# --account ACCOUNT 微信账号 -# --key KEY (可选)密钥 -# --db_path DB_PATH (可选)已登录账号的微信文件夹路径 -# -vlp VLP (可选)微信版本偏移文件路径 +# -h, --help show this help message and exit +# --mobile MOBILE 手机号 +# --name NAME 微信昵称 +# --account ACCOUNT 微信账号 +# --key KEY (可选)密钥 +# --db_path DB_PATH (可选)已登录账号的微信文件夹路径 +# -vlp VERSION_LIST_PATH, --version_list_path VERSION_LIST_PATH +# (可选)微信版本偏移文件路径,如有,则自动更新 -wxdump wx_info -h +wxdump info -h #usage: main.py wx_info [-h] [-vlp VLP] #options: # -h, --help show this help message and exit # -vlp VLP (可选)微信版本偏移文件路径 -wxdump wx_db -h +wxdump db_path -h #usage: main.py wx_db [-h] [-r REQUIRE_LIST] [-wf WF] #options: # -h, --help show this help message and exit -# -r REQUIRE_LIST, --require_list REQUIRE_LIST -# (可选)需要的数据库名称(eg: -r MediaMSG;MicroMsg;FTSMSG;MSG;Sns;Emotion ) -# -wf WF (可选)'WeChat Files'路径 +# -r , --require_list (可选)需要的数据库名称(eg: -r MediaMSG;MicroMsg;FTSMSG;MSG;Sns;Emotion ) +# -wf , --wx_files (可选)'WeChat Files'路径 +# -id WXID, --wxid WXID +# (可选)wxid_,用于确认用户文件夹 wxdump decrypt -h #usage: main.py decrypt [-h] -k KEY -i DB_PATH -o OUT_PATH #options: -# -h, --help show this help message and exit -# -k KEY, --key KEY 密钥 -# -i DB_PATH, --db_path DB_PATH -# 数据库路径(目录or文件) -# -o OUT_PATH, --out_path OUT_PATH -# 输出路径(必须是目录),输出文件为 out_path/de_{original_name} +# -h, --help show this help message and exit +# -k , --key 密钥 +# -i , --db_path 数据库路径(目录or文件) +# -o , --out_path 输出路径(必须是目录)[默认为当前路径下decrypted文件夹] -wxdump show_records -h +wxdump dbshow -h #usage: wxdump show_records [-h] -msg -micro -media -fs #options: -# -h, --help show this help message and exit -# -msg , --msg_path 解密后的 MSG.db 的路径 -# -micro , --micro_path 解密后的 MicroMsg.db 的路径 -# -media , --media_path 解密后的 MediaMSG.db 的路径 -# -fs , --filestorage_path 文件夹FileStorage的路径 - -wxdump analyse -h -#usage: main.py analyse [-h] [--arg ARG] -#options: -# -h, --help show this help message and exit -# --arg ARG 参数 +# -msg , --msg_path 解密后的 MSG.db 的路径 +# -micro , --micro_path +# 解密后的 MicroMsg.db 的路径 +# -media , --media_path +# 解密后的 MediaMSG.db 的路径 +# -fs , --filestorage_path +# (可选)文件夹FileStorage的路径(用于显示图片) wxdump all -h #usage: main.py all [-h] @@ -243,10 +240,35 @@ from pywxdump.decrypted import batch_decrypt batch_decrypt("key", "db_path", "out_path") -# 5. 解析数据库 -from pywxdump.analyse import read_img_dat, read_emoji, decompress_CompressContent, read_audio_buf, read_audio +``` -pass +### 2.3 构建可执行文件exe + +将下面的代码保存为`build.py`,然后运行`python build.py`即可。(或者执行[build_exe.py](./tests/build_exe.py)) + +```python +import site +import os + +code = """from pywxdump.command import console_run;console_run()""" + +# 创建文件夹 +os.makedirs("dist", exist_ok=True) +# 将代码写入文件 +with open("dist/tmp.py", "w", encoding="utf-8") as f: + f.write(code) + +# 获取安装包的路径 +package_path = site.getsitepackages() +if package_path: + package_path = package_path[1] # 假设取第一个安装包的路径 + version_list_path = os.path.join(package_path, 'pywxdump', 'version_list.json') + # 执行打包命令 + cmd = f'pyinstaller --onefile --clean --add-data "{version_list_path};pywxdump" dist/tmp.py' + print(cmd) + os.system(cmd) +else: + print("未找到安装包路径") ``` 【注】: @@ -273,6 +295,8 @@ pass 4. 自行备份(日常备份自己留存) 5. 等等............... +[![Star History Chart](https://api.star-history.com/svg?repos=xaoyaoo/pywxdump&type=Date)](https://star-history.com/#xaoyaoo/pywxdump&Date) + # 四、免责声明(非常重要!!!!!!!) 本项目仅允许在授权情况下对数据库进行备份,严禁用于非法目的,否则自行承担所有相关责任。使用该工具则代表默认同意该条款; @@ -281,7 +305,6 @@ pass # 五、许可证 - ```text MIT License @@ -306,4 +329,5 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -``` \ No newline at end of file +``` + diff --git a/doc/CE获取基址.md b/doc/CE获取基址.md index 04bb337..47aede9 100644 --- a/doc/CE获取基址.md +++ b/doc/CE获取基址.md @@ -51,19 +51,18 @@ KEY的基址即:**2FFF970-000024=2FFF94C** 十进制地址为:50329932 -代码块中的五个十进制按顺序代表:微信昵称、微信账号、微信手机号、微信邮箱(高版本失效,这个随便填)、微信KEY、微信原始ID(wxid_******) +代码块中的五个十进制按顺序代表:微信昵称、微信账号、微信手机号、微信邮箱(高版本失效,这个随便填)、微信KEY -```jsx +```json { - "微信版本号", - new List - { + "微信版本号": + [ 50320784, 50321712, 50320640, 38986104, 50321676 - } + ] } ``` diff --git a/pywxdump/bias_addr/get_bias_addr.py b/pywxdump/bias_addr/get_bias_addr.py index e8b33f1..a4e4f08 100644 --- a/pywxdump/bias_addr/get_bias_addr.py +++ b/pywxdump/bias_addr/get_bias_addr.py @@ -138,49 +138,6 @@ class BiasAddr: return keyWinAddr - module.lpBaseOfDll - def get_wxid_bias(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'wxid_' - publicWxidList = pymem.pattern.pattern_scan_all(self.pm.process_handle, keyBytes, return_multiple=True) - - import ahocorasick - def search_substrings(text, substrings): - A = ahocorasick.Automaton() - for index, s in enumerate(substrings): - A.add_word(s, (index, s)) - A.make_automaton() - - results = [] - for end_index, (insert_order, original_value) in A.iter(text): - start_index = end_index - len(original_value) + 1 - results.append(int(start_index / 2)) - return results - - patterns = [] - for addr in publicWxidList: - keyBytes = addr.to_bytes(byteLen, byteorder="little", signed=True) # 低位在前 - patterns.append(keyBytes.hex()) - text = pm.read_bytes(module.lpBaseOfDll, module.SizeOfImage).hex() - - wxidaddrs = search_substrings(text, patterns) - # print("wxidaddrs", wxidaddrs) - - # wxidaddr = 0 - # for addr in wxidaddrs: - # print(addr - 63488256) - # wxidaddr = int.from_bytes(pm.read_bytes(addr + module.lpBaseOfDll, byteLen), byteorder='little') - # print("wxidaddr", hex(wxidaddr)) - # wxid = pm.read_bytes(wxidaddr, 24).split(b"\x00")[0] - # print("wxid", wxid) - - return wxidaddrs[-2] - 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): @@ -257,20 +214,17 @@ class BiasAddr: key, bais = verify_key(maybe_key, wx_db_path) return bais - def run(self): + def run(self, is_logging=False, version_list_path=None): self.version = self.get_file_version(self.process_name) if not self.islogin: - return "[-] WeChat No Run" + 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: - wxid_bias = self.get_wxid_bias() - except: - wxid_bias = 0 - try: key_bias = self.get_key_bias_test() except: @@ -283,7 +237,17 @@ class BiasAddr: key_bias = self.get_key_bias(self.db_path, account_bias) else: key_bias = 0 - return {self.version: [name_bias, account_bias, mobile_bias, 0, key_bias, wxid_bias]} + 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 if __name__ == '__main__': @@ -310,13 +274,4 @@ if __name__ == '__main__': db_path = args.db_path # 调用 run 函数,并传入参数 - rdata = BiasAddr(account, mobile, name, key, db_path).run() - print("{版本:昵称,账号,手机号,邮箱,KEY,原始ID(wxid_******)}") - print(rdata) - - # 添加到version_list.json - with open("../version_list.json", "r", encoding="utf-8") as f: - data = json.load(f) - data.update(rdata) - with open("../version_list.json", "w", encoding="utf-8") as f: - json.dump(data, f, ensure_ascii=False, indent=4) + rdata = BiasAddr(account, mobile, name, key, db_path).run(True, "../version_list.json") diff --git a/pywxdump/command.py b/pywxdump/command.py index f2353a6..e69789c 100644 --- a/pywxdump/command.py +++ b/pywxdump/command.py @@ -17,14 +17,16 @@ from . import * class MainBiasAddr(): def init_parses(self, parser): + self.mode = "bias" # 添加 'bias_addr' 子命令解析器 - sb_bias_addr = parser.add_parser("bias_addr", help="获取微信基址偏移") + sb_bias_addr = parser.add_parser(self.mode, help="获取微信基址偏移") sb_bias_addr.add_argument("--mobile", type=str, help="手机号", required=True) sb_bias_addr.add_argument("--name", type=str, help="微信昵称", required=True) sb_bias_addr.add_argument("--account", type=str, help="微信账号", required=True) sb_bias_addr.add_argument("--key", type=str, help="(可选)密钥") sb_bias_addr.add_argument("--db_path", type=str, help="(可选)已登录账号的微信文件夹路径") - sb_bias_addr.add_argument("-vlp", type=str, help="(可选)微信版本偏移文件路径,如有,则自动更新", + sb_bias_addr.add_argument("-vlp", '--version_list_path', type=str, + help="(可选)微信版本偏移文件路径,如有,则自动更新", default=None) self.sb_bias_addr = sb_bias_addr return sb_bias_addr @@ -40,89 +42,61 @@ class MainBiasAddr(): account = args.account key = args.key db_path = args.db_path - vlp = args.vlp + vlp = args.version_list_path # 调用 run 函数,并传入参数 - rdata = BiasAddr(account, mobile, name, key, db_path).run() - print("{版本:微信昵称,微信账号,微信手机号,微信邮箱,微信KEY,微信原始ID(wxid_******)}") - print(rdata) - - if vlp is not None: - # 添加到version_list.json - version_list = json.load(open(vlp, "r", encoding="utf-8")) - version_list.update(rdata) - json.dump(version_list, open(vlp, "w", encoding="utf-8"), ensure_ascii=False, indent=4) - + rdata = BiasAddr(account, mobile, name, key, db_path).run(True, vlp) return rdata class MainWxInfo(): def init_parses(self, parser): + self.mode = "info" # 添加 'wx_info' 子命令解析器 - sb_wx_info = parser.add_parser("wx_info", help="获取微信信息") + sb_wx_info = parser.add_parser(self.mode, help="获取微信信息") sb_wx_info.add_argument("-vlp", type=str, help="(可选)微信版本偏移文件路径", default=VERSION_LIST_PATH) return sb_wx_info def run(self, args): # 读取微信各版本偏移 - VERSION_LIST_PATH = args.vlp - version_list = json.load(open(VERSION_LIST_PATH, "r", encoding="utf-8")) - result = read_info(version_list) # 读取微信信息 - - print("=" * 32) - if isinstance(result, str): # 输出报错 - print(result) - else: # 输出结果 - for i, rlt in enumerate(result): - for k, v in rlt.items(): - print(f"[+] {k:>7}: {v}") - print(end="-" * 32 + "\n" if i != len(result) - 1 else "") - print("=" * 32) + path = args.vlp + version_list = json.load(open(path, "r", encoding="utf-8")) + result = read_info(version_list, True) # 读取微信信息 return result class MainWxDbPath(): def init_parses(self, parser): + self.mode = "db_path" # 添加 'wx_db_path' 子命令解析器 - sb_wx_db_path = parser.add_parser("wx_db", help="获取微信文件夹路径") + sb_wx_db_path = parser.add_parser(self.mode, help="获取微信文件夹路径") sb_wx_db_path.add_argument("-r", "--require_list", type=str, help="(可选)需要的数据库名称(eg: -r MediaMSG;MicroMsg;FTSMSG;MSG;Sns;Emotion )", default="all", metavar="") sb_wx_db_path.add_argument("-wf", "--wx_files", type=str, help="(可选)'WeChat Files'路径", default=None, metavar="") + sb_wx_db_path.add_argument("-id", "--wxid", type=str, help="(可选)wxid_,用于确认用户文件夹", + default=None, metavar="") return sb_wx_db_path def run(self, args): # 从命令行参数获取值 require_list = args.require_list msg_dir = args.wx_files + wxid = args.wxid - user_dirs = get_wechat_db(require_list, msg_dir) - - if isinstance(user_dirs, str): - print(user_dirs) - else: - for user, user_dir in user_dirs.items(): - print(f"[+] {user}") - for n, paths in user_dir.items(): - print(f" {n}:") - for path in paths[:2]: - print(f" {path}") - if len(paths) > 2: - print(f" ...") - print("-" * 32) - print(f"[+] 共 {len(user_dirs)} 个微信账号") - + user_dirs = get_wechat_db(require_list, msg_dir, wxid, True) # 获取微信数据库路径 return user_dirs class MainDecrypt(): def init_parses(self, parser): + self.mode = "decrypt" # 添加 'decrypt' 子命令解析器 - sb_decrypt = parser.add_parser("decrypt", help="解密微信数据库") + sb_decrypt = parser.add_parser(self.mode, help="解密微信数据库") sb_decrypt.add_argument("-k", "--key", type=str, help="密钥", required=True, metavar="") sb_decrypt.add_argument("-i", "--db_path", type=str, help="数据库路径(目录or文件)", required=True, metavar="") - sb_decrypt.add_argument("-o", "--out_path", type=str, - help="输出路径(必须是目录),输出文件为 out_path/de_{original_name}", required=True, + sb_decrypt.add_argument("-o", "--out_path", type=str, default=os.path.join(os.getcwd(), "decrypted"), + help="输出路径(必须是目录)[默认为当前路径下decrypted文件夹]", required=False, metavar="") return sb_decrypt @@ -132,29 +106,32 @@ class MainDecrypt(): db_path = args.db_path out_path = args.out_path + if not os.path.exists(db_path): + print("[-] 数据库路径不存在") + return + + if not os.path.exists(out_path): + os.makedirs(out_path) + print(f"[+] 创建输出文件夹:{out_path}") + # 调用 decrypt 函数,并传入参数 - result = batch_decrypt(key, db_path, out_path) - if isinstance(result, list): - for i in result: - if isinstance(i, str): - print(i) - else: - print(f'[+] "{i[1]}" -> "{os.path.relpath(i[2], out_path)}"') - else: - print(result) + result = batch_decrypt(key, db_path, out_path, True) + return result class MainShowChatRecords(): def init_parses(self, parser): + self.mode = "dbshow" # 添加 'decrypt' 子命令解析器 - sb_decrypt = parser.add_parser("show_records", help="聊天记录查看[需要安装flask]") + sb_decrypt = parser.add_parser(self.mode, help="聊天记录查看[需要安装flask]") sb_decrypt.add_argument("-msg", "--msg_path", type=str, help="解密后的 MSG.db 的路径", required=True, metavar="") sb_decrypt.add_argument("-micro", "--micro_path", type=str, help="解密后的 MicroMsg.db 的路径", required=True, metavar="") sb_decrypt.add_argument("-media", "--media_path", type=str, help="解密后的 MediaMSG.db 的路径", required=True, metavar="") - sb_decrypt.add_argument("-fs", "--filestorage_path", type=str, help="文件夹FileStorage的路径", required=True, + sb_decrypt.add_argument("-fs", "--filestorage_path", type=str, + help="(可选)文件夹FileStorage的路径(用于显示图片)", required=False, metavar="") return sb_decrypt @@ -162,16 +139,26 @@ class MainShowChatRecords(): # 从命令行参数获取值 try: from flask import Flask, request, jsonify, render_template, g + import logging from .show_chat.main_window import app_show_chat, get_user_list except Exception as e: print(e) print("[-] 请安装flask( pip install flask )") return + if not os.path.exists(args.msg_path) or not os.path.exists(args.micro_path) or not os.path.exists( + args.media_path): + print(os.path.exists(args.msg_path), os.path.exists(args.micro_path), os.path.exists(args.media_path)) + print("[-] 输入数据库路径不存在") + return + + 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 @@ -180,77 +167,96 @@ class MainShowChatRecords(): app.register_blueprint(app_show_chat) - app.run() - - -class MainAnalyseWxDb(): - def init_parses(self, parser): - # 添加 'parse_wx_db' 子命令解析器 - sb_parse_wx_db = parser.add_parser("analyse", help="解析微信数据库(未完成)") - sb_parse_wx_db.add_argument("--arg", type=str, help="参数") - return sb_parse_wx_db - - def run(self, args): - print(f"解析微信数据库(未完成)") + print("[+] 请使用浏览器访问 http://127.0.0.1:5000/ 查看聊天记录") + app.run(debug=False) class MainAll(): def init_parses(self, parser): + self.mode = "all" # 添加 'all' 子命令解析器 - sb_all = parser.add_parser("all", help="执行所有操作(除获取基址偏移、Analyse)") + sb_all = parser.add_parser(self.mode, help="获取微信信息,解密微信数据库,查看聊天记录") return sb_all def run(self, args): # 获取微信信息 - args.vlp = VERSION_LIST_PATH - result_WxInfo = MainWxInfo().run(args) - keys = [i.get('key', "") for i in result_WxInfo] - if not keys: - print("[-] 未获取到密钥") - return - wxids = [i.get('wxid', "") for i in result_WxInfo] + WxInfo = read_info(VERSION_LIST, True) - args.require_list = 'all' - args.wx_files = None - result_WxDbPath = MainWxDbPath().run(args) - wxdbpaths = [path for user_dir in result_WxDbPath.values() for paths in user_dir.values() for path in paths] - wxdblen = len(wxdbpaths) - print(f"[+] 共 {wxdblen} 个微信数据库(包含所有本地曾登录的微信)") - print("=" * 32) + for user in WxInfo: + key = user.get("key", "") + if not key: + print("[-] 未获取到密钥") + return + wxid = user.get("wxid", None) - out_path = os.path.join(os.getcwd(), "decrypted") - print(f"[*] 解密后文件夹:{out_path} ") - print(f"[*] 解密中...(用时较久,耐心等待)") - if not os.path.exists(out_path): - os.makedirs(out_path) + WxDbPath = get_wechat_db('all', None, wxid=wxid, is_logging=True) # 获取微信数据库路径 - rd = {} - for key in keys: - rd[key] = batch_decrypt(key, wxdbpaths, out_path) + wxdbpaths = [path for user_dir in WxDbPath.values() for paths in user_dir.values() for path in paths] + if len(wxdbpaths) == 0: + print("[-] 未获取到数据库路径") + return - result_Decrypt = [None] * wxdblen - for i in range(wxdblen): - for k, v in rd.items(): - if isinstance(v[i], list): - result_Decrypt[i] = v[i] - break + wxdblen = len(wxdbpaths) + print(f"[+] 共发现 {wxdblen} 个微信数据库") + print("=" * 32) + + out_path = os.path.join(os.getcwd(), "decrypted", wxid) if wxid else os.path.join(os.getcwd(), "decrypted") + print(f"[*] 解密后文件夹:{out_path} ") + print(f"[*] 解密中...(用时较久,耐心等待)") + if not os.path.exists(out_path): + os.makedirs(out_path) + + # 判断out_path是否为空目录 + if os.listdir(out_path): + isdel = input(f"[*] 输出文件夹不为空({out_path})\n 是否删除?(y/n):") + if isdel.lower() == 'y' or isdel.lower() == 'yes': + for root, dirs, files in os.walk(out_path, topdown=False): + for name in files: + os.remove(os.path.join(root, name)) + for name in dirs: + os.rmdir(os.path.join(root, name)) + + # 调用 decrypt 函数,并传入参数 # 解密 + code, ret = batch_decrypt(key, wxdbpaths, out_path, False) + if not code: + print(ret) + return + print("[+] 解密完成") + print("-" * 32) + errors = [] + out_dbs = [] + for code1, ret1 in ret: + if code1 == False: + errors.append(ret1) else: - result_Decrypt[i] = v[i] + print( + f'[+] success "{os.path.relpath(ret1[0], os.path.commonprefix(wxdbpaths))}" -> "{os.path.relpath(ret1[1], os.getcwd())}"') + out_dbs.append(ret1[1]) + print("-" * 32) + print("[-] " + f"共 {len(errors)} 个文件解密失败;") + # print("; ".join([f'"{wxdbpaths[i]}"' for i in errors])) + print("=" * 32) - print("[+] 解密完成") - print("-" * 32) + if len(out_dbs) <= 0: + print("[-] 未获取到解密后的数据库路径") + return - errors = [] - for i in range(wxdblen): - if isinstance(result_Decrypt[i], str): - errors.append(i) - else: - print( - f'[+] success "{os.path.relpath(result_Decrypt[i][1], os.path.commonprefix(wxdbpaths))}" -> "{os.path.relpath(result_Decrypt[i][2], os.getcwd())}"') - print("-" * 32) - print("[-] " + f"共 {len(errors)} 个文件解密失败;") - # print("; ".join([f'"{wxdbpaths[i]}"' for i in errors])) - print("=" * 32) + user_path = out_dbs[0].split("MSG") + FileStorage_path = os.path.join(user_path[0], "FileStorage") + + # 查看聊天记录 + MSGDB = [i for i in out_dbs if "de_MSG" in i] + MSGDB = MSGDB[-1] if MSGDB else None + MicroMsgDB = [i for i in out_dbs if "de_MicroMsg" in i] + MicroMsgDB = MicroMsgDB[-1] if MicroMsgDB else None + MediaMSGDB = [i for i in out_dbs if "de_MediaMSG" in i] + MediaMSGDB = MediaMSGDB[-1] if MediaMSGDB else None + + args.msg_path = MSGDB + args.micro_path = MicroMsgDB + args.media_path = MediaMSGDB + args.filestorage_path = FileStorage_path + MainShowChatRecords().run(args) def console_run(): @@ -260,53 +266,44 @@ def console_run(): # 添加子命令解析器 subparsers = parser.add_subparsers(dest="mode", help="""运行模式:""", required=True, metavar="mode") - # 添加 'bias_addr' 子命令解析器 + modes = {} + # 添加 'bias' 子命令解析器 main_bias_addr = MainBiasAddr() sb_bias_addr = main_bias_addr.init_parses(subparsers) + modes[main_bias_addr.mode] = main_bias_addr - # 添加 'wx_info' 子命令解析器 + # 添加 'info' 子命令解析器 main_wx_info = MainWxInfo() sb_wx_info = main_wx_info.init_parses(subparsers) + modes[main_wx_info.mode] = main_wx_info - # 添加 'wx_db_path' 子命令解析器 + # 添加 'db_path' 子命令解析器 main_wx_db_path = MainWxDbPath() sb_wx_db_path = main_wx_db_path.init_parses(subparsers) + modes[main_wx_db_path.mode] = main_wx_db_path # 添加 'decrypt' 子命令解析器 main_decrypt = MainDecrypt() sb_decrypt = main_decrypt.init_parses(subparsers) + modes[main_decrypt.mode] = main_decrypt - # 添加 'show_chat_records' 子命令解析器 + # 添加 '' 子命令解析器 main_show_chat_records = MainShowChatRecords() - sb_show_chat_records = main_show_chat_records.init_parses(subparsers) - - # 添加 'parse_wx_db' 子命令解析器 - main_parse_wx_db = MainAnalyseWxDb() - sb_parse_wx_db = main_parse_wx_db.init_parses(subparsers) + sb_dbshow = main_show_chat_records.init_parses(subparsers) + modes[main_show_chat_records.mode] = main_show_chat_records # 添加 'all' 子命令解析器 main_all = MainAll() sb_all = main_all.init_parses(subparsers) + modes[main_all.mode] = main_all args = parser.parse_args() # 解析命令行参数 if not any(vars(args).values()): parser.print_help() + # 根据不同的 'mode' 参数,执行不同的操作 - if args.mode == "bias_addr": - main_bias_addr.run(args) - elif args.mode == "wx_info": - main_wx_info.run(args) - elif args.mode == "wx_db": - main_wx_db_path.run(args) - elif args.mode == "decrypt": - main_decrypt.run(args) - elif args.mode == "show_chat_records": - main_show_chat_records.run(args) - elif args.mode == "parse": - main_parse_wx_db.run(args) - elif args.mode == "all": - main_all.run(args) + modes[args.mode].run(args) if __name__ == '__main__': diff --git a/pywxdump/decrypted/decrypt.py b/pywxdump/decrypted/decrypt.py index 116b17d..e04efeb 100644 --- a/pywxdump/decrypted/decrypt.py +++ b/pywxdump/decrypted/decrypt.py @@ -16,12 +16,21 @@ DEFAULT_ITER = 64000 # 通过密钥解密数据库 def decrypt(key: str, db_path, out_path): - if not os.path.exists(db_path): - return f"[-] db_path:'{db_path}' File not found!" + """ + 通过密钥解密数据库 + :param key: 密钥 64位16进制字符串 + :param db_path: 待解密的数据库路径(必须是文件) + :param out_path: 解密后的数据库输出路径(必须是文件) + :return: + """ + if not os.path.exists(db_path) or not os.path.isfile(db_path): + return False, f"[-] db_path:'{db_path}' File not found!" if not os.path.exists(os.path.dirname(out_path)): - return f"[-] out_path:'{out_path}' File not found!" + return False, f"[-] out_path:'{out_path}' File not found!" + if len(key) != 64: - return f"[-] key:'{key}' Error!" + return False, f"[-] key:'{key}' Len Error!" + password = bytes.fromhex(key.strip()) with open(db_path, "rb") as file: blist = file.read() @@ -36,7 +45,7 @@ def decrypt(key: str, db_path, out_path): hash_mac.update(b'\x01\x00\x00\x00') if hash_mac.digest() != first[-32:-12]: - return f"[-] Password Error! (key:'{key}'; db_path:'{db_path}'; out_path:'{out_path}' )" + return False, f"[-] Key Error! (key:'{key}'; db_path:'{db_path}'; out_path:'{out_path}' )" newblist = [blist[i:i + DEFAULT_PAGESIZE] for i in range(DEFAULT_PAGESIZE, len(blist), DEFAULT_PAGESIZE)] @@ -52,18 +61,22 @@ def decrypt(key: str, db_path, out_path): decrypted = t.decrypt(i[:-48]) deFile.write(decrypted) deFile.write(i[-48:]) - return [True, db_path, out_path, key] + return True, [db_path, out_path, key] -def batch_decrypt(key: str, db_path: Union[str, List[str]], out_path: str): +def batch_decrypt(key: str, db_path: Union[str, List[str]], out_path: str, is_logging: bool = False): if not isinstance(key, str) or not isinstance(out_path, str) or not os.path.exists(out_path) or len(key) != 64: - return f"[-] (key:'{key}' or out_path:'{out_path}') Error!" + error = f"[-] (key:'{key}' or out_path:'{out_path}') Error!" + if is_logging: print(error) + return False, error process_list = [] if isinstance(db_path, str): if not os.path.exists(db_path): - return f"[-] db_path:'{db_path}' not found!" + error = f"[-] db_path:'{db_path}' not found!" + if is_logging: print(error) + return False, error if os.path.isfile(db_path): inpath = db_path @@ -81,7 +94,10 @@ def batch_decrypt(key: str, db_path: Union[str, List[str]], out_path: str): os.makedirs(os.path.dirname(outpath)) process_list.append([key, inpath, outpath]) else: - return f"[-] db_path:'{db_path}' Error " + error = f"[-] db_path:'{db_path}' Error " + if is_logging: print(error) + return False, error + elif isinstance(db_path, list): rt_path = os.path.commonprefix(db_path) if not os.path.exists(rt_path): @@ -89,7 +105,9 @@ def batch_decrypt(key: str, db_path: Union[str, List[str]], out_path: str): for inpath in db_path: if not os.path.exists(inpath): - return f"[-] db_path:'{db_path}' not found!" + erreor = f"[-] db_path:'{db_path}' not found!" + if is_logging: print(erreor) + return False, erreor inpath = os.path.normpath(inpath) rel = os.path.relpath(os.path.dirname(inpath), rt_path) @@ -98,7 +116,9 @@ def batch_decrypt(key: str, db_path: Union[str, List[str]], out_path: str): os.makedirs(os.path.dirname(outpath)) process_list.append([key, inpath, outpath]) else: - return f"[-] db_path:'{db_path}' Error " + error = f"[-] db_path:'{db_path}' Error " + if is_logging: print(error) + return False, error result = [] for i in process_list: @@ -110,7 +130,21 @@ def batch_decrypt(key: str, db_path: Union[str, List[str]], out_path: str): if not os.listdir(os.path.join(root, dir)): os.rmdir(os.path.join(root, dir)) - return result + if is_logging: + print("=" * 32) + success_count = 0 + fail_count = 0 + for code, ret in result: + if code == False: + print(ret) + fail_count += 1 + else: + print(f'[+] "{ret[0]}" -> "{ret[1]}"') + success_count += 1 + print("-" * 32) + print(f"[+] 共 {len(result)} 个文件, 成功 {success_count} 个, 失败 {fail_count} 个") + print("=" * 32) + return True, result if __name__ == '__main__': diff --git a/pywxdump/show_chat/main_window.py b/pywxdump/show_chat/main_window.py index 15237e8..ef9afda 100644 --- a/pywxdump/show_chat/main_window.py +++ b/pywxdump/show_chat/main_window.py @@ -70,6 +70,8 @@ def load_base64_img_data(start_time, end_time, username_md5, FileStorage_path): 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): diff --git a/pywxdump/version_list.json b/pywxdump/version_list.json index a9c5fbf..cf32069 100644 --- a/pywxdump/version_list.json +++ b/pywxdump/version_list.json @@ -4,383 +4,335 @@ 328122328, 328123056, 328121976, - 328123020, - 0 + 328123020 ], "3.3.0.115": [ 31323364, 31323744, 31324472, 31323392, - 31324436, - 0 + 31324436 ], "3.3.0.84": [ 31315212, 31315592, 31316320, 31315240, - 31316284, - 0 + 31316284 ], "3.3.0.93": [ 31323364, 31323744, 31324472, 31323392, - 31324436, - 0 + 31324436 ], "3.3.5.34": [ 30603028, 30603408, 30604120, 30603056, - 30604100, - 0 + 30604100 ], "3.3.5.42": [ 30603012, 30603392, 30604120, 30603040, - 30604084, - 0 + 30604084 ], "3.3.5.46": [ 30578372, 30578752, 30579480, 30578400, - 30579444, - 0 + 30579444 ], "3.4.0.37": [ 31608116, 31608496, 31609224, 31608144, - 31609188, - 0 + 31609188 ], "3.4.0.38": [ 31604044, 31604424, 31605152, 31604072, - 31605116, - 0 + 31605116 ], "3.4.0.50": [ 31688500, 31688880, 31689608, 31688528, - 31689572, - 0 + 31689572 ], "3.4.0.54": [ 31700852, 31701248, 31700920, 31700880, - 31701924, - 0 + 31701924 ], "3.4.5.27": [ 32133788, 32134168, 32134896, 32133816, - 32134860, - 0 + 32134860 ], "3.4.5.45": [ 32147012, 32147392, 32147064, 32147040, - 32148084, - 0 + 32148084 ], "3.5.0.20": [ 35494484, 35494864, 35494536, 35494512, - 35495556, - 0 + 35495556 ], "3.5.0.29": [ 35507980, 35508360, 35508032, 35508008, - 35509052, - 0 + 35509052 ], "3.5.0.33": [ 35512140, 35512520, 35512192, 35512168, - 35513212, - 0 + 35513212 ], "3.5.0.39": [ 35516236, 35516616, 35516288, 35516264, - 35517308, - 0 + 35517308 ], "3.5.0.42": [ 35512140, 35512520, 35512192, 35512168, - 35513212, - 0 + 35513212 ], "3.5.0.44": [ 35510836, 35511216, 35510896, 35510864, - 35511908, - 0 + 35511908 ], "3.5.0.46": [ 35506740, 35507120, 35506800, 35506768, - 35507812, - 0 + 35507812 ], "3.6.0.18": [ 35842996, 35843376, 35843048, 35843024, - 35844068, - 0 + 35844068 ], "3.6.5.7": [ 35864356, 35864736, 35864408, 35864384, - 35865428, - 0 + 35865428 ], "3.6.5.16": [ 35909428, 35909808, 35909480, 35909456, - 35910500, - 0 + 35910500 ], "3.7.0.26": [ 37105908, 37106288, 37105960, 37105936, - 37106980, - 0 + 37106980 ], "3.7.0.29": [ 37105908, 37106288, 37105960, 37105936, - 37106980, - 0 + 37106980 ], "3.7.0.30": [ 37118196, 37118576, 37118248, 37118224, - 37119268, - 0 + 37119268 ], "3.7.5.11": [ 37883280, 37884088, 37883136, 37883008, - 37884052, - 0 + 37884052 ], "3.7.5.23": [ 37895736, 37896544, 37895592, 37883008, - 37896508, - 0 + 37896508 ], "3.7.5.27": [ 37895736, 37896544, 37895592, 37895464, - 37896508, - 0 + 37896508 ], "3.7.5.31": [ 37903928, 37904736, 37903784, 37903656, - 37904700, - 0 + 37904700 ], "3.7.6.24": [ 38978840, 38979648, 38978696, 38978604, - 38979612, - 0 + 38979612 ], "3.7.6.29": [ 38986376, 38987184, 38986232, 38986104, - 38987148, - 0 + 38987148 ], "3.7.6.44": [ 39016520, 39017328, 39016376, 38986104, - 39017292, - 0 + 39017292 ], "3.8.0.31": [ 46064088, 46064912, 46063944, 38986104, - 46064876, - 0 + 46064876 ], "3.8.0.33": [ 46059992, 46060816, 46059848, 38986104, - 46060780, - 0 + 46060780 ], "3.8.0.41": [ 46064024, 46064848, 46063880, 38986104, - 46064812, - 0 + 46064812 ], "3.8.1.26": [ 46409448, 46410272, 46409304, 38986104, - 46410236, - 0 + 46410236 ], "3.9.0.28": [ 48418376, 48419280, 48418232, 38986104, - 48419244, - 0 + 48419244 ], "3.9.2.23": [ 50320784, 50321712, 50320640, 38986104, - 50321676, - 50592864 + 50321676 ], "3.9.2.26": [ 50329040, 50329968, 50328896, 38986104, - 50329932, - 0 + 50329932 ], "3.9.5.81": [ 61650872, 61652208, 61650680, 0, - 61652144, - 0 + 61652144 ], "3.9.5.91": [ 61654904, 61656240, 61654712, 38986104, - 61656176, - 61677112 + 61656176 ], "3.9.6.19": [ 61997688, 61997464, 61997496, 38986104, - 61998960, - 0 + 61998960 ], "3.9.6.33": [ 62030600, 62031936, 62030408, 0, - 62031872, - 0 + 62031872 ], "3.9.7.15": [ 63482696, 63484032, 63482504, 0, - 63483968, - 0 + 63483968 ], "3.9.7.25": [ 63482760, 63484096, 63482568, 0, - 63484032, - 0 + 63484032 ], "3.9.7.29": [ 63486984, 63488320, 63486792, 0, - 63488256, - 63488352 + 63488256 ], "3.9.8.15": [ 64996632, 64997968, 64996440, 0, - 64997904, - 65011632 + 64997904 ] } \ No newline at end of file diff --git a/pywxdump/wx_info/get_wx_db.py b/pywxdump/wx_info/get_wx_db.py index fbd80b3..dfd057d 100644 --- a/pywxdump/wx_info/get_wx_db.py +++ b/pywxdump/wx_info/get_wx_db.py @@ -10,7 +10,9 @@ import re import winreg from typing import List, Union -def get_wechat_db(require_list: Union[List[str], str] = "all", msg_dir: str = None): + +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) @@ -27,18 +29,28 @@ def get_wechat_db(require_list: Union[List[str], str] = "all", msg_dir: str = No msg_dir = os.path.join(w_dir, "WeChat Files") if not os.path.exists(msg_dir): - return "[-] 目录不存在" + error = "[-] 目录不存在" + if is_logging: print(error) + return error 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) + 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): @@ -46,7 +58,9 @@ def get_wechat_db(require_list: Union[List[str], str] = "all", msg_dir: str = No for require in require_list: pattern[require] = re.compile(r"%s.*\.db$" % require) else: - return "[-] 参数错误" + error = "[-] 参数错误" + if is_logging: print(error) + return error # 获取数据库路径 for user, user_dir in user_dirs.items(): # 遍历用户目录 @@ -57,19 +71,21 @@ def get_wechat_db(require_list: Union[List[str], str] = "all", msg_dir: str = No 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) - if isinstance(user_dirs, str): - print(user_dirs) - else: - for user, user_dir in user_dirs.items(): - print(f"[+] {user}") - for n, paths in user_dir.items(): - print(f" {n}:") - for path in paths: - print(f" {path}") + user_dirs = get_wechat_db(require_list, is_logging=True) diff --git a/pywxdump/wx_info/get_wx_info.py b/pywxdump/wx_info/get_wx_info.py index a44ce95..030d9a2 100644 --- a/pywxdump/wx_info/get_wx_info.py +++ b/pywxdump/wx_info/get_wx_info.py @@ -5,9 +5,9 @@ # Author: xaoyaoo # Date: 2023/08/21 # ------------------------------------------------------------------------------- -import argparse import json import ctypes +import pymem from win32com.client import Dispatch import psutil @@ -24,13 +24,15 @@ def get_info_without_key(h_process, address, n_size=64): return text.strip() if text.strip() != "" else "None" -def get_info_wxid(h_process, address, n_size=32, address_len=8): - array = ctypes.create_string_buffer(address_len) - if ReadProcessMemory(h_process, void_p(address), array, address_len, 0) == 0: return "None" - address = int.from_bytes(array, byteorder='little') # 逆序转换为int地址(key地址) - wxid = get_info_without_key(h_process, address, n_size) - if not wxid.startswith("wxid_"): wxid = "None" - return wxid +def get_info_wxid(h_process, n_size=64): + pm = pymem.Pymem("WeChat.exe") + addrs = pymem.pattern.pattern_scan_all(pm.process_handle, b'wxid_', return_multiple=True) + for addr in addrs: + wxidtmp = get_info_without_key(h_process, addr, n_size) + if wxidtmp.startswith("wxid_") and r'\FileStorage\MsgAttach' in wxidtmp: + wxid = wxidtmp.split(r'\FileStorage\MsgAttach')[0] + return wxid + return "None" # 读取内存中的key @@ -45,16 +47,18 @@ def get_key(h_process, address, address_len=8): # 读取微信信息(account,mobile,name,mail,wxid,key) -def read_info(version_list): +def read_info(version_list, is_logging=False): wechat_process = [] result = [] - + error = "" for process in psutil.process_iter(['name', 'exe', 'pid', 'cmdline']): if process.name() == 'WeChat.exe': wechat_process.append(process) if len(wechat_process) == 0: - return "[-] WeChat No Run" + error = "[-] WeChat No Run" + if is_logging: print(error) + return error for process in wechat_process: tmp_rd = {} @@ -64,7 +68,9 @@ def read_info(version_list): bias_list = version_list.get(tmp_rd['version'], None) if not isinstance(bias_list, list): - return f"[-] WeChat Current Version {tmp_rd['version']} Is Not Supported" + error = f"[-] WeChat Current Version {tmp_rd['version']} Is Not Supported" + if is_logging: print(error) + return error wechat_base_address = 0 for module in process.memory_maps(grouped=False): @@ -72,7 +78,9 @@ def read_info(version_list): wechat_base_address = int(module.addr, 16) break if wechat_base_address == 0: - return f"[-] WeChat WeChatWin.dll Not Found" + error = f"[-] WeChat WeChatWin.dll Not Found" + if is_logging: print(error) + return error Handle = ctypes.windll.kernel32.OpenProcess(0x1F0FFF, False, process.pid) @@ -81,7 +89,6 @@ def read_info(version_list): mobile_baseaddr = wechat_base_address + bias_list[2] mail_baseaddr = wechat_base_address + bias_list[3] key_baseaddr = wechat_base_address + bias_list[4] - wxid_baseaddr = wechat_base_address + bias_list[5] addrLen = 4 if tmp_rd['version'] in ["3.9.2.23", "3.9.2.26"] else 8 @@ -89,14 +96,27 @@ def read_info(version_list): tmp_rd['mobile'] = get_info_without_key(Handle, mobile_baseaddr, 64) if bias_list[2] != 0 else "None" tmp_rd['name'] = get_info_without_key(Handle, name_baseaddr, 64) if bias_list[0] != 0 else "None" tmp_rd['mail'] = get_info_without_key(Handle, mail_baseaddr, 64) if bias_list[3] != 0 else "None" - tmp_rd['wxid'] = get_info_wxid(Handle, wxid_baseaddr, 24, addrLen) if bias_list[5] != 0 else "None" + tmp_rd['wxid'] = get_info_wxid(Handle, 64) tmp_rd['key'] = get_key(Handle, key_baseaddr, addrLen) if bias_list[4] != 0 else "None" result.append(tmp_rd) + if is_logging: + print("=" * 32) + if isinstance(result, str): # 输出报错 + print(result) + else: # 输出结果 + for i, rlt in enumerate(result): + for k, v in rlt.items(): + print(f"[+] {k:>7}: {v}") + print(end="-" * 32 + "\n" if i != len(result) - 1 else "") + print("=" * 32) + return result if __name__ == "__main__": + import argparse + parser = argparse.ArgumentParser() parser.add_argument("--vlfile", type=str, help="手机号", required=False) parser.add_argument("--vldict", type=str, help="微信昵称", required=False) @@ -117,14 +137,4 @@ if __name__ == "__main__": with open(VERSION_LIST_PATH, "r", encoding="utf-8") as f: VERSION_LIST = json.load(f) - result = read_info(VERSION_LIST) # 读取微信信息 - - print("=" * 32) - if isinstance(result, str): # 输出报错 - print(result) - else: # 输出结果 - for i, rlt in enumerate(result): - for k, v in rlt.items(): - print(f"[+] {k:>7}: {v}") - print(end="-" * 32 + "\n" if i != len(result) - 1 else "") - print("=" * 32) + result = read_info(VERSION_LIST, True) # 读取微信信息 diff --git a/setup.py b/setup.py index 45bcb8b..81294f2 100644 --- a/setup.py +++ b/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.2" +version = "2.2.5" setup( name="pywxdump", author="xaoyaoo", diff --git a/tests/gen_exe.py b/tests/build_exe.py similarity index 100% rename from tests/gen_exe.py rename to tests/build_exe.py