劫持微信聊天記錄並分析還原 —— 解密資料庫(二)

Rainbow_Technology發表於2024-11-05

  • 本工具設計的初衷是用來獲取微信賬號的相關資訊並解析PC版微信的資料庫。

  • 程式以 Python 語言開發,可讀取、解密、還原微信資料庫並幫助使用者檢視聊天記錄,還可以將其聊天記錄匯出為csv、html等格式用於AI訓練,自動回覆或備份等等作用。下面我們將深入探討這個工具的各個方面及其工作原理。


【完整演示工具下載】

https://www.chwm.vip/index.html?aid=23


  1. 我們先根據上一篇文章《劫持微信聊天記錄並分析還原 —— 帳號資訊擷取(一)》擷取的帳號資訊來嘗試解密微信資料庫。

  1. 詳細命令:
    decrypt -k "04395c**********5e47d8" -i "C:\Users\admin\Documents\WeChat Files\wxid_b11\Msg" -o "C:\Users\admin\AppData\Local\Temp\wx_tmp"

3.引數詳解:
-k 為指定key密匙 04395c******************************5e47d8
-i 為微信資料庫路徑 wx_dir + Msg
-o 為解密存放的路徑 C:\Users\admin\AppData\Local\Temp\wx_tmp(系統臨時目錄)

  1. 然後我們可以看到提示解密 114 個檔案,成功 36 個,失敗 78 個。其中顯示失敗的檔案我們可以忽略(因為有些並不是資料庫檔案),只要有一個成功的那麼就是解密成功。


部分實現程式碼

# -*- coding: utf-8 -*-#
# -------------------------------------------------------------------------------
# Name:         decryption.py
# Description:
# Author:       Rainbow
# Date:         2024/11/05
# 微信資料庫採用的加密演算法是256位的AES-CBC。資料庫的預設的頁大小是4096位元組即4KB,其中每一個頁都是被單獨加解密的。
# 加密檔案的每一個頁都有一個隨機的初始化向量,它被儲存在每一頁的末尾。
# 加密檔案的每一頁都存有著訊息認證碼,演算法使用的是HMAC-SHA1(安卓資料庫使用的是SHA512)。它也被儲存在每一頁的末尾。
# 每一個資料庫檔案的開頭16位元組都儲存了一段唯一且隨機的鹽值,作為HMAC的驗證和資料的解密。
# 為了保證資料部分長度是16位元組即AES塊大小的整倍數,每一頁的末尾將填充一段空位元組,使得保留欄位的長度為48位元組。
# 綜上,加密檔案結構為第一頁4KB資料前16位元組為鹽值,緊接著4032位元組資料,再加上16位元組IV和20位元組HMAC以及12位元組空位元組;而後的頁均是4048位元組長度的加密資料段和48位元組的保留段。
# -------------------------------------------------------------------------------
import hmac
import hashlib
import os
from typing import Union, List
from Cryptodome.Cipher import AES
# from Crypto.Cipher import AES # 如果上面的匯入失敗,可以嘗試使用這個

from .utils import wx_core_error, wx_core_loger

SQLITE_FILE_HEADER = "SQLite format 3\x00"  # SQLite檔案頭

KEY_SIZE = 32
DEFAULT_PAGESIZE = 4096


# 透過金鑰解密資料庫
@wx_core_error
def decrypt(key: str, db_path: str, out_path: str):
    """
    透過金鑰解密資料庫
    :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 False, f"[-] out_path:'{out_path}' File not found!"

    if len(key) != 64:
        return False, f"[-] key:'{key}' Len Error!"

    password = bytes.fromhex(key.strip())

    try:
        with open(db_path, "rb") as file:
            blist = file.read()
    except Exception as e:
        return False, f"[-] db_path:'{db_path}' {e}!"

    salt = blist[:16]
    first = blist[16:4096]
    if len(salt) != 16:
        return False, f"[-] db_path:'{db_path}' File Error!"
    mac_salt = bytes([(salt[i] ^ 58) for i in range(16)])
    byteHmac = hashlib.pbkdf2_hmac("sha1", password, salt, 64000, KEY_SIZE)
    mac_key = hashlib.pbkdf2_hmac("sha1", byteHmac, mac_salt, 2, KEY_SIZE)
    hash_mac = hmac.new(mac_key, blist[16:4064], hashlib.sha1)
    hash_mac.update(b'\x01\x00\x00\x00')

    if hash_mac.digest() != first[-32:-12]:
        return False, f"[-] Key Error! (key:'{key}'; db_path:'{db_path}'; out_path:'{out_path}' )"

    with open(out_path, "wb") as deFile:
        deFile.write(SQLITE_FILE_HEADER.encode())
        for i in range(0, len(blist), 4096):
            tblist = blist[i:i + 4096] if i > 0 else blist[16:i + 4096]
            deFile.write(AES.new(byteHmac, AES.MODE_CBC, tblist[-48:-32]).decrypt(tblist[:-48]))
            deFile.write(tblist[-48:])

    return True, [db_path, out_path, key]

@wx_core_error
def batch_decrypt(key: str, db_path: Union[str, List[str]], out_path: str, is_print: bool = False):
    """
    批次解密資料庫
    :param key: 金鑰 64位16進位制字串
    :param db_path: 待解密的資料庫路徑(檔案或資料夾)
    :param out_path: 解密後的資料庫輸出路徑(資料夾)
    :param is_logging: 是否列印日誌
    :return: (bool, [[input_db_path, output_db_path, key],...])
    """
    if not isinstance(key, str) or not isinstance(out_path, str) or not os.path.exists(out_path) or len(key) != 64:
        error = f"[-] (key:'{key}' or out_path:'{out_path}') Error!"
        wx_core_loger.error(error, exc_info=True)
        return False, error

    process_list = []

    if isinstance(db_path, str):
        if not os.path.exists(db_path):
            error = f"[-] db_path:'{db_path}' not found!"
            wx_core_loger.error(error, exc_info=True)
            return False, error

        if os.path.isfile(db_path):
            inpath = db_path
            outpath = os.path.join(out_path, 'de_' + os.path.basename(db_path))
            process_list.append([key, inpath, outpath])

        elif os.path.isdir(db_path):
            for root, dirs, files in os.walk(db_path):
                for file in files:
                    inpath = os.path.join(root, file)
                    rel = os.path.relpath(root, db_path)
                    outpath = os.path.join(out_path, rel, 'de_' + file)

                    if not os.path.exists(os.path.dirname(outpath)):
                        os.makedirs(os.path.dirname(outpath))
                    process_list.append([key, inpath, outpath])
        else:
            error = f"[-] db_path:'{db_path}' Error "
            wx_core_loger.error(error, exc_info=True)
            return False, error

    elif isinstance(db_path, list):
        rt_path = os.path.commonprefix(db_path)
        if not os.path.exists(rt_path):
            rt_path = os.path.dirname(rt_path)

        for inpath in db_path:
            if not os.path.exists(inpath):
                error = f"[-] db_path:'{db_path}' not found!"
                wx_core_loger.error(error, exc_info=True)
                return False, error

            inpath = os.path.normpath(inpath)
            rel = os.path.relpath(os.path.dirname(inpath), rt_path)
            outpath = os.path.join(out_path, rel, 'de_' + os.path.basename(inpath))
            if not os.path.exists(os.path.dirname(outpath)):
                os.makedirs(os.path.dirname(outpath))
            process_list.append([key, inpath, outpath])
    else:
        error = f"[-] db_path:'{db_path}' Error "
        wx_core_loger.error(error, exc_info=True)
        return False, error

    result = []
    for i in process_list:
        result.append(decrypt(*i))  # 解密

    # 刪除空資料夾
    for root, dirs, files in os.walk(out_path, topdown=False):
        for dir in dirs:
            if not os.listdir(os.path.join(root, dir)):
                os.rmdir(os.path.join(root, dir))

    if is_print:
        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

相關文章