解锁并提取Linux客户端微信数据库 (vibe coded)
1# -*- coding: utf-8 -*-#
2# WeChat 4.x (Linux) database decryption.
3#
4# SQLCipher 4: AES-256-CBC, HMAC-SHA512, page=4096, reserve=80 (16 IV + 64 HMAC).
5# Keys are extracted as raw derived keys from process memory (no PBKDF2 needed).
6import hmac
7import hashlib
8import os
9from typing import Dict, Tuple
10from Cryptodome.Cipher import AES
11
12from .utils import wx_core_error, wx_core_loger
13
14SQLITE_FILE_HEADER = "SQLite format 3\x00"
15
16KEY_SIZE = 32
17DEFAULT_PAGESIZE = 4096
18HMAC_SIZE = 64 # SHA-512 digest
19RESERVE = 80 # 16 IV + 64 HMAC-SHA512
20
21
22@wx_core_error
23def decrypt(enc_key_hex: str, db_path: str, out_path: str):
24 """
25 使用从进程内存提取的 raw key 解密 SQLCipher 4 数据库。
26 :param enc_key_hex: 64 位十六进制字符串 (32 bytes, 已派生的 AES 密钥)
27 :param db_path: 待解密的数据库路径
28 :param out_path: 解密后的数据库输出路径
29 """
30 if not os.path.exists(db_path) or not os.path.isfile(db_path):
31 return False, f"[-] db_path:'{db_path}' File not found!"
32 if not os.path.exists(os.path.dirname(out_path)):
33 return False, f"[-] out_path:'{out_path}' File not found!"
34 if len(enc_key_hex) != 64:
35 return False, f"[-] enc_key_hex:'{enc_key_hex}' Len Error!"
36
37 enc_key = bytes.fromhex(enc_key_hex.strip())
38
39 try:
40 with open(db_path, "rb") as file:
41 blist = file.read()
42 except Exception as e:
43 return False, f"[-] db_path:'{db_path}' {e}!"
44
45 if len(blist) < DEFAULT_PAGESIZE:
46 return False, f"[-] db_path:'{db_path}' File too small!"
47
48 salt = blist[:16]
49 if len(salt) != 16:
50 return False, f"[-] db_path:'{db_path}' File Error!"
51
52 # Verify HMAC before decryption
53 # Page layout: [encrypted_data][IV 16 bytes][HMAC-SHA512 64 bytes]
54 # HMAC covers encrypted_data + IV (everything except the HMAC itself)
55 mac_salt = bytes([(salt[i] ^ 58) for i in range(16)])
56 mac_key = hashlib.pbkdf2_hmac("sha512", enc_key, mac_salt, 2, KEY_SIZE)
57
58 first_page_data = blist[16:DEFAULT_PAGESIZE]
59 h = hmac.new(mac_key, first_page_data[:-HMAC_SIZE], hashlib.sha512)
60 h.update(b'\x01\x00\x00\x00')
61 if h.digest() != first_page_data[-HMAC_SIZE:]:
62 return False, f"[-] Key Error! (key:'{enc_key_hex}'; db_path:'{db_path}')"
63
64 with open(out_path, "wb") as deFile:
65 deFile.write(SQLITE_FILE_HEADER.encode())
66 for i in range(0, len(blist), DEFAULT_PAGESIZE):
67 if i == 0:
68 page = blist[16:DEFAULT_PAGESIZE]
69 else:
70 page = blist[i:i + DEFAULT_PAGESIZE]
71 if len(page) < RESERVE:
72 deFile.write(page)
73 continue
74 # [encrypted_data][IV 16][HMAC 64]
75 iv = page[-(HMAC_SIZE + 16):-HMAC_SIZE]
76 encrypted = page[:-(HMAC_SIZE + 16)]
77 decrypted = AES.new(enc_key, AES.MODE_CBC, iv).decrypt(encrypted)
78 deFile.write(decrypted)
79 deFile.write(page[-(HMAC_SIZE + 16):])
80
81 return True, [db_path, out_path, enc_key_hex]
82
83
84@wx_core_error
85def batch_decrypt(db_keys: Dict[str, Tuple[str, str]], out_path: str, is_print: bool = False):
86 """
87 批量解密 SQLCipher 4 数据库。
88 :param db_keys: {db_path: (enc_key_hex, salt_hex)} 从 memscan.extract_all_keys() 获取
89 :param out_path: 输出目录
90 :param is_print: 是否打印日志
91 """
92 if not os.path.exists(out_path):
93 os.makedirs(out_path)
94
95 all_paths = list(db_keys.keys())
96 rt_path = os.path.commonprefix(all_paths)
97 if not os.path.isdir(rt_path):
98 rt_path = os.path.dirname(rt_path)
99
100 result = []
101 for db_path, (enc_key_hex, _salt_hex) in db_keys.items():
102 rel = os.path.relpath(os.path.dirname(db_path), rt_path)
103 outpath = os.path.join(out_path, rel, 'de_' + os.path.basename(db_path))
104 if not os.path.exists(os.path.dirname(outpath)):
105 os.makedirs(os.path.dirname(outpath))
106 result.append(decrypt(enc_key_hex, db_path, outpath))
107
108 # Clean empty dirs
109 for root, dirs, files in os.walk(out_path, topdown=False):
110 for d in dirs:
111 dirpath = os.path.join(root, d)
112 if not os.listdir(dirpath):
113 os.rmdir(dirpath)
114
115 if is_print:
116 print("=" * 32)
117 success_count = 0
118 fail_count = 0
119 for code, ret in result:
120 if not code:
121 print(ret)
122 fail_count += 1
123 else:
124 print(f'[+] "{ret[0]}" -> "{ret[1]}"')
125 success_count += 1
126 print("-" * 32)
127 print(f"[+] 共 {len(result)} 个文件, 成功 {success_count} 个, 失败 {fail_count} 个")
128 print("=" * 32)
129 return True, result