ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

客体重用机制深度解析:从内存清零到虚拟化安全防护

客体重用机制深度解析:从内存清零到虚拟化安全防护 前几天在一台新开的云服务器上做安全基线检查顺手用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_alloc1和init_on_free1跑数据库和编译任务没有遇到可感知的退化。Windows这边有个很有名的设计叫零页线程Zero Page Thread。系统在后台持续把空闲物理页清零分配内存时优先分配给已经清零的页。这种做法把清除从分配的关键路径上剥离出去属于一种预清零策略原理和Linux的init_on_free不同的地方在于它是在空闲时主动做而不是等到释放那一刻才做。两种策略各有优点本质都是在回答下个使用者看到的页是否是干净的。2.2 磁盘与文件系统删除不等于清除这是我认为最容易被误解的一点。很多人的认知里文件删掉了数据就没了但文件系统删除文件时通常只移除目录项和inode标记数据块里写的内容原封不动地躺在那里。只有当新文件恰好分配到这个块并且执行了写入那些旧字节才会被覆盖。更麻烦的是覆盖往往不是整块的——新文件内容比旧文件短时块的尾部可能仍然保留着上一轮的数据。实际测试里你可以自己做一个实验在ext4或NTFS上创建一个包含大量可识别字符串的文件删除它然后创建一系列新的小文件或使用dd写入较短的数据再用十六进制编辑器查看对应块。大概率会发现部分字符串死而复生。这些残留数据如果落在/tmp或者上传下载目录里碰到含有敏感信息的旧文件就是一个典型的信息泄露点。解决思路同样明确敏感数据不要依赖删除要主动覆盖或加密。Linux上用shredWindows上可以用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 bs4k count16384 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 bs1M count16 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 bs1M count16 2/dev/null done # 卸载或只读扫描使用 forensic 方式检查被删除文件的恢复情况如果你没有采集过文件系统的元数据这一步可能只在ext4/xfs上有效。要点是观察删除后的数据块在什么条件下会被复用、会不会部分覆盖。5.2 加固建议一套可照抄的配置清单工程加固需要分操作系统层、存储层、虚拟化层三个维度分别做。以下是我在实际项目中验证过的做法可以直接参考。层级措施说明Linux内核开启init_on_alloc1和init_on_free1分配/释放时清零最直接的内核级客体重用保护Linux内核开启page_poison1释放页填充固定字节帮助检测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清零[ ] 新分配磁盘是否做过非零块扫描这套清单不一定覆盖所有环境但它能把客体重用这个抽象概念翻译成一个个可验证的动作。安全这门功夫很大程度就体现在这些旧数据归谁看的角落里。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进