关键词:VeraCrypt、TrueCrypt、加密卷、volume header、PIM、KDF、AES-XTS、Serpent-Twofish
难度:高级
前置知识:密码学基础、分区表结构、磁盘镜像
一、概述
VeraCrypt 是 TrueCrypt 的社区续作(基于 7.1a 代码),目前是开源领域最主要的磁盘/文件级加密容器工具。
它在取证中出现的频率高于 BitLocker,原因很实际:它不依赖 TPM、不需要 Windows 激活、能运行在任意系统上(包括 Linux live CD)。这三项便利同时是"用户愿意用"和"恶意人员会用"的原因。
但它带来的取证处境与 Windows 自带的全卷加密完全相反:
VeraCrypt 没有后门、没有弱加密、没有"忘记密码"选项。 官方设计目标就是"如果你忘了口令,数据就永久丢失"。
所以现实路径只有三类,没有第四条:
- 找到口令(明文残留、命令行历史、剪贴板、内存、浏览器保存的密码、密码管理器)
- 找到明文副本(挂载时未擦除、备份、临时文件、页交换中的残留)
- 利用实现残留(挂载后未卸载、隐藏卷外层、卷影副本、日志)
本篇最重要的方法论判断是:不要先想"怎么解",而要先想"值不值得解"。
VeraCrypt 的口令派生过程由 PIM 控制,但PIM 值不写在卷头里——它是挂载时由操作者每次输入的参数。这带来一个反直觉但决定性的取证事实:
你无法从镜像里读出这个卷用的 PIM 是多少。 这意味着「先读卷头看 PIM 值,再决定值不值得投入爆破」这条常见思路,在 VeraCrypt 上根本执行不了——它建立在一个不存在的判据上。
真正能读出来的只有盐值(64 字节)——它是卷头里唯一的明文字段。Magic、Header Version、扇区大小、算法与 KDF 全部位于加密区内,只能在口令解开后读到;即便解开了,算法与 KDF 也是靠遍历候选组合 + CRC-32 反推出来的。
所以正确的第一步是:确认卷头存在且可定位,然后立刻转向成本低得多的几条路径。
花三天去攻一个 PIM 未知的卷,比花五分钟去搜 shell 历史里的命令行要浪费得多。
本篇处理的证据形态:
| 证据 |
位置 |
判据 / 说明 |
| 卷头(volume header) |
文件容器主卷头在偏移 0、隐藏卷头在 64 KiB、数据从 128 KiB 起;系统卷主头在加密区起始、备份头在末尾 |
★ 卷头没有明文签名:唯一未加密的只有前 64 字节盐值,VERA 在加密区内,字面量搜索定位不到 |
| 盐值 Salt |
卷头偏移 0,64 字节 |
唯一的明文字段,每卷唯一,提取 hash 必需 |
Magic VERA / Header Version |
偏移 64 / 68 |
在密文内,仅在口令正确后可用于验证,不可用作定位判据 |
| 加密算法 / KDF / 扇区大小 |
不在卷头明文里 |
遍历候选组合后用两个 CRC-32 反推 |
| PIM 值 |
不存放在卷头里 |
只能从用户记录、配置或内存中旁证(见 2.3 的例外说明) |
| 文件容器 |
.hc / .tc / 无扩展名 |
卷容量 = 文件大小 − 128 KiB,按文件大小挂载会报"文件系统损坏" |
| 隐藏卷 |
卷头偏移 65536 处存在第二个卷头(同样无明文签名) |
空闲空间熵值异常高是主要判据;隐藏卷的存在无法在解锁前被证明 |
| shell 历史 |
~/.bash_history、~/.zsh_history |
veracrypt -p "口令" ... 直接命中,是最有效的现实路径之一 |
| 挂载残留 |
进程、设备映射、页交换、文件系统缓存 |
查获时卷仍挂载的话,明文可能就在盘上明文区 |
| 密码管理器 |
KeePass .kdbx 等 |
主密码未知时走离线验证路径 |
记住这张表里最重要的两行:卷头和 shell 历史。前者决定"能不能做",后者决定"要不要做"。
三条现实路径的命中率对照——这是本篇最实用的部分,因为它们的成本差了几个数量级:
| 路径 |
典型命中点 |
成本 |
为什么有效 |
| 命令行残留 |
~/.bash_history、~/.zsh_history、终端 scrollback、部署脚本 |
一次 strings + grep |
创建和使用加密容器的操作几乎必然经过命令行,而 -p 参数会带上明文口令 |
| 浏览器保存的凭据 |
Login Data 的 origin_url / username_value |
一次 SQLite 查询 |
用户在网页上输入过这个口令(备份、迁移、复述)时会被保存 |
| 内存与页交换 |
内核页、进程内存、/proc/kcore |
需已有内存镜像 |
挂载时解出的主密钥可能仍驻留,但不是 ASCII 文本,别指望搜关键字 |
| 挂载残留 |
进程、设备映射、文件系统缓存、盘上明文区 |
需查获时卷仍挂载 |
常见于服务器与办公机现场扣押——明文可能就躺在盘上 |
| 口令攻击 |
弱口令字典、掩码、上下文候选 |
可能不可行 |
先确认候选数量:PIM 不在卷头里,单候选代价只能实测(GPU 上典型每秒数千次候选)。候选数量才是决定性的量 |
这张表的排序就是优先级。前三项加起来的成本可能只有十分钟,而最后一项可能耗尽整个项目的算力预算却毫无产出。
先做便宜的、且命中率高的,做不到再做贵的。
在取证流程中的位置:本篇属于分析环节,但它的第一动作发生在采集之前——如果你还不知道盘上有没有加密容器,就无法决定是做全盘镜像还是分区镜像。
上游依赖是镜像制作(挂载时必须只读,本篇的取标准动作清单里这是第一条)与证据格式与转换(从 E01 里切出容器文件)。
挂载成功之后,本篇就结束了,剩下的是文件系统解析:容器内通常是标准 ext4 / NTFS / FAT32,分别对应ext4 结构与 inode、NTFS 结构与 MFT 解析、FAT 系列结构。
找口令时最常命中的两条旁证路径是Shell 痕迹与命令历史与浏览器密码库与 Token 解密。
本篇能回答什么:盘上是否有 VeraCrypt / TrueCrypt 容器、是文件容器还是系统卷、卷头是否完整可解析、是否设有隐藏卷、是否曾处于挂载状态、口令是否在明文痕迹中出现过,以及在拿到候选口令后能否挂载成功(挂载成功即反向证明算法与 KDF 猜对了)。
本篇不能回答什么:在没有口令的前提下不能读出卷内任何内容——这是设计目标,不是能力边界。
也不能回答"这个卷用的 PIM 是多少"(它不写在盘上任何位置,见 2.3)、"用的什么加密算法与 KDF"(同样不在卷头明文里,需口令解开后才可知)、"用户是否故意隐藏了什么"(隐藏卷的存在只说明用户用了这个功能,不说明动机)、"这个容器是谁创建的"(卷头里没有创建者信息,需要靠外联痕迹或时间戳旁证)、以及"这个口令是唯一能打开它的口令"(VeraCrypt 不限制口令猜测次数,但也不提供任何校验位提示,唯一的验证就是实际挂载成功)。
读者前提:你需要先能在镜像里定位卷头并读出它的明文字段(盐值、Magic、Header Version,这一步不依赖任何专用工具);理解"挂载加密容器会写入数据"这件事并接受只读纪律(只读挂载与安全接入);以及对"不可恢复"这个结论的证据标准有基本认识(取证流程与原则)。
本篇不提供任何口令破解手段——涉及弱口令的离线验证方法论见通用密码恢复,那里的授权要求与优先级表同样适用于本篇。
二、核心原理
2.1 容器类型与定位
VeraCrypt 支持两种容器形态,取证时必须先区分:
| 类型 |
特征 |
取证要点 |
| 文件容器 |
普通文件(.hc、.tc、无扩展名) |
内部有隐藏的加密卷文件系统,卷大小与宿主文件可能不一致 |
| 系统加密卷 |
整个分区/整个磁盘 |
主卷头在加密区起始,备份头在分区末尾 |
文件容器的关键陷阱:VeraCrypt 文件容器的卷头在文件开头,且卷的实际大小 ≠ 文件大小。
卷头不是一个结构,而是一组(header group):卷头本体占 64 KiB(有效数据只有前 512 字节,其余填充随机数据),主卷头在偏移 0x00000,隐藏卷头在偏移 0x10000(64 KiB 处),加密数据从 0x20000(128 KiB)开始。所以文件容器的真实卷容量是「文件大小减去 128 KiB 再按扇区向下取整」,直接按文件大小挂载会报文件系统损坏或尾部读越界。
系统加密卷的布局不同:主卷头之后紧跟引导区,备份头写在加密区末尾。「只找到一处卷头」在这个格式上不是一个可用的判据——因为卷头没有明文签名,压根搜不到。隐藏卷的存在性判断见 3.5。
卷头是 VeraCrypt 取证的核心对象。但它有一个一旦记错就会让整个调查方向跑偏的性质:卷头里没有任何明文签名。
官方格式规范的原话是「VeraCrypt 卷没有签名或 ID 字符串,在解密之前它们看起来完全由随机数据组成」。这不是隐喻,是字面事实。
偏移 0x00(64 字节):Salt —— ★唯一未加密的字段★
───────────────────────────────────────── 以下之后全部加密 ─────
偏移 0x40(4 字节):Magic "VERA"(0x56455241)—— 位于加密区内
偏移 0x44(2 字节):Header Version = 0x0005
偏移 0x48(2 字节):Required Program Version
偏移 0x4C(4 字节):Key Area CRC-32(覆盖 256–511 字节的主密钥区)
偏移 0x58(8 字节):Reserved(v5 不再放卷创建时间)
偏移 0x68(8 字节):Reserved(v5 不再放卷修改时间)
偏移 0x78(8 字节):Hidden Volume Size(0 = 普通卷)
偏移 0x88(8 字节):Volume Size
偏移 0x9C(8 字节):Master Key Scope Start
偏移 0xA4(8 字节):Encrypted Area Length
偏移 0xB4(4 字节):Flags —— bit0 = 系统加密,bit1 = 非系统原地加密
偏移 0xB8(4 字节):Sector Size(512–4096)
偏移 0xFC(4 字节):Header CRC-32(覆盖 64–251 字节)
偏移 0x100(256 字节):主密钥 + XTS 辅助密钥
"全部加密"这条线画在偏移 64,也就是紧跟在 64 字节盐值之后。VERA 这个字面量位于密文里——只有在口令正确派生出头密钥并解密之后,它才会从随机字节变成 56 45 52 41。官方规范里这一列的表头就写着 Encryption Status,VERA 那一行明确标注为 Encrypted。
三条必须记住的结构事实,它们直接决定后面所有操作怎么写:** VERA 不是可搜索的明文标记。** 搜 VERACRYPT 搜不到是一层问题;搜 VERA 同样搜不到——因为那 4 个字节在盘上就是随机数据。未解锁的卷用任何字面量搜索都定位不到。 这不是搜法技巧的问题,是格式设计的直接后果。可用的判据只有间接的:区域熵值逼近 8.0 bit/byte、无文件系统结构、无长零串、文件大小是 512 的整数倍——这构成一个可疑候选,而且永远只能是候选。
盐值是 64 字节,不是 512 字节。 512 字节是「卷头有效区」的大小,不是盐值长度。这个数字记错会导致提取 hash 时切多 448 字节随机数据,hashcat 全部算不出结果,而且不会报错——只是永远不出 cracked。反过来也说明一件事:提取 hash 只需前 64 字节盐值,而 -m 号必须覆盖全部算法/KDF 候选。
卷头里没有算法 ID、没有 KDF ID、没有 PIM。 偏移 64 之后的所有字段都被加密,算法与 KDF 是通过遍历候选组合、逐个尝试解密、校验两个 CRC-32 反推出来的。这就是为什么「不解锁就读出算法」在原理上做不到,也是为什么密码攻击必须对每个 (算法, KDF, PIM) 组合各算一遍。
| 字段 |
位置 |
是否明文 |
取证价值 |
| Salt |
0x00,64 字节 |
是(唯一) |
提取 hash 的必需字段,每卷唯一 |
Magic VERA |
0x40,4 字节 |
否,在密文内 |
仅在口令正确后用于验证 |
| Header Version |
0x44,2 字节 |
否 |
解密后恒为 0x0005 |
| 隐藏卷大小 / 卷大小 |
0x78 / 0x88 |
否 |
需口令解开后才可读 |
| Flags(系统加密位) |
0xB4 |
否 |
判断是否系统加密,需口令 |
| CRC-32 ×2 |
0x4C / 0xFC |
否 |
算法反推的判据,见 2.4 |
| 主密钥区 |
0x100,256 字节 |
否 |
真正的数据解密密钥 |
| PIM 值 |
不存在于卷头 |
— |
见 2.3 |
2.3 PIM 的取证意义
PIM(Personal Iterations Multiplier)是 VeraCrypt 1.12 引入的参数,控制卷头密钥派生的计算量。它的取证意义与它的存储方式有关,而这两件事常被搞混:
PIM 不存放在卷头里。 对文件容器和普通的挂载场景,它是操作者每次输入的参数。
官方文档说的是:VeraCrypt 从不将任何口令或 PIM 值保存到磁盘。
但这句话有例外,必须限定范围:系统加密卷的高级预启动配置可以把 PIM 以未加密方式写入引导记录或预启动配置文件——因为引导环境在用户解锁之前就要用这个值来挂载系统卷,来不及、也没有条件保护它。
所以准确的表述是三层:
| 场景 |
PIM 是否落盘 |
取证含义 |
| 文件容器 |
不落盘 |
镜像里不存在这个信息 |
| 系统加密 · 正常挂载 |
不落盘 |
同上 |
| 系统加密 · 高级预启动配置 |
可能以明文写入 MBR / DCS 配置 |
可以从引导区读到 |
这带来一个反直觉但很重要的取证结论:对文件容器,你无法从镜像里读出这个卷用的 PIM 是多少。 因此「先读卷头看 PIM 值,再决定值不值得投入爆破」这条常见思路,在文件容器上根本无法执行——它建立在一个不存在的判据上。
而对系统加密卷,如果盘上存在高级预启动配置,方向可能反过来:PIM 就躺在引导区里,是明文可读的。 这一条值得单独查一遍,成本极低(读 MBR 与配置扇区),收益是直接拿到一个决定投入规模的关键参数。
PIM 影响的只是每个候选口令的验证代价,公式按官方定义(PBKDF2-HMAC):
系统加密,不使用 SHA-512 / Whirlpool: 迭代 = PIM × 2048
系统加密,使用 SHA-512 / Whirlpool: 迭代 = 15000 + PIM × 1000
非系统加密 / 文件容器: 迭代 = 15000 + PIM × 1000
关键:迭代次数不是 PIM × 500000。 1.12 之前 VeraCrypt 用固定迭代(文件容器 500,000 次),
那个数字与 PIM 没有乘数关系。1.12 之后不指定 PIM 或 PIM=0 时,
官方才回退到旧默认值:文件容器 500,000 次迭代,等效 PIM = 485。
把「默认迭代 500,000」当成「PIM=1 时的迭代数」是把两代参数体系混在了一起。
若 KDF 选的是 Argon2id(VeraCrypt 1.12+ 提供),PIM 改的是另一组参数:
内存成本 m_cost = min(64 MiB + (PIM − 1) × 32 MiB, 1024 MiB)
时间成本 t_cost = 3 + ⌊(PIM − 1) / 3⌋ (PIM ≤ 31)
= 13 + (PIM − 31) (PIM > 31)
并行度固定为 1
不指定 PIM 时 Argon2id 的默认参数等效 PIM = 12(416 MiB / 6 次)。
那么 PIM 到底还值不值得关注? 值得,但关注点要换:
- 它决定单候选代价,但单候选代价不等于总代价。 总代价 = 每候选的 PBKDF2 计算量 × 候选数量,而后者往往才是决定性的
- 文件容器的 PIM 未知时,不能给出「不可行」的结论。 因为卷头里没有这个数字,你甚至无法证明它高
- 系统加密卷先查一遍预启动配置。 这是唯一可能拿到 PIM 明文的地方,几条命令的事
- 实测速率比推算公式可靠。 hashcat 的 VeraCrypt 模式在消费级 GPU 上量级是每秒数千次候选(如 SHA-512 单算法约 5,700 H/s),这个实测值比任何迭代公式都更能回答「值不值得投」
取证第一步不是「读卷头」——卷头没有明文可读的结构字段。
正确的第一步是分流:先判断容器类型(文件容器 / 系统加密卷),系统加密卷再花两条命令查预启动配置,文件容器直接转向低成本路径。
2.4 密钥派生、算法反推与主密钥的关系
口令 + 盐值(Salt, 64 字节明文) + 迭代参数(由 PIM 决定)
↓
PBKDF2-HMAC-SHA512 / Argon2id
↓
头密钥 (Header Key)
↓
用它解密卷头 64–511 字节(密文)
↓
两个 CRC-32 校验 → 通过才说明「算法 + KDF + 口令」三者都对
↓
得到主密钥 MK
↓
解密文件系统的实际数据
这解释了 VeraCrypt 的一个取证友好特性:加密卷内的文件系统是标准格式。
解开卷头后,卷内就是一个普通的 ext4 / NTFS / FAT32,可以正常挂载分析。
也解释了「为什么无法在解锁前读出算法」。 上图里 CRC 校验在口令解密之后,而解密用的头密钥依赖 KDF 与迭代参数——所以只能反过来做:把「算法 × KDF × 候选口令」的所有组合挨个试一遍,用那两个 CRC 当判据,命中即锁定算法。真实工具的挂载过程正是这样遍历的,hashcat 也要指定 -m 13721(SHA-512 + XTS 512 bit)这类模式,选错模式即永远算不出结果。
盐值在这个环节里是必需的,所以提取 hash 时必须切对:
# 提取前 512 字节作为 hashcat 的输入(已实测可用)
dd if=DigiForensics.vc of=veracrypt.hash bs=512 count=1
2.5 TrueCrypt 与 VeraCrypt 的兼容
| 项目 |
TrueCrypt 7.1a |
VeraCrypt |
| 卷头 Magic |
VERA(4 字节,在加密区内) |
VERA(4 字节,同样在加密区内)—— 完全相同 |
| Header Version |
0x0005 |
0x0005—— 完全相同 |
| 默认 KDF |
Whirlpool 或 SHA-512 |
SHA-512,新增 Argon2id |
| 默认算法 |
AES-XTS |
AES-XTS |
| 迭代参数 |
固定(文件容器 500,000 次) |
固定值或 PIM 派生 |
| 互相兼容 |
VeraCrypt 可打开 TrueCrypt 卷 |
TrueCrypt 不能打开 VeraCrypt 卷 |
取证判断:两者的卷头在结构上无法区分——Magic 相同、Header Version 相同,都是 VERA + 版本 5。而且这层比较有个前提问题:未解锁时两个 Magic 都是密文,连读都读不到。
能区分的只有两处间接线索:① 真正挂载时试出来的结果——VeraCrypt 能打开 TrueCrypt 卷而反向不行;② 候选口令的派生行为差异(新卷可能用了 Argon2id,旧卷不可能)。
这是一个值得单独记住的陷阱形态:卷头既不承载工具版本信息,也不提供明文定位能力。
分析员看到「搜不到 VERA」就写「这不是 VeraCrypt 卷」,或者凭工具印象断言「这是 TrueCrypt 7.1a 卷」,两个结论都没有证据支撑。
注意 TrueCrypt 7.1a 的 AES 实现曾有设计争议,这影响的是「卷本身是否可靠」,而不是「怎么判断它是哪种工具创建的」。
三、操作步骤
3.1 识别 VeraCrypt / TrueCrypt 卷
先接受一个前提:这一步无法给出确定结论。格式设计的目标就是让未解锁的卷与随机数据不可区分,所以任何输出都只能是候选。报告里写「发现疑似加密容器」是可辩护的,写「未发现加密容器」是不可辩护的。
# 1) 候选筛选 A:文件大小。容器总长是 512 的整数倍,最小约 292 KB
find /mnt/image -type f -size +292k -printf '%s\ %p\
' \
| awk -F'\ ' '{if ($1 % 512 == 0) print}' | sort -rn | head -30
# 2) 候选筛选 B:熵值。加密区熵值逼近 8.0;压缩包、已加密归档、VM 镜像同样会命中
python3 - <<'PY'
import math, collections, sys
def ent(b):
n=len(b); c=collections.Counter(b)
return -sum((v/n)*math.log2(v/n) for v in c.values())
for p in sys.argv[1:]:
d=open(p,'rb').read(1<<20)
print(f"{ent(d):.4f} {p}")
PY
# 3) 候选筛选 C:结构缺失。确认它不像任何已知文件系统
file /mnt/image/suspect.bin # 会报 data —— 这是正确输出,不构成任何判断
fsstat -f -t ext4 /mnt/image/suspect.bin 2>&1 | head -2
# 4) 提取 hashcat 输入。卷头有效区是前 512 字节,但只有前 64 字节盐值是明文
dd if=/mnt/image/suspect.bin of=veracrypt.hash bs=512 count=1
筛完之后能确定的和不能确定的:
能确定:区域无文件系统结构、熵值高、大小为 512 倍数
不能确定:这是 VeraCrypt 卷、LUKS 卷、加密归档、VM 磁盘镜像,还是普通的高熵文件
卷头内部结构(记住它,但注意它现在全是密文):
偏移 0x00(64 字节):Salt —— 唯一明文字段,提取 hash 用它
偏移 0x40 之后 :全部加密,含 VERA / Version / Flags / CRC / 主密钥
为什么「看一眼 hex 就知道用的什么算法」做不到:算法与 KDF 都不在明文里,必须知道候选组合才能算 hashcat 的 -m 号。官方文档给出的完整组合是 3 种加密算法(AES / Serpent / Twofish)× 级联组合 × 若干 PRF——遍历组合、用两个 CRC-32 反推,是口令验证与算法识别的同一个过程。
| KDF |
单算法(AES / Serpent / Twofish) |
双算法(AES-Twofish) |
三算法(AES-Twofish-Serpent) |
| RIPEMD160(TrueCrypt 旧卷) |
-m 13711 |
-m 13712 |
-m 13713 |
| SHA-512 |
-m 13721 |
-m 13722 |
-m 13723 |
| Whirlpool |
`-m 137 |
|
|