先说结论:微信4.0图片采用的加密方式是AES-EBC 128 + 异或加密

| 大小(字节) | 内容/类型 | 说明 |
|---|---|---|
| 6 | 0x07085631 | .dat文件标识符 |
| 4 | int (小端序) | AES-EBC128 加密长度 |
| 4 | int (小端序) | 异或加密长度 |
| 1 | 0x01 | 未知 |

文件末尾采用异或加密,加密长度最大为1MB,多余部分未加密。
图片需要频繁使用,不会采用很复杂的加密方式,否则会严重影响性能,从3.0升级到4.0导入数据很快就完成了,也证明了加密方式不复杂(后来发现实际上是从3.0导入的数据没有更换加密方式)

找几个dat文件对比发现dat文件的都以 07 08 56 31 08 07 00 04 00 00开头,姑且认为 07 08 56 31 08 07 00 04 00 00就是dat文件的标识符。
继续往后看,发现有很多重复的数据 **V )noUt _3, **为什么会出现重复?有什么特征?
再继续看,发现重复数据是以16个字节为单位循环重复,也就是每16个字节为一个组。

对比图片原数据发现原始图片数据这一块全是 01 。
所以:为什么会重复出现?因为原数据就是重复的。
有什么特征:每16字节为一组,相同的数据加密后得到密文相同。
问一下AI

重复出现的16个字节位置并不是从0-15(右边原图) 而是从 15-下一个14(左边dat文件)

依次往前顺着找下去,猜想右边原图 **FF D8 FF E0 10 4A 46 49 46 00 01 01 00 00 01 **对应的密文是 左边 15 49 98 55 0E 05 78 97 38 E8 18 25 FE 97 04 E6

再找几张jpg图片验证一下,发现跟上面一样

这就说明了dat文件的前15个字节是特殊的文件头,并且前几个字节是标识符,后面还有一些表示其他含义的字节暂且未知,从第16个字节开始是加密后的密文
继续往下看,好像看不出来东西了,再发一张图片试试

**Google Inc 2016 **这是什么东西,密文怎么可能有这样的字符串。
对比一下原数据,发现从0x00000400开始后面的数据没有加密,直接就是原数据。00 00 04 00 这个数据在文件头的15个字节(07 08 56 31 08 07 00 04 00 00 00 00 10 00 01)出现了,可能是某种关联(如果采用小端序的话就刚好对应)。

一口气拉到文件最后,发现后面的数据又不一样了!!!。

问一下AI

找了两个jpg图片发现FFD9之后还有一些其他的东西,但是没有什么规律,于是自己构造一些特殊的图片,发给微信看看加密之后是什么样的。

pythonimg_path = "./微信图片_2025-03-12_103844_154.jpg"
with open(img_path,'rb') as f:
binary_bytes = f.read()
other_bytes = b'\x01' * 16 + b'\x00' * 16 + b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x2001234567890123456789001122334455667788990000111122223333444455556666777788889999000111222333444555666777888999'
with open('./test.jpg','wb') as f:
f.write(binary_bytes+other_bytes)



密文和原文高度相似,并不是前面的EBC加密方式,联想到微信3.0采用的异或加密方式,发现采用的就是跟之前一样的异或加密方式。


结合猜想1、猜想2和结论1可以得出dat文件结构如下图所示:
dat包含文件头、AES-EBC加密、非加密、异或加密四个部分

前两个问题用控制变量法花一些时间多找几个不同的图片很容易就能分析出来,大家可以自己去分析一下。
| 大小(字节) | 内容/类型 | 说明 |
|---|---|---|
| 6 | 0x07085631 | .dat文件标识符 |
| 4 | int (小端序) | AES-EBC128 加密长度 |
| 4 | int (小端序) | 异或加密长度 |
| 1 | 0x01 | 未知 |

文件末尾采用异或加密,加密长度最大为1MB,多余部分未加密。
例如:一个文件小于1kB,则全部是AES加密,如果大于1kB且小于1MB(实际是1MB零1KB),则前1KB部分采用AES加密,剩余部分采用异或加密。如果文件大于1MB,则前1KB采用AES加密,后面1MB采用异或加密,中间部分未加密。

对于第三个问题,首先异或密钥很容易找到,根据异或运算的可逆性,结合jpg文件末尾的 **FF D9 **两个字节,可以找找一张微信生成的缩略图(一般为_t.dat结尾),两次异或运算即可得到异或密钥。代码如下:
pythondef get_decode_code_v4(wx_dir):
cache_dir = os.path.join(wx_dir, 'cache')
if not os.path.isdir(wx_dir) or not os.path.exists(cache_dir):
raise ValueError(f'微信路径输入错误,请检查:{wx_dir}')
ok_flag = False
for root, dirs, files in os.walk(cache_dir):
if ok_flag:
break
for file in files:
if file.endswith(".dat"):
# 构造源文件和目标文件的完整路径
src_file_path = os.path.join(root, file)
with open(src_file_path, 'rb') as f:
data = f.read()
if not data.startswith(b'\x07\x08V1\x08\x07'):
continue
file_tail = data[-2:]
jpg_known_tail = b'\xff\xd9'
# 推导出密钥
xor_key = [c ^ p for c, p in zip(file_tail, jpg_known_tail)]
if len(set(xor_key)) == 1:
print(f'[*] 找到异或密钥: 0x{xor_key[0]:x}')
return xor_key[0]
return -1
注意
AES-EBC加密密钥根据观察法(瞪眼法)可以得出:aes_key = b'cfcd208495d565ef'
开个玩笑哈!虽然是简单的EBC加密方式,但是密钥也是有128位的,如果一个个试的话怕是这辈子也试不出来答案了,那怎么办呢?
如果一个小偷要去偷富人的保险柜但是又不知道保险柜密码他会怎么办,很简单:等富人来开保险柜的时候把密码偷偷记下来,正所谓家贼难防。微信读取图片肯定需要密钥来解密,他只要用密钥,就一定会在内存里留下痕迹,小偷甚至不需要密钥在哪,只需要把内存按16字节分组,已经有明文和密文了,把所有内存都试一下就行了,一个普通的计算机几分钟就能试出来正确的密钥。
很恐怖的是微信居然把所有人的图片加密密钥都设置为同一个,小偷只需要把自己家密码试出来就能开遍所有富豪的保险柜。
pythonimport os
from Crypto.Cipher import AES
def decode_dat_v4(xor_key: int, file_path, out_path, dst_name='') -> str | bytes:
"""
适用于微信4.0图片.dat,解密文件,并生成图片
:param xor_key: int 异或密钥
:param file_path: dat文件路径
:param out_path: 输出文件夹
:param dst_name: 输出文件名,默认为输入文件名
:return:
"""
if not os.path.exists(file_path) or os.path.isdir(file_path):
return ''
# 读取加密文件的内容
with open(file_path, 'rb') as f:
header = f.read(0xf)
encrypt_length = struct.unpack_from('<H', header, 6)[0]
encrypt_length0 = encrypt_length // 16 * 16 + 16
encrypted_data = f.read(encrypt_length0)
res_data = f.read()
aes_key = b'cfcd208495d565ef'
# 初始化AES解密器(ECB模式)
cipher = AES.new(aes_key, AES.MODE_ECB)
# 解密数据
decrypted_data = cipher.decrypt(encrypted_data)
# 获取图片后缀名
image_type = get_image_type(decrypted_data[:10])
output_file_name = os.path.basename(file_path)[:-4] if not dst_name else dst_name
output_file = os.path.join(out_path, output_file_name + '.' + image_type)
if os.path.exists(output_file):
return output_file
# 移除填充(假设使用的是PKCS7或PKCS5填充)
pad_length = decrypted_data[-1] # 获取填充长度
decrypted_data = decrypted_data[:-pad_length]
# 将解密后的数据写入输出文件
with open(output_file, 'wb') as f:
f.write(decrypted_data)
f.write(res_data[0:-0x100000])
f.write(bytes([byte ^ xor_key for byte in res_data[-0x100000:]]))
# print(f"解密完成,已保存到: {output_file}")
return output_file
本文作者:司小远
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!