搜索全站

输入至少 2 个字符,查找相关内容。

↑↓ 选择 Enter 打开Esc 关闭

APFS 容器与快照

苹果 2017 年用 APFS 换掉 HFS+,换上三个 NTFS、ext4 都没有的结构。每一个都直接改写取证的做法。

最后更新 2026-10-09版本 v2.0维护 DigiForensics查看历史 1

一、概述

APFS 分析需要区分容器与卷,并结合 B-tree、快照和写时复制机制定位文件。容器布局、快照状态及加密配置会影响可提取的数据范围。

新问题 取证后果
一个分区里挤了多个卷(系统卷、数据卷、恢复卷、预启动、VM 卷) 拿分区偏移当卷偏移解析,结果全错
元数据只有一棵 B-tree 定位一个文件要理解 B-tree 节点结构,"查表"思路失效
原生快照 + 写时复制 删除的文件可能根本没被删,只是被标记为"不属于当前快照"

第三条是核心,也是接手 APFS 检材时最先要建立的认识。NTFS 的删除是真删除——数据簇进空闲位图,事后靠 $LogFile 回放还有一线希望。APFS 的"删除"只是在元数据 B-tree 里移除一个目录项指针;数据块本身在快照存在期间不会被覆盖。换个 xid 重新走一遍树,文件可能好好地躺在那里。说"可逆"要留个前提:块没被后续写入冲掉才算。

FileVault 是否开启应按设备逐一核实。在配备 Apple 芯片或 T2 芯片的 Mac 上,底层卷加密与 FileVault 的用户凭据保护属于不同层次;不能从 macOS 版本或 APFS 格式推断 FileVault 状态。参见 Apple:FileVault 卷加密。

这一层在分析环节的格式解析阶段,位置是"卷层之后、目录树之前"。上游是文件系统取证总览的第 3 步"逐卷判定文件系统类型"——如果那一步把 APFS 容器当成一个卷,一开始就走错了。

先后关系不是形式问题。2.1 那句话是全篇的判断原则:APFS 上唯一稳定的"外部可见面"是容器超级块和卷列表,所以第一步永远是读容器,而不是读卷。第三章步骤 1 标题里"不是卷"三个字,就是这个原则的操作化。

下游三件事:快照枚举结果决定你能看到几个时间点的系统状态(步骤 3);FileVault 密钥处理决定内容能不能读(步骤 4);提取与验证决定结论能不能写(步骤 5)。macOS 侧的工件分析在别处成篇——FSEvents 与时间机器 与 Spotlight 索引与元数据 都建立在这一层建立的空间与时间坐标之上。

落到具体证据上:

  • NXSB 容器超级块 —— 位于分区 + 32 字节处,签名 4e58 5342。不受 FileVault 影响——否则系统无法挂载。块大小字段在紧邻位置(第五章案例里读到 4096)
  • 卷列表 —— 在 NXSB 内部,同样不受加密影响。第五章案例里读到 6 个卷:Macintosh HD(oid=2, block_addr=40)、Macintosh HD - Data(oid=3, block_addr=80)、Preboot、VM 等
  • APSB 卷超级块 —— 位于各卷起点,签名 4150 5342。部分字段可见(如最后挂载/创建时间),其余加密
  • B-tree 节点 / 文件名 / 时间戳 / 文件内容 —— 全部加密。这是 2.5 那张表的分界线所在
  • checkpoint 描述符 —— 卷内,加密。它维护"哪个块属于哪个事务状态",理解它才能正确判断快照
  • 系统自动本地快照 —— 第五章案例在无密钥情况下成功枚举出 3 个(xid=4/9/15),最新一个 2024-08-21,距设备扣押仅 8 天
  • VM 卷 —— 交换内容所在。这是一个独立的卷,不在 Macintosh HD - Data 里,这也是"容器 ≠ 卷"的直接体现
  • DigiForensics.dd —— raw APFS 容器镜像。它是容器镜像,不是某个卷的镜像,第五章案例里容器分区起始扇区是 775920,所有偏移都要加上这个基址

走完这一层能判断的是这些:为什么必须先读容器再读卷、无密钥时 APFS 上哪些问题仍然能可靠回答,以及——同样重要的——哪些表述在无密钥时绝对不能写。

最后一条是最实用的一条边界。没有密钥,仍然能可靠地回答:"这是不是 APFS 卷、容器里有几个卷、每卷多少块、卷的最后挂载时间"。回答不了:"有哪些文件、文件名是什么、什么时候改的、内容是什么"。所以报告里必须把结构性结论(无密钥也成立)与内容性结论(需密钥)分开写。

2.5 那句"不要犯的错"值得单独强调:把"读不出文件名"写成"卷内无数据"或"已被彻底擦除",这两种表述在法律和事实层面都站不住。

有几件事不归这一层管:没有密钥时怎么拿到密钥,FileVault 的密钥层级与恢复密钥途径是加密篇的领域;Time Machine 备份集怎么解析,增量备份链是独立的一套结构;macOS 的应用层工件,统一日志、LaunchAgents、Keychain 那些在 macOS 板块各篇;Btrfs / ZFS 的快照机制,同为 CoW 但容器结构与快照语义不同,不能直接套用这里的判据。

往下读之前需要三样前置:GPT 分区结构(全程在"分区 + 偏移"上计算)、十六进制分析(NXSB / APSB 的字节判定靠这个)、以及只读挂载规范(为什么不能直接 mount 一个 APFS 容器去读——见只读挂载与安全接入)。

还有一个心理前提要提前打好:你需要接受"这一层大部分场景下你能做的事比想象中少"。第五章那个案例里,无密钥能给出的最强结构性结论是"3 个快照、最新一个距扣押 8 天"。委托方真正要的"某文件是否存在、最后修改时间",回答不了。先接受这个上限,再看这一层能给你什么。

二、核心原理

2.1 两层结构:容器(Container)不等于卷(Volume)

APFS 上最容易搞错的一点,是把 NXSB 和 APSB 当成同一层的两个名字。它们不是——混淆会导致偏移全错。

GPT 分区 (通常是分区 2)
┌──────────────────────────────────────────────────────┐
│  APFS 容器 (Container)  ← 魔数 NXSB,偏移 32 (0x20)  │
│  ┌────────────────────────────────────────────────┐  │
│  │ 卷 0: Macintosh HD       ← 魔数 APSB(卷首)   │  │
│  │ 卷 1: Macintosh HD - Data                      │  │
│  │ 卷 2: Preboot                                   │  │
│  │ 卷 3: Recovery                                  │  │
│  │ 卷 4: VM(交换,m 独立)                        │  │
│  │ 卷 5: Preboot Recovery / VM 各自独立            │  │
│  └────────────────────────────────────────────────┘  │
│  物理块池:容器内所有卷共享同一批块,块被统一分配    │
└──────────────────────────────────────────────────────┘
概念 魔数 位置 作用
容器超级块 NXSB NXSB 分区起始 + 32(0x20) 定义整个物理块池的大小、块大小、卷的个数与各卷 oid/xid
卷超级块 APSB APSB 每个卷自己的起点 定义该卷的文件系统参数(inode 数、块数、根目录 xid、checkpoint 描述符)

破案时先问一个具体问题:这个卷有多少块? 报告里"Macintosh HD - Data 占 3.2 TB"这句话的数据来自 NXSB 的 nx_block_count——它描述的是整个容器能用的块数,而每个 APSB 里的 apfs_phys_obj_count 是这个卷自己占的块数。两者不在一个量级上,因为共享池里还塞着别的卷。

答完这个才轮到偏移。所有偏移换算的基准是块大小,而这个字段写在 NXSB 里(案例读到 4096),不在 APSB 里。

# ① 确认容器在(NXSB 在偏移 32,不是 0)
xxd -s 32 -l 4 /work/DigiForensics.dd
# 期望:4e 58 53 42 = "NXSB"

# ② 关键:APSB 绝对不在分区起始,它在卷的物理块边界上
#    卷起点必须从 NXSB 里的 volno[].block_addr 读取,不能猜
xxd -l 4096 -s $(( 40 * 4096 )) /work/DigiForensics.dd | head -3
# 若在某个 4096 倍数偏移看到 41 50 53 42 = "APSB",该偏移即卷起点

最常见的错误:默认"分区起始就是卷起始",去分区起始找 APSB,什么都没找到就判定"未格式化"。实际差值可能很大(系统卷之后还有预启动卷、恢复卷等)。

2.2 元数据是一棵 B-tree(不是一张表)

NTFS 有 $MFT、ext4 有 inode 表,APFS 两者都没有——所有文件、目录、符号链接的元数据统一存进一棵平衡 B-tree 的叶节点里(因此常被称为 octree,因为 8 个指针字段)。

B-tree 根节点
┌─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐
│ p0      │ p1      │ p2      │ p3      │ p4      │ p5      │ p6      │ p7      │
└────┬────┴────┬────┴────┬────┴────┬────┴─────────┴─────────┴─────────┴─────────┘
     │         │         │         │            (深度 > 0 = 索引节点,继续下钻)
     ▼         ▼         ▼         ▼
  ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐
  │ 内部节点│ │ ...   │ │ 内部节点│ │  ...   │      深度 0 = 叶节点
  │ (summary)│      │ │      │ │      │
  └───────┘ └───────┘ └───────┘ └───────┘
                    │
                    ▼  (只有叶节点存真实记录)
  ┌──────────────────────────────────────────────────┐
  │ oid │ 记录类型 → 文件名 | 父 oid | 时间戳 | extents… │  ← 每条 kvo 记录
  └──────────────────────────────────────────────────┘
关键设计 说明 取证影响
统一 key-value-object 任何对象都是「键值对列表」,没有"文件类型"之分 解析器要按 type 字段区分,不能靠位置猜
叶节点才存完整记录 内部节点只存摘要(key 范围 + 子指针) 只读第一层会漏掉绝大部分文件
可变长记录 + 校验和 记录内有 crc32c 校验 校验失败 = 该区被改写或损坏,是重要的取证信号
CoW:节点不可原地修改 改一个字节 → 复制整条路径到新块 旧块内容保留在原处,直到被后续写入覆盖

CoW 是本篇最重要的取证机制。先看它救回来的东西:改一个字节,整条路径复制到新块,旧块内容原样留在原处,直到被后续写入覆盖。所以"某个文件被改过"的老版本,可能完整躺在未被覆盖的旧块里。这是 APFS 相对 NTFS 的一个独特优势。

但要注意反向陷阱。同一份内容可能在盘上存在多个物理副本——CoW 的副产品。直接按签名在原始镜像上做文件雕刻(见 10)会重复命中同一内容 N 次。做 carving 时必须做哈希去重。

换句话说:同一个特性既是取证优势,也是噪声来源。

2.3 checkpoint:维护"哪个块属于哪个事务状态"

APFS 没有 NTFS 那种"可回放的变更日志"。它的做法是周期性写一个 checkpoint,记录当前整个文件系统的映射状态:

checkpoint 字段 作用 取证价值
checkpoint descriptor B-tree 叶节点指针数组 指向当前最新的叶节点集合
mapped_block_count 已映射块总数 与 apfs_phys_obj_count 比对可发现悬挂块
ckp_xid checkpoint 事务号 单调递增的版本标识
next_ckp_id 下一个 checkpoint 的位置 顺此可串起所有 checkpoint,构成时间序列
ckp_maybe_zap 标记"此 checkpoint 之后数据需谨慎" 非 0 = 系统关机时此 checkpoint 正在使用,是未完成写入的信号

取证逻辑:next_ckp_id 是一条单向链表,从最新 checkpoint 往回走,能枚举出设备历史上所有 checkpoint 及其事务号。

这在概念上比 NTFS 的日志更直接。不需要回放,直接挑一个时刻的快照来读。

而且同时保留多个 checkpoint 是 APFS 的常态。这意味着你可以读取"删除发生之前"的 checkpoint 状态——纯 NTFS 上做不到。

2.4 快照:APFS 取证的核心价值

快照(Snapshot)记录的是某个 xid(事务号)时刻的卷状态。它是元数据层面的时间定格,与备份文件不同——所有文件的元数据仍在 B-tree 里,只是树被组织成了"当前状态"和"快照状态"两个视角。

特性 说明
CoW 支撑 快照创建后,被快照覆盖的块不原地写,而是写到新块 → 旧数据存活
元数据共享 未修改的文件,快照与当前版本共用同一份数据块,几乎不占空间
系统自动创建 macOS 定期(通常每日)自动创建本地快照,不限于 Time Machine
可被用户创建 手动快照、第三方工具快照
Time Machine 快照 共享同一套 APFS 快照机制,只是由 TM 服务触发

取证的三个核心价值:

(1)删除前状态可直接读取。这是最强的一条。

NTFS 上"恢复删除文件"依赖 $LogFile 回放,复杂且常失败;APFS 上则是换一个 xid 重新走一遍 B-tree。文件可能好好地躺在那里:

# 列出容器内所有快照及其 xid
apfsutil /work/DigiForensics.dd

(2)Time Machine 备份与本地快照同构。因为 Time Machine 用的就是 APFS 快照,所以:"最后一次自动备份是什么时候"这个问题,在 APFS 上可以通过枚举快照创建时间直接回答——不需要去解析 Time Machine 的 .tmbackup 元数据库。

(3)跨卷观察同一个动作。一个用户动作(比如删除文件)会同时改动多个卷的元数据。快照让你能对比同一时刻不同卷的状态,还原"这个操作牵涉哪些卷"。

关键提醒:快照保护的是已被快照覆盖的块不被原地改写,但不阻止新数据写入这些块。如果设备在删除后继续被大量使用(装软件、下载文件、跑系统更新),旧块会被后来的写入覆盖,快照对应的物理内容随之消失。快照的价值随"删除后使用时长"快速衰减——这是评估检材时必须问的问题(关机距今多久)。

2.5 加密边界:哪些结构在加密层之外

结构 偏移 是否受 FileVault 影响
NXSB 容器超级块 分区 + 32 ❌ 不受影响(否则无法挂载)
卷列表 / 各卷 oid、xid NXSB 内部 ❌ 不受影响
APSB 卷超级块头部 各卷起点 ⚠️ 部分字段可见
B-tree 节点 / 文件名 / 时间戳 卷内 ✅ 全部加密
文件内容 卷内 ✅ 全部加密
checkpoint 描述符 卷内 ✅ 加密

这个边界是 APFS 取证最实用的知识。

没有密钥时,你仍然能可靠地回答:"这是不是一个 APFS 卷?容器里有几个卷?每个卷多少块?卷的最后一次挂载/创建时间(部分字段在 APSB 头部)?"

回答不了的是:"有哪些文件、文件名是什么、什么时候改的、内容是什么。"

因此报告里必须明确区分:"结构性结论"(无密钥也成立)与"内容性结论"(需密钥)。

不要犯的错:把"读不出文件名"写成"卷内无数据"或"已被彻底擦除"。这两种表述在法律和事实层面都站不住。


三、操