ToB企服应用市场:ToB评测及商务社交产业平台

标题: 劫持微信聊天记载并分析还原 —— 合并解密后的数据库(三) [打印本页]

作者: 嚴華    时间: 2024-11-8 19:52
标题: 劫持微信聊天记载并分析还原 —— 合并解密后的数据库(三)

【完备演示工具下载】
https://www.chwm.vip/index.html?aid=23
我们接着上一篇文章《劫持微信聊天记载并分析还原 —— 解密数据库(二)》将解密后的微信数据库合并为一整个DB文件。
微信数据库存放目录:

解密后的微信数据库:

详细命令:
merge -i "C:\Users\admin\AppData\Local\Temp\wx_tmp" -o "C:\Users\admin\AppData\Local\Temp\wxdb_all.db"

运行命令后,我们可以看到在此位置 "C:\Users\admin\AppData\Local\Temp"  下天生了一个以 “wxdb_all.db” 命名的文件,这便是解密后的微信数据库合并成功的文件,下一步我们将详细解读微信数据库的结构以及如何打开并访问里面的内容。
部分现实代码:
  1. # -*- coding: utf-8 -*-#
  2. # -------------------------------------------------------------------------------
  3. # Name:         merge_db.py
  4. # Description:  
  5. # Author:       Rainbow(www.chwm.vip)
  6. # Date:         2024/11/08
  7. # -------------------------------------------------------------------------------
  8. import logging
  9. import os
  10. import shutil
  11. import sqlite3
  12. import subprocess
  13. import time
  14. from typing import List
  15. from .decryption import batch_decrypt
  16. from .wx_info import get_core_db
  17. from .utils import wx_core_loger, wx_core_error, CORE_DB_TYPE
  18. @wx_core_error
  19. def execute_sql(connection, sql, params=None):
  20.     """
  21.     执行给定的SQL语句,返回结果。
  22.     参数:
  23.         - connection: SQLite连接
  24.         - sql:要执行的SQL语句
  25.         - params:SQL语句中的参数
  26.     """
  27.     try:
  28.         # connection.text_factory = bytes
  29.         cursor = connection.cursor()
  30.         if params:
  31.             cursor.execute(sql, params)
  32.         else:
  33.             cursor.execute(sql)
  34.         return cursor.fetchall()
  35.     except Exception as e:
  36.         try:
  37.             connection.text_factory = bytes
  38.             cursor = connection.cursor()
  39.             if params:
  40.                 cursor.execute(sql, params)
  41.             else:
  42.                 cursor.execute(sql)
  43.             rdata = cursor.fetchall()
  44.             connection.text_factory = str
  45.             return rdata
  46.         except Exception as e:
  47.             wx_core_loger.error(f"**********\nSQL: {sql}\nparams: {params}\n{e}\n**********", exc_info=True)
  48.             return None
  49. @wx_core_error
  50. def check_create_sync_log(connection):
  51.     """
  52.     检查是否存在表 sync_log,用于记录同步记录,包括微信数据库路径,表名,记录数,同步时间
  53.     :param connection: SQLite连接
  54.     :return: True or False
  55.     """
  56.     out_cursor = connection.cursor()
  57.     # 检查是否存在表 sync_log,用于记录同步记录,包括微信数据库路径,表名,记录数,同步时间
  58.     sync_log_status = execute_sql(connection, "SELECT name FROM sqlite_master WHERE type='table' AND name='sync_log'")
  59.     if len(sync_log_status) < 1:
  60.         #  db_path 微信数据库路径,tbl_name 表名,src_count 源数据库记录数,current_count 当前合并后的数据库对应表记录数
  61.         sync_record_create_sql = ("CREATE TABLE sync_log ("
  62.                                   "id INTEGER PRIMARY KEY AUTOINCREMENT,"
  63.                                   "db_path TEXT NOT NULL,"
  64.                                   "tbl_name TEXT NOT NULL,"
  65.                                   "src_count INT,"
  66.                                   "current_count INT,"
  67.                                   "createTime INT DEFAULT (strftime('%s', 'now')), "
  68.                                   "updateTime INT DEFAULT (strftime('%s', 'now'))"
  69.                                   ");")
  70.         out_cursor.execute(sync_record_create_sql)
  71.         # 创建索引
  72.         out_cursor.execute("CREATE INDEX idx_sync_log_db_path ON sync_log (db_path);")
  73.         out_cursor.execute("CREATE INDEX idx_sync_log_tbl_name ON sync_log (tbl_name);")
  74.         # 创建联合索引,防止重复
  75.         out_cursor.execute("CREATE UNIQUE INDEX idx_sync_log_db_tbl ON sync_log (db_path, tbl_name);")
  76.         connection.commit()
  77.     out_cursor.close()
  78.     return True
  79. @wx_core_error
  80. def check_create_file_md5(connection):
  81.     """
  82.     检查是否存在表 file_md5,用于记录文件信息,后续用于去重等操作,暂时闲置
  83.     """
  84.     pass
  85. @wx_core_error
  86. def merge_db(db_paths: List[dict], save_path: str = "merge.db", is_merge_data: bool = True,
  87.              startCreateTime: int = 0, endCreateTime: int = 0):
  88.     """
  89.     合并数据库 会忽略主键以及重复的行。
  90.     :param db_paths: [{"db_path": "xxx", "de_path": "xxx"},...]
  91.                         db_path表示初始路径,de_path表示解密后的路径;初始路径用于保存合并的日志情况,解密后的路径用于读取数据
  92.     :param save_path: str 输出文件路径
  93.     :param is_merge_data: bool 是否合并数据(如果为False,则只解密,并创建表,不插入数据)
  94.     :param startCreateTime: 开始时间戳 主要用于MSG数据库的合并
  95.     :param endCreateTime:  结束时间戳 主要用于MSG数据库的合并
  96.     :return:
  97.     """
  98.     if os.path.isdir(save_path):
  99.         save_path = os.path.join(save_path, f"merge_{int(time.time())}.db")
  100.     if isinstance(db_paths, list):
  101.         # alias, file_path
  102.         databases = {f"dbi_{i}": (db['db_path'],
  103.                                   db.get('de_path', db['db_path'])
  104.                                   ) for i, db in enumerate(db_paths)
  105.                      }
  106.     else:
  107.         raise TypeError("db_paths 类型错误")
  108.     outdb = sqlite3.connect(save_path)
  109.     is_sync_log = check_create_sync_log(outdb)
  110.     if not is_sync_log:
  111.         wx_core_loger.warning("创建同步记录表失败")
  112.     out_cursor = outdb.cursor()
  113.     # 将MSG_db_paths中的数据合并到out_db_path中
  114.     for alias, db in databases.items():
  115.         db_path = db[0]
  116.         de_path = db[1]
  117.         # 附加数据库
  118.         sql_attach = f"ATTACH DATABASE '{de_path}' AS {alias}"
  119.         out_cursor.execute(sql_attach)
  120.         outdb.commit()
  121.         sql_query_tbl_name = f"SELECT tbl_name, sql FROM {alias}.sqlite_master WHERE type='table' ORDER BY tbl_name;"
  122.         tables = execute_sql(outdb, sql_query_tbl_name)
  123.         for table in tables:
  124.             table, init_create_sql = table[0], table[1]
  125.             table = table if isinstance(table, str) else table.decode()
  126.             init_create_sql = init_create_sql if isinstance(init_create_sql, str) else init_create_sql.decode()
  127.             if table == "sqlite_sequence":
  128.                 continue
  129.             if "CREATE TABLE".lower() not in str(init_create_sql).lower():
  130.                 continue
  131.             # 获取表中的字段名
  132.             sql_query_columns = f"PRAGMA table_info({table})"
  133.             columns = execute_sql(outdb, sql_query_columns)
  134.             if table == "ChatInfo" and len(columns) > 12:  # bizChat中的ChatInfo表与MicroMsg中的ChatInfo表字段不同
  135.                 continue
  136.             col_type = {
  137.                 (i[1] if isinstance(i[1], str) else i[1].decode(),
  138.                  i[2] if isinstance(i[2], str) else i[2].decode())
  139.                 for i in columns}
  140.             columns = [i[0] for i in col_type]
  141.             if not columns or len(columns) < 1:
  142.                 continue
  143.             # 创建表table
  144.             sql_create_tbl = f"CREATE TABLE IF NOT EXISTS {table} AS SELECT *  FROM {alias}.{table} WHERE 0 = 1;"
  145.             out_cursor.execute(sql_create_tbl)
  146.             # 创建包含 NULL 值比较的 UNIQUE 索引
  147.             index_name = f"{table}_unique_index"
  148.             coalesce_columns = ','.join(f"COALESCE({column}, '')" for column in columns)
  149.             sql = f"CREATE UNIQUE INDEX IF NOT EXISTS {index_name} ON {table} ({coalesce_columns})"
  150.             out_cursor.execute(sql)
  151.             # 插入sync_log
  152.             sql_query_sync_log = f"SELECT src_count FROM sync_log WHERE db_path=? AND tbl_name=?"
  153.             sync_log = execute_sql(outdb, sql_query_sync_log, (db_path, table))
  154.             if not sync_log or len(sync_log) < 1:
  155.                 sql_insert_sync_log = "INSERT INTO sync_log (db_path, tbl_name, src_count, current_count) VALUES (?, ?, ?, ?)"
  156.                 out_cursor.execute(sql_insert_sync_log, (db_path, table, 0, 0))
  157.             outdb.commit()
  158.             if is_merge_data:
  159.                 # 比较源数据库和合并后的数据库记录数
  160.                 log_src_count = execute_sql(outdb, sql_query_sync_log, (db_path, table))[0][0]
  161.                 src_count = execute_sql(outdb, f"SELECT COUNT(*) FROM {alias}.{table}")[0][0]
  162.                 if src_count <= log_src_count:
  163.                     wx_core_loger.info(f"忽略 {db_path} {de_path} {table} {src_count} {log_src_count}")
  164.                     continue
  165.                 # 构建数据查询sql
  166.                 sql_base = f"SELECT {','.join([i for i in columns])} FROM {alias}.{table} "
  167.                 where_clauses, params = [], []
  168.                 if "CreateTime" in columns:
  169.                     if startCreateTime > 0:
  170.                         where_clauses.append("CreateTime > ?")
  171.                         params.append(startCreateTime)
  172.                     if endCreateTime > 0:
  173.                         where_clauses.append("CreateTime < ?")
  174.                         params.append(endCreateTime)
  175.                 # 如果有WHERE子句,将其添加到SQL语句中,并添加ORDER BY子句
  176.                 sql = f"{sql_base} WHERE {' AND '.join(where_clauses)} ORDER BY CreateTime" if where_clauses else sql_base
  177.                 src_data = execute_sql(outdb, sql, tuple(params))
  178.                 if not src_data or len(src_data) < 1:
  179.                     continue
  180.                 # 插入数据
  181.                 sql = f"INSERT OR IGNORE INTO {table} ({','.join([i for i in columns])}) VALUES ({','.join(['?'] * len(columns))})"
  182.                 try:
  183.                     out_cursor.executemany(sql, src_data)
  184.                     # update sync_log
  185.                     sql_update_sync_log = ("UPDATE sync_log "
  186.                                            "SET src_count = ? ,"
  187.                                            f"current_count=(SELECT COUNT(*) FROM {table}) "
  188.                                            "WHERE db_path=? AND tbl_name=?")
  189.                     out_cursor.execute(sql_update_sync_log, (src_count, db_path, table))
  190.                 except Exception as e:
  191.                     wx_core_loger.error(
  192.                         f"error: {db_path}\n{de_path}\n{table}\n{sql}\n{src_data}\n{len(src_data)}\n{e}\n",
  193.                         exc_info=True)
  194.         # 分离数据库
  195.         sql_detach = f"DETACH DATABASE {alias}"
  196.         out_cursor.execute(sql_detach)
  197.         outdb.commit()
  198.     out_cursor.close()
  199.     outdb.close()
  200.     return save_path
  201. # @wx_core_error
  202. # def merge_db1(db_paths: list[dict], save_path: str = "merge.db", is_merge_data: bool = True,
  203. #               startCreateTime: int = 0, endCreateTime: int = 0):
  204. #     """
  205. #     合并数据库 会忽略主键以及重复的行。
  206. #     :param db_paths: [{"db_path": "xxx", "de_path": "xxx"},...]
  207. #                         db_path表示初始路径,de_path表示解密后的路径;初始路径用于保存合并的日志情况,解密后的路径用于读取数据
  208. #     :param save_path: str 输出文件路径
  209. #     :param is_merge_data: bool 是否合并数据(如果为False,则只解密,并创建表,不插入数据)
  210. #     :param startCreateTime: 开始时间戳 主要用于MSG数据库的合并
  211. #     :param endCreateTime:  结束时间戳 主要用于MSG数据库的合并
  212. #     :return:
  213. #     """
  214. #     if os.path.isdir(save_path):
  215. #         save_path = os.path.join(save_path, f"merge_{int(time.time())}.db")
  216. #
  217. #     if isinstance(db_paths, list):
  218. #         # alias, file_path
  219. #         databases = {f"MSG{i}": (db['db_path'],
  220. #                                  db.get('de_path', db['db_path'])
  221. #                                  ) for i, db in enumerate(db_paths)
  222. #                      }
  223. #     else:
  224. #         raise TypeError("db_paths 类型错误")
  225. #
  226. #     from sqlalchemy import create_engine, MetaData, Table, select, insert, Column, UniqueConstraint
  227. #     from sqlalchemy.orm import sessionmaker
  228. #     from sqlalchemy import inspect, PrimaryKeyConstraint
  229. #
  230. #     outdb = create_engine(f"sqlite:///{save_path}", echo=False)
  231. #
  232. #     # 创建Session实例
  233. #     Session = sessionmaker()
  234. #     Session.configure(bind=outdb)
  235. #     session = Session()
  236. #
  237. #     # 将MSG_db_paths中的数据合并到out_db_path中
  238. #     for alias, db in databases.items():
  239. #         db_path = db[0]
  240. #         de_path = db[1]
  241. #
  242. #         db_engine = create_engine(f"sqlite:///{de_path}", echo=False)
  243. #
  244. #         # 反射源数据库的表结构
  245. #         metadata = MetaData()
  246. #         metadata.reflect(bind=db_engine)
  247. #
  248. #         # 创建表
  249. #         outdb_metadata = MetaData()
  250. #         inspector = inspect(db_engine)
  251. #         table_names = [i for i in inspector.get_table_names() if i not in ["sqlite_sequence"]]
  252. #         for table_name in table_names:
  253. #             # 创建表table
  254. #             columns_list_dict = inspector.get_columns(table_name)
  255. #             col_names = [i['name'] for i in columns_list_dict]
  256. #             columns = [Column(i['name'], i['type'], primary_key=False) for i in columns_list_dict]
  257. #             table = Table(table_name, outdb_metadata, *columns)
  258. #             if len(columns) > 1:  # 联合索引
  259. #                 unique_constraint = UniqueConstraint(*col_names, name=f"{table_name}_unique_index")
  260. #                 table.append_constraint(unique_constraint)
  261. #             else:
  262. #                 table.append_constraint(PrimaryKeyConstraint(*col_names))
  263. #             table.create(outdb, checkfirst=True)
  264. #
  265. #         # 将源数据库中的数据插入目标数据库
  266. #         outdb_metadata = MetaData()
  267. #         for table_name in metadata.tables:
  268. #             source_table = Table(table_name, metadata, autoload_with=db_engine)
  269. #             outdb_table = Table(table_name, outdb_metadata, autoload_with=outdb)
  270. #
  271. #             # 查询源表中的所有数据
  272. #             query = select(source_table)
  273. #             with db_engine.connect() as connection:
  274. #                 result = connection.execute(query).fetchall()
  275. #
  276. #             # 插入到目标表中
  277. #             for row in result:
  278. #                 row_data = row._asdict()
  279. #
  280. #                 # 尝试将所有文本数据转换为 UTF-8
  281. #                 for key, value in row_data.items():
  282. #                     if isinstance(value, str):
  283. #                         row_data[key] = value.encode("utf-8")
  284. #
  285. #                 insert_stmt = insert(outdb_table).values(row_data)
  286. #                 try:
  287. #                     session.execute(insert_stmt)
  288. #                 except Exception as e:
  289. #                     pass
  290. #         db_engine.dispose()
  291. #
  292. #     # 提交事务
  293. #     session.commit()
  294. #     # 关闭Session
  295. #     session.close()
  296. #     outdb.dispose()
  297. #     return save_path
  298. @wx_core_error
  299. def decrypt_merge(wx_path: str, key: str, outpath: str = "",
  300.                   merge_save_path: str = None,
  301.                   is_merge_data=True, is_del_decrypted: bool = True,
  302.                   startCreateTime: int = 0, endCreateTime: int = 0,
  303.                   db_type=None) -> (bool, str):
  304.     """
  305.     解密合并数据库 msg.db, microMsg.db, media.db,注意:会删除原数据库
  306.     :param wx_path: 微信路径 eg: C:\\*******\\WeChat Files\\wxid_*********
  307.     :param key: 解密密钥
  308.     :param outpath: 输出路径
  309.     :param merge_save_path: 合并后的数据库路径
  310.     :param is_merge_data: 是否合并数据(如果为False,则只解密,并创建表,不插入数据)
  311.     :param is_del_decrypted: 是否删除解密后的数据库(除了合并后的数据库)
  312.     :param startCreateTime: 开始时间戳 主要用于MSG数据库的合并
  313.     :param endCreateTime:  结束时间戳 主要用于MSG数据库的合并
  314.     :param db_type: 数据库类型,从核心数据库中选择
  315.     :return: (true,解密后的数据库路径) or (false,错误信息)
  316.     """
  317.     if db_type is None:
  318.         db_type = []
  319.     outpath = outpath if outpath else "decrypt_merge_tmp"
  320.     merge_save_path = os.path.join(outpath,
  321.                                    f"merge_{int(time.time())}.db") if merge_save_path is None else merge_save_path
  322.     decrypted_path = os.path.join(outpath, "decrypted")
  323.     if not wx_path or not key or not os.path.exists(wx_path):
  324.         wx_core_loger.error("参数错误", exc_info=True)
  325.         return False, "参数错误"
  326.     # 解密
  327.     code, wxdbpaths = get_core_db(wx_path, db_type)
  328.     if not code:
  329.         wx_core_loger.error(f"获取数据库路径失败{wxdbpaths}", exc_info=True)
  330.         return False, wxdbpaths
  331.     # 判断out_path是否为空目录
  332.     if os.path.exists(decrypted_path) and os.listdir(decrypted_path):
  333.         for root, dirs, files in os.walk(decrypted_path, topdown=False):
  334.             for name in files:
  335.                 os.remove(os.path.join(root, name))
  336.             for name in dirs:
  337.                 os.rmdir(os.path.join(root, name))
  338.     if not os.path.exists(decrypted_path):
  339.         os.makedirs(decrypted_path)
  340.     wxdbpaths = {i["db_path"]: i for i in wxdbpaths}
  341.     # 调用 decrypt 函数,并传入参数   # 解密
  342.     code, ret = batch_decrypt(key=key, db_path=list(wxdbpaths.keys()), out_path=decrypted_path, is_print=False)
  343.     if not code:
  344.         wx_core_loger.error(f"解密失败{ret}", exc_info=True)
  345.         return False, ret
  346.     out_dbs = []
  347.     for code1, ret1 in ret:
  348.         if code1:
  349.             out_dbs.append(ret1)
  350.     parpare_merge_db_path = []
  351.     for db_path, out_path, _ in out_dbs:
  352.         parpare_merge_db_path.append({"db_path": db_path, "de_path": out_path})
  353.     merge_save_path = merge_db(parpare_merge_db_path, merge_save_path, is_merge_data=is_merge_data,
  354.                                startCreateTime=startCreateTime, endCreateTime=endCreateTime)
  355.     if is_del_decrypted:
  356.         shutil.rmtree(decrypted_path, True)
  357.     if isinstance(merge_save_path, str):
  358.         return True, merge_save_path
  359.     else:
  360.         return False, "未知错误"
  361. @wx_core_error
  362. def merge_real_time_db(key, merge_path: str, db_paths: [dict] or dict, real_time_exe_path: str = None):
  363.     """
  364.     合并实时数据库消息,暂时只支持64位系统
  365.     :param key:  解密密钥
  366.     :param merge_path:  合并后的数据库路径
  367.     :param db_paths:  [dict] or dict eg: {'wxid': 'wxid_***', 'db_type': 'MicroMsg',
  368.                         'db_path': 'C:\**\wxid_***\Msg\MicroMsg.db', 'wxid_dir': 'C:\***\wxid_***'}
  369.     :param real_time_exe_path:  实时数据库合并工具路径
  370.     :return:
  371.     """
  372.     try:
  373.         import platform
  374.     except:
  375.         raise ImportError("未找到模块 platform")
  376.     # 判断系统位数是否为64位,如果不是则抛出异常
  377.     if platform.architecture()[0] != '64bit':
  378.         raise Exception("System is not 64-bit.")
  379.     if isinstance(db_paths, dict):
  380.         db_paths = [db_paths]
  381.     merge_path = os.path.abspath(merge_path)  # 合并后的数据库路径,必须为绝对路径
  382.     merge_path_base = os.path.dirname(merge_path)  # 合并后的数据库路径
  383.     if not os.path.exists(merge_path_base):
  384.         os.makedirs(merge_path_base)
  385.     endbs = []
  386.     for db_info in db_paths:
  387.         db_path = os.path.abspath(db_info['db_path'])
  388.         if not os.path.exists(db_path):
  389.             # raise FileNotFoundError("数据库不存在")
  390.             continue
  391.         endbs.append(os.path.abspath(db_path))
  392.     endbs = '" "'.join(list(set(endbs)))
  393.     if not os.path.exists(real_time_exe_path if real_time_exe_path else ""):
  394.         current_path = os.path.dirname(__file__)  # 获取当前文件夹路径
  395.         real_time_exe_path = os.path.join(current_path, "tools", "realTime.exe")
  396.     if not os.path.exists(real_time_exe_path):
  397.         raise FileNotFoundError("未找到实时数据库合并工具")
  398.     real_time_exe_path = os.path.abspath(real_time_exe_path)
  399.     # 调用cmd命令
  400.     cmd = f'{real_time_exe_path} "{key}" "{merge_path}" "{endbs}"'
  401.     # os.system(cmd)
  402.     # wx_core_loger.info(f"合并实时数据库命令:{cmd}")
  403.     p = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=merge_path_base,
  404.                          creationflags=subprocess.CREATE_NO_WINDOW)
  405.     out, err = p.communicate()  # 查看返回值
  406.     if out and out.decode("utf-8").find("SUCCESS") >= 0:
  407.         wx_core_loger.info(f"合并实时数据库成功{out}")
  408.         return True, merge_path
  409.     else:
  410.         wx_core_loger.error(f"合并实时数据库失败\n{out}\n{err}")
  411.         return False, (out, err)
  412. @wx_core_error
  413. def all_merge_real_time_db(key, wx_path, merge_path: str, real_time_exe_path: str = None):
  414.     """
  415.     合并所有实时数据库
  416.     注:这是全量合并,会有可能产生重复数据,需要自行去重
  417.     :param key:  解密密钥
  418.     :param wx_path:  微信文件夹路径 eg:C:\*****\WeChat Files\wxid*******
  419.     :param merge_path:  合并后的数据库路径 eg: C:\\*******\\WeChat Files\\wxid_*********\\merge.db
  420.     :param real_time_exe_path:  实时数据库合并工具路径
  421.     :return:
  422.     """
  423.     if not merge_path or not key or not wx_path or not wx_path:
  424.         return False, "msg_path or media_path or wx_path or key is required"
  425.     try:
  426.         from wxdump import get_core_db
  427.     except ImportError:
  428.         return False, "未找到模块 wxdump"
  429.     db_paths = get_core_db(wx_path, CORE_DB_TYPE)
  430.     if not db_paths[0]:
  431.         return False, db_paths[1]
  432.     db_paths = db_paths[1]
  433.     code, ret = merge_real_time_db(key=key, merge_path=merge_path, db_paths=db_paths,
  434.                                    real_time_exe_path=real_time_exe_path)
  435.     if code:
  436.         return True, merge_path
  437.     else:
  438.         return False, ret
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4