搜索全站

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

↑↓ 选择 Enter 打开Esc 关闭

FAT 系列结构

1980 年的 FAT 至今还在服役。它对取证教学的价值恰恰来自"简单"——整卷的簇分配表摊平成一张线性数组放在卷首,元数据只有一张 FAT 表加定长目录项,没有间接层。

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

一、概述

1980 年的 FAT 至今还在服役。它对取证教学的价值恰恰来自"简单"——整卷的簇分配表摊平成一张线性数组放在卷首,元数据只有一张 FAT 表加定长目录项,没有间接层。

这个结构意味着任何一步解析结果都能用十六进制手工复核。工具报出一个文件名,你能翻到那个目录项的字节;工具说文件从簇 41230 开始,你能顺着 FAT 表数过去。这种可复核性是 NTFS 的 B-tree 与 MFT 给不了的——那些结构一旦工具解析失败,你很难手工兜底。

代价同样明显。目录项定长 32 字节,删除只改 1 个字节(首字符改成 0xE5),但也因此没有权限、没有日志、没有时间线精细度。FAT 不记录"谁在什么时候删掉了什么",它只记录"这 32 字节现在是这个状态"。这句话决定了大量结论的边界。

这一层在分析环节的格式解析阶段,与 NTFS、ext4、APFS 那几篇平级,入口在文件系统取证总览的第 3 步——那一篇的第 4 组"不能挂载时的新兴文件系统"就是为 FAT 与 F2FS 这类准备的。

FAT 的实际出现频率可能超出很多人的预期:U 盘 / 移动硬盘 / SD 卡(FAT32 或 exFAT)、行车记录仪与相机、软盘与工业控制器(FAT12/FAT16),以及几乎每台现代 PC 都有的 EFI 系统分区。

最后一项值得单独提醒,因为它在实务中最容易被忽略:bootx64.efi、grubx64.efi 都存放在 ESP 分区里,分析启动项篡改或 UEFI 引导劫持时核心目标是 ESP,而不是系统盘。这类案件的取证目标根本不在 NTFS 卷上。

上游依赖卷层解析。第五章案例里那个"簇大小是 64 KB 而不是假设的 4 KB"的发现,本质上就是这一步没做好会导致后面全错位。下游支撑两个方向:已删除文件的恢复与内容提取(这里是核心),以及把提取结果送去做内容类型识别(见文件头与文件签名)。

落到具体证据上:

  • DigiForensics.dd 的分区起始扇区 2048 —— raw 单分区镜像。所有偏移都要加这个基址,第五章案例里 fsstat -o 2048 与 dd bs=512 skip=2048 就是在做这件事
  • exFAT 主引导记录 —— 签名 EXFAT 在偏移 3,参数区 0x40–0x6F:PartitionOffset / VolumeLength / FatOffset / FatLength / ClusterHeapOffset / ClusterCount / RootDirectoryCluster。exFAT 没有标准 BPB,这一点与 FAT32/FAT16 根本不同
  • FAT 表 —— 本身就是簇链,无需解析指针。第 5 步遍历集群链与 blkls 抽未分配空间都靠它
  • 32 字节目录项 —— 删除只改首字符为 0xE5。0xE5 之外的首字符(0x05、0x0E、0x0F、0x20)各有特殊含义,第五章案例里数出"首字符为 0x05 的正常项 186 个",扫目录项时把这批正常项误当删除项是最常见的误报源
  • exFAT 目录项集 —— 一个文件 = 0x85 文件项 + 1 个 0xC0 流扩展项 + 1–17 个 0xC1 文件名项。六种类型码的项长度都是 32 字节,遍历时固定按 32 字节步进,长名靠多个 0xC1 项拼接而非靠变长项
  • SetChecksum —— 0x85 项 0x02–0x03,16 位滚动校验,遍历项集全部字节并跳过主项那两个字节本身,每字节右旋 1 位后累加取模 0x10000。exFAT 没有 FAT/VFAT LFN 的 0x0D 短名校验和,不可套用
  • DOS 时间戳 —— FAT32 的时间只有 2 秒精度,且存的是本地时间、没有时区偏移字段
  • exFAT 的 4 字节 Unix 时间戳 —— 配 0x14–0x18 的 10 毫秒增量与时区偏移字段,这是 exFAT 相对 FAT32 的一个实质性改进
  • ESP 分区里的 bootx64.efi / grubx64.efi —— 引导链相关的具体文件名

走完这一层能判断的是这些:BPB 每个字段该填什么(而不是照抄模板)、FAT 表怎么手工遍历、删除项怎么判定且不误报、exFAT 的项集怎么按正确的步长遍历,以及为什么 exFAT 上删除文件的长名能完整恢复而 FAT32 上不能。

最后一条差异是最有价值的对比:exFAT 直接存 255 字符 UTF-16 名,删除只清 InUse 位(0x85→0x05),长文件名与内容定位信息都还在;FAT32 删除会抹掉全部 LFN 段、只留自动生成的短名。第五章案例里三个视频的长名全部恢复,这一整套结果在 FAT32 上不可能出现。

有几件事不归这一层管:文件被删后内容是否可恢复,取决于簇链有没有被覆盖,簇链状态必须逐个查,不能从"已删除"直接推出"可恢复";是谁删的、什么时候删的,FAT 不记录删除动作的时刻,目录项只存最后修改时间,这是格式的硬限制而不是工具问题;大文件的碎片重组,无碎片管理意味着大文件必然碎片化,重组逻辑在第三章步骤 3,但跨越非连续簇的读取必然更慢也更容易出错;加密卷的内容见BitLocker 全卷加密取证。

往下读之前需要扇区、簇、偏移量这三个概念的准确理解——全程在"字节偏移 = 基址 + 相对偏移"上计算,第五章那个 64 KB 簇的发现就是基址与簇大小算错的后果。另外需要十六进制分析能力:BPB 每个字段的偏移与长度都要自己核(2.2 那节明确说"偏移必须逐字段核对")。

建议动手手工解析一个 FAT 目录项。挑一个小 U 盘,用 xxd 找到一个文件名,然后自己按 2.4 那张位域表把创建/修改/访问时间三个字段解出来。这个练习做完,2.6 那张 exFAT 类型码表你就再也不会记混——它的六个类型码(0x81/0x82/0x83/0x85/0xC0/0xC1)记住靠的是见过,不是背出来的。

二、核心原理

2.1 三个变体:差异只在每条簇记录的位宽

特性 FAT12 FAT16 FAT32
簇记录位宽 / 表项宽度 / 簇数区间 12 位 / 1.5 字节 / 4085–4088 16 位 / 2 字节 / 65525–65536 32 位 / 4 字节 / 65525 以上
卷上限 / 单文件上限 32 MB / 32 MB 2 GB / 32 MB 2 TB / 4 GB−1
根目录 卷首定长区域,无簇链 卷首定长区域 普通簇链(root_cluster)

判别不能看"表里写着什么",要看实际簇数:由 BPB 的总扇区数与 FAT 长度算出真实簇数再对照上表。边界效应:簇数随卷增大而减小(同样容量用更大的簇),16 GB U 盘用 4 KB 簇时已逼近 FAT16 上限,因此大容量 FAT32 卷在实践中改用 8/16/64 KB 簇。看到"标称 FAT32 的 U 盘"必须实测簇大小,不能假设 4 KB。

2.2 BPB:偏移必须逐字段核对

共用  0x0B 2 每扇区字节数★ 0x0D 1 每簇扇区数★ 0x0E 2 保留扇区数 0x10 1 FAT 数量
      0x11 2 根目录项数(FAT32=0)  0x13 2 总扇区数(16位,不够填 0)  0x15 2 每FAT扇区数
      0x1B 4 隐藏扇区数  0x1F 4 总扇区数(32位)★
FAT32 0x23 4 每FAT扇区数(32位)★  0x27 2 标志(bit7=0x80 镜像备份)  0x2B 2 根目录起始簇号★
      0x2F 2 FSInfo 扇区  0x31 2 备份引导扇区  0x42 4 卷序列号★  0x51 8 "FAT32   "★
FAT12/16 0x24 1 驱动器号  0x26 1 引导签名 0x29  0x27 4 卷序列号★  0x36 8 "FAT12  "/"FAT16  "★
簇大小 = 0x0B 值 × 0x0D 值  FAT 起始 = 0x0E 值 × 每扇区字节数
每 FAT 字节 = 0x15 值 × 每扇区字节数(FAT32 用 0x23 值)
数据区起始 = FAT 起始 + FAT 数量 × 每 FAT 字节(FAT12/16 再加 根目录项数×32 向上取整到扇区)
总簇数 = (总扇区数 − 保留扇区 − FAT 数量×每FAT扇区 − 根目录扇区) ÷ 每簇扇区数

卷序列号是报告中的卷标识,与 NTFS Volume Serial、APFS Volume UUID 地位相同。结论必须写成"卷序列号 3A7F-91C2 的 FAT32 分区",而不是"某个 U 盘"。FAT32 的卷序列号在 0x42,FAT12/16 在 0x27——同一工具读不同变体时不要记混偏移。

2.3 FAT 表:一张摊平的簇链数组

破案时最常问的一句话是"这个文件从哪开始、被分到哪些簇"。答案就在这张表里——它是 FAT 家族唯一的数据结构。

每簇一个表项,表项值就是"下一个簇号"(簇 0、1 保留)。

以 FAT16 为例:簇 0/1 保留;文件 A 占簇 2→4(表项 0004/0005),簇 3 已释放(表项 0000),这正是文件 A 被缩短或删除后的现场;簇 6 表项 FFF7 是坏簇;文件 B 占簇 6,表项 FFF8 表示链尾。

表项值语义:0x0000 = 空闲,链在这里断了(通常因为文件已删除、簇被释放);坏簇标记 = 物理损坏,原有内容不可恢复(FAT12 0xFF7 / FAT16 0xFFF7 / FAT32 0x0FFFFFF7,是单一值);≥ EOC 阈值 = 链尾,文件正常结束(FAT16 0xFFF8 / FAT32 0x0FFFFFF8,是范围值);其余 = 下一个簇号,即碎片化跳转。

关键结论:0x0000 表示文件已删除,而非文件损坏。删除会把链上所有表项清零,但目录项里的起始簇还在;若原文件连续,按起始簇顺序读取即可重建,这正是 FAT 删除文件仍可完整恢复的技术原因。反过来,碎片化文件删除后必然无法完整重建,因为无从得知第 2 块在哪。要区分坏簇与 EOC,只能判断"是否等于坏簇标记值",因为 EOC 是范围而坏簇是单一值。

2.4 32 字节目录项:删除只改 1 个字节

FAT32    0x00 8 8.3主名★ 0x08 3 8.3扩展名★ 0x0B 1 属性★ 0x0C 1 NT保留位
         0x0E 2 创建时间 0x10 2 创建日期 0x12 2 最后访问日期 0x14 2 起始簇高位★
         0x16 2 最后写入时间 0x18 2 最后写入日期 0x1A 2 起始簇低位★ 0x1C 4 文件大小★
FAT12/16 0x0D 2 最后访问日期 0x0F 2 创建时间 0x11 2 创建日期 0x13 2 最后写入时间 0x15 2 最后写入日期
         0x17 2 起始簇 0x19 4 文件大小★
★ 属性位:0x0F=LFN项 0x10=目录 0x20=存档 0x08=卷标   ★ NT保留位:0x08=基名小写 0x10=扩展名大写

最容易记错的一点:文件大小在 FAT32 位于 0x1C、起始簇在 0x14(高)+0x1A(低);但在 FAT12/16 中文件大小在 0x19、起始簇只有 0x17 一个 16 位字段。 用错偏移会读出完全错误的"大小",进而导致提取长度和哈希全部错误。

删除动作只有两步:① 目录项首字节 0x?? → 0xE5;② 簇链上所有表项 → 0x0000。

以主名 CONTRACT + 扩展名 DOC 为例(0x00–0x07 = 43 4F 4E 54 52 41 43 54,0x08–0x0A = 20 44 4F 43,0x0B 属性 = 20),删除后变为 E5 4F 4E 54 52 41 43 54 20 44 4F 43 20——仅首字节被覆盖,其余 31 字节原样保留。

关键结论:删除后文件名(除首字符)、属性、三个时间戳、起始簇号、文件大小全部还在,丢失的只有一个首字符和后续簇链。由此推出三件事:① 恢复工具要求用户猜首字符,因为该字节无法从盘上找回;② 起始簇与大小还在,若原文件连续则内容可完整提取;③ 碎片化文件无法恢复。另外,目录项的文件大小恒为 0,判断目录要看属性字节的 0x10 位。 首字节三态(务必做对):0x00 = 从未使用,后续全空闲,应立即停止该目录扫描;0xE5 = 已删除,唯一的删除标记;0x05 = 首字符实际就是 EOT,是合法文件名字符,误判会产生大量假删除项。

2.5 LFN:长文件名机制

8.3 无法表达长名。VFAT(Windows 95)为此引入 LFN:长名被切成每段 13 个字符的多个 32 字节目录项,存放于对应短目录项之前,按段号倒序排列(段号最小的一段最靠近短目录项)。

"Very Long Filename.docx" 占 5 个连续的 32 字节项(磁盘上从左到右 = 段4→段1→短项):
 [0x44 "x"+填充][0x43 "me.doc"][0x42 "ng Fila"][0x41 "Very Lo"][短项 VE~1.DOC 属性≠0x0F]
  ★ 读取时必须先按「首字节 & 0x3F」升序排序,顺序反了就会拼出乱码

LFN 项结构:0x00 序号码(最高位 0x40 标记"最后一段",低 6 位是段序号);0x01–0x0A 名称字符 1–5;0x0B 属性恒为 0x0F,这是识别 LFN 项的唯一判据;0x0C 类型恒为 0;0x0D 短名 8.3 字段的校验和;0x0E–0x19 名称字符 6–11;0x1A–0x1B FstClusLO 恒为 0(LFN 项不存簇号);0x1C–0x1F 名称字符 12–13。每段 5+6+2 = 13 个 UTF-16LE 字符。

三个必须记准的规则:(1)0x40 是独立的"最后一段"标志位,与序号是"或"关系,上限 255 字符需 20 段(0x54…0x41,因 20×13=260≥255);常见错误是把 0x40 当序号的一部分去减算,导致拼接顺序反了或长度算错,正确取法是 序号 = 字节 & 0x3F。(2)LFN 项的首字节同样会被置 0xE5:删除时整个条目组全部被置 0xE5**,因此已删除文件的短名通常还在(除首字符),长名一律丢失——恢复工具给出的 DOCUME~1.DOC 是短名,不是原名。(3)0x0D 校验和把长名与短名绑定,算法为右循环移位 r = (((r&1)<<7) + (r>>1) + 字节) & 0xFF,对 8.3 的 11 字节(含首字符)依次执行;校验和值只存放在 LFN 项的 0x0D,短名项同一偏移是 NT 保留字节、规范要求必须为 0,从短名项取校验和会永远得到"不匹配"。

取证上的关键点:目录项可能被后续写入部分覆盖,出现"孤立的 LFN 段"。严谨的解析器必须做校验和验证,绝不能把无主的 LFN 段拼成一个看起来很像真的文件名;校验和不匹配时只报 8.3 短名。 此外,已删除项的首字符 0xE5 参与了原校验和的计算,校验删除项时必须枚举候选首字符,不能直接用 0xE5 比对。

2.6 exFAT:目录项集与三个关键改动

exFAT 是 2008 年微软为突破 FAT32 的 4 GB 单文件限制而设计的,不是 FAT32 的向后兼容扩展。

它没有标准 BPB,参数位于主引导记录(各 8 字节):0x40 PartitionOffset、0x48 VolumeLength、0x50 FatOffset、0x58 FatLength、0x60 ClusterHeapOffset、0x68 ClusterCount、0x6C RootDirectoryCluster;0x70 VolumeSerialNumber(4 字节)。

根目录本身也是普通簇链,起点即 RootDirectoryCluster,其内混排着位图项、大小写转换表项、卷标项和文件项集。

三个关键改动:文件名——FAT32 需 LFN 项配 8.3 短名,exFAT 直接存 255 字符 UTF-16 名,删除后长名仍完整保留;簇链——FAT32 每簇一表项,exFAT 的 NoFatChain 让连续文件不占 FAT 表项,可按长度直接定位;时间戳——FAT32 是 DOS 格式(2 秒精度),exFAT 是 4 字节 Unix 时间戳,配 0x14–0x18 的 10 毫秒增量与时区偏移字段。

目录项类型码(识别错位全靠这张表;EntryType 的 bit7 = InUse、bit6 = Secondary、bit5 = Critical):

类型码 项 长度 关键字段
0x81 分配位图项 32 0x01 BitmapFlags、0x14 FirstCluster、0x18 DataLength
0x82 大小写转换表项 32 0x04 表校验和、0x14/0x18
0x83 卷标项 32 0x01 字符数、0x02 UTF-16 卷标;未命名时类型码为 0x03
0x85 文件目录项 32 0x01 SecondaryCount(2–18)、0x02 SetChecksum、0x04 属性、0x08/0x0C/0x10 三时间戳、0x14/0x15 创建与修改的 10ms 增量、0x16/0x17/0x18 三个 UTC 偏移
0xC0 流扩展项 32 0x01 GeneralSecondaryFlags、0x03 名长度、0x04 NameHash、0x14 FirstCluster、0x18 DataLength
0xC1 文件名项 32 0x02 起 15 个 UTF-16 字符(每个 2 字节,共 30 字节);255 字符长名需要 17 个该项

混排规则:一个文件 = 0x85 + 1 个 0xC0 + 1–17 个 0xC1,所有项一律 32 字节、按 32 字节步进(0xC1 也是 32 字节:1 字节类型 + 1 字节标志 + 15 个 UTF-16 字符)。若误按 16 字节步进,项集会从第二个文件名项起整体错位,解析出的名字变成乱码且不会报错。流扩展项 0x01 字节的 bit0 = AllocationPossible、bit1 = NoFatChain(掩码 0x02)。 删除只清 InUse 位(0x85→0x05),其余字段原样保留,因此 SD 卡上已删除的连续文件,长文件名与内容定位信息都还在——元数据保留得更完整,工具只是因此表现更好。代价是必须做项集校验:SetChecksum(0x85 的 0x02–0x03)是 16 位滚动校验,遍历项集全部字节、跳过主项 0x02–0x03 这两个字节本身,每字节右旋 1 位后累加并对 0x10000 取模。exFAT 没有 FAT/VFAT LFN 的 0x0D 短名校验和,不可套用该规则。但 exFAT 同样无权限、无日志、无时间线,目录项也不记录删除动作的时刻。


三、操作步骤

3.1 第 1 步:定位卷与实测参数

mmls /work/DigiForensics.dd              # Slot 01 Start: 0000002048  FAT32 ESP
dd if=/work/DigiForensics.dd of=/work/DigiForensics-fat.dd bs=512 skip=2048 status=none
xxd -s 0x0B -l 2 /work/DigiForensics-fat.dd   # 每扇区字节数
xxd -s 0x0D -l 1 /work/DigiForensics-fat.dd   # 每簇扇区数 ★簇大小 = 二者相乘
fsstat -o 2048 /work/DigiForensics.dd          # TSK 侧交叉验证全部 BPB 字段

必须实测并写入报告、不得假设的量:每扇区字节数、每簇扇区数(1–128)、簇大小(4/8/16/64 KB)、保留扇区数、每 FAT 扇区数、根目录起始簇号。

3.2 第 2 步:扫描目录项、判定删除、组装 LFN

fls -o 2048 -r -p /work/DigiForensics.dd > /evidence/fat-fls.txt && grep -ci deleted /evidence/fat-fls.txt
istat -o 2048 /work/DigiForensics.dd 460        # size 与三个时间戳

FAT 只有三个时间戳(创建/修改/访问),精度 2 秒,且很多 FAT32 卷的创建时间不可用。不要在 FAT 上构建秒级时间线。

核心脚本(逐 32 字节项扫描,处理删除判定、LFN 校验与组装):

import struct IMG = "/work/DigiForensics-fat.dd" with open(IMG, "rb") as f: b = f.read(0x60) SECTOR, SPF = struct.unpack_from("<H", b, 0x0B)[0], b[0x0D] RES, NFAT, ROOTENT = struct.unpack_from("<H", b, 0x0E)[0], b[0x10], struct.unpack_from("<H", b, 0x11)[0] FAT32 = struct.unpack_from("<H", b, 0x16)[0] == 0 # 每FAT扇区数(16位)为0才是FAT32 SEC = struct.unpack_from("<I", b, 0x23 if FAT32 else 0x15)[0] # ★ 每FAT扇区数:FAT32 在 0x23,FAT12/16 在 0x15 ROOTCLUS = struct.unpack_from("<H", b, 0x2B)[0] if FAT32 else 0 CLUSTER, FATLEN = SECTOR * SPF, SEC * SECTOR ROOT_SECT = ((ROOTENT * 32) + SECTOR - 1) // SECTOR # FAT12/16 定长根目录占多少扇区 DATA_OFF = (RES + NFAT * SEC + ROOT_SECT) * SECTOR # ★ 全部从 BPB 实测,不硬编码 U16, U32 = lambda x: struct.unpack("<H", x)[0], lambda x: struct.unpack("<I", x)[0]

def cksum(s11):