Files
zWorkFlow/find_all_keys.py
H3CoF6 e8de1249a4 feat: 离线计算图片密钥 (#69)
* feat: 离线计算图片密钥

* fix(find_all_keys): address review feedback on #69

Apply 5 fixes per @ylytdeng's review:

- find_xor_key: return None when last-byte ^ 0xD9 doesn't match the
  first-byte-derived xor_key (was returning xor_key in both branches,
  so the validation was a no-op)
- multiprocessing cleanup: split single-line terminate, add
  p.join(timeout=1) loop to avoid orphan workers
- replace 3 bare `except:` with `except Exception:` so KeyboardInterrupt
  can break the brute-force loop
- add actionable hint ("请先在微信中查看 2-3 张图片") when xor_key or
  ciphertext can't be derived from attach_dir
- drop try/except ImportError fallback on `from Crypto.Cipher import AES`
  (and the now-dead `if not AES` guards); pycryptodome is already a hard
  dependency elsewhere in the project

Original algorithm and multiprocessing implementation by @H3CoF6 in #69.
Review by @ylytdeng: https://github.com/ylytdeng/wechat-decrypt/pull/69

Co-authored-by: H3CoF6 <190114211+H3CoF6@users.noreply.github.com>

---------

Co-authored-by: Belugary <53219544+Belugary@users.noreply.github.com>
Co-authored-by: H3CoF6 <190114211+H3CoF6@users.noreply.github.com>
2026-05-05 22:46:48 +08:00

220 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import functools
import platform
import sys
import os
import glob
import json
import hashlib
import multiprocessing
import time
from config import load_config
from Crypto.Cipher import AES
def find_v2_ciphertext(attach_dir):
v2_magic = b'\x07\x08V2\x08\x07'
pattern = os.path.join(attach_dir, "*", "*", "Img", "*_t.dat")
dat_files = sorted(glob.glob(pattern), key=os.path.getmtime, reverse=True)
for f in dat_files[:100]:
try:
with open(f, 'rb') as fp:
header = fp.read(31)
if header[:6] == v2_magic and len(header) >= 31:
return header[15:31], os.path.basename(f)
except Exception:
continue
return None, None
def find_xor_key(attach_dir):
v2_magic = b'\x07\x08V2\x08\x07'
pattern = os.path.join(attach_dir, "*", "*", "Img", "*_t.dat")
dat_files = sorted(glob.glob(pattern), key=os.path.getmtime, reverse=True)
tail_counts = {}
for f in dat_files[:32]:
try:
sz = os.path.getsize(f)
with open(f, 'rb') as fp:
head = fp.read(6)
fp.seek(sz - 2)
tail = fp.read(2)
if head == v2_magic and len(tail) == 2:
key = (tail[0], tail[1])
tail_counts[key] = tail_counts.get(key, 0) + 1
except Exception:
continue
if not tail_counts:
return None
most_common = max(tail_counts, key=tail_counts.get)
x, y = most_common
xor_key = x ^ 0xFF
if (y ^ 0xD9) == xor_key:
return xor_key
return None
def try_key(key_bytes, ciphertext):
try:
cipher = AES.new(key_bytes, AES.MODE_ECB)
dec = cipher.decrypt(ciphertext)
if dec[:3] == b'\xFF\xD8\xFF': return 'JPEG'
if dec[:4] == b'\x89PNG': return 'PNG'
if dec[:4] == b'RIFF': return 'WEBP'
if dec[:4] == b'wxgf': return 'WXGF'
if dec[:3] == b'GIF': return 'GIF'
except Exception:
pass
return None
def _brute_worker(start_i, end_i, xor_key, bin_suffix, base_wxid_bytes, ciphertext_16, result_queue):
for i in range(start_i, end_i):
uin = (i << 8) | xor_key
uin_bytes = str(uin).encode('ascii')
if hashlib.md5(uin_bytes).digest()[:2] == bin_suffix:
h_aes = hashlib.md5(uin_bytes + base_wxid_bytes).hexdigest()
aes_key_16 = h_aes[:16].encode('ascii')
if try_key(aes_key_16, ciphertext_16):
result_queue.put((uin, aes_key_16.decode('ascii')))
return
def find_image_key_offline(cfg):
print("\n" + "=" * 60)
print(" 尝试提取图片 AES 密钥")
print("=" * 60)
db_dir = cfg.get("db_dir", "")
if not db_dir:
print("未配置 db_dir")
return
base_dir = os.path.dirname(db_dir)
attach_dir = os.path.join(base_dir, 'msg', 'attach')
folder = os.path.basename(base_dir)
base_wxid, suffix = "", ""
if '_' in folder:
parts = folder.rsplit('_', 1)
if len(parts) == 2 and len(parts[1]) == 4:
base_wxid, suffix = parts
if not base_wxid or not suffix:
print(f"[!] 目录名不符合 wxid_..._suffix 格式: {folder},跳过爆破")
return
print(f"[*] 解析到 wxid={base_wxid}, suffix={suffix}")
xor_key = find_xor_key(attach_dir)
if xor_key is None:
print("[!] 找不到足够的 _t.dat 文件推导 XOR key跳过爆破")
print(" 请先在微信中查看 2-3 张图片,让缩略图缓存到本地后再重试。")
return
print(f"[*] 找到 XOR key: 0x{xor_key:02x}")
ciphertext, ct_file = find_v2_ciphertext(attach_dir)
if not ciphertext:
print("[!] 找不到 V2 加密的图片文件,跳过爆破")
print(" 请先在微信中查看 2-3 张图片,让缩略图缓存到本地后再重试。")
return
print(f"[*] 启动多进程 UIN 空间爆破...")
t0 = time.time()
bin_suffix = bytes.fromhex(suffix)
base_wxid_bytes = base_wxid.encode('ascii')
cpu_count = multiprocessing.cpu_count()
total = 1 << 24
chunk = total // cpu_count
result_queue = multiprocessing.Queue()
processes = []
for i in range(cpu_count):
start, end = i * chunk, (i + 1) * chunk if i != cpu_count - 1 else total
p = multiprocessing.Process(
target=_brute_worker,
args=(start, end, xor_key, bin_suffix, base_wxid_bytes, ciphertext, result_queue)
)
p.start()
processes.append(p)
found = None
try:
while any(p.is_alive() for p in processes):
if not result_queue.empty():
found = result_queue.get()
break
time.sleep(0.1)
finally:
for p in processes:
p.terminate()
for p in processes:
p.join(timeout=1)
elapsed = time.time() - t0
if found:
print(f"[+] 爆破成功! UIN={found[0]}, 耗时={elapsed:.1f}s")
aes_key = found[1]
print(f" image_aes_key = {aes_key}")
cfg['image_aes_key'] = aes_key
cfg['image_xor_key'] = xor_key
config_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.json")
with open(config_file, 'w', encoding='utf-8') as f:
json.dump(cfg, f, indent=4, ensure_ascii=False)
print(f"[+] 已保存到 config.json")
else:
print(f"[-] 未能在 UIN 空间找到有效密钥 (耗时={elapsed:.1f}s)")
print(" 可能原因: 目录名被重命名过,或者不是标准账号目录。")
@functools.lru_cache(maxsize=1)
def _load_impl():
system = platform.system().lower()
if system == "windows":
import find_all_keys_windows as impl
return impl
if system == "linux":
import find_all_keys_linux as impl
return impl
if system == "darwin":
raise RuntimeError(
"macOS 请先运行 C 版扫描器提取数据库密钥:\n"
"\n"
" sudo ./find_all_keys_macos\n"
"\n"
" 完成后再运行 python main.py decrypt"
)
raise RuntimeError(
f"当前平台暂不支持通过 find_all_keys.py 提取内存数据库密钥: {platform.system()}"
)
def get_pids():
return _load_impl().get_pids()
def main():
cfg = load_config()
find_image_key_offline(cfg)
return _load_impl().main()
if __name__ == "__main__":
multiprocessing.freeze_support()
try:
main()
except RuntimeError as exc:
print(f"\n[ERROR] {exc}")
sys.exit(1)