搜索全站

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

↑↓ 选择 Enter 打开Esc 关闭

F2FS 结构

F2FS 是三星为 NAND 闪存设计的文件系统,Android 手机上见到的多半是它。

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

一、概述

F2FS 是三星为 NAND 闪存设计的文件系统,Android 手机上见到的多半是它。

取证上的麻烦不在字段多,而在它的每个设计决定都来自闪存的物理限制。先把这层因果理清,后面的结构就都顺理成章了。

闪存特性 后果 F2FS 的应对
不能原地覆盖 已有数据不能改写,只能擦除后重写 日志型结构:新版本写到别处,旧版本等 GC 才擦除
擦除以"块"为单位 擦除粒度远大于写入粒度 段(Segment) 作为擦除单位,固定 2 MB
擦写次数有限 频繁擦写会损坏闪存 冷热数据分离:高频更新的元数据单独放
顺序写远快于随机写 随机写代价高 主区顺序写,日志区承接随机写

把这四条代进去,F2FS 取证最核心的一个问题就浮出来了:

文件"当前"的内容不写在原位置,而是写在最近一次更新写入的位置。旧版本在垃圾回收(GC)执行前一直留在盘上。

这意味着 F2FS 上"删除"的文件数据可能完整保留——但其位置由 NAT 和当前 checkpoint 决定,不能像 ext4 那样靠 inode 列表直接枚举已删文件。

跟 ext4 对照一下差异所在:

维度 ext4 F2FS
元数据形态 inode 数组 + 间接块 日志型元数据 + 映射表
已删文件枚举 debugfs lsdel 直接列出 无直接等价命令,需解析 NAT + checkpoint
旧版本数据 通常被覆盖或仅剩残留 GC 前完整保留旧版本
工具成熟度 高(e2fsprogs + TSK) 低,以专用工具为主

二、核心原理

2.1 分区布局:六个区域

F2FS 把整个分区切成六个区域,每一块的起始块地址都在超级块里明写。这张布局表是破案时的导航图——先确认自己在哪一层,再谈解析。

┌──────────┬──────────┬──────────┬──────────┬──────────┬──────────────┐
│ 超级块区   │ 检查点区  │ 段信息区  │ 元数据区  │ 汇总区    │  主数据区     │
│ (SB)     │ (CP)     │ (SIT)    │ (NAT)    │ (SSA)    │ (Main Area)  │
├──────────┼──────────┼──────────┼──────────┼──────────┼──────────────┤
│ 1 段 2MB  │ 2 段 4MB │ 2 段 4MB │ 2~34 段  │ 1 段 2MB │  剩余全部    │
└──────────┴──────────┴──────────┴──────────┴──────────┴──────────────┘
区域 全称 作用 取证价值
SB Superblock 魔数、版本、各区域块地址、块大小 定位一切的起点
CP Checkpoint 文件系统当前状态快照 决定"当前"文件映射
SIT Segment Information Table 每段的有效块数、类型、有位图 段类型揭示冷热分离
NAT Node Address Table inode 号 → node 块地址 恢复文件内容的核心映射
SSA Summary Area 段内块摘要、当前写指针 定位段内最新数据
MA Main Area 实际的用户数据与元数据 提取文件内容的区域

段(Segment)是擦除单位,固定 2 MB(512 个 4 KB 块)。段的划分让"擦除"这件事有了明确边界。

2.2 超级块:魔数 0xF2F52010

主超级块位于文件系统起始 + 1024 字节(0x400),与 ext 系列的偏移习惯一致。魔数为 F2FS_MAGIC = 0xF2F52010(小端读作 10 20 F5 F2)。

偏移  长度  字段                   取证意义
0x00  4    magic = 0xF2F52010      文件系统确认
0x04  4    major_ver               主版本号(与工具兼容性相关)
0x08  4    minor_ver               次版本号
0x0C  4    log_sectorsize          log2 扇区大小,典型 9(512 字节)
0x10  4    log_sectors_per_block   log2 每块扇区数
0x14  4    log_blocksize           log2 块大小,典型 12(4096 字节)
0x18  4    log_blocks_per_seg      log2 每段块数,典型 9(512 块 = 2 MB)
0x1C  4    segs_per_sec            每区段数
0x20  4    secs_per_zone           每区扇区数
0x24  4    checksum_offset         校验和偏移
0x28  8    block_count             用户块总数(**全表只有这一项是 8 字节**)
0x30  4    section_count           区总数
0x34  4    segment_count           段总数
0x38  4    segment_count_ckpt      检查点区段数(通常 2)
0x3C  4    segment_count_sit       SIT 段数
0x40  4    segment_count_nat       NAT 段数
0x44  4    segment_count_ssa       SSA 段数
0x48  4    segment_count_main      主区段数
0x4C  4    segment0_blkaddr        segment 0 起始块
0x50  4    cp_blkaddr              检查点区起始块
0x54  4    sit_blkaddr             SIT 起始块
0x58  4    nat_blkaddr             NAT 起始块
0x5C  4    ssa_blkaddr             SSA 起始块
0x60  4    main_blkaddr            主区起始块
0x64  4    root_ino                根 inode 号
0x68  4    node_ino                node inode 号
0x6C  4    meta_ino                meta inode 号

换算公式:块大小 = 2^log_blocksize,段大小 = 2^log_blocks_per_seg × 块大小。这三个 log_ 字段必须实测,不能假设——典型值是扇区 512 / 块 4096 / 段 2 MB,但格式化参数可改。部分实现还在后续块(如 0x1400)放第二份超级块,备份位置依版本而异,需结合实际实现验证。

xxd -s 1024 -l 16 /work/DigiForensics.dd
# 00000400: 1020 f5f2 0100 1000 0900 0000  .. .....   ← 0xF2F52010 ✓

2.3 Checkpoint:定义"当前"状态

内容 作用
checkpoint_ver 检查点版本号,每次更新递增——比对两个 checkpoint 即可知道文件系统发生过写入
user_block_count / valid_block_count 块总量与有效块量,差值反映垃圾量
free_segment_count 空闲段数,偏小说明 GC 压力存在
cur_node_segno / cur_node_blkoff 当前 node 写位置(node = 元数据块)
cur_data_segno / cur_data_blkoff 当前数据写位置
ckpt_flags 是否需要恢复、是否需要回滚
next_free_nid 下一个可用 node ID,累计分配量,可作"曾创建过多少文件对象"的量级参考

F2FS 用的是双 checkpoint 机制,两个区交替写,每个 checkpoint 自带版本号和 CRC。

这带来一个很实用的性质:拿到两个 checkpoint 就能判断哪个更新、哪个损坏,也能看出两次写入之间文件系统发生了多大变化。

valid_block_count 下降说明期间发生了删除或覆盖。

ckpt_flags 里出现"需要恢复"标志,说明上次没有正常卸载。此时元数据里可能有未完成的写入,结论须标注"可能存在未完成操作"。

2.4 NAT:inode 到物理块的映射表

NAT 是 F2FS 取证最核心的结构,把 inode 号(ino)映射到该 inode 所在的物理块地址。每条 f2fs_nat_entry 为 9 字节:version(1) | ino(4) | block_addr(4)。

从 inode 到内容的完整链条:

目录项 → ino
  ↓ 查 NAT
block_addr(inode 所在块)
  ↓ 读块
f2fs_inode:i_inline? → 内联数据 或 i_addr[0..11] 直接块 + i_nid 间接节点
  ↓ 逐级解析
direct_node / indirect_node / double_indirect_node
  ↓
全部数据块地址 → 按序读出 → 拼回文件

链条中任一环缺失即无法还原。恢复文件时要把 NAT 命中情况、inode 是否内联、间接节点是否完整都记录下来——这些就是判断"文件能否完整恢复"的依据。

NAT 里的映射是"当前有效版本"的映射。旧版本的映射在旧 checkpoint 对应的旧 NAT 页里——这正是"GC 前旧数据仍在"的技术原因,也是找回旧版本文件的入口。

2.5 SIT 与段类型:冷热分离的证据

SIT 的每条 f2fs_sit_entry 记录一个段的状态:vblocks(2)(低 10 位 = 有效块数,高 6 位 = 段类型)、valid_map(64)(512 位位图,标记段内哪些块有效)、mtime(8)(该段最后一次 GC 时间)。

段类型 含义 取证含义
SUM 汇总段(SSA) 系统元数据
NODE 元数据段(inode、目录) 文件频繁被改的区域
DATA 数据段(文件内容) 用户文件主体
COLD 冷数据段 长期未改的数据(旧照片、视频)
WARM / HOT 温 / 热数据段 近期被频繁读写
UNUSED 空闲段 可能已 GC 或待写入

mtime 尤其值得看。某段 mtime 很旧但仍被引用,说明该数据长期未变动;某段 mtime 突然很新,说明该段近期被 GC 过,原有内容已被迁移或擦除。

某段变成 UNUSED,其上的数据很可能已被擦除,残留可能性极低。valid_map 位为 0 的块已经不是该文件的数据了。

SSA 的每条 f2fs_summary 为 7 字节(nid(4) | version(1) | ofs_in_node(2)),作用是"给定一个段,就能知道段内哪一块属于哪个文件、是第几块"——这是把散落块重新拼回文件的关键索引。段摘要区末尾还有 footer(段类型与 CRC),footer 损坏会导致该段摘要全部不可用。

2.6 针对闪存的设计带来的取证局限

局限 原因 实际影响
旧数据会被 GC 抹除 空间不足时 GC 迁移有效块并擦除段 回收越彻底,旧版本越不可得;无"永久保存"保证
无等价的"已删文件列表" 没有 lsdel 这样的现成结构 必须自行解析 NAT + checkpoint,成本高
inline data 藏在小对象里 小文件/小目录项直接存在 inode 或 dentry 中 容易漏检;小于约 4 KB 的文件默认内联
node 与 data 分段 元数据与数据不在同一段 只扫数据区会漏掉全部元数据
工具支持有限 TSK 对 F2FS 支持很弱 以 f2fs-tools 与专用解析器为主
移动端镜像常不完整 逻辑分区起始位置常需自行搜索 不能假设超级块在分区起始

GC 时机由空间压力驱动,不由"用户删除"直接驱动。 所以"删除时间"与"GC 时间"不是一回事——GC 只是让旧数据消失的过程,把 GC 时间当作删除时间是错误的推理。


三、操作步骤

3.1 第 1 步:定位并验证 F2FS 超级块

移动设备镜像常是整块闪存转储,F2FS 分区起点未知,需要搜索魔数:

grep -aob $'\x10\x20\xf5\xf2' /work/DigiForensics.dd | head -5
# 1258291200                          ← 命中点 1
# 26843545600                        ← 命中点 2(需逐一验证)

命中之后必须验证上下文,因为这 4 字节在随机数据中也可能出现:

xxd -s 1258291200 -l 16 /work/DigiForensics.dd
# 00480000: 1020 f5f2 0100 0e00 0900 0000  .. .....   ← magic + 版本 + log_sectorsize=9
xxd -s $((1258291200+0x14)) -l 16 /work/DigiForensics.dd
# 00480014: 0c00 0000 0900 0000  ...                ← log_blocksize=12(4096)

验证要点:log_sectorsize 应为 9(512 字节);log_blocksize 通常 12(4096);log_blocks_per_seg 通常 9(512 块 = 2 MB)。出现 0 或明显异常值就是误命中。

3.2 第 2 步:切出 F2FS 分区