From c3cbe6b1a68140cdbb977539c0a679167a8155ed Mon Sep 17 00:00:00 2001 From: maomao3334 <15202965922@163.com> Date: Sat, 29 Aug 2026 13:29:34 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=BE=AE=E4=BF=A1=204.1.?= =?UTF-8?q?x=20=E7=89=88=E6=9C=AC=E5=AF=86=E9=92=A5=E6=8F=90=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新 verify_enc_key 函数支持 HMAC-SHA1(新版微信 3.9+/4.1.x) - 保持向后兼容 HMAC-SHA512(旧版微信) - 基于 wcdb-key-tool 运行时扫描方法 - 修复新版微信密钥提取失败问题 Co-Authored-By: Claude Opus 5 --- wechat_cli/keys/common.py | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/wechat_cli/keys/common.py b/wechat_cli/keys/common.py index c281b67..3c5921d 100644 --- a/wechat_cli/keys/common.py +++ b/wechat_cli/keys/common.py @@ -17,15 +17,35 @@ def verify_enc_key(enc_key, db_page1): - """通过 HMAC-SHA512 校验 page 1 验证 enc_key 是否正确。""" + """通过 HMAC-SHA1 (新版) 或 HMAC-SHA512 (旧版) 校验 page 1 验证 enc_key 是否正确。 + + 微信 3.9+ 使用 HMAC-SHA1,更早版本使用 HMAC-SHA512。 + """ salt = db_page1[:SALT_SZ] mac_salt = bytes(b ^ 0x3A for b in salt) - mac_key = hashlib.pbkdf2_hmac("sha512", enc_key, mac_salt, 2, dklen=KEY_SZ) - hmac_data = db_page1[SALT_SZ: PAGE_SZ - 80 + 16] - stored_hmac = db_page1[PAGE_SZ - 64: PAGE_SZ] - hm = hmac_mod.new(mac_key, hmac_data, hashlib.sha512) - hm.update(struct.pack(" Date: Sat, 29 Aug 2026 13:58:31 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E9=9B=86=E6=88=90=20Windows=204.1+=20Confi?= =?UTF-8?q?g.Cipher=20=E5=AF=86=E9=92=A5=E6=89=AB=E6=8F=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 Config.Cipher 运行时扫描逻辑 - 使用 XOR 解码从内存中提取加密密钥 - 优先使用 Config.Cipher 扫描,回退到传统方法 - 基于 wcdb-key-tool 的实现 Co-Authored-By: Claude Opus 5 --- wechat_cli/keys/scanner_windows.py | 274 +++++++++++++++++++++++++++-- 1 file changed, 255 insertions(+), 19 deletions(-) diff --git a/wechat_cli/keys/scanner_windows.py b/wechat_cli/keys/scanner_windows.py index 04fbf3c..04166f9 100644 --- a/wechat_cli/keys/scanner_windows.py +++ b/wechat_cli/keys/scanner_windows.py @@ -8,7 +8,7 @@ import subprocess import time -from .common import collect_db_files, scan_memory_for_keys, cross_verify_keys, save_results +from .common import collect_db_files, scan_memory_for_keys, cross_verify_keys, save_results, KEY_SZ, verify_enc_key print = functools.partial(print, flush=True) @@ -16,6 +16,16 @@ MEM_COMMIT = 0x1000 READABLE = {0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80} +# Windows 4.1+ Config.Cipher 扫描常量 +WINDOWS_CONFIG_CIPHER_NAME = b"com.Tencent.WCDB.Config.Cipher" +WINDOWS_CONFIG_XOR_MASK = bytes.fromhex( + "d2c7442458020000004889442450488b" + "450048844c2448488944254048584c24" +) +WINDOWS_MAX_USER_ADDRESS = 0x0000_8000_0000_0000 +WINDOWS_CONFIG_BLOB_MAX = 1024 +WINDOWS_CONFIG_LITERAL_RE = re.compile(rb"[xX]'([0-9a-fA-F]{64,192})'") + class MBI(ctypes.Structure): _fields_ = [ @@ -71,6 +81,218 @@ def _enum_regions(h): return regs +def _xor_repeat(data, mask): + """XOR 解码:用 mask 循环异或 data""" + return bytes(value ^ mask[index % len(mask)] for index, value in enumerate(data)) + + +def _u64_from(data, offset): + """从 data 的 offset 位置读取小端 uint64""" + if offset < 0 or offset + 8 > len(data): + return 0 + return struct.unpack_from("= 15 and data not in {b"\x00" * KEY_SZ, b"\xff" * KEY_SZ} + + +def _find_bytes_in_regions(regions, read_fn, needle): + """在内存区域中查找字节串,返回所有匹配地址""" + addresses = set() + overlap = max(0, len(needle) - 1) + for base, size in regions: + offset = 0 + tail = b"" + tail_base = base + chunk_size = 2 * 1024 * 1024 + + while offset < size: + current_size = min(chunk_size, size - offset) + chunk = read_fn(base + offset, current_size) or b"" + data_base = tail_base if tail else base + offset + data = tail + chunk + + if data: + pos = data.find(needle) + while pos >= 0: + addresses.add(data_base + pos) + pos = data.find(needle, pos + 1) + + if overlap: + tail = data[-overlap:] + tail_base = data_base + max(0, len(data) - len(tail)) + else: + tail = b"" + tail_base = base + offset + current_size + else: + tail = b"" + tail_base = base + offset + current_size + offset += current_size + + return addresses + + +def _windows_v411_config_key_candidates(blob): + """从 Config.Cipher blob 中提取密钥候选""" + if not blob or len(blob) > WINDOWS_CONFIG_BLOB_MAX: + return [] + decoded = _xor_repeat(blob, WINDOWS_CONFIG_XOR_MASK) + out = [] + seen = set() + + for match in WINDOWS_CONFIG_LITERAL_RE.finditer(decoded): + run = match.group(1).decode("ascii").lower() + starts = [0] + if len(run) > 96: + starts.extend(range(0, len(run) - 63, 32)) + starts.append(len(run) - 64) + + for start in dict.fromkeys(starts): + if start < 0 or start + 64 > len(run): + continue + enc_key_hex = run[start:start + 64] + try: + enc_key = bytes.fromhex(enc_key_hex) + except ValueError: + continue + if not _probable_32_byte_key(enc_key): + continue + + embedded_salt = None + if start + 96 <= len(run): + embedded_salt = run[start + 64:start + 96] + + item = (enc_key_hex, embedded_salt) + if item not in seen: + seen.add(item) + out.append(item) + + return out + + +def _verify_direct_key_candidate(enc_key_hex, embedded_salt, db_files, salt_to_dbs, key_map, remaining_salts): + """验证单个密钥候选""" + if not remaining_salts: + return 0 + try: + enc_key = bytes.fromhex(enc_key_hex) + except ValueError: + return 0 + if not _probable_32_byte_key(enc_key): + return 0 + + matched = 0 + target_salts = [embedded_salt] if embedded_salt in remaining_salts else list(remaining_salts) + + for salt_hex in target_salts: + if salt_hex not in remaining_salts: + continue + for _rel, _path, _sz, s, page1 in db_files: + if s == salt_hex and verify_enc_key(enc_key, page1): + key_map[salt_hex] = enc_key_hex + remaining_salts.discard(salt_hex) + matched += 1 + break + + return matched + + +def _scan_config_cipher(pid, h, regions, db_files, salt_to_dbs, key_map, remaining_salts): + """Windows 4.1+ Config.Cipher 扫描""" + if not remaining_salts: + return 0 + + # 查找 Config.Cipher 名称字符串 + needle_addresses = _find_bytes_in_regions(regions, lambda addr, sz: _read_mem(h, addr, sz), WINDOWS_CONFIG_CIPHER_NAME) + if not needle_addresses: + return 0 + + print(f"[*] Config.Cipher 扫描: 找到 {len(needle_addresses)} 个名称匹配") + + # 构建地址+长度对的字节模式 + pair_patterns = [ + struct.pack("= 0: + qaddr = base + pos + node_base = qaddr - 0x10 + node = _read_mem(h, node_base, 0x50) + if not node or len(node) < 0x40: + pos = data.find(pattern, pos + 1) + continue + + if _u64_from(node, 0x10) not in needle_addresses or _u64_from(node, 0x18) != len(WINDOWS_CONFIG_CIPHER_NAME): + pos = data.find(pattern, pos + 1) + continue + + config_ptr = _u64_from(node, 0x28) + if not (0x10000 <= config_ptr < WINDOWS_MAX_USER_ADDRESS): + pos = data.find(pattern, pos + 1) + continue + + seen_config_ptrs.add(config_ptr) + + # 读取 Config 对象 + obj = _read_mem(h, config_ptr + 0x88, 0x28) + if not obj or len(obj) < 0x18: + pos = data.find(pattern, pos + 1) + continue + + data_ptr = _u64_from(obj, 0x8) + data_len = _u64_from(obj, 0x10) + if not (0 < data_len <= WINDOWS_CONFIG_BLOB_MAX and 0x10000 <= data_ptr < WINDOWS_MAX_USER_ADDRESS): + pos = data.find(pattern, pos + 1) + continue + + blob = _read_mem(h, data_ptr, int(data_len)) + if not blob or len(blob) != data_len: + pos = data.find(pattern, pos + 1) + continue + + # 提取并验证密钥候选 + for enc_key_hex, embedded_salt in _windows_v411_config_key_candidates(blob): + candidate = (enc_key_hex, embedded_salt) + if candidate in seen_candidates: + continue + seen_candidates.add(candidate) + candidate_count += 1 + + matched = _verify_direct_key_candidate( + enc_key_hex, embedded_salt, db_files, salt_to_dbs, + key_map, remaining_salts + ) + if matched: + matched_count += matched + + pos = data.find(pattern, pos + 1) + + if matched_count: + print(f"[+] Config.Cipher 扫描匹配 {matched_count}/{len(salt_to_dbs)} salts (候选={candidate_count})") + + return matched_count + + + def extract_keys(db_dir, output_path, pid=None): """提取 Windows 微信数据库密钥。 @@ -100,7 +322,11 @@ def extract_keys(db_dir, output_path, pid=None): all_hex_matches = 0 t0 = time.time() + # 优先尝试 Config.Cipher 扫描(Windows 4.1+) for pid_val, mem_kb in pids: + if not remaining_salts: + break + h = kernel32.OpenProcess(0x0010 | 0x0400, False, pid_val) if not h: print(f"[WARN] 无法打开进程 PID={pid_val},跳过") @@ -110,27 +336,37 @@ def extract_keys(db_dir, output_path, pid=None): regions = _enum_regions(h) total_bytes = sum(s for _, s in regions) total_mb = total_bytes / 1024 / 1024 - print(f"\n[*] 扫描 PID={pid_val} ({total_mb:.0f}MB, {len(regions)} 区域)") + print(f"\n[*] PID={pid_val} ({total_mb:.0f}MB, {len(regions)} 区域)") - scanned_bytes = 0 - for reg_idx, (base, size) in enumerate(regions): - data = _read_mem(h, base, size) - scanned_bytes += size - if not data: - continue + # 先尝试 Config.Cipher 扫描 + matched = _scan_config_cipher(pid_val, h, regions, db_files, salt_to_dbs, key_map, remaining_salts) + if matched and not remaining_salts: + print(f"[+] Config.Cipher 扫描已覆盖所有数据库") + kernel32.CloseHandle(h) + break - all_hex_matches += scan_memory_for_keys( - data, hex_re, db_files, salt_to_dbs, - key_map, remaining_salts, base, pid_val, print, - ) - - if (reg_idx + 1) % 200 == 0: - elapsed = time.time() - t0 - progress = scanned_bytes / total_bytes * 100 if total_bytes else 100 - print( - f" [{progress:.1f}%] {len(key_map)}/{len(salt_to_dbs)} salts matched, " - f"{all_hex_matches} hex patterns, {elapsed:.1f}s" + # 如果还有剩余,回退到传统扫描 + if remaining_salts: + print(f"[*] 传统内存扫描 (剩余 {len(remaining_salts)} salts)") + scanned_bytes = 0 + for reg_idx, (base, size) in enumerate(regions): + data = _read_mem(h, base, size) + scanned_bytes += size + if not data: + continue + + all_hex_matches += scan_memory_for_keys( + data, hex_re, db_files, salt_to_dbs, + key_map, remaining_salts, base, pid_val, print, ) + + if (reg_idx + 1) % 200 == 0: + elapsed = time.time() - t0 + progress = scanned_bytes / total_bytes * 100 if total_bytes else 100 + print( + f" [{progress:.1f}%] {len(key_map)}/{len(salt_to_dbs)} salts matched, " + f"{all_hex_matches} hex patterns, {elapsed:.1f}s" + ) finally: kernel32.CloseHandle(h) From acfcd609c1d2fb75c67a0f13339eb3eb1810d55e Mon Sep 17 00:00:00 2001 From: maomao3334 <15202965922@163.com> Date: Sat, 29 Aug 2026 14:00:43 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20struct=20=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E5=AF=BC=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 --- wechat_cli/keys/scanner_windows.py | 1 + 1 file changed, 1 insertion(+) diff --git a/wechat_cli/keys/scanner_windows.py b/wechat_cli/keys/scanner_windows.py index 04166f9..f1aa670 100644 --- a/wechat_cli/keys/scanner_windows.py +++ b/wechat_cli/keys/scanner_windows.py @@ -5,6 +5,7 @@ import functools import os import re +import struct import subprocess import time