前几天在一台新开的云服务器上做安全基线检查,顺手用hexdump扫了一下第一块数据盘,结果看到了不应该存在的东西——文件系统签名之前还残留了一段可读的文本片段。这个现象在"操作系统与虚拟化安全"这堂课里有一个专门的名词,叫客体重用机制。很多做运维和开发的朋友第一次听到这个词会懵,但它管的事其实特别朴素:一块内存、一个磁盘块、一页缓存,上一个使用者留下的数据,凭什么不能让下一个使用者看到。
这篇内容就围绕这个机制展开,包括它在操作系统里怎么落地、在虚拟化环境里为什么更难办、攻击者会怎么利用失效的客体重用机制,以及我们自己该怎么验证和加固。适合安全工程师、虚拟化平台运维、云平台管理员,以及正在复习系统安全知识、想真正搞清楚"剩余信息保护"到底在保护什么的同学。
1. 客体重用机制到底是什么:先解决"旧数据归谁看"的问题
1.1 从一个云硬盘的残留数据说起
我那次检查的操作并不复杂:新建一台虚拟机,挂载一块全新的数据盘,不格式化,直接dd几个扇区出来看。结果在某些偏移位置看到了非零数据,用strings还能提取出片段。这放在真实攻击场景里意味着什么?意味着如果这台虚拟机之前属于另一个租户或另一个部门,而平台在重新分配磁盘时没有做清除操作,新租户就可能直接读到旧租户的业务数据。
在安全和操作系统的语境里,发起访问的进程、用户被称为"主体",被访问的数据载体——内存页、文件、磁盘块、寄存器——被称为"客体"。客体重用机制要求的是:当一个客体被归还给系统、再由系统分配给另一个主体之前,必须保证所有残留数据都不可读。这不是一个华而不实的设计要求,而是跟访问控制同等重要的基础安全属性。
1.2 从TCSEC到现代基线检查:为什么安全标准单列一条
客体重用(Object Reuse)在安全评估体系里是个老资格概念。早期的可信计算机系统评估标准(TCSEC,也就是常说的橘皮书)把安全要求划分成多个级别,其中很早就出现了"对象重用"这一类:系统必须确保客体在被重用之前,不会向新主体暴露之前主体留下的信息。这里说的客体覆盖范围很广,包括内存、外部存储、寄存器,甚至硬件设备中的缓冲区。
后来的通用标准和国内等级保护里的"剩余信息保护"要求,本质上都是这个思想的延续。但理论归理论,实际工程里做没做到,完全看细节。我在做基线检查时养成的一个习惯就是,不只看配置文件里写了什么,还要真正去读"释放后再次分配的资源",这就是客体重用机制在实战层面的检验方式。
1.3 用酒店房间来理解:退房之后必须打扫
客体重用机制特别适合用一个生活场景来理解:假设酒店的房间是一个"客体",客人在房间里留下的物品、密码、文件就是"残留信息"。酒店不可能让下一个客人直接住进上一任客人还没打扫的房间,否则前一个房客的护照复印件、手写笔记甚至保险柜密码都可能被下一个人看到。
操作系统也一样。进程A申请了一块内存,写入密钥和明文数据,释放后系统把这块内存交给进程B。如果系统不做清理,进程B只需要扫描这块区域就能还原出进程A的敏感信息。所以客体重用机制的核心动作是两个:一是分配时清零,二是释放时清零。具体采取哪一种,不同场景有不同取舍,后面章节细说。
1.4 客体重用覆盖的客体范围
把所有可能残留数据的"载体"列一遍,会发现比想象中广得多。我在下面的表格里整理了常见客体类型以及对应的残留风险:
| 客体类型 | 典型残留信息 | 风险触发场景 |
|---|---|---|
| 物理内存页 | 密钥、明文数据、进程环境变量 | 进程释放后重新分配给其他进程 |
| 磁盘块/文件 | 旧文件内容、临时数据 | 文件删除后新文件复用同一数据块 |
| 交换分区 | 被换出的进程匿名内存 | 系统内存压力大时频繁换入换出 |
| 休眠文件 | 整个内存镜像 | 笔记本休眠后镜像未做加密或未清除 |
| 文件系统元数据 | 旧文件名、inode时间戳、扩展属性 | 格式化或删除后块被复用 |
| 虚拟CPU寄存器 | 上下文数据、密钥片段 | 虚拟机迁移或vCPU重新调度后 |
| 虚拟设备缓冲区 | 网络数据包、显存内容 | VM之间复用同一个虚拟设备后端 |
| 缓存(CPU Cache/TLB) | 数据碎片 | 侧信道攻击场景 |
这张表可以帮助判断一个系统到底有没有做好客体重用。凡是"资源从一个实体转移到另一个实体"的路径,都需要确认没有数据残留。
2. 操作系统层面的客体重用场景:能漏的远不止内存
2.1 内存页的分配与清零:Linux和Windows各自的做法
内存是最典型的客体。Linux内核在分配物理页面时,伙伴系统会从空闲链表里取页。如果调用者使用了类似__GFP_ZERO的标记,内核会在把页交给调用者之前清零;如果没有这个标记,理论上调用者拿到的可能是上一个使用者留下的内容。
现代内核提供了一套更严格的安全机制,比如init_on_alloc和init_on_free这两个启动参数。前者让所有分配的页面都先清零,后者在页面释放时立即清零。有人可能担心性能开销,实测中开销确实存在,但对于安全敏感的业务来说,这个代价是可以接受的。我在自己的服务器上开了init_on_alloc=1和init_on_free=1,跑数据库和编译任务没有遇到可感知的退化。
Windows这边有个很有名的设计叫零页线程(Zero Page Thread)。系统在后台持续把空闲物理页清零,分配内存时优先分配给已经清零的页。这种做法把"清除"从分配的关键路径上剥离出去,属于一种预清零策略,原理和Linux的init_on_free不同的地方在于,它是在空闲时主动做,而不是等到释放那一刻才做。两种策略各有优点,本质都是在回答"下个使用者看到的页是否是干净的"。
2.2 磁盘与文件系统:删除不等于清除
这是我认为最容易被误解的一点。很多人的认知里,文件删掉了数据就没了,但文件系统删除文件时通常只移除目录项和inode标记,数据块里写的内容原封不动地躺在那里。只有当新文件恰好分配到这个块并且执行了写入,那些旧字节才会被覆盖。更麻烦的是,覆盖往往不是整块的——新文件内容比旧文件短时,块的尾部可能仍然保留着上一轮的数据。
实际测试里,你可以自己做一个实验:在ext4或NTFS上创建一个包含大量可识别字符串的文件,删除它,然后创建一系列新的小文件或使用dd写入较短的数据,再用十六进制编辑器查看对应块。大概率会发现部分字符串"死而复生"。这些残留数据如果落在/tmp或者上传下载目录里,碰到含有敏感信息的旧文件,就是一个典型的信息泄露点。
解决思路同样明确:敏感数据不要依赖"删除",要主动覆盖或加密。Linux上用shred,Windows上可以用cipher /w或者专业的擦除工具。要注意的是SSD和机械盘机制不同,SSD有磨损均衡、预留空间和垃圾回收,逻辑地址的覆盖不保证物理存储单元真的被改写,所以对SSD来说,全盘加密比反复覆盖更可靠。
2.3 swap、休眠文件与临时目录:三个容易漏的地方
swap是把匿名内存页换到磁盘的机制,问题在于,页换出的时候写入磁盘,换回内存的时候,磁盘上那块区域并不会被清除。也就是说,交换分区会积累历史上所有被换出的数据。如果攻击者能读到swap设备——比如拿到物理磁盘或者云平台的管理员权限——就可能恢复出大量进程数据。
休眠文件更直接,它本质就是一份完整的内存镜像。Windows的hiberfil.sys、Linux的swap空间在休眠模式下,都是把整个RAM写进去。所以要么加密休眠镜像,要么干脆禁用休眠,要么配合全盘加密使用。不要以为用自己的电脑就没事,一台丢失的笔记本如果开了休眠,而镜像没加密,拿到硬盘的人理论上可以还原出你休眠前的内存内容。
临时目录也是重灾区。很多程序把临时文件写到/tmp或%TEMP%,用完不一定删除,删除了也不一定清干净。把/tmp挂载为 tmpfs(内存文件系统)是个常见做法,重启即清空,但要注意,tmpfs里的文件删除后,内存页返回内核再分配给其他进程时同样要依赖清零机制。所以安全做法是"加密 + 清零 + 短生命周期"三者配合。
2.4 文件系统元数据:常被忽略的残留
文件系统本身的管理数据结构也是客体。比如inode里记录的历史时间戳、文件所有者、扩展属性,目录项里遗留下来的文件名,都可能在被释放后重新分配给其他文件。格式化磁盘时,只有少数元数据区域被重置,大量inode和目录块可能保留上一次文件系统的痕迹。
有经验的取证人员会通过这些残留元数据还原出"已删除文件"的名字、创建时间甚至部分内容。这就是为什么涉密介质退役时,强调要使用专门的擦除工具,而不是简单格式化一遍。普通格式化相当于只擦掉了"目录索引",数据本体还在。
3. 虚拟化环境:客体重用风险被放大了一个数量级
3.1 虚拟机内存的宿主编排:物理页怎么在VM之间流转
虚拟化引入了一个新的资源流转路径:虚拟机内存来自宿主机物理内存。一个guest释放的物理页,经过hypervisor的管理,可能被分配给另一个guest。如果宿主机在把物理页交给新guest之前没有清零,跨虚拟机的内存残留就可能发生。
早期的虚拟化平台上确实出现过类似的隐患——guest通过某种途径读到宿主机或其他guest数据的事件。这也是为什么主流虚拟化平台在分配内存给虚拟机时,都会有清零动作,或者依赖宿主机操作系统的分配清零机制。但问题在于,不同虚拟化后端的实现成熟度不一样。比如QEMU/KVM使用普通进程模型模拟guest内存时,新映射的匿名页通常是清零的;但在某些需要预先分配或大页内存的场景下,物理页的来源可能不复用零页,如果VMM没有主动清零,风险就会重新出现。
对安全要求高的环境,我会建议关闭不必要的内存页共享功能。内存页共享(比如KSM或透明页共享)允许不同虚拟机映射到相同物理页以节省内存,但它在"哪个guest能读哪份物理页"这件事上引入了额外的复杂度,一旦合并判断出错或未及时解除合并,数据边界就可能模糊。安全优先时,宁可牺牲一点内存复用率,也要降低跨VM数据串味的可能。
3.2 磁盘置备模式:thin provisioning和"新磁盘"的真相
云环境里最容易被忽略的客体重用问题发生在磁盘上。虚拟磁盘有两种常见置备方式:厚置备和精简置备。厚置备会一次性分配完整大小,精简置备一开始只分配很小空间,按需增长。
风险在于,一些平台在做精简置备时,新分配的存储块可能直接来自存储系统的空闲空间,而这些空间此前属于其他虚拟机甚至其他租户的已删除数据。如果存储层不做清零,新虚拟机读到的就是别的虚拟机的残留数据。我开头提到的那个实验,就是这么复现的。平台管理员如果知道自己的存储后端是什么类型、分配路径有没有清零动作,这个风险其实可控,但很多情况下,存储层的细节对租户完全透明,租户只能自己做好加密。
所以对使用云盘的用户,我的建议很简单:重要数据一律使用加密文件系统或全盘加密。不是不信任平台,而是在客体重用机制无法证伪的情况下,加密是唯一能让你不依赖平台实现"残留不可读"的方法。
3.3 快照、克隆与迁移:数据副本的生命周期管理
快照是虚拟机安全里另一个必须关注的点。快照文件会记录虚拟机的磁盘状态,有些快照还包含内存状态。删除快照后,这些存储块变回空闲状态,如果不经清除就重新分配,同样属于客体重用问题。
克隆更直接。从一个模板克隆一台新虚拟机,模板里的数据会原样复制。如果模板本身包含过期密钥、历史配置文件、临时文件——这些"残留"会被合法地带到每一台克隆机上。这个跟客体重用机制不太一样,更像"模板污染",但后果类似。我的经验是,模板要定期重建,不要在一个用了两年的模板上不断打补丁,因为里面的残留只会越来越多。
迁移场景还有一个容易忽略的细节:虚拟机从一台宿主机迁移到另一台后,源宿主机上的内存页、磁盘缓存如果不清除,等于把敏感数据留在了原地。很多平台默认迁移后源端会清理,但清理是否彻底,建议通过实际测试验证,而不是看默认配置。
3.4 vCPU寄存器、虚拟设备与vTPM:最后一个容易被忘掉的角落
CPU寄存器是比内存更小的客体。进程切换时,寄存器内容被保存和恢复;虚拟机切换时,vCPU的寄存器状态同样需要在宿主和guest之间交换。如果虚拟化层保存/恢复逻辑有缺陷,或者vCPU被重新分配给另一个guest时没有清空旧的寄存器上下文,理论上会产生跨VM的寄存器泄露。
设备缓冲区也是类似的道理:虚拟网卡接收队列、虚拟显卡的显存、虚拟磁盘的cache,都可能在被两个guest轮流使用后残留数据。还有vTPM(虚拟可信平台模块),它里面保存的密钥、计数器状态如果处理不当,也会成为敏感客体。这类问题比较底层,靠应用层解决不了,只能依赖虚拟化平台本身的实现质量。所以选择虚拟化产品时,安全更新频率、漏洞响应速度反而比某些功能指标更重要。
4. 攻击者视角:客体重用失效是怎么被利用的
4.1 被动泄漏:分配了内存但不初始化
攻击者不一定需要多复杂的漏洞。很多系统服务在分配内存后没有正确初始化就直接使用了缓冲区,把敏感信息写进日志、网络响应或调试输出。这种问题在C语言程序中尤其常见,比如一个数据结构里的char buf[BUF_SIZE],局部变量不初始化,里面的内容来自栈上之前的函数调用残留;如果这个缓冲区被发送到网络,就等于把其他进程的残留数据打包送给攻击者。
安全圈常说的"未初始化内存泄露",本质上就是客体重用机制在编程层面的违约。内核驱动的信息泄露漏洞也常见这类原因:从内核内存池分配缓冲区,没有清零就拷贝到用户态。我印象里Linux内核历史上多次出现过此类漏洞通告,修复方式通常就是加memset或改用kzalloc这类自动清零的分配接口。这提醒我们,客体重用不只是硬件或虚拟化平台的事,应用代码和驱动代码同样在承担责任。
4.2 主动构造:释放后重利用与取证
相比被动等待,主动利用更有威胁。攻击者可以先摸清系统分配资源的规律,让目标进程使用某一块内存/磁盘空间,然后抢在系统重新分配之后扫描被释放的区域,提取残留数据。这个思路跟堆风水里的"占位"技术有相似之处,但目的不是写,而是读。
内存取证工具的底层逻辑正好相反。Volatility这类工具可以通过分析内存镜像,寻找已经被释放但还没被覆写的数据结构。在真实案件和攻防演练里,从内存dump中恢复出虚拟机管理程序的口令、进程明文数据,都是常规操作。这从侧面说明:只要系统没有做释放清零,数据就比想象中活得久得多。
磁盘角度的主动利用更常见。很多"数据恢复"就是这么做的:删除文件、格式化分区后,数据块还在;用恢复工具扫描全盘,根据文件签名重组文件。这个能力同样能被攻击者用来读取目标系统上一个租户或个人留下的数据。二手硬盘交易市场反复出现的"买到别人隐私"新闻,本质就是客体重用机制在"消费者设备"上失效。
4.3 我复现过的一次"新磁盘含旧数据"实验
回到开头那个云硬盘实验,我完整说一遍过程,方便你自己复现。
先说环境:一个小型内部虚拟化平台,存储用的是本地目录,虚拟磁盘以文件形式存在。平台管理员创建了一个新的VM,磁盘文件是自动生成的空文件。我直接登录VM,对/dev/vdb做了读取。
# 只读扫描整个磁盘前64MB,跳过完全为0的块 dd if=/dev/vdb bs=4k count=16384 2>/dev/null | hexdump -C | head -50正常情况下,新生成的空文件读出来应该全是0。但我看到了非零数据,用strings提取后半数可辨识:
strings -n 8 /dev/vdb | head -20结果里出现了类似主机名、路径片段、甚至一段疑似配置文件的文本。平台的存储后端显然复用了之前被删除的虚拟磁盘文件块,而且没有清零。这个实验让我养成了三个习惯:凡是云上新磁盘,先用dd/hexdump过一遍;凡是虚拟机退役,磁盘必须安全擦除;凡是可疑环境,一律加密。
你如果想复现,不需要云平台,自己在本机就能做:创建一个稀疏文件当虚拟磁盘,往里写一段可识别数据,删除文件,然后再创建两个大小相近的稀疏文件,扫描新文件是否有旧数据。注意这依赖文件系统的块分配行为,不一定百分百复现,但值得一试。
4.4 虚拟化平台的历史公告与防护演进
虚拟化厂商对客体重用问题的重视不是一天两天了。早年的hypervisor在内存分配上确实粗放,后来陆续出现跨VM信息泄露的研究报告,各平台才开始补齐"在分配给guest之前清零"的机制。到了现在,主流平台基本都会保证新分配内存为零,至少在逻辑上做到。
但要注意,逻辑上的"清零"和物理上的"清零"仍然有差距。比如宿主机开启了内存热插拔、大页内存预分配,或者使用某些透传设备(PCIe passthrough、GPU直通),guest会直接接触物理资源,绕过了VMM的清零逻辑。这类场景要求管理员有更高的安全意识:透传设备在重新分配前是否复位?GPU显存里的数据是否会跨VM残留?这些问题的答案,往往需要翻硬件厂商的文档,而不是只看虚拟化平台设置。
5. 工程落地:验证、加固与生命周期管理
5.1 怎么验证一台主机是否真的做了客体重用
我不建议只凭配置项或者厂商报告判断"平台已经清零"——验证才是硬道理。
内存层面的验证比较难,因为用户态很难直接感知内核分配页是否清零。一个可行的做法是运行一个反复分配大缓冲区并扫描数据的测试程序,把分配结果通过/proc观察内存页的周期变化,但这属于间接证据。更直接的方式是写一个内核模块,向伙伴系统申请无标记页面,检查其内容是否全零,不过这需要你有内核开发能力。对大多数团队来说,内存部分信任内核的init_on_alloc/init_on_free配置即可,先把验证重点放在磁盘上。
磁盘验证最容易落地,我每次做基线检查必做的动作:
- 新创建一块虚拟磁盘,不格式化,用
dd读取并统计非零块数量; - 删除一个填充了特征数据的文件,然后用取证工具恢复,检查是否能恢复出内容;
- 对SSD执行
blkdiscard或对文件系统执行fstrim,再检查未分配区域; - 如果有条件,用
badblocks -w对退役磁盘做全盘写入验证,确认覆盖生效。
一个可用的命令组合是:
# 创建一个包含特征数据的文件 dd if=/dev/urandom of=/tmp/marker.bin bs=1M count=16 head -c 64 /tmp/marker.bin | sha256sum > /tmp/marker.sha256 # 删除该文件,然后创建多个新文件,观察块是否被复用 rm -f /tmp/marker.bin sync for i in $(seq 1 20); do dd if=/dev/zero of=/tmp/fill_$i bs=1M count=16 2>/dev/null done # 卸载或只读扫描,使用 forensic 方式检查被删除文件的恢复情况如果你没有采集过文件系统的元数据,这一步可能只在ext4/xfs上有效。要点是观察删除后的数据块在什么条件下会被复用、会不会部分覆盖。
5.2 加固建议:一套可照抄的配置清单
工程加固需要分操作系统层、存储层、虚拟化层三个维度分别做。以下是我在实际项目中验证过的做法,可以直接参考。
| 层级 | 措施 | 说明 |
|---|---|---|
| Linux内核 | 开启init_on_alloc=1和init_on_free=1 | 分配/释放时清零,最直接的内核级客体重用保护 |
| Linux内核 | 开启page_poison=1 | 释放页填充固定字节,帮助检测use-after-free并减少信息残留 |
| Linux内核 | slab_nomerge关闭slab合并 | 防止不同类型的对象共用slab缓存导致跨对象残留 |
| 交换分区 | 启用dm-crypt加密swap,或改用zram | 防止换出页里的明文数据长期保留在磁盘上 |
| 休眠 | 禁用休眠或启用加密休眠 | 避免完整内存镜像落盘 |
| 文件系统 | 敏感目录使用加密文件系统(LUKS、BitLocker) | 即使底层块残留,没有密钥也无法还原 |
| 临时目录 | /tmp挂载为tmpfs | 重启清空,缩短数据生命周期 |
| SSD | 定期fstrim,退役时执行ATA Secure Erase | 对齐SSD物理擦除特性 |
| 数据删除 | 使用shred/wipe覆盖敏感文件后再删除 | 不依赖文件系统的"删除"语义 |
| 虚拟化 | 新建磁盘选择"快速置零"或完整置零模式 | 注意区分精简置备的零填充语义 |
| 虚拟化 | 关闭KSM/透明页共享(安全优先时) | 降低跨VM内存合并的边界风险 |
| 虚拟化 | 虚拟机退役流程中增加磁盘安全擦除步骤 | 防止退役磁盘残留被再次分配 |
| 应用编码 | 使用explicit_bzero/SecureZeroMemory清零密钥缓冲区 | 编译器优化不会跳过这类清理调用 |
这里特别想说一下explicit_bzero。很多开发者在程序退出前用memset清空密钥缓冲区,但编译器可能认为这个memset是"无用操作"并优化掉——因为缓冲区不会再被读取。这就是为什么需要专门设计的不被优化的清零函数。这个细节很小,但放在客体重用视角看非常有代表性:你以为你在清理,实际编译器帮你把这个清理动作"优化"没了。
5.3 生命周期管理:从创建到销毁都要有规矩
客体重用不是一个静态配置项,它跟资源生命周期强相关。一个完整的资源生命周期至少包含创建、分配、使用、释放、销毁五个阶段,每个阶段都要回答"上一轮数据会不会留下来"。
创建阶段:磁盘、虚拟机、内存池建立时,确认底层空间来源是否清零。分配阶段:敏感数据要加密,加密密钥本身要使用安全内存管理函数。使用阶段:临时数据尽量放内存,敏感文件用后即焚。释放阶段:操作系统的页面清零、swap清理、临时文件删除策略都要开启。销毁阶段:虚拟机退役流程要包含数据擦除和擦除验证,而不仅仅是"删除虚拟机"按钮。
我在帮一家企业做虚拟化环境安全加固时,专门制定过一套虚拟机退役SOP:先备份(如果需要),再踢出负载,然后执行磁盘加密擦除或ATA Secure Erase,最后删除虚拟机并记录擦除日志。审计人员抽查时能看到"擦除时间、擦除方式、擦除验证结果"三个字段,这套流程到现在还在用。
5.4 给安全基线检查留一张自检清单
最后把我平时做检查时使用的清单精简成一份,方便你直接照着看。
- [ ] 内核是否开启
init_on_alloc/init_on_free,以及是否开启page_poison - [ ] swap是否加密,或是否使用zram;休眠是否禁用/加密
- [ ]
/tmp是否使用tmpfs,或是否有定期清理策略 - [ ] 敏感文件删除是否使用覆盖工具,还是只执行
rm - [ ] SSD是否定期执行TRIM,退役SSD是否执行Secure Erase
- [ ] 新建虚拟磁盘是否全零分配;存储后端是否有清零机制
- [ ] 虚拟机退役流程是否包含擦除验证
- [ ] KSM/透明页共享是否关闭(安全要求高时)
- [ ] 应用密钥缓冲区是否使用
explicit_bzero/SecureZeroMemory清零 - [ ] 新分配磁盘是否做过非零块扫描
这套清单不一定覆盖所有环境,但它能把客体重用这个抽象概念翻译成一个个可验证的动作。安全这门功夫,很大程度就体现在这些"旧数据归谁看"的角落里。