2025-03-29
留痕
00

目录

.dat文件头(15个字节)
解析过程
文件头解析
特征:每16字节为一组,相同的数据加密后得到密文相同。
猜想1:dat文件采用的是AES-EBC加密方式
结论1:dat文件部分数据没有加密
猜想2:dat文件后面采用的是异或加密
三个问题:
.dat文件头(15个字节)
于是,很容易写出dat文件的解密代码
相关链接

Windows 版微信长期以来采用简单的异或加密方式处理图片,这种方式虽操作简便,却容易被破解。随着微信 4.0 的全面重构,图片加密方式也随之升级。尽管加密算法千变万化,但仍有规律可循。本文将带你一步步解析微信 4.0 的图片加密机制。

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

image.png

.dat文件头(15个字节)

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

image.png

文件末尾采用异或加密,加密长度最大为1MB,多余部分未加密。


解析过程

图片需要频繁使用,不会采用很复杂的加密方式,否则会严重影响性能,从3.0升级到4.0导入数据很快就完成了,也证明了加密方式不复杂(后来发现实际上是从3.0导入的数据没有更换加密方式)

文件头解析

image.png

找几个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个字节为一个组。

image.png

对比图片原数据发现原始图片数据这一块全是 01

所以:为什么会重复出现?因为原数据就是重复的。

有什么特征:每16字节为一组,相同的数据加密后得到密文相同。

特征:每16字节为一组,相同的数据加密后得到密文相同。

问一下AI

image.png

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

image.png

依次往前顺着找下去,猜想右边原图 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

image.png

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

image.png

这就说明了dat文件的前15个字节是特殊的文件头,并且前几个字节是标识符,后面还有一些表示其他含义的字节暂且未知,从第16个字节开始是加密后的密文

猜想1:dat文件采用的是AES-EBC加密方式

继续往下看,好像看不出来东西了,再发一张图片试试

image.png

Google Inc 2016 这是什么东西,密文怎么可能有这样的字符串。

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

结论1:dat文件部分数据没有加密

image.png

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

image.png

问一下AI

image.png

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

image.png

python
img_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)

image.png

手动构造的原始图片: 原文

image.png

发送到微信得到的加密文件: 密文

image.png

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

image.png

猜想2:dat文件后面采用的是异或加密

结合猜想1、猜想2和结论1可以得出dat文件结构如下图所示:

dat包含文件头、AES-EBC加密、非加密、异或加密四个部分

image.png

三个问题:

  1. AES、非加密、异或加密的起止位置是什么?
  2. dat文件头存了什么信息?
  3. 异或密钥是什么?AES-EBC加密密钥是什么?

前两个问题用控制变量法花一些时间多找几个不同的图片很容易就能分析出来,大家可以自己去分析一下。

.dat文件头(15个字节)

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

image.png

文件末尾采用异或加密,加密长度最大为1MB,多余部分未加密。

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

image.png

对于第三个问题,首先异或密钥很容易找到,根据异或运算的可逆性,结合jpg文件末尾的 FF D9 两个字节,可以找找一张微信生成的缩略图(一般为_t.dat结尾),两次异或运算即可得到异或密钥。代码如下:

python
def 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'

2025年4月1日微信4.0.3正式版更改了这个密钥,我又观察了一下,防止微信再更改就先保密一段时间,等稳定了再放出来。

于是,很容易写出dat文件的解密代码

python
import 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

相关链接

如果对你有用的话,可以打赏哦
打赏
ali pay
wechat pay

本文作者:司小远

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!