{"contents":"r\"\"\"WeChat .dat image file decryption module.\n\nSupports three encryption formats:\n - V1: AES-128-ECB with fixed key cfcd208495d565ef + XOR(0x88)\n - V2: AES-128-ECB with per-install key from process memory + XOR(0x88)\n - Legacy XOR: single-byte XOR, key auto-detected from image magic bytes\n\nV1/V2 file structure:\n [6B signature] [4B aes_size LE] [4B xor_size LE] [1B padding]\n [aligned_aes_data] [raw_data] [xor_data]\n\nReference: github.com/ylytdeng/wechat-decrypt\n\"\"\"\n\nimport os\nimport struct\nimport sys\n\nV1_MAGIC = b'\\x07\\x08V1\\x08\\x07'\nV2_MAGIC = b'\\x07\\x08V2\\x08\\x07'\nV1_KEY = b'cfcd208495d565ef' # md5(\"0\")[:16]\n\n# Known image magic bytes (longer signatures first to avoid false positives)\nIMAGE_MAGIC = {\n 'png': bytes([0x89, 0x50, 0x4E, 0x47]),\n 'gif': bytes([0x47, 0x49, 0x46, 0x38]),\n 'tif': bytes([0x49, 0x49, 0x2A, 0x00]),\n 'webp': bytes([0x52, 0x49, 0x46, 0x46]),\n 'jpg': bytes([0xFF, 0xD8, 0xFF]),\n 'bmp': bytes([0x42, 0x4D]),\n}\n\n\ndef is_v1v2(data: bytes) -\u003e bool:\n \"\"\"Check if data starts with V1 or V2 magic.\"\"\"\n return len(data) \u003e= 6 and data[:6] in (V1_MAGIC, V2_MAGIC)\n\n\ndef detect_image_format(header: bytes) -\u003e str:\n \"\"\"Detect image format from decrypted header bytes.\"\"\"\n if len(header) \u003c 2:\n return 'bin'\n if header[:3] == bytes([0xFF, 0xD8, 0xFF]):\n return 'jpg'\n if header[:4] == bytes([0x89, 0x50, 0x4E, 0x47]):\n return 'png'\n if header[:3] == b'GIF':\n return 'gif'\n if header[:2] == b'BM':\n return 'bmp'\n if header[:4] == b'RIFF' and len(header) \u003e= 12 and header[8:12] == b'WEBP':\n return 'webp'\n if header[:4] == bytes([0x49, 0x49, 0x2A, 0x00]):\n return 'tif'\n if header[:4] == b'wxgf':\n return 'hevc'\n return 'bin'\n\n\ndef _detect_xor_key(data: bytes) -\u003e int | None:\n \"\"\"Auto-detect single-byte XOR key from legacy format.\"\"\"\n if len(data) \u003c 4:\n return None\n # Try longer magic signatures first\n for fmt, magic in IMAGE_MAGIC.items():\n if fmt == 'bmp':\n continue # handle separately\n key = data[0] ^ magic[0]\n match = all(\n (data[i] ^ key) == magic[i]\n for i in range(1, min(len(magic), len(data)))\n )\n if match:\n return key\n # BMP: 2-byte magic, needs extra validation\n bmp_key = data[0] ^ 0x42\n if len(data) \u003e= 2 and (data[1] ^ bmp_key) == 0x4D:\n if len(data) \u003e= 14:\n dec = bytes(b ^ bmp_key for b in data[:14])\n bmp_size = struct.unpack_from('\u003cI', dec, 2)[0]\n bmp_offset = struct.unpack_from('\u003cI', dec, 10)[0]\n if 14 \u003c= bmp_offset \u003c= 1078 and bmp_size \u003c len(data) * 2:\n return bmp_key\n return None\n\n\ndef decrypt_image(data: bytes, aes_key: bytes = None) -\u003e tuple[bytes, str] | None:\n \"\"\"Decrypt a WeChat .dat image.\n\n Args:\n data: Raw .dat file contents.\n aes_key: 16-byte AES key for V2 format. Ignored for V1/legacy.\n\n Returns:\n (decrypted_bytes, format_string) or None on failure.\n \"\"\"\n if len(data) \u003c 6:\n return None\n\n sig = data[:6]\n\n # V1 / V2 format\n if sig in (V1_MAGIC, V2_MAGIC):\n return _decrypt_v1v2(data, sig, aes_key)\n\n # Legacy XOR format\n xor_key = _detect_xor_key(data)\n if xor_key is None:\n return None\n decrypted = bytes(b ^ xor_key for b in data)\n fmt = detect_image_format(decrypted[:16])\n return (decrypted, fmt)\n\n\ndef _decrypt_v1v2(data: bytes, sig: bytes, aes_key: bytes = None) -\u003e tuple[bytes, str] | None:\n \"\"\"Decrypt V1/V2 format: AES-ECB + raw + XOR(0x88).\"\"\"\n from Cryptodome.Cipher import AES\n\n if sig == V1_MAGIC:\n key = V1_KEY\n else:\n if aes_key is None:\n return None\n key = aes_key if isinstance(aes_key, bytes) else aes_key.encode('ascii')\n key = key[:16]\n if len(key) \u003c 16:\n return None\n\n if len(data) \u003c 15:\n return None\n\n aes_size, xor_size = struct.unpack_from('\u003cII', data, 6)\n\n # aes_size is already 16-byte aligned (the encrypted data size)\n offset = 15 # 6 sig + 4 aes_size + 4 xor_size + 1 pad\n if offset + aes_size \u003e len(data):\n return None\n\n # AES-ECB decrypt (no PKCS7 padding — aes_size is the exact encrypted size)\n aes_data = data[offset:offset + aes_size]\n try:\n cipher = AES.new(key, AES.MODE_ECB)\n dec_aes = cipher.decrypt(aes_data)\n except (ValueError, KeyError):\n return None\n offset += aes_size\n\n # Raw data (unencrypted)\n raw_end = len(data) - xor_size\n raw_data = data[offset:raw_end] if offset \u003c raw_end else b''\n\n # XOR(0x88) section\n xor_data = data[raw_end:]\n dec_xor = bytes(b ^ 0x88 for b in xor_data)\n\n decrypted = dec_aes + raw_data + dec_xor\n fmt = detect_image_format(decrypted[:16])\n return (decrypted, fmt)\n\n\ndef decrypt_image_header(data: bytes, aes_key: bytes) -\u003e bytes | None:\n \"\"\"Decrypt only the first AES block (16 bytes) for validation.\n\n Used by memscan to test candidate keys quickly without decrypting\n the entire file.\n \"\"\"\n from Cryptodome.Cipher import AES\n\n if len(data) \u003c 15 + 16:\n return None\n\n sig = data[:6]\n if sig not in (V1_MAGIC, V2_MAGIC):\n return None\n\n key = aes_key if isinstance(aes_key, bytes) else aes_key.encode('ascii')\n key = key[:16]\n if len(key) \u003c 16:\n return None\n\n offset = 15\n aes_block = data[offset:offset + 16]\n try:\n cipher = AES.new(key, AES.MODE_ECB)\n dec = cipher.decrypt(aes_block)\n return dec\n except Exception:\n return None\n\n\n# Valid image magic bytes for header validation\n_VALID_HEADERS = [\n bytes([0xFF, 0xD8, 0xFF]),\n bytes([0x89, 0x50, 0x4E, 0x47]),\n b'GIF',\n b'BM',\n b'RIFF',\n bytes([0x49, 0x49, 0x2A, 0x00]),\n b'wxgf',\n]\n\n\ndef is_valid_image_header(header: bytes) -\u003e bool:\n \"\"\"Check if decrypted header starts with a known image magic.\"\"\"\n return any(header[:len(m)] == m for m in _VALID_HEADERS)\n\n\n# --- CLI ---\n\nif __name__ == \"__main__\":\n if len(sys.argv) \u003c 2:\n print(\"Usage: python -m wxdump_linux.linux.decode_image \u003cdat_file\u003e [aes_key_hex] [output_file]\")\n sys.exit(1)\n\n dat_file = sys.argv[1]\n key_hex = sys.argv[2] if len(sys.argv) \u003e 2 else None\n out_file = sys.argv[3] if len(sys.argv) \u003e 3 else None\n\n if not os.path.exists(dat_file):\n print(f\"File not found: {dat_file}\")\n sys.exit(1)\n\n with open(dat_file, 'rb') as f:\n data = f.read()\n\n aes_key = bytes.fromhex(key_hex) if key_hex else None\n result = decrypt_image(data, aes_key)\n\n if result is None:\n print(\"Decryption failed. For V2 files, provide AES key as second argument.\")\n sys.exit(1)\n\n img_bytes, fmt = result\n if out_file is None:\n base = os.path.splitext(dat_file)[0]\n for suffix in ('_t', '_h'):\n if base.endswith(suffix):\n base = base[:-len(suffix)]\n break\n out_file = f\"{base}.{fmt}\"\n\n with open(out_file, 'wb') as f:\n f.write(img_bytes)\n\n print(f\"OK: {out_file} ({fmt}, {len(img_bytes):,} bytes)\")\n","is_binary":false,"path":"wxdump_linux/linux/decode_image.py","ref":""}