解锁并提取Linux客户端微信数据库 (vibe coded)
at 245 lines 7.1 kB view raw
1r"""WeChat .dat image file decryption module. 2 3Supports three encryption formats: 4 - V1: AES-128-ECB with fixed key cfcd208495d565ef + XOR(0x88) 5 - V2: AES-128-ECB with per-install key from process memory + XOR(0x88) 6 - Legacy XOR: single-byte XOR, key auto-detected from image magic bytes 7 8V1/V2 file structure: 9 [6B signature] [4B aes_size LE] [4B xor_size LE] [1B padding] 10 [aligned_aes_data] [raw_data] [xor_data] 11 12Reference: github.com/ylytdeng/wechat-decrypt 13""" 14 15import os 16import struct 17import sys 18 19V1_MAGIC = b'\x07\x08V1\x08\x07' 20V2_MAGIC = b'\x07\x08V2\x08\x07' 21V1_KEY = b'cfcd208495d565ef' # md5("0")[:16] 22 23# Known image magic bytes (longer signatures first to avoid false positives) 24IMAGE_MAGIC = { 25 'png': bytes([0x89, 0x50, 0x4E, 0x47]), 26 'gif': bytes([0x47, 0x49, 0x46, 0x38]), 27 'tif': bytes([0x49, 0x49, 0x2A, 0x00]), 28 'webp': bytes([0x52, 0x49, 0x46, 0x46]), 29 'jpg': bytes([0xFF, 0xD8, 0xFF]), 30 'bmp': bytes([0x42, 0x4D]), 31} 32 33 34def is_v1v2(data: bytes) -> bool: 35 """Check if data starts with V1 or V2 magic.""" 36 return len(data) >= 6 and data[:6] in (V1_MAGIC, V2_MAGIC) 37 38 39def detect_image_format(header: bytes) -> str: 40 """Detect image format from decrypted header bytes.""" 41 if len(header) < 2: 42 return 'bin' 43 if header[:3] == bytes([0xFF, 0xD8, 0xFF]): 44 return 'jpg' 45 if header[:4] == bytes([0x89, 0x50, 0x4E, 0x47]): 46 return 'png' 47 if header[:3] == b'GIF': 48 return 'gif' 49 if header[:2] == b'BM': 50 return 'bmp' 51 if header[:4] == b'RIFF' and len(header) >= 12 and header[8:12] == b'WEBP': 52 return 'webp' 53 if header[:4] == bytes([0x49, 0x49, 0x2A, 0x00]): 54 return 'tif' 55 if header[:4] == b'wxgf': 56 return 'hevc' 57 return 'bin' 58 59 60def _detect_xor_key(data: bytes) -> int | None: 61 """Auto-detect single-byte XOR key from legacy format.""" 62 if len(data) < 4: 63 return None 64 # Try longer magic signatures first 65 for fmt, magic in IMAGE_MAGIC.items(): 66 if fmt == 'bmp': 67 continue # handle separately 68 key = data[0] ^ magic[0] 69 match = all( 70 (data[i] ^ key) == magic[i] 71 for i in range(1, min(len(magic), len(data))) 72 ) 73 if match: 74 return key 75 # BMP: 2-byte magic, needs extra validation 76 bmp_key = data[0] ^ 0x42 77 if len(data) >= 2 and (data[1] ^ bmp_key) == 0x4D: 78 if len(data) >= 14: 79 dec = bytes(b ^ bmp_key for b in data[:14]) 80 bmp_size = struct.unpack_from('<I', dec, 2)[0] 81 bmp_offset = struct.unpack_from('<I', dec, 10)[0] 82 if 14 <= bmp_offset <= 1078 and bmp_size < len(data) * 2: 83 return bmp_key 84 return None 85 86 87def decrypt_image(data: bytes, aes_key: bytes = None) -> tuple[bytes, str] | None: 88 """Decrypt a WeChat .dat image. 89 90 Args: 91 data: Raw .dat file contents. 92 aes_key: 16-byte AES key for V2 format. Ignored for V1/legacy. 93 94 Returns: 95 (decrypted_bytes, format_string) or None on failure. 96 """ 97 if len(data) < 6: 98 return None 99 100 sig = data[:6] 101 102 # V1 / V2 format 103 if sig in (V1_MAGIC, V2_MAGIC): 104 return _decrypt_v1v2(data, sig, aes_key) 105 106 # Legacy XOR format 107 xor_key = _detect_xor_key(data) 108 if xor_key is None: 109 return None 110 decrypted = bytes(b ^ xor_key for b in data) 111 fmt = detect_image_format(decrypted[:16]) 112 return (decrypted, fmt) 113 114 115def _decrypt_v1v2(data: bytes, sig: bytes, aes_key: bytes = None) -> tuple[bytes, str] | None: 116 """Decrypt V1/V2 format: AES-ECB + raw + XOR(0x88).""" 117 from Cryptodome.Cipher import AES 118 119 if sig == V1_MAGIC: 120 key = V1_KEY 121 else: 122 if aes_key is None: 123 return None 124 key = aes_key if isinstance(aes_key, bytes) else aes_key.encode('ascii') 125 key = key[:16] 126 if len(key) < 16: 127 return None 128 129 if len(data) < 15: 130 return None 131 132 aes_size, xor_size = struct.unpack_from('<II', data, 6) 133 134 # aes_size is already 16-byte aligned (the encrypted data size) 135 offset = 15 # 6 sig + 4 aes_size + 4 xor_size + 1 pad 136 if offset + aes_size > len(data): 137 return None 138 139 # AES-ECB decrypt (no PKCS7 padding — aes_size is the exact encrypted size) 140 aes_data = data[offset:offset + aes_size] 141 try: 142 cipher = AES.new(key, AES.MODE_ECB) 143 dec_aes = cipher.decrypt(aes_data) 144 except (ValueError, KeyError): 145 return None 146 offset += aes_size 147 148 # Raw data (unencrypted) 149 raw_end = len(data) - xor_size 150 raw_data = data[offset:raw_end] if offset < raw_end else b'' 151 152 # XOR(0x88) section 153 xor_data = data[raw_end:] 154 dec_xor = bytes(b ^ 0x88 for b in xor_data) 155 156 decrypted = dec_aes + raw_data + dec_xor 157 fmt = detect_image_format(decrypted[:16]) 158 return (decrypted, fmt) 159 160 161def decrypt_image_header(data: bytes, aes_key: bytes) -> bytes | None: 162 """Decrypt only the first AES block (16 bytes) for validation. 163 164 Used by memscan to test candidate keys quickly without decrypting 165 the entire file. 166 """ 167 from Cryptodome.Cipher import AES 168 169 if len(data) < 15 + 16: 170 return None 171 172 sig = data[:6] 173 if sig not in (V1_MAGIC, V2_MAGIC): 174 return None 175 176 key = aes_key if isinstance(aes_key, bytes) else aes_key.encode('ascii') 177 key = key[:16] 178 if len(key) < 16: 179 return None 180 181 offset = 15 182 aes_block = data[offset:offset + 16] 183 try: 184 cipher = AES.new(key, AES.MODE_ECB) 185 dec = cipher.decrypt(aes_block) 186 return dec 187 except Exception: 188 return None 189 190 191# Valid image magic bytes for header validation 192_VALID_HEADERS = [ 193 bytes([0xFF, 0xD8, 0xFF]), 194 bytes([0x89, 0x50, 0x4E, 0x47]), 195 b'GIF', 196 b'BM', 197 b'RIFF', 198 bytes([0x49, 0x49, 0x2A, 0x00]), 199 b'wxgf', 200] 201 202 203def is_valid_image_header(header: bytes) -> bool: 204 """Check if decrypted header starts with a known image magic.""" 205 return any(header[:len(m)] == m for m in _VALID_HEADERS) 206 207 208# --- CLI --- 209 210if __name__ == "__main__": 211 if len(sys.argv) < 2: 212 print("Usage: python -m wxdump_linux.linux.decode_image <dat_file> [aes_key_hex] [output_file]") 213 sys.exit(1) 214 215 dat_file = sys.argv[1] 216 key_hex = sys.argv[2] if len(sys.argv) > 2 else None 217 out_file = sys.argv[3] if len(sys.argv) > 3 else None 218 219 if not os.path.exists(dat_file): 220 print(f"File not found: {dat_file}") 221 sys.exit(1) 222 223 with open(dat_file, 'rb') as f: 224 data = f.read() 225 226 aes_key = bytes.fromhex(key_hex) if key_hex else None 227 result = decrypt_image(data, aes_key) 228 229 if result is None: 230 print("Decryption failed. For V2 files, provide AES key as second argument.") 231 sys.exit(1) 232 233 img_bytes, fmt = result 234 if out_file is None: 235 base = os.path.splitext(dat_file)[0] 236 for suffix in ('_t', '_h'): 237 if base.endswith(suffix): 238 base = base[:-len(suffix)] 239 break 240 out_file = f"{base}.{fmt}" 241 242 with open(out_file, 'wb') as f: 243 f.write(img_bytes) 244 245 print(f"OK: {out_file} ({fmt}, {len(img_bytes):,} bytes)")