containerd 检查点漏洞深度拆解:当「热迁移」功能变成容器逃逸的四个跳板——从标签传播、镜像缓存投毒到设备隔离绕过的全链路实战
2026年6月,containerd 一次性修复了7个CVE,其中4个高危漏洞(宿主机RCE、跨Pod RCE、设备隔离绕过、信息泄露)全部指向同一个边缘功能——容器检查点(checkpoint/restore)。这个功能早在2023年就合入主分支,但安全审查却滞后近三年。本文深入拆解四条攻击链的技术原理,并提出「延迟审计法」——盯住容器运行时新功能上线的12至18个月窗口期,在边缘路径被大规模部署之前主动挖掘漏洞。
一、背景:从「边缘功能」到「高危漏洞集」
1.1 检查点功能的诞生与演进
容器检查点(Checkpoint/Restore)是容器热迁移的核心技术,允许将运行中的容器状态完整保存到磁盘,然后在另一个节点恢复运行。这个功能的价值显而易见:
- 有状态服务迁移:数据库、消息队列等有状态应用无需停机即可迁移
- 故障快速恢复:容器崩溃后从检查点恢复,减少冷启动时间
- 开发调试:保存问题现场,异地复现调试
CRIU(Checkpoint/Restore In Userspace)是这个功能的底层实现。它通过操作系统的 /proc 文件系统捕获进程的所有内核态资源:
内存状态 → /proc/[pid]/maps、/proc/[pid]/smaps
文件描述符 → 打开的文件、socket、管道状态
线程上下文 → 寄存器状态、调用栈
命名空间 → PID、网络、挂载点等命名空间信息
典型检查点过程耗时公式:
T_checkpoint = T_memory + T_threads + T_files
≈ (RSS/带宽) + (线程数×上下文大小) + (文件描述符数×元数据大小)
1.2 功能合入时间线
| 时间 | 事件 | 安全状态 |
|---|---|---|
| 2023年Q1 | CRIU 支持合入 containerd 主分支 | 未经充分安全审查 |
| 2023-2025 | 功能默认关闭,少数用户尝鲜 | 攻击面未暴露 |
| 2026年Q1 | Kubernetes 1.33 原生支持检查点 API | 攻击面扩大 |
| 2026年6月 | 7个CVE集中披露 | 安全审计后知后觉 |
关键问题:这个功能从合入到修复,中间有近三年时间处于「安全审查真空期」。
二、技术原理:CRIU 如何工作
2.1 检查点阶段(Checkpoint)
CRIU 冻结进程的完整流程:
// 伪代码:containerd 调用 CRIU 的检查点接口
func checkpointContainer(containerID string) error {
// 1. 发送冻结信号
if err := criu.SendSignal(containerID, SIG冻结); err != nil {
return err
}
// 2. 注入寄生代码(Parasite Code Injection)
// CRIU 在进程地址空间注入一段代码,用于收集进程状态
parasite := injectParasiteCode(containerID)
// 3. 收集进程状态
state := parasite.CollectState()
// 4. 写入检查点镜像
return writeCheckpointImage(containerID, state)
}
寄生代码注入是 CRIU 的核心技术。它通过 ptrace 系统调用在目标进程中执行一段精心设计的汇编代码,这段代码:
- 保存所有寄存器状态
- 遍历
/proc/self/maps收集内存布局 - 将内存页写入检查点文件
- 收集打开的文件描述符
- 记录信号处理器、定时器等状态
2.2 恢复阶段(Restore)
恢复是检查点的逆过程,但更复杂:
// CRIU 恢复进程的核心逻辑(简化版)
int restore_process(checkpoint_image_t *img) {
// 1. 创建新进程框架
pid_t pid = fork();
// 2. 恢复命名空间
for (each namespace in img->namespaces) {
clone(namespace.flags);
}
// 3. 恢复内存映射
for (each vma in img->memory_regions) {
void *addr = mmap(vma.start, vma.size,
vma.prot, vma.flags);
// 写入检查点保存的内存数据
write_memory_content(addr, vma.data);
}
// 4. 恢复文件描述符
for (each fd in img->file_descriptors) {
open_by_handle_at(fd.handle);
}
// 5. 恢复寄存器状态
set_registers(img->registers);
// 6. 让进程继续运行
return 0;
}
2.3 containerd 的检查点 API
containerd 在 2.0 版本引入了检查点 API:
// containerd 的检查点 API 定义(简化)
type CheckpointService interface {
// 创建检查点
CreateCheckpoint(ctx context.Context, req *CreateCheckpointRequest) (*CreateCheckpointResponse, error)
// 从检查点恢复
RestoreCheckpoint(ctx context.Context, req *RestoreCheckpointRequest) (*RestoreCheckpointResponse, error)
// 列出检查点
ListCheckpoints(ctx context.Context, req *ListCheckpointsRequest) (*ListCheckpointsResponse, error)
}
type CreateCheckpointRequest struct {
ContainerID string // 容器ID
CheckpointID string // 检查点ID
Exit bool // 检查点后是否退出容器
ImageRef string // 检查点镜像引用
Annotations map[string]string // 注解
}
关键设计决策:containerd 将检查点保存为 OCI 镜像格式,这意味着:
- 检查点可以推送到镜像仓库
- 可以在不同节点间传输
- 可以像普通镜像一样管理和分发
这个设计决策,正是漏洞的根源之一。
三、漏洞一:标签传播攻击(Label Propagation)
3.1 漏洞原理
当 containerd 创建检查点镜像时,会将原容器的标签(Labels)复制到检查点镜像中:
// containerd 检查点创建时的标签复制逻辑(漏洞版本)
func buildCheckpointImage(container Container) Image {
manifest := &oci.Manifest{
Config: &oci.Image{
// 问题:直接复制容器标签,未做安全检查
Labels: container.Labels,
},
}
return manifest
}
问题:容器标签可能包含敏感信息或恶意内容。例如:
{
"io.kubernetes.pod.name": "kube-system/etcd-master",
"io.kubernetes.pod.uid": "abc123",
"io.kubernetes.pod.namespace": "kube-system",
"malicious.label": "file:///etc/shadow"
}
当检查点镜像被推送到公共镜像仓库,或被其他用户拉取时,这些标签会「传播」到下游。
3.2 攻击场景
场景1:敏感信息泄露
# 攻击者在受控容器中设置敏感标签
kubectl label pod my-pod secret-key="AKIAIOSFODNN7EXAMPLE"
# 创建检查点
crictl checkpoint my-pod
# 检查点镜像包含敏感标签
crane manifest checkpoint-image:v1 | jq .config.Labels
# {
# "secret-key": "AKIAIOSFODNN7EXAMPLE"
# }
场景2:跨命名空间权限提升
在 Kubernetes 中,某些标签用于权限控制:
# 攻击者设置特权标签
labels:
pod-security.kubernetes.io/enforce: privileged
pod-security.kubernetes.io/audit: privileged
当检查点恢复时,这些标签会被应用到新容器,绕过命名空间的安全策略。
3.3 修复方案
containerd 2.1 版本引入了标签白名单机制:
// 修复后的标签过滤逻辑
var allowedCheckpointLabels = map[string]bool{
"io.containerd.checkpoint.*": true,
"org.opencontainers.*": true,
}
func filterLabelsForCheckpoint(labels map[string]string) map[string]string {
filtered := make(map[string]string)
for k, v := range labels {
if isLabelAllowed(k, allowedCheckpointLabels) {
filtered[k] = v
}
}
return filtered
}
防御建议:
- 升级到 containerd 2.1 或更高版本
- 在 Pod 安全策略中禁止检查点功能
- 对检查点镜像进行安全扫描
四、漏洞二:镜像缓存投毒(Image Cache Poisoning)
4.1 漏洞原理
这是最危险的漏洞,可导致宿主机RCE。攻击链如下:
攻击者创建恶意容器 → 创建检查点 → 检查点包含恶意文件 →
推送到镜像仓库 → 其他节点拉取 → 缓存投毒 → 恢复时执行恶意代码
核心问题在于:检查点镜像包含了原容器的完整文件系统状态,但没有验证这些文件的来源和完整性。
4.2 攻击步骤详解
第一步:准备恶意容器
# Dockerfile.malicious
FROM alpine:latest
# 在检查点恢复时会执行的恶意脚本
COPY malicious.sh /malicious.sh
RUN chmod +x /malicious.sh
# 植入后门
RUN echo "* * * * * /malicious.sh" >> /var/spool/cron/crontabs/root
第二步:创建检查点
# 启动恶意容器
docker run -d --name malicious-container malicious-image:latest
# 等待容器运行
sleep 5
# 创建检查点
crictl checkpoint malicious-container --export=malicious-checkpoint.tar
# 检查点包含完整的文件系统状态,包括恶意文件
tar -tvf malicious-checkpoint.tar | grep malicious
# -rw-r--r-- 0/0 1234 2026-06-01 10:00 malicious.sh
第三步:推送到镜像仓库
# 加载检查点为镜像
docker load < malicious-checkpoint.tar
# 标记并推送
docker tag checkpoint:malicious-container victim-registry.com/legitimate-image:latest
docker push victim-registry.com/legitimate-image:latest
第四步:受害者拉取并恢复
# 受害者拉取「合法」镜像
docker pull victim-registry.com/legitimate-image:latest
# 从检查点恢复
crictl restore --image=victim-registry.com/legitimate-image:latest
# 恶意代码被执行
# /malicious.sh 在恢复时自动运行
4.3 修复方案
containerd 引入了检查点镜像签名验证:
// 检查点镜像验证流程
func verifyCheckpointImage(image Image) error {
// 1. 验证镜像签名
if err := verifyImageSignature(image); err != nil {
return fmt.Errorf("signature verification failed: %w", err)
}
// 2. 验证镜像层完整性
for _, layer := range image.Layers() {
if err := verifyLayerIntegrity(layer); err != nil {
return fmt.Errorf("layer %s integrity check failed: %w", layer.Digest, err)
}
}
// 3. 扫描恶意内容
if err := scanForMaliciousContent(image); err != nil {
return fmt.Errorf("malicious content detected: %w", err)
}
return nil
}
防御建议:
- 启用镜像签名验证:只拉取和恢复经过签名的检查点镜像
- 隔离检查点仓库:使用专用的内部仓库存储检查点镜像
- 实施内容扫描:对检查点镜像进行安全扫描
五、漏洞三:设备注解绕过(Device Annotation Bypass)
5.1 漏洞原理
容器检查点包含设备配置信息。containerd 在恢复检查点时,会根据设备注解重新创建设备:
// 漏洞版本的设备恢复逻辑
func restoreDevices(checkpoint Checkpoint) ([]containerd.Device, error) {
var devices []containerd.Device
// 问题:直接使用检查点中的设备注解,未做权限检查
for _, dev := range checkpoint.Spec.Linux.Devices {
devices = append(devices, containerd.Device{
Path: dev.Path,
Type: dev.Type,
// 关键:攻击者可以指定任意宿主机设备
Major: dev.Major,
Minor: dev.Minor,
})
}
return devices, nil
}
问题:攻击者可以在检查点中注入设备注解,指定宿主机的敏感设备(如 /dev/sda、/dev/mem),恢复时绕过设备隔离。
5.2 实际危害
一旦攻击成功,攻击者可以:
- 读取整个磁盘:获取所有容器的文件系统
- 修改系统文件:植入后门,持久化控制
- 提取密钥和凭证:Kubernetes Secrets、服务账号Token
- 提权到宿主机:通过
/dev/mem直接操作内核内存
5.3 修复方案
containerd 引入了设备白名单:
// 修复后的设备恢复逻辑
var allowedDevices = map[string]bool{
"/dev/null": true,
"/dev/zero": true,
"/dev/urandom": true,
"/dev/random": true,
"/dev/tty": true,
"/dev/console": true,
"/dev/ptmx": true,
}
func restoreDevices(checkpoint Checkpoint) ([]containerd.Device, error) {
var devices []containerd.Device
for _, dev := range checkpoint.Spec.Linux.Devices {
// 验证设备是否在白名单中
if !allowedDevices[dev.Path] {
log.Warnf("device %s is not in allowed list, skipping", dev.Path)
continue
}
devices = append(devices, containerd.Device{
Path: dev.Path,
Type: dev.Type,
Major: dev.Major,
Minor: dev.Minor,
})
}
return devices, nil
}
防御建议:
- 升级 containerd:确保使用修复版本
- Pod 安全策略:禁止容器访问块设备和内存设备
- 审计设备配置:检查容器是否有异常设备挂载
六、漏洞四:符号链接遍历(Symlink Traversal)
6.1 漏洞原理
检查点镜像中可能包含符号链接。当恢复检查点时,containerd 会按顺序创建这些链接。问题在于:在符号链接创建和目标文件写入之间存在时间窗口,攻击者可以替换链接目标。
这是典型的 TOCTOU(Time-of-Check Time-of-Use)漏洞:
检查点镜像解压 → 创建符号链接 /tmp/link → 指向 /etc/passwd
↓
攻击者修改链接目标 → 指向 /etc/shadow
↓
写入文件内容 → 实际写入到 /etc/shadow
6.2 危害分析
符号链接遍历漏洞可导致:
- 任意文件写入:覆盖宿主机的关键配置文件
- 权限提升:修改
/etc/sudoers,获得 root 权限 - 持久化控制:植入后门脚本
6.3 修复方案
containerd 引入了安全解压机制:
// 安全解压实现
func safeUnpackCheckpoint(layer io.Reader, rootfs string) error {
tr := tar.NewReader(layer)
for {
header, err := tr.Next()
if err == io.EOF {
break
}
switch header.Typeflag {
case tar.TypeSymlink:
// 1. 验证链接目标是否在根文件系统内
target := filepath.Clean(header.Linkname)
if !filepath.IsAbs(target) {
target = filepath.Join(filepath.Dir(header.Name), target)
}
// 2. 检查是否逃逸
if strings.HasPrefix(filepath.Clean(target), "../") {
return fmt.Errorf("symlink %s points outside rootfs", header.Name)
}
// 3. 禁止指向敏感路径
sensitivePaths := []string{"/etc/passwd", "/etc/shadow", "/etc/sudoers"}
for _, sensitive := range sensitivePaths {
if target == sensitive {
return fmt.Errorf("symlink cannot point to sensitive path %s", sensitive)
}
}
}
}
return nil
}
七、「延迟审计法」:如何发现边缘功能的安全漏洞
7.1 方法论
这批漏洞的发现揭示了一个重要规律:
容器运行时的新功能,在上线后的 12-18 个月是安全审计的黄金窗口期。
原因:
- 功能默认关闭:初期用户少,攻击面未暴露
- 文档不完善:安全边界未清晰定义
- 生产部署滞后:企业需要时间评估和部署
- 安全研究跟进慢:社区关注点在核心功能
7.2 审计流程
第一阶段(功能上线后 0-6 个月)
├── 阅读源码和文档
├── 理解功能设计意图
├── 识别攻击面
└── 标记潜在风险点
第二阶段(功能上线后 6-12 个月)
├── 构建测试环境
├── 逐个验证风险点
├── 尝试构造攻击链
└── 记录可利用路径
第三阶段(功能上线后 12-18 个月)
├── 完善利用代码
├── 测试实际影响
├── 负责任披露
└── 协助厂商修复
7.3 其他值得关注的功能
基于「延迟审计法」,以下是 containerd 和 Kubernetes 中值得关注的功能:
| 功能 | 上线时间 | 审计窗口期 | 风险等级 |
|---|---|---|---|
| Sandbox Containers | 2024 Q4 | 2025 Q2-Q3 | 高(隔离边界) |
| User Namespaces | 2025 Q1 | 2025 Q3-Q4 | 中(权限映射) |
| In-place Pod Update | 2025 Q2 | 2025 Q4-2026 Q1 | 高(状态一致性) |
| Volume Resize | 2025 Q3 | 2026 Q1-Q2 | 中(数据完整性) |
| DRA (Dynamic Resource Allocation) | 2025 Q4 | 2026 Q2-Q3 | 高(设备访问) |
八、总结与展望
8.1 核心教训
- 边缘功能也需要安全审查:不要因为功能「小众」就忽视安全
- 功能设计影响安全性:将检查点保存为 OCI 镜像,引入了镜像仓库的攻击面
- 延迟审计是有效的:新功能的 12-18 个月窗口期是发现漏洞的黄金时间
- 纵深防御是必要的:单点防御不足以应对组合攻击
8.2 防御清单
| 层次 | 措施 | 优先级 |
|---|---|---|
| 镜像层 | 启用镜像签名验证 | 高 |
| 镜像层 | 加密检查点镜像 | 高 |
| 运行时 | 升级 containerd 到修复版本 | 高 |
| 运行时 | 禁用检查点功能(如不需要) | 中 |
| 运行时 | 启用敏感信息过滤 | 中 |
| 网络层 | 隔离检查点仓库 | 中 |
| 审计层 | 监控检查点 API 调用 | 中 |
| 审计层 | 定期扫描检查点镜像 | 低 |
| 策略层 | Pod 安全策略限制 | 高 |
8.3 未来趋势
容器检查点技术仍在发展,未来可能出现:
- GPU 状态检查点:CRIU 已支持 CUDA 检查点,但安全边界未定义
- 分布式检查点:跨多个容器的原子性检查点
- 增量检查点:只保存内存差异,减少数据量
- 实时迁移:零停机迁移,要求更高的性能和可靠性
这些新特性都将带来新的安全挑战。
8.4 写在最后
容器安全是一个持续的过程,而不是一次性的检查。containerd 检查点漏洞告诉我们:
安全漏洞往往隐藏在「功能丰富」与「安全简单」的交界处。
当新功能上线时,问自己三个问题:
- 这个功能改变了什么安全边界?
- 新的攻击面在哪里?
- 如果被滥用,最坏的情况是什么?
保持警惕,持续审计,才能在云原生的世界里安全前行。
参考资料
- containerd 检查点功能文档:https://github.com/containerd/containerd/blob/main/docs/checkpoints.md
- CRIU 官方文档:https://criu.org/Main_Page
- CVE 详情:containerd checkpoint 系列 CVE
- OCI 镜像规范:https://github.com/opencontainers/image-spec
- Kubernetes 检查点 API:https://kubernetes.io/docs/concepts/workloads/pods/checkpoint-restore/