解锁并提取Linux客户端微信数据库 (vibe coded)
at 129 lines 4.7 kB view raw
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