""" 企业微信 (WXWork) 进程内存密钥提取模块 通过 Windows API 读取 WXWork.exe 进程内存, 扫描并提取 wxSQLite3 AES-128 原始密钥 策略: 1. 找到所有 WXWork 相关进程 2. 枚举可读内存区域 3. 搜索已知模式 (cipher 结构体特征 / "sAlT" / "aes128" 字符串) 4. 从匹配位置附近提取 16 字节候选密钥 5. 用数据库第 1 页验证候选密钥 """ import ctypes import ctypes.wintypes as wintypes import hashlib import os import struct import sys import time # ---- Windows API 常量 ---- PROCESS_VM_READ = 0x0010 PROCESS_QUERY_INFORMATION = 0x0400 TH32CS_SNAPPROCESS = 0x00000002 MEM_COMMIT = 0x1000 PAGE_READWRITE = 0x04 PAGE_READONLY = 0x02 PAGE_EXECUTE_READWRITE = 0x40 PAGE_EXECUTE_READ = 0x20 READABLE_PROTECTIONS = { PAGE_READWRITE, PAGE_READONLY, PAGE_EXECUTE_READWRITE, PAGE_EXECUTE_READ, } class MEMORY_BASIC_INFORMATION(ctypes.Structure): _fields_ = [ ("BaseAddress", ctypes.c_void_p), ("AllocationBase", ctypes.c_void_p), ("AllocationProtect", wintypes.DWORD), ("PartitionId", wintypes.WORD), ("RegionSize", ctypes.c_size_t), ("State", wintypes.DWORD), ("Protect", wintypes.DWORD), ("Type", wintypes.DWORD), ] class PROCESSENTRY32(ctypes.Structure): _fields_ = [ ("dwSize", wintypes.DWORD), ("cntUsage", wintypes.DWORD), ("th32ProcessID", wintypes.DWORD), ("th32DefaultHeapID", ctypes.POINTER(ctypes.c_ulong)), ("th32ModuleID", wintypes.DWORD), ("cntThreads", wintypes.DWORD), ("th32ParentProcessID", wintypes.DWORD), ("pcPriClassBase", ctypes.c_long), ("dwFlags", wintypes.DWORD), ("szExeFile", ctypes.c_char * 260), ] def get_wxwork_pids(): """查找所有 WXWork 相关进程 ID""" pids = [] kernel32 = ctypes.WinDLL('kernel32', use_last_error=True) CreateToolhelp32Snapshot = kernel32.CreateToolhelp32Snapshot CreateToolhelp32Snapshot.argtypes = [wintypes.DWORD, wintypes.DWORD] CreateToolhelp32Snapshot.restype = wintypes.HANDLE Process32First = kernel32.Process32First Process32First.argtypes = [wintypes.HANDLE, ctypes.POINTER(PROCESSENTRY32)] Process32First.restype = wintypes.BOOL Process32Next = kernel32.Process32Next Process32Next.argtypes = [wintypes.HANDLE, ctypes.POINTER(PROCESSENTRY32)] Process32Next.restype = wintypes.BOOL CloseHandle = kernel32.CloseHandle CloseHandle.argtypes = [wintypes.HANDLE] snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) if snapshot == -1: return pids pe = PROCESSENTRY32() pe.dwSize = ctypes.sizeof(PROCESSENTRY32) if Process32First(snapshot, ctypes.byref(pe)): while True: exe_name = pe.szExeFile.lower() if b'wxwork' in exe_name or b'wework' in exe_name: pids.append(pe.th32ProcessID) if not Process32Next(snapshot, ctypes.byref(pe)): break CloseHandle(snapshot) return pids def read_process_memory_regions(pid, max_total=512 * 1024 * 1024): """读取进程的可读内存区域, 返回 [(address, data), ...]""" kernel32 = ctypes.WinDLL('kernel32', use_last_error=True) OpenProcess = kernel32.OpenProcess OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD] OpenProcess.restype = wintypes.HANDLE VirtualQueryEx = kernel32.VirtualQueryEx VirtualQueryEx.argtypes = [wintypes.HANDLE, ctypes.c_void_p, ctypes.POINTER(MEMORY_BASIC_INFORMATION), ctypes.c_size_t] VirtualQueryEx.restype = ctypes.c_size_t ReadProcessMemory = kernel32.ReadProcessMemory ReadProcessMemory.argtypes = [wintypes.HANDLE, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t)] ReadProcessMemory.restype = wintypes.BOOL CloseHandle = kernel32.CloseHandle CloseHandle.argtypes = [wintypes.HANDLE] h_process = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, False, pid) if not h_process: return [] regions = [] total_read = 0 addr = 0 mbi = MEMORY_BASIC_INFORMATION() try: while addr < 0x7FFFFFFF0000: # 用户空间上限 if VirtualQueryEx(h_process, ctypes.c_void_p(addr), ctypes.byref(mbi), ctypes.sizeof(mbi)) == 0: break # c_void_p 值为 0 时在 Python 中解析为 None, 需转成整数 base_addr = mbi.BaseAddress if base_addr is None: base_addr = 0 base_addr = int(base_addr) size = mbi.RegionSize if size > 0 and mbi.State == MEM_COMMIT and mbi.Protect in READABLE_PROTECTIONS: # 限制单块读取大小 read_size = min(size, 64 * 1024 * 1024) buffer = ctypes.create_string_buffer(read_size) bytes_read = ctypes.c_size_t(0) if ReadProcessMemory(h_process, ctypes.c_void_p(base_addr), buffer, read_size, ctypes.byref(bytes_read)): data = buffer.raw[:bytes_read.value] if len(data) > 0: regions.append((base_addr, data)) total_read += len(data) if total_read >= max_total: break addr = base_addr + size if addr <= 0: break finally: CloseHandle(h_process) return regions def find_key_candidates_in_data(data, base_addr=0): """ 在内存数据中搜索 wxSQLite3 cipher 结构体特征, 提取候选密钥 返回候选密钥列表 [(key_bytes, address, reason), ...] """ candidates = [] # 模式 1: 搜索 "sAlT" 字符串, 其前面的 20 字节可能是 [key(16) + page_no(4)] salt_offset = 0 while True: idx = data.find(b'sAlT', salt_offset) if idx == -1: break # 检查前 20 字节: 可能是 raw_key + page_no(LE) if idx >= 20: before = data[idx - 20:idx] key_candidate = before[:16] page_no_bytes = before[16:20] # page_no 应该是一个小整数 (1, 2, 3, ...) page_no = struct.unpack("= 16: key_candidate = data[idx - 16:idx] candidates.append((key_candidate, base_addr + idx - 16, "sAlT前16字节")) salt_offset = idx + 4 # 模式 2: 搜索 "aes128cbc" / "aes128" / "AES-128" 字符串 for pattern in [b'aes128cbc', b'aes-128-cbc', b'AES-128-CBC', b'aes128', b'wxSQLite3', b'wxsqlite3']: pat_offset = 0 while True: idx = data.find(pattern, pat_offset) if idx == -1: break # 在模式附近搜索 16 字节密钥 start = max(0, idx - 256) end = min(len(data), idx + len(pattern) + 256) nearby = data[start:end] # 提取所有可能的 16 字节候选 (步进 4 对齐以减少噪音) for i in range(0, len(nearby) - 16, 4): cand = nearby[i:i + 16] # 过滤: 密钥不应包含大量 0x00 zero_ratio = cand.count(0) / 16 if zero_ratio < 0.5: candidates.append((cand, base_addr + start + i, f"{pattern}附近")) pat_offset = idx + len(pattern) # 模式 3: 搜索 16 字节长度标记 + 指针结构 # wxSQLite3 cipher 结构通常包含: int nKey = 16; void *zKey = ptr; void *db = ptr; # 搜索 "10000000" (LE uint32 16) 后面跟指针, 指针指向的地址可能存有密钥 # 这个模式比较复杂, 暂时用简单方式: 搜索 16 字节序列中连续出现 0x10 0x00 0x00 0x00 idx16 = 0 while True: idx = data.find(b'\x10\x00\x00\x00', idx16) if idx == -1: break # 检查后面 8 字节是否像指针 (高 4 字节 0x00 或 0x7F) if idx + 12 <= len(data): ptr_bytes = data[idx + 4:idx + 12] # 检查 4-8 字节的指针值 ptr_val = struct.unpack("= 16: key_candidate = data[idx - 16:idx] # 过滤: 密钥不应全零或全相同 if len(set(key_candidate)) > 4: candidates.append((key_candidate, base_addr + idx - 16, "page1派生材料: key+page_no+sAlT")) mat_offset = idx + len(page1_material) # 模式 6: wxSQLite3 cipher 结构体检测 # 结构体通常包含: [ptr] [nKey=16 LE] [ptr zKey] [nIV=16 LE] [ptr] ... # 搜索 nKey=16 后面跟指针, 然后指针指向的地址附近可能有密钥 # 在数据中搜索 16 字节的密钥模式: 高熵 + 指针结构 # 搜索特征: \x10\x00\x00\x00 + 指针 (8字节) + \x10\x00\x00\x00 struct_offset = 0 while True: idx = data.find(b'\x10\x00\x00\x00', struct_offset) if idx == -1: break # 检查后面是否跟指针 if idx + 12 <= len(data): ptr_bytes = data[idx + 4:idx + 12] ptr_val = struct.unpack("