FAT12/16/32 文件系统
FAT 是U盘最主力的文件系统,取证比赛出现频率最高。核心思想:文件内容按簇(Cluster)存放,用一张FAT 表记录”每个簇的下一个簇是谁”,形成簇链。找到分区起点的方法见 1 MBR分区表,本文所有偏移均相对分区起点(DBR 所在扇区)。
总体布局
FAT32:
扇区 内容
0x00 ┌──────────────────────────────┐
│ DBR(引导扇区,含BPB) │ ← "FAT32 " 在扇区内 0x52
0x01 │ FSInfo(空闲簇信息) │
0x02 │ 保留扇区(常为引导代码) │
... │ ... │
0x06 │ 备份 DBR │ ← 通常 BkBootSec=6
0x07 │ 备份 FSInfo │
... │ ...(直到 RsvdSecCnt 为止) │
├──────────────────────────────┤
│ FAT1 │ ← 大小 = FATSz32
│ FAT2(FAT1 的完整拷贝) │ ← NumFATs 通常为 2
├──────────────────────────────┤
│ 数据区(从簇 2 开始编号) │ ← 根目录就是簇 2 开头的普通目录
└──────────────────────────────┘
FAT12/16 与 FAT32 的差别:无 FSInfo、通常无备份 DBR,且 FAT 区与数据区之间多一块独立的根目录区(大小固定,由 RootEntCnt 决定):
┌──────────────────────────────┐
│ DBR("FAT12 "/"FAT16 " 在 0x36)│
│ 其余保留扇区(RsvdSecCnt 通常为 1) │
├──────────────────────────────┤
│ FAT1 / FAT2 │
├──────────────────────────────┤
│ 根目录区 │ ← 固定 RootEntCnt×32 字节,仅存根目录
├──────────────────────────────┤
│ 数据区(簇 2 起) │
└──────────────────────────────┘
簇号从 2 开始:簇 0、1 不存在于磁盘上,对应 FAT 表项 0、1 被保留作他用(见下文)。因此所有”簇号→偏移”换算都有 -2。
DBR / BPB 结构
DBR(DOS Boot Record)是分区的 0 号扇区,前 36 字节是 BPB(BIOS Parameter Block)。所有多字节字段均为小端。
FAT12/16 引导扇区(共512字节)
typedef struct _FAT16_DBR { // 共512字节,偏移为扇区内偏移
BYTE JmpBoot[3]; // 0x00 跳转指令: EB xx 90 或 E9 xx xx
char OEMName[8]; // 0x03 OEM字符串, 如 "MSDOS5.0"(仅标识,不参与解析)
WORD BytsPerSec; // 0x0B 每扇区字节数: 512/1024/2048/4096, 通常512
BYTE SecPerClus; // 0x0D 每簇扇区数: 必须是2的幂(1/2/4/.../128)
WORD RsvdSecCnt; // 0x0E 保留扇区数: FAT12/16通常为1
BYTE NumFATs; // 0x10 FAT表份数: 通常为2
WORD RootEntCnt; // 0x11 根目录项数: FAT12/16非0(如512), FAT32必须为0
WORD TotSec16; // 0x13 小扇区总数: 总扇区<0x10000时使用, 否则为0
BYTE Media; // 0x15 介质描述符: 0xF8硬盘/U盘, 0xF0软盘(见取值表)
WORD FATSz16; // 0x16 每FAT扇区数(16位): FAT12/16用, FAT32必须为0
WORD SecPerTrk; // 0x18 每磁道扇区数: CHS遗留, 通常0x3F
WORD NumHeads; // 0x1A 磁头数: CHS遗留, 通常0xFF
DWORD HiddSec; // 0x1C 隐藏扇区数: 分区前的扇区数=MBR中的LBAStart
DWORD TotSec32; // 0x20 大扇区总数: TotSec16为0时使用
// ---- 以下是扩展引导记录(EBPB)----
BYTE DrvNum; // 0x24 驱动器号: 0x00软盘 0x80硬盘
BYTE Reserved1; // 0x25 保留(NT使用), 为0
BYTE BootSig; // 0x26 扩展引导标志: 0x29(表示后三字段有效)
DWORD VolID; // 0x27 卷序列号 ← 取证关联点
char VolLab[11]; // 0x2B 卷标, 空格填充, 常为 "NO NAME "
char FilSysType[8]; // 0x36 文件系统类型串 "FAT12 "/"FAT16 " ← 仅参考,不可信
BYTE BootCode[448]; // 0x3E 引导代码
WORD Signature; // 0x1FE 引导签名: 磁盘字节为 55 AA (按小端WORD读=0xAA55)
} FAT16_DBR;FAT32 引导扇区(共512字节)
0x00~0x23 与 FAT12/16 完全相同,但注意 RootEntCnt(0x11)、TotSec16(0x13)、FATSz16(0x16) 三个字段必须为 0,从 0x24 起是 FAT32 专有字段:
typedef struct _FAT32_DBR { // 共512字节,偏移为扇区内偏移
BYTE JmpBoot[3]; // 0x00 跳转指令: EB xx 90
char OEMName[8]; // 0x03 OEM字符串, 如 "MSDOS5.0"
WORD BytsPerSec; // 0x0B 每扇区字节数: 通常512
BYTE SecPerClus; // 0x0D 每簇扇区数: 2的幂
WORD RsvdSecCnt; // 0x0E 保留扇区数: FAT32通常为32(0x20)
BYTE NumFATs; // 0x10 FAT表份数: 通常为2
WORD RootEntCnt; // 0x11 根目录项数: FAT32必须为0
WORD TotSec16; // 0x13 小扇区总数: FAT32必须为0
BYTE Media; // 0x15 介质描述符: 通常0xF8
WORD FATSz16; // 0x16 每FAT扇区数(16位): FAT32必须为0
WORD SecPerTrk; // 0x18 每磁道扇区数: 通常0x3F
WORD NumHeads; // 0x1A 磁头数: 通常0xFF
DWORD HiddSec; // 0x1C 隐藏扇区数
DWORD TotSec32; // 0x20 大扇区总数
// ---- FAT32 专有扩展区 ----
DWORD FATSz32; // 0x24 每FAT占扇区数(32位) ← 算数据区起点必用
WORD ExtFlags; // 0x28 扩展标志: bit7=1表示单FAT活动模式,低4位为活动FAT号;通常为0(镜像双写)
WORD FSVer; // 0x2A 文件系统版本: 必须为0
DWORD RootClus; // 0x2C 根目录起始簇号: 通常为2
WORD FSInfo; // 0x30 FSInfo扇区号(相对保留区起点): 通常为1
WORD BkBootSec; // 0x32 备份引导扇区号(相对保留区起点): 通常为6
BYTE Reserved[12]; // 0x34 保留, 全0
BYTE DrvNum; // 0x40 驱动器号: 0x80
BYTE Reserved1; // 0x41 保留, 为0
BYTE BootSig; // 0x42 扩展引导标志: 0x29
DWORD VolID; // 0x43 卷序列号 ← 取证关联点
char VolLab[11]; // 0x47 卷标, 空格填充
char FilSysType[8]; // 0x52 固定 "FAT32 " ← 识别特征,但不可作为判断依据
BYTE BootCode[420]; // 0x5A 引导代码
WORD Signature; // 0x1FE 引导签名: 磁盘字节为 55 AA (按小端WORD读=0xAA55)
} FAT32_DBR;介质描述符取值(Media,1字节)
FAT[0] 的低 8 位也存同一个值。
| 值 | 含义 |
|---|---|
| 0xF0 | 3.5寸 1.44MB 软盘;部分移动介质也用 |
| 0xF8 | 硬盘 / U盘 ← 最常见 |
| 0xF9 | 5.25寸 / 3.5寸 720KB~1.2MB 软盘 |
| 0xFA | 5.25寸 320KB 软盘 / RAM 盘 |
| 0xFB | 3.5寸 640KB 软盘 |
| 0xFC | 5.25寸 180KB 软盘 |
| 0xFD | 5.25寸 360KB 软盘 |
| 0xFE | 5.25寸 160KB 软盘 / 8寸单面 |
| 0xFF | 5.25寸 320KB 软盘 / 8寸双面 |
FSInfo 结构(仅 FAT32,通常在保留区 1 号扇区)
记录空闲簇数和分配起点,仅供加速分配用,不参与解析正确性,损坏时用备份(通常 7 号扇区)或重新扫描 FAT 重建。
typedef struct _FSINFO { // 共512字节,偏移为扇区内偏移
DWORD LeadSig; // 0x000 前导签名: 固定 0x41615252 ("RRaA")
BYTE Reserved1[480]; // 0x004 保留, 全0
DWORD StrucSig; // 0x1E4 结构签名: 固定 0x61417272 ("rrAa")
DWORD FreeCount; // 0x1E8 空闲簇数; 0xFFFFFFFF=未知
DWORD NxtFree; // 0x1EC 下一个可用簇号(分配起点提示); 0xFFFFFFFF=未知
BYTE Reserved2[12]; // 0x1F0 保留, 全0
DWORD TrailSig; // 0x1FC 尾部签名: 固定 0xAA550000
} FSINFO;FAT 表项
FAT 是表项数组,表项下标 = 簇号,表项内容 = 该文件下一个簇的簇号(或特殊值)。三种 FAT 的唯一区别是表项宽度:
| 类型 | 表项宽度 | 有效位 | 说明 |
|---|---|---|---|
| FAT12 | 12 bit | 12 | 两个字节装1.5个表项,奇偶拆分(见下) |
| FAT16 | 16 bit | 16 | 每表项2字节,直接小端读 |
| FAT32 | 32 bit | 28 | 高4位保留,读取后必须 & 0x0FFFFFFF |
表项特殊值
| 含义 | FAT12 | FAT16 | FAT32(已掩高4位) |
|---|---|---|---|
| 空闲簇 | 0x000 | 0x0000 | 0x00000000 |
| 坏簇(不可用) | 0xFF7 | 0xFFF7 | 0x0FFFFFF7 |
| 簇链结束 EOC | ≥ 0xFF8 | ≥ 0xFFF8 | ≥ 0x0FFFFFF8 |
| 正常值 | 2 ~ 0xFEF | 2 ~ 0xFFEF | 2 ~ 0x0FFFFFEF |
注意:判断 EOC 用 >=,EOC 标记的常用填充值是最大值(FAT32 写盘时常写 0x0FFFFFFF)。
表项 0 和 1 的固定内容
- 表项 0:低 8 位 = 介质描述符,其余位全 1。例如介质 0xF8 时,FAT16 为
F8 FF,FAT32 为F8 FF FF 0F。 - 表项 1:正常状态为 EOC 值。其最高两个有效位用作脏标志(FAT16 的 bit15、FAT32 的 bit27 = 卸载时未正常弹出标记;次高位 = 上次挂载发生过 I/O 错误标记),Windows 挂载时清除、卸载时置位——取证上可判断”最后一次是否安全弹出”。
FAT12 表项奇偶拆分
12 位表项两个一组挤在 3 个字节里,偏移与取法(n 为簇号):
表项字节偏移 = n + n/2(向下取整)
n 为偶数: 值 = (该处16位小端读数) & 0x0FFF
n 为奇数: 值 = (该处16位小端读数) >> 4
FAT12 基本只在小容量题中出现,遇到时最容易算错,建议直接在 010 Editor 里用 FAT12 模板核对。
簇号 → 文件偏移 换算公式
这是手工恢复文件的核心公式,务必记牢:
FAT12/16:
根目录区扇区数 = (RootEntCnt × 32) / BytsPerSec ← 整数除法,结果必为整数
数据区起点扇区 = RsvdSecCnt + NumFATs × FATSz16 + 根目录区扇区数
FAT32:
数据区起点扇区 = RsvdSecCnt + NumFATs × FATSz32 ← 无独立根目录区
簇 N 的文件偏移 = (数据区起点扇区 + (N - 2) × SecPerClus) × BytsPerSec
+ 分区起点偏移(若有分区表)
全部字段都来自 DBR 的 BPB,所以 BPB 被篡改时所有簇定位都会算错(见取证要点)。
目录项(32 字节短文件名项)
目录本质是”存放目录项数组的文件”。FAT12/16 根目录在固定根目录区,其余目录和 FAT32 的一切目录(含根目录)都是数据区里的簇链。每个文件/子目录占一个 32 字节短文件名项(SFN),有长文件名则前面再挂若干 LFN 项。
typedef struct _DIR_ENTRY { // 共32字节,偏移为目录项内偏移
char Name[11]; // 0x00 短文件名 8+3: 前8主名+后3扩展名,空格填充,大写,无点
BYTE Attr; // 0x0B 属性位域(见下表)
BYTE NTRes; // 0x0C 保留(NT小写标记): bit3=主名小写 bit4=扩展名小写
BYTE CrtTimeTenth; // 0x0D 创建时间的10毫秒分量: 0~199
WORD CrtTime; // 0x0E 创建时间(位打包,见下)
WORD CrtDate; // 0x10 创建日期(位打包,见下)
WORD LstAccDate; // 0x12 最后访问日期(无时间分量)
WORD FstClusHI; // 0x14 起始簇号高16位(FAT12/16必须为0)
WORD WrtTime; // 0x16 最后修改时间(位打包)
WORD WrtDate; // 0x18 最后修改日期(位打包)
WORD FstClusLO; // 0x1A 起始簇号低16位 ← 完整簇号 = (HI<<16)|LO
DWORD FileSize; // 0x1C 文件大小(字节); 目录项恒为0
} DIR_ENTRY;首字节特殊值(Name[0])
| 值 | 含义 |
|---|---|
| 0x00 | 空项,且其后的目录项全部未使用(扫描可提前终止) |
| 0xE5 | 已删除项 ← 恢复的关键标志 |
| 0x05 | 文件名首字符本来就是 0xE5(日文片假名),用 0x05 顶替存储 |
| 0x2E | ”.” 或 ”..” 目录项(0x2E 即 ASCII 点号) |
属性字节位域(Attr,0x0B)
| 位 | 值 | 含义 |
|---|---|---|
| 0 | 0x01 | 只读 Read Only |
| 1 | 0x02 | 隐藏 Hidden |
| 2 | 0x04 | 系统 System |
| 3 | 0x08 | 卷标 Volume Label(该项是卷标,FstClus 必须为0) |
| 4 | 0x10 | 子目录 Directory |
| 5 | 0x20 | 归档 Archive |
| 0~3 | 0x0F | 长文件名项(LFN) ← 0x01|0x02|0x04|0x08 的组合,正常文件不可能出现 |
Attr & 0x0F == 0x0F 是判断 LFN 项的标准方法;Attr 为 0x0F 的项不参与普通文件枚举。
日期时间的位打包格式
时间(WORD,小端,2字节):
15 11 10 5 4 0
┌────────────┬────────────┬───────────┐
│ 时(0~23) │ 分(0~59) │ 秒/2(0~29) │ ← 真实秒数 = 字段值 × 2,所以时间戳粒度2秒
└────────────┴────────────┴───────────┘
日期(WORD,小端,2字节):
15 9 8 5 4 0
┌────────────┬────────────┬───────────┐
│ 年-1980 │ 月(1~12) │ 日(1~31) │ ← 真实年份 = 字段值 + 1980
└────────────┴────────────┴───────────┘
手工解码示例:某目录项 WrtTime/WrtDate 处字节为 7A 5D 46 5C:
WrtTime = 0x5D7A = 0101 1101 0111 1010b
时 = 01011b = 11, 分 = 101011b = 43, 秒/2 = 11010b = 26 → 11:43:52
WrtDate = 0x5C46 = 0101 1100 0100 0110b
年 = 0101110b = 46 → 1980+46 = 2026, 月 = 0010b = 2, 日 = 00110b = 6 → 2026-02-06
长文件名(LFN)目录项
文件名超过 8.3 规则(或含小写/特殊字符)时,系统在短文件名项之前连续放置若干个 LFN 项。每个 LFN 项也是 32 字节,存 13 个 UCS-2 字符(小端 UTF-16,共26字节):
typedef struct _LFN_ENTRY { // 共32字节,偏移为目录项内偏移
BYTE Order; // 0x00 序号: 最后一项 = 0x40|序号(如0x41),其余为 序号(1,2,3...)
WORD Name1[5]; // 0x01 字符1~5 (UCS-2小端, 10字节)
BYTE Attr; // 0x0B 固定 0x0F ← LFN 标志
BYTE Type; // 0x0C 固定 0x00
BYTE Chksum; // 0x0D 短文件名校验和(对短名11字节计算,用于配对验证)
WORD Name2[6]; // 0x0E 字符6~11 (12字节)
WORD FstClusLO; // 0x1A 固定 0x0000
WORD Name3[2]; // 0x1C 字符12~13 (4字节)
} LFN_ENTRY;存放顺序是倒序:紧邻短文件名项的 LFN 项序号最小(0x01,存文件名的最前面13个字符),越靠前序号越大,带 0x40 标记的那项(存文件名末尾)排在最前。举例,文件 ThisIsALongFileName.txt 的目录布局:
第N项 LFN Order=0x42 存 "leName.txt"(10字符) + 0x0000结束符 + 0xFFFF填充 ← 文件名结尾
第N+1项 LFN Order=0x01 存 "ThisIsALongFi"(前13字符) ← 文件名开头
第N+2项 SFN 短名 "THISIS~1TXT"
文件名结尾处理:先写一个 0x0000 终止符,剩余槽位填 0xFFFF。校验和算法:对短名 11 字节循环 sum = ((sum&1)<<7) + (sum>>1) + name[i](8位无符号回绕)。LFN 与短名 Chksum 不匹配 → 说明短名项被删除/复用过,该 LFN 作废——恢复长文件名已删除文件时靠这个确认配对关系。
删除与恢复原理
FAT 下删除文件(不进回收站)只做两件事,内容本身不动:
- 目录项首字节改为 0xE5(FAT32 下 Windows 还会把 FstClusHI 清零,导致大簇号文件丢高位)。
- 该文件在 FAT 中的整条簇链表项清零(标记为空闲簇)。
文件内容还躺在数据区,直到被新数据覆盖。手工恢复思路:
- 在目录区找到 0xE5 开头的目录项,读出 FileSize 和起始簇(FAT32 注意补回高位:高位被清,若镜像不大可假设高位=0;大镜像需从内容特征猜)。
- 簇链已断,假设文件连续存放:从起始簇开始,按
ceil(FileSize / 簇大小)个连续簇直接 carve 出来。 - 用文件头尾特征校验(如 JPEG 的
FF D8 ... FF D9);不连续的文件(大文件、碎片化盘)需按内容特征或备份 FAT2 重建簇链。 - FAT2 是 FAT1 的镜像:删除时两份通常同时被清,但若题镜像只改了 FAT1,FAT2 里可能完整保留簇链——比对 FAT1/FAT2 差异是 CTF 高频考点。
反方向:已删除文件的残留目录项、残留簇内容、被截断的 LFN,都是藏 flag 的常见位置。
取证要点
- 卷序列号(VolID)关联:DBR 的 0x27/0x43 卷序列号会进入 Windows 的 LNK 快捷方式(目标卷信息)和注册表
MountedDevices等位置,可与 1 MBR分区表 的磁盘签名互补,用于”证明这个U盘上的文件在这台机器上被打开过”。 - 备份 DBR:FAT32 备份在 BkBootSec(通常 6 号扇区,备份 FSInfo 在 7 号)。主 DBR 被清零/加密时,直接跳 6 号扇区找备份恢复 BPB;注意备份的 Signature 也是
55 AA,识别特征是 0x52 处"FAT32 "。FAT12/16 规范上没有备份 DBR,坏了只能靠同类镜像猜 BPB。 - CTF 常篡改字段及后果:
SecPerClus(0x0D):非 2 的幂或与实际不符 → 簇→偏移换算整体错位,数据区文件全部解析出垃圾。RsvdSecCnt(0x0E):改大/改小 → FAT 区和数据区起点算错,FAT 表读到的全是目录/文件内容。RootEntCnt(0x11):仅 FAT12/16 有效 → 根目录区扇区数算错,数据区起点偏移;FAT32 中此字段非 0 本身就是异常信号。FATSz16/FATSz32:同上,直接决定数据区起点。- 校验方法:用字段反推总扇区数与分区大小比对,或检查 FAT1 开头是否为
F8 FF FF ...形态、数据区簇2开头是否为合法目录项。
- FSInfo 不可信:FreeCount/NxtFree 只是提示值,分析空闲空间必须扫 FAT;出题人可能在 FSInfo 里藏不影响挂载的数据。
- 保留区空隙:FAT32 保留区通常 32 个扇区,实际只用 0、1、2、6、7 号,其余保留扇区是全 0 或引导代码残留——常见藏数据区;同理 FAT1 末尾不足整扇区部分、FAT2 之后到数据区之间的对齐空隙也值得看。
- 进一步的多格式对比和 010 Editor 实操见 5 实战解析要点;exFAT/NTFS 的对应结构见 3 exFAT文件系统、4 NTFS文件系统。