@@ -0,0 +1,455 @@
"""
从企业微信(WXWork)进程内存中提取所有数据库的缓存raw key
企业微信使用 WCDB/SQLCipher 加密,但与个人微信参数不同:
- 个人微信: AES-256-CBC, 32字节密钥, HMAC-SHA512
- 企业微信: AES-128-CBC, 16字节密钥, 每页根据page index重新派生IV/key
密钥在内存中的缓存格式仍为 x ' <hex> ' ,但 key 为16字节(32 hex chars)
"""
import ctypes
import ctypes . wintypes as wt
import functools
import hashlib
import hmac as hmac_mod
import json
import os
import re
import struct
import subprocess
import sys
import time
from key_scan_common import collect_db_files
print = functools . partial ( print , flush = True )
# ── Windows 内存读取原语 ──────────────────────────────────────────────
kernel32 = ctypes . windll . kernel32
MEM_COMMIT = 0x1000
READABLE = { 0x02 , 0x04 , 0x08 , 0x10 , 0x20 , 0x40 , 0x80 }
class MBI ( ctypes . Structure ) :
_fields_ = [
( " BaseAddress " , ctypes . c_uint64 ) , ( " AllocationBase " , ctypes . c_uint64 ) ,
( " AllocationProtect " , wt . DWORD ) , ( " _pad1 " , wt . DWORD ) ,
( " RegionSize " , ctypes . c_uint64 ) , ( " State " , wt . DWORD ) ,
( " Protect " , wt . DWORD ) , ( " Type " , wt . DWORD ) , ( " _pad2 " , wt . DWORD ) ,
]
def read_mem ( h , addr , sz ) :
buf = ctypes . create_string_buffer ( sz )
n = ctypes . c_size_t ( 0 )
if kernel32 . ReadProcessMemory ( h , ctypes . c_uint64 ( addr ) , buf , sz , ctypes . byref ( n ) ) :
return buf . raw [ : n . value ]
return None
def enum_regions ( h ) :
regs = [ ]
addr = 0
mbi = MBI ( )
while addr < 0x7FFFFFFFFFFF :
if kernel32 . VirtualQueryEx ( h , ctypes . c_uint64 ( addr ) , ctypes . byref ( mbi ) , ctypes . sizeof ( mbi ) ) == 0 :
break
if mbi . State == MEM_COMMIT and mbi . Protect in READABLE and 0 < mbi . RegionSize < 500 * 1024 * 1024 :
regs . append ( ( mbi . BaseAddress , mbi . RegionSize ) )
nxt = mbi . BaseAddress + mbi . RegionSize
if nxt < = addr :
break
addr = nxt
return regs
# ── 常量 ─────────────────────────────────────────────────────────────
WXWORK_PROCESS = " WXWork.exe "
SQLITE_HEADER_HEX = b " SQLite format 3 \x00 " . hex ( )
PAGE_SZ = 4096
SALT_SZ = 16
# 企业微信可能的加密参数组合 (按可能性排序)
# (key_sz, hmac_hash_name, hmac_sz, pbkdf2_iter, reserve_sz)
VERIFY_CONFIGS = [
# WCDB optimized cipher with AES-128, HMAC-SHA512 (最可能)
( 16 , " sha512 " , 64 , 2 , 80 ) ,
# WCDB with AES-128, HMAC-SHA256
( 16 , " sha256 " , 32 , 2 , 48 ) ,
# SQLCipher 3 defaults with AES-128
( 16 , " sha512 " , 64 , 4000 , 80 ) ,
( 16 , " sha256 " , 32 , 4000 , 48 ) ,
# AES-256 回退 (与个人微信相同参数)
( 32 , " sha512 " , 64 , 2 , 80 ) ,
]
def verify_enc_key_wxwork ( enc_key , db_page1 ) :
""" 尝试多种参数组合验证密钥,返回 (成功?, 使用的配置描述) """
key_sz = len ( enc_key )
for cfg_key_sz , hmac_hash , hmac_sz , iterations , reserve_sz in VERIFY_CONFIGS :
if key_sz != cfg_key_sz :
continue
salt = db_page1 [ : SALT_SZ ]
mac_salt = bytes ( b ^ 0x3A for b in salt )
mac_key = hashlib . pbkdf2_hmac ( hmac_hash , enc_key , mac_salt , iterations , dklen = cfg_key_sz )
hmac_data = db_page1 [ SALT_SZ : PAGE_SZ - reserve_sz + 16 ]
stored_hmac = db_page1 [ PAGE_SZ - hmac_sz : PAGE_SZ ]
hash_fn = getattr ( hashlib , hmac_hash )
hm = hmac_mod . new ( mac_key , hmac_data , hash_fn )
hm . update ( struct . pack ( " <I " , 1 ) )
if hm . digest ( ) == stored_hmac :
desc = f " AES- { cfg_key_sz * 8 } , HMAC- { hmac_hash . upper ( ) } , iter= { iterations } "
return True , desc
return False , " "
# ── WXWork 进程发现 ──────────────────────────────────────────────────
def get_wxwork_pids ( ) :
""" 返回所有 WXWork.exe 进程的 (pid, mem_kb) 列表,按内存降序 """
r = subprocess . run (
[ " tasklist " , " /FI " , f " IMAGENAME eq { WXWORK_PROCESS } " , " /FO " , " CSV " , " /NH " ] ,
capture_output = True , text = True ,
)
pids = [ ]
for line in r . stdout . strip ( ) . split ( ' \n ' ) :
if not line . strip ( ) :
continue
p = line . strip ( ' " ' ) . split ( ' " , " ' )
if len ( p ) > = 5 :
pid = int ( p [ 1 ] )
mem = int ( p [ 4 ] . replace ( ' , ' , ' ' ) . replace ( ' K ' , ' ' ) . strip ( ) or ' 0 ' )
pids . append ( ( pid , mem ) )
if not pids :
raise RuntimeError ( f " { WXWORK_PROCESS } 未运行 " )
pids . sort ( key = lambda x : x [ 1 ] , reverse = True )
for pid , mem in pids :
print ( f " [+] { WXWORK_PROCESS } PID= { pid } ( { mem / / 1024 } MB) " )
return pids
# ── WXWork 数据目录自动检测 ──────────────────────────────────────────
def auto_detect_wxwork_db_dir ( ) :
""" 扫描 % USERPROFILE % \\ Documents \\ WXWork \\ * \\ Data 寻找包含加密DB的目录 """
docs = os . path . join ( os . environ . get ( " USERPROFILE " , " " ) , " Documents " , " WXWork " )
if not os . path . isdir ( docs ) :
return None
candidates = [ ]
for name in os . listdir ( docs ) :
data_dir = os . path . join ( docs , name , " Data " )
if not os . path . isdir ( data_dir ) :
continue
has_encrypted = False
for fname in os . listdir ( data_dir ) :
if not fname . endswith ( " .db " ) :
continue
fpath = os . path . join ( data_dir , fname )
if os . path . getsize ( fpath ) < PAGE_SZ :
continue
with open ( fpath , " rb " ) as f :
header = f . read ( 16 )
if header != b " SQLite format 3 \x00 " :
has_encrypted = True
break
if has_encrypted :
candidates . append ( data_dir )
if not candidates :
return None
if len ( candidates ) == 1 :
return candidates [ 0 ]
if not sys . stdin . isatty ( ) :
return candidates [ 0 ]
print ( " [!] 检测到多个企业微信数据目录: " )
for i , c in enumerate ( candidates , 1 ) :
print ( f " { i } . { c } " )
print ( " 0. 跳过,稍后手动配置 " )
try :
while True :
choice = input ( f " 请选择 [0- { len ( candidates ) } ]: " ) . strip ( )
if choice == " 0 " :
return None
if choice . isdigit ( ) and 1 < = int ( choice ) < = len ( candidates ) :
return candidates [ int ( choice ) - 1 ]
print ( " 无效输入,请重新选择 " )
except ( EOFError , KeyboardInterrupt ) :
print ( )
return None
def filter_encrypted_dbs ( db_files , salt_to_dbs ) :
""" 过滤掉未加密的数据库( salt 等于 SQLite header 的) """
filtered_files = [
entry for entry in db_files if entry [ 3 ] != SQLITE_HEADER_HEX
]
filtered_salts = {
s : dbs for s , dbs in salt_to_dbs . items ( ) if s != SQLITE_HEADER_HEX
}
removed = len ( db_files ) - len ( filtered_files )
if removed :
print ( f " [*] 跳过 { removed } 个未加密数据库 " )
return filtered_files , filtered_salts
# ── 企业微信内存扫描 ─────────────────────────────────────────────────
def scan_memory_for_wxwork_keys ( data , hex_re , db_files , salt_to_dbs , key_map ,
remaining_salts , base_addr , pid , print_fn ) :
""" 扫描内存,匹配 hex 模式并用企业微信参数验证密钥。
企业微信 key=16字节(32 hex), salt=16字节(32 hex)
可能的缓存格式:
- x ' <32hex_key><32hex_salt> ' = 64 hex total
- x ' <32hex_key> ' = 32 hex (key only)
- x ' <64hex_key><32hex_salt> ' = 96 hex (AES-256 回退)
"""
matches = 0
for m in hex_re . finditer ( data ) :
hex_str = m . group ( 1 ) . decode ( )
addr = base_addr + m . start ( )
matches + = 1
hex_len = len ( hex_str )
# 尝试不同的解释方式
candidates = [ ]
if hex_len == 32 :
# 纯 16字节 key
candidates . append ( ( hex_str , None ) )
elif hex_len == 64 :
# 优先: 32hex key + 32hex salt (WeCom AES-128)
candidates . append ( ( hex_str [ : 32 ] , hex_str [ 32 : ] ) )
# 回退: 64hex = 32字节 key (personal WeChat AES-256)
candidates . append ( ( hex_str , None ) )
elif hex_len == 96 :
# 优先: 64hex key + 32hex salt (personal WeChat AES-256)
candidates . append ( ( hex_str [ : 64 ] , hex_str [ 64 : ] ) )
# 也尝试: 32hex key + ... + 32hex salt
candidates . append ( ( hex_str [ : 32 ] , hex_str [ - 32 : ] ) )
elif hex_len > 96 and hex_len % 2 == 0 :
candidates . append ( ( hex_str [ : 64 ] , hex_str [ - 32 : ] ) )
candidates . append ( ( hex_str [ : 32 ] , hex_str [ - 32 : ] ) )
for enc_key_hex , salt_hex in candidates :
if len ( enc_key_hex ) not in ( 32 , 64 ) :
continue
enc_key = bytes . fromhex ( enc_key_hex )
if salt_hex and salt_hex in remaining_salts :
# salt 匹配已知数据库
for rel , path , sz , s , page1 in db_files :
if s == salt_hex :
ok , desc = verify_enc_key_wxwork ( enc_key , page1 )
if ok :
key_map [ salt_hex ] = enc_key_hex
remaining_salts . discard ( salt_hex )
dbs = salt_to_dbs [ salt_hex ]
print_fn ( f " \n [FOUND] salt= { salt_hex } " )
print_fn ( f " enc_key= { enc_key_hex } " )
print_fn ( f " params: { desc } " )
print_fn ( f " PID= { pid } 地址: 0x { addr : 016X } " )
print_fn ( f " 数据库: { ' , ' . join ( dbs ) } " )
break
elif not salt_hex and remaining_salts :
# 没有 salt, 暴力尝试所有未匹配的数据库
for rel , path , sz , salt_hex_db , page1 in db_files :
if salt_hex_db in remaining_salts :
ok , desc = verify_enc_key_wxwork ( enc_key , page1 )
if ok :
key_map [ salt_hex_db ] = enc_key_hex
remaining_salts . discard ( salt_hex_db )
dbs = salt_to_dbs [ salt_hex_db ]
print_fn ( f " \n [FOUND] salt= { salt_hex_db } " )
print_fn ( f " enc_key= { enc_key_hex } " )
print_fn ( f " params: { desc } " )
print_fn ( f " PID= { pid } 地址: 0x { addr : 016X } " )
print_fn ( f " 数据库: { ' , ' . join ( dbs ) } " )
break
if not remaining_salts :
break
return matches
def cross_verify_wxwork_keys ( db_files , salt_to_dbs , key_map , print_fn ) :
""" 用已找到的 key 交叉验证未匹配的 salt。 """
missing_salts = set ( salt_to_dbs . keys ( ) ) - set ( key_map . keys ( ) )
if not missing_salts or not key_map :
return
print_fn ( f " \n 还有 { len ( missing_salts ) } 个 salt 未匹配,尝试交叉验证... " )
for salt_hex in list ( missing_salts ) :
for rel , path , sz , s , page1 in db_files :
if s == salt_hex :
for known_salt , known_key_hex in key_map . items ( ) :
enc_key = bytes . fromhex ( known_key_hex )
ok , desc = verify_enc_key_wxwork ( enc_key , page1 )
if ok :
key_map [ salt_hex ] = known_key_hex
print_fn ( f " [CROSS] salt= { salt_hex } 可用 key from salt= { known_salt } " )
missing_salts . discard ( salt_hex )
break
def save_wxwork_results ( db_files , salt_to_dbs , key_map , db_dir , out_file , print_fn ) :
""" 输出扫描结果并保存 JSON。 """
print_fn ( f " \n { ' = ' * 60 } " )
print_fn ( f " 结果: { len ( key_map ) } / { len ( salt_to_dbs ) } salts 找到密钥 " )
result = { }
for rel , path , sz , salt_hex , page1 in db_files :
if salt_hex in key_map :
result [ rel ] = {
" enc_key " : key_map [ salt_hex ] ,
" salt " : salt_hex ,
" size_mb " : round ( sz / 1024 / 1024 , 1 )
}
print_fn ( f " OK: { rel } ( { sz / 1024 / 1024 : .1f } MB) " )
else :
print_fn ( f " MISSING: { rel } (salt= { salt_hex } ) " )
if not result :
print_fn ( f " \n [!] 未提取到任何密钥,保留已有的 { out_file } (如存在) " )
raise RuntimeError ( " 未能从任何企业微信进程中提取到密钥 " )
result [ " _db_dir " ] = db_dir
with open ( out_file , ' w ' , encoding = ' utf-8 ' ) as f :
json . dump ( result , f , indent = 2 , ensure_ascii = False )
print_fn ( f " \n 密钥保存到: { out_file } " )
missing = [ rel for rel , path , sz , salt_hex , page1 in db_files if salt_hex not in key_map ]
if missing :
print_fn ( f " \n 未找到密钥的数据库: " )
for rel in missing :
print_fn ( f " { rel } " )
# ── 配置加载 ─────────────────────────────────────────────────────────
def _load_wxwork_config ( ) :
""" 从 config.json 加载企业微信配置,必要时自动检测 """
from config import _config_file_path , _app_base_dir
config_file = _config_file_path ( )
cfg = { }
if os . path . exists ( config_file ) :
try :
with open ( config_file , encoding = " utf-8 " ) as f :
cfg = json . load ( f )
except json . JSONDecodeError :
cfg = { }
db_dir = cfg . get ( " wxwork_db_dir " , " " )
if not db_dir or not os . path . isdir ( db_dir ) :
detected = auto_detect_wxwork_db_dir ( )
if detected :
print ( f " [+] 自动检测到企业微信数据目录: { detected } " )
cfg [ " wxwork_db_dir " ] = detected
with open ( config_file , " w " , encoding = " utf-8 " ) as f :
json . dump ( cfg , f , indent = 4 , ensure_ascii = False )
print ( f " [+] 已保存到: { config_file } " )
db_dir = detected
else :
print ( " [!] 未能自动检测企业微信数据目录 " )
print ( f " 请在 { config_file } 中设置 wxwork_db_dir 字段 " )
print ( " 路径格式: C: \\ Users \\ <用户> \\ Documents \\ WXWork \\ <account_id> \\ Data " )
sys . exit ( 1 )
keys_file = cfg . get ( " wxwork_keys_file " , " wxwork_keys.json " )
base = _app_base_dir ( )
if not os . path . isabs ( keys_file ) :
keys_file = os . path . join ( base , keys_file )
return { " wxwork_db_dir " : db_dir , " wxwork_keys_file " : keys_file }
# ── 主流程 ───────────────────────────────────────────────────────────
def main ( ) :
cfg = _load_wxwork_config ( )
db_dir = cfg [ " wxwork_db_dir " ]
out_file = cfg [ " wxwork_keys_file " ]
print ( " = " * 60 )
print ( " 提取所有企业微信数据库密钥 " )
print ( " = " * 60 )
# 1. 收集所有DB文件及其salt
db_files , salt_to_dbs = collect_db_files ( db_dir )
db_files , salt_to_dbs = filter_encrypted_dbs ( db_files , salt_to_dbs )
print ( f " \n 找到 { len ( db_files ) } 个加密数据库, { len ( salt_to_dbs ) } 个不同的salt " )
for salt_hex , dbs in sorted ( salt_to_dbs . items ( ) , key = lambda x : len ( x [ 1 ] ) , reverse = True ) :
print ( f " salt { salt_hex } : { ' , ' . join ( dbs ) } " )
# 2. 打开所有企业微信进程
pids = get_wxwork_pids ( )
# 宽松正则:匹配 32+ hex chars (16字节key起)
hex_re = re . compile ( b " x ' ([0-9a-fA-F] { 32,192}) ' " )
key_map = { }
remaining_salts = set ( salt_to_dbs . keys ( ) )
all_hex_matches = 0
t0 = time . time ( )
for pid , mem_kb in pids :
h = kernel32 . OpenProcess ( 0x0010 | 0x0400 , False , pid )
if not h :
print ( f " [WARN] 无法打开进程 PID= { pid } ,跳过 " )
continue
try :
regions = enum_regions ( h )
total_bytes = sum ( s for _ , s in regions )
total_mb = total_bytes / 1024 / 1024
print ( f " \n [*] 扫描 PID= { pid } ( { 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
all_hex_matches + = scan_memory_for_wxwork_keys (
data , hex_re , db_files , salt_to_dbs ,
key_map , remaining_salts , base , pid , 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 )
if not remaining_salts :
print ( f " \n [+] 所有密钥已找到,跳过剩余进程 " )
break
elapsed = time . time ( ) - t0
print ( f " \n 扫描完成: { elapsed : .1f } s, { len ( pids ) } 个进程, { all_hex_matches } hex模式 " )
cross_verify_wxwork_keys ( db_files , salt_to_dbs , key_map , print )
save_wxwork_results ( db_files , salt_to_dbs , key_map , db_dir , out_file , print )
if __name__ == ' __main__ ' :
try :
main ( )
except RuntimeError as e :
print ( f " \n [ERROR] { e } " )
sys . exit ( 1 )