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 位也存同一个值。

值含义
0xF03.5寸 1.44MB 软盘;部分移动介质也用
0xF8硬盘 / U盘 ← 最常见
0xF95.25寸 / 3.5寸 720KB~1.2MB 软盘
0xFA5.25寸 320KB 软盘 / RAM 盘
0xFB3.5寸 640KB 软盘
0xFC5.25寸 180KB 软盘
0xFD5.25寸 360KB 软盘
0xFE5.25寸 160KB 软盘 / 8寸单面
0xFF5.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 的唯一区别是表项宽度:

类型表项宽度有效位说明
FAT1212 bit12两个字节装1.5个表项,奇偶拆分(见下)
FAT1616 bit16每表项2字节,直接小端读
FAT3232 bit28高4位保留,读取后必须 & 0x0FFFFFFF

表项特殊值

含义FAT12FAT16FAT32(已掩高4位)
空闲簇0x0000x00000x00000000
坏簇(不可用)0xFF70xFFF70x0FFFFFF7
簇链结束 EOC≥ 0xFF8≥ 0xFFF8≥ 0x0FFFFFF8
正常值2 ~ 0xFEF2 ~ 0xFFEF2 ~ 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)

位值含义
00x01只读 Read Only
10x02隐藏 Hidden
20x04系统 System
30x08卷标 Volume Label(该项是卷标,FstClus 必须为0)
40x10子目录 Directory
50x20归档 Archive
0~30x0F长文件名项(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 下删除文件(不进回收站)只做两件事,内容本身不动:

  1. 目录项首字节改为 0xE5(FAT32 下 Windows 还会把 FstClusHI 清零,导致大簇号文件丢高位)。
  2. 该文件在 FAT 中的整条簇链表项清零(标记为空闲簇)。

文件内容还躺在数据区,直到被新数据覆盖。手工恢复思路:

  1. 在目录区找到 0xE5 开头的目录项,读出 FileSize 和起始簇(FAT32 注意补回高位:高位被清,若镜像不大可假设高位=0;大镜像需从内容特征猜)。
  2. 簇链已断,假设文件连续存放:从起始簇开始,按 ceil(FileSize / 簇大小) 个连续簇直接 carve 出来。
  3. 用文件头尾特征校验(如 JPEG 的 FF D8 ... FF D9);不连续的文件(大文件、碎片化盘)需按内容特征或备份 FAT2 重建簇链。
  4. FAT2 是 FAT1 的镜像:删除时两份通常同时被清,但若题镜像只改了 FAT1,FAT2 里可能完整保留簇链——比对 FAT1/FAT2 差异是 CTF 高频考点。

反方向:已删除文件的残留目录项、残留簇内容、被截断的 LFN,都是藏 flag 的常见位置。

取证要点

  1. 卷序列号(VolID)关联:DBR 的 0x27/0x43 卷序列号会进入 Windows 的 LNK 快捷方式(目标卷信息)和注册表 MountedDevices 等位置,可与 1 MBR分区表 的磁盘签名互补,用于”证明这个U盘上的文件在这台机器上被打开过”。
  2. 备份 DBR:FAT32 备份在 BkBootSec(通常 6 号扇区,备份 FSInfo 在 7 号)。主 DBR 被清零/加密时,直接跳 6 号扇区找备份恢复 BPB;注意备份的 Signature 也是 55 AA,识别特征是 0x52 处 "FAT32 "。FAT12/16 规范上没有备份 DBR,坏了只能靠同类镜像猜 BPB。
  3. CTF 常篡改字段及后果:
    • SecPerClus(0x0D):非 2 的幂或与实际不符 → 簇→偏移换算整体错位,数据区文件全部解析出垃圾。
    • RsvdSecCnt(0x0E):改大/改小 → FAT 区和数据区起点算错,FAT 表读到的全是目录/文件内容。
    • RootEntCnt(0x11):仅 FAT12/16 有效 → 根目录区扇区数算错,数据区起点偏移;FAT32 中此字段非 0 本身就是异常信号。
    • FATSz16/FATSz32:同上,直接决定数据区起点。
    • 校验方法:用字段反推总扇区数与分区大小比对,或检查 FAT1 开头是否为 F8 FF FF ... 形态、数据区簇2开头是否为合法目录项。
  4. FSInfo 不可信:FreeCount/NxtFree 只是提示值,分析空闲空间必须扫 FAT;出题人可能在 FSInfo 里藏不影响挂载的数据。
  5. 保留区空隙:FAT32 保留区通常 32 个扇区,实际只用 0、1、2、6、7 号,其余保留扇区是全 0 或引导代码残留——常见藏数据区;同理 FAT1 末尾不足整扇区部分、FAT2 之后到数据区之间的对齐空隙也值得看。
  6. 进一步的多格式对比和 010 Editor 实操见 5 实战解析要点;exFAT/NTFS 的对应结构见 3 exFAT文件系统、4 NTFS文件系统。