diff --git a/Program/Program.py b/Program/get_wx_info.py similarity index 98% rename from Program/Program.py rename to Program/get_wx_info.py index 2bd130e..ec1786e 100644 --- a/Program/Program.py +++ b/Program/get_wx_info.py @@ -187,7 +187,7 @@ def read_info(version_list): return rd if __name__ == "__main__": - version_list = json.load(open("version_list.json", "r", encoding="utf-8")) + version_list = json.load(open("../version_list.json", "r", encoding="utf-8")) rd = read_info(version_list) for i in rd: for k, v in i.items(): diff --git a/decrypted/__init__.py b/decrypted/__init__.py new file mode 100644 index 0000000..7e65b3e --- /dev/null +++ b/decrypted/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*-# +# ------------------------------------------------------------------------------- +# Name: __init__.py.py +# Description: +# Author: xaoyaoo +# Date: 2023/08/21 +# ------------------------------------------------------------------------------- + + +if __name__ == '__main__': + pass diff --git a/decrypted/decrypt1.py b/decrypted/decrypt1.py new file mode 100644 index 0000000..fd599ad --- /dev/null +++ b/decrypted/decrypt1.py @@ -0,0 +1,301 @@ +import glob +import os +import hmac +import hashlib +import re +import shutil +import sqlite3 +import subprocess +import winreg + +from Cryptodome.Cipher import AES + +SQLITE_FILE_HEADER = "SQLite format 3\x00" +IV_SIZE = 16 +HMAC_SHA1_SIZE = 20 +KEY_SIZE = 32 +DEFAULT_PAGESIZE = 4096 +DEFAULT_ITER = 64000 + + +# 通过密钥解密数据库 +def decrypt(key, filePath, decryptedPath): + password = bytes.fromhex(key.replace(" ", "")) + with open(filePath, "rb") as file: + blist = file.read() + + salt = blist[:16] + byteKey = hashlib.pbkdf2_hmac("sha1", password, salt, DEFAULT_ITER, KEY_SIZE) + first = blist[16:DEFAULT_PAGESIZE] + + mac_salt = bytes([(salt[i] ^ 58) for i in range(16)]) + mac_key = hashlib.pbkdf2_hmac("sha1", byteKey, mac_salt, 2, KEY_SIZE) + hash_mac = hmac.new(mac_key, first[:-32], hashlib.sha1) + hash_mac.update(b'\x01\x00\x00\x00') + + if hash_mac.digest() == first[-32:-12]: + print("Decryption Success") + else: + print("Password Error") + + newblist = [blist[i:i + DEFAULT_PAGESIZE] for i in range(DEFAULT_PAGESIZE, len(blist), DEFAULT_PAGESIZE)] + + with open(decryptedPath, "wb") as deFile: + deFile.write(SQLITE_FILE_HEADER.encode()) + t = AES.new(byteKey, AES.MODE_CBC, first[-48:-32]) + decrypted = t.decrypt(first[:-48]) + deFile.write(decrypted) + deFile.write(first[-48:]) + + for i in newblist: + t = AES.new(byteKey, AES.MODE_CBC, i[-48:-32]) + decrypted = t.decrypt(i[:-48]) + deFile.write(decrypted) + deFile.write(i[-48:]) + + +# 通过外部程序获取微信数据库的key +def get_wx_key(): + """ + 执行 GoWxDump.exe -wxinfo 获取微信数据库的key + :return: + """ + # 获取当前文件路径的上一级目录 + current_path = os.path.dirname(os.path.abspath(__file__)) + current_path = os.path.abspath(os.path.join(current_path, "../..")) + # 获取GoWxDump.exe的路径 + gowxdump_path = os.path.join(current_path, "Release", "GoWxDump.exe") + # 判断GoWxDump.exe是否存在 + if not os.path.exists(gowxdump_path): + print("GoWxDump.exe not found") + return + command = gowxdump_path + " -wxinfo" + output = subprocess.check_output(command, shell=True, encoding='latin-1') + + wx_key = output.split("WeChat Key:")[-1].strip() + return wx_key + + +# 获取微信数据根目录 +def get_wechat_dir(): + """ + 读取注册表获取微信消息目录 + :return: + """ + try: + # 打开注册表的微信路径:HKEY_CURRENT_USER\Software\Tencent\WeChat\FileSavePath + key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Tencent\WeChat", 0, winreg.KEY_READ) + # 获取key的值 + value, _ = winreg.QueryValueEx(key, "FileSavePath") + # 关闭注册表项 + winreg.CloseKey(key) + w_dir = value + except Exception as e: + print("读取注册表错误:", str(e)) + return str(e) + + # 如果 w_dir 为 "MyDocument:" + if w_dir == "MyDocument:": + # 获取 %USERPROFILE%/Documents 目录 + 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): + raise FileNotFoundError("目录不存在") + return msg_dir + + +# 获取微信消息目录下的所有用户目录 +def get_wechat_user_dir(wechat_root): + """ + // 获取微信消息目录下的所有用户目录,排除All Users目录和Applet目录,返回一个map,key用户id,value用户目录 + :param wechat_root: 微信消息目录 + :return: + """ + user_dirs = {} + # 获取微信消息目录下的所有用户目录 + files = os.listdir(wechat_root) + for file_name in files: + # 排除All Users目录和Applet目录 + if file_name == "All Users" or file_name == "Applet" or file_name == "WMPF": + continue + user_dirs[file_name] = os.path.join(wechat_root, file_name) + return user_dirs + + +# copy msg.db到tmp目录,并创建decrypted目录 +def copy_msg_db(data_dir): + # 判断目录是否存在 + if not os.path.exists(data_dir): + raise FileNotFoundError("目录不存在") + + # 判断运行目录是否存在tmp目录,如果不存在则创建 + tmp_dir = os.path.join(os.getcwd(), "tmp") + if not os.path.exists(tmp_dir): + os.mkdir(tmp_dir) + + # 正则匹配,将所有MSG数字.db文件拷贝到tmp目录,不扫描子目录 + for root, dirs, files in os.walk(data_dir): + for file_name in files: + if re.match(r".*MSG.*\.db", file_name): + src_path = os.path.join(root, file_name) + dst_path = os.path.join(tmp_dir, file_name) + shutil.copyfile(src_path, dst_path) + + if "MicroMsg.db" in files: + src_path = os.path.join(root, "MicroMsg.db") + dst_path = os.path.join(tmp_dir, "MicroMsg.db") + shutil.copyfile(src_path, dst_path) + + # 如果不存在decrypted目录则创建 + decrypted_dir = os.path.join(os.getcwd(), "") + if not os.path.exists(decrypted_dir): + os.mkdir(decrypted_dir) + return tmp_dir, decrypted_dir + + +# 合并相同名称的数据库 +def merge_db(db_path): + dbs_paths = {} + for root, dirs, files in os.walk(db_path): + for file_name in files: + if "db-shm" in file_name or "db-wal" in file_name: + continue + if "FTSMSG" in file_name: + src_path = os.path.join(root, file_name) + dbs_paths["FTSMSG_all.db"] = dbs_paths.get("FTSMSG_all.db", []) + dbs_paths["FTSMSG_all.db"].append(src_path) + elif "MediaMSG" in file_name: + src_path = os.path.join(root, file_name) + dbs_paths["MediaMSG_all.db"] = dbs_paths.get("MediaMSG_all.db", []) + dbs_paths["MediaMSG_all.db"].append(src_path) + elif "MSG" in file_name: + src_path = os.path.join(root, file_name) + dbs_paths["MSG_all.db"] = dbs_paths.get("MSG_all.db", []) + dbs_paths["MSG_all.db"].append(src_path) + + for db_name, db_files in dbs_paths.items(): + if db_name != "MSG_all.db": + continue + + save_path = os.path.join(db_path, db_name) + merged_conn = sqlite3.connect(save_path) + merged_cursor = merged_conn.cursor() + + for db_file in db_files: + c0 = merged_cursor.execute("select tbl_name from sqlite_master where type='table'") + r0 = c0.fetchall() + r0 = [row[0] for row in r0] + + conn = sqlite3.connect(db_file) + cursor = conn.cursor() + c = cursor.execute("select tbl_name,sql from sqlite_master where type='table'") + tbls = [] + for row in c: + if row[0] == "sqlite_sequence": + continue + if "mmTokenizer" in row[1]: + continue + tbls.append(row[0]) + if row[0] in r0: + continue + try: + merged_cursor.execute(row[1]) + except Exception as e: + print(e) + print(db_file) + print(row[1]) + print(r0) + raise e + merged_conn.commit() + for row in tbls: + c1 = cursor.execute("select * from " + row) + for r in c1: + columns = conn.execute("PRAGMA table_info(" + row + ")").fetchall() + if len(columns) > 1: + columns = [column[1] for column in columns[1:]] + values = r[1:] + # query = "INSERT INTO " + row + " (" + ",".join(columns) + ") VALUES (" + ",".join( + # ["?" for _ in range(len(values))]) + ")" + else: + columns = [columns[0][1]] + values = [r[0]] + query_1 = "select * from " + row + " where " + columns[0] + "=?" + c2 = merged_cursor.execute(query_1, values) + if len(c2.fetchall()) > 0: + continue + query = "INSERT INTO " + row + " (" + ",".join(columns) + ") VALUES (" + ",".join( + ["?" for _ in range(len(values))]) + ")" + + try: + merged_cursor.execute(query, values) + except Exception as e: + print() + print("error") + print(e) + print(db_file) + print(query, values) + print(len(values)) + raise e + merged_conn.commit() + + conn.close() + print(db_file) + + merged_conn.close() + # merge_databases(save_path, db_file) + + +def merge_databases(db1, db2): + con3 = sqlite3.connect(db1) + + con3.execute("ATTACH DATABASE '" + db2 + "' as dba") + + con3.execute("BEGIN") + for row in con3.execute("SELECT * FROM dba.sqlite_master WHERE type='table'"): + # 此处的ignore就是为了忽略重复ID导致的异常 + combine = "INSERT OR IGNORE INTO " + row[1] + " SELECT * FROM dba." + row[1] + print(combine) + con3.execute(combine) + con3.commit() + con3.execute("detach database dba") + + +if __name__ == '__main__': + # 获取微信数据库的key + wx_key = get_wx_key() + + # 获取微信消息目录 + wechat_msg_dir = get_wechat_dir() + user_msg_dirs = get_wechat_user_dir(wechat_msg_dir) + if len(user_msg_dirs) == 1: + data_dir = list(user_msg_dirs.values())[0] + else: + for i, user_dir in enumerate(user_msg_dirs): + print(i, user_dir) + index = int(input("请选择要导出的用户:")) + data_dir = list(user_msg_dirs.values())[index] + + print("复制微信的msg数据文件...") + # 复制微信的msg数据文件 + tmp_dir, decrypted_dir = copy_msg_db(os.path.join(data_dir, "Msg")) + + print("解密数据库...") + # 解密数据库 + for file_name in os.listdir(tmp_dir): + if re.match(r".*\.db$", file_name): + src_path = os.path.join(tmp_dir, file_name) + dst_path = os.path.join(decrypted_dir, file_name) + decrypt(wx_key, src_path, dst_path) + + # 删除临时目录 + shutil.rmtree(tmp_dir) + + # decrypted_dir = os.path.join(os.getcwd(), "decrypted") + print("合并数据库...") + # 合并数据库 + merge_db(decrypted_dir) diff --git a/Program/version_list.json b/version_list.json similarity index 100% rename from Program/version_list.json rename to version_list.json