一、概述
NTFS 把所有元数据集中在一张定长表($MFT)里,ext 系相反:元数据分散在成百上千个 inode 和索引节点中。
这个差异不是风格问题,它直接决定两种取证思维:
- NTFS 是查一张表,绝大多数问题在表内解决
- ext4 是查一棵结构树,必须沿"超级块 → 块组 → 组描述符 → inode 表 → 索引节点"逐层下钻
走错层的代价是静默的:在错误偏移读到的字节不会报错,只会给出"看起来合理但完全错误"的结构。
这一层在分析环节的文件系统元数据层,与 NTFS 那一组文章平级,不是它的续篇。上游是分区定位(mmls → 确定 ext4 分区起始扇区),下游三个方向:
- 删除痕迹判读 —— 目录项被删但 inode 完好,这是 ext 系最常见的可恢复形态
- 空间映射与内容提取 —— extent 把逻辑块映射到物理块,这是"拿到 inode 之后怎么把数据取出来"
- 时间戳语义解读 —— ext 的四个时间戳含义与 NTFS 不完全对应,误读会让时间线整体偏移
它是 Linux 取证的入口,后续的日志分析、工件地图、持久化痕迹都建立在"能正确读出 inode 与时间戳"这个能力之上。
落到具体证据上:
- 超级块(偏移 1024,魔术
0xEF53) —— 不在偏移 0,前 1024 字节留给引导代码。这个偏移是硬指标级别的常见错误,不能猜。超级块里有关键参数:块大小、inode 大小、inode 总数、特性标志、状态位、最后挂载时间、journal inode 号
s_state 状态位 —— clean / clean with errors / not clean。clean with errors 意味着已正确卸载但曾有错误,不是脏挂载,两者的处置不同
Filesystem features 特性标志 —— has_journal、extent、64bit、dir_index、ext_attr。这一行是判定 ext2/3/4 版本的唯一依据
- inode 表 —— 每个文件和目录一个 inode,存类型、权限、UID/GID、大小、时间戳、指向数据的指针。inode 本身没有文件名
- 四个时间戳 ——
atime、ctime、mtime、dtime。dtime 非 0 是删除状态的判据(NTFS 没有对应字段)
links_count —— 硬链接计数。links_count = 0 与 dtime != 0 共同确认为已删除,只看一个会误判
- extent 映射 —— ext4 默认启用,形如
logical 0 to 1 phys 131073 to 131074。与 ext2/3 的间接块指针是完全不同的机制,按老经验解读会全部读错
i_block 间接块 —— ext2/3 的三层间接寻址(直接/一级/二级/三级)。ext3 及以下没有 extent,遇到老镜像仍要会
- xattr 扩展属性 —— 藏在 inode 之外的元数据,
ea_list 可列。security.selinux、user.* 都在这里,ls -l 看不到
debugfs 的只读模式 —— 全部操作的入口。绝不能加 -w,那会写回镜像、破坏原始状态
DigiForensics.dd 的分区切片 —— 用 dd 按起始扇区切出。超级块在切片的 1024 处(不是原镜像的 1024)
走完这一层能回答的是这些:这个分区是不是 ext、块与 inode 的关键参数是多少、哪些 inode 处于已删除状态、已删除 inode 的原始路径还能不能找回来、以及某个 inode 的数据块在物理上位于哪里。
回答不了的几件事:
- 文件名(当目录项已删) —— ext 系删除通常只删目录项,inode 完好但名字不可得。
ncheck 返回 <not found> 是常态,这时只能靠大小、属主、内容语义等间接特征推断身份,且必须在报告里写明"身份为推断"
- "是谁删的" —— inode 里没有用户身份,只有 UID/GID。UID 1000 是谁要靠
/etc/passwd 反查,而反查依赖镜像里那份 passwd 文件本身可信
- 崩溃前未落盘的操作 —— journal 里有,但那属于日志层(2.5 与 Steady-State 讨论的同类问题)
- 数据块被覆盖后的内容 —— 簇链断了就取不到,只能走雕刻路线
- xattr 里的任意值语义 —— 属性名可自定,看到
user.foo 只能确认"存在自定义属性",不能推断含义
往下读之前需要会读分区表(知道 mmls 输出里每个分区的起始扇区含义)、理解块与簇的概念、以及知道 debugfs 默认只读。
如果完全没接触过 ext 系列,2.1 的版本对照表要慢慢看——ext2/3/4 三者的差异(有无 journal、有无 extent、inode 大小)会一路影响后面每一层的解读。
另外建议先读一遍 文件系统取证总览 建立"哪些结构属于元数据"的划分,再进入 inode 层。
超级块到 inode 表的逐层结构推导在核心原理,debugfs 的只读操作链在操作步骤。
静默出错是这一层最大的特征——偏移错一层不会报错,只会给你看起来合理的错误结构。所以常见陷阱的 4.1 组(偏移与版本误用)值得在动手前先读。
完整走一遍见案例示例,debugfs 全部只读命令见命令速查。
二、核心原理
2.1 ext2/3/4 的演进与差异
| 特性 |
ext2 |
ext3 |
ext4 |
| 日志 |
❌ |
✅ |
✅(改进) |
| 目录索引 htree |
❌ |
✅ |
✅ |
| extent 映射 |
❌ 仅块间接 |
❌ |
✅ 默认启用 |
| 亚秒时间戳 |
❌ |
❌ |
✅ |
| xattr 扩展属性 |
❌ |
❌ |
✅ |
| inline data |
❌ |
❌ |
✅ |
| 64 位 / 最大容量 |
❌ / 32 TB |
部分 / 32 TB |
✅ / 1 EB |
取证含义:有日志 = 元数据改动可回放 = 非正常关机时元数据可能"待回放";有 extent = 用一串"逻辑块→物理块"映射描述数据,比多层间接块简洁但不直观;有 xattr = 部分元数据(SELinux、ACL、用户自定义数据)不在 inode 主结构里**。
2.2 超级块:位于偏移 1024
主超级块位于文件系统起始 + 1024 字节(0x400),魔数 s_magic = 0xEF53(小端读作 53 EF),长度 1024 字节,所有字段均为小端。
偏移 长度 字段 偏移 长度 字段
0x00 4 s_inodes_count 0x38 2 s_magic 0xEF53
0x04 4 s_blocks_count_lo 0x3A 2 s_state 1干净/2有错/FFFFFFFF未卸载
0x14 4 s_first_data_block 0x40 4 s_lastcheck 上次 fsck 时间
0x18 4 s_log_block_size 0x4C 4 s_rev_level 0=原始 1=动态 inode
0x20 4 s_blocks_per_group 0x58 2 s_inode_size 128 或 256
0x28 4 s_inodes_per_group 0x5A 2 s_block_group_nr
0x2C 4 s_mtime 最后挂载时间 0xE0 4 s_journal_inum 0=无日志(ext2)
0x30 4 s_wtime 最后写时间 0x108 4 s_mkfs_time 创建时间
块大小换算:块大小 = 1024 << s_log_block_size。0 → 1024 字节、2 → 4096 字节。不要假设是 4096——1024、2048、4096 都合法,旧卷常见 1024。
超级块备份:启用 sparse_super 时,块组 1、3、5、7、9、25、27 各有一份(共 7 份)。主超级块损坏时仍有备份可读,这是 ext 系相对 NTFS 的一个取证优势。
xxd -s 1024 -l 16 /work/DigiForensics-part3.dd # 期望 53ef(0xEF53,小端)
2.3 块组与组描述符
ext4 把整个文件系统切分成等大的块组,每组包含块位图、inode 位图、inode 表和数据块。
组描述符表(GDT) 记录每组的关键位置:bg_block_bitmap_lo(块位图块号)、bg_inode_bitmap_lo(inode 位图块号)、bg_inode_table_lo(inode 表起始块号)、bg_free_blocks_count_lo / bg_free_inodes_count_lo(空闲计数)。
块组 0 的特殊性:超级块和 GDT 占据开头位置,因此块组 0 的数据区从第 2 块或更靠后开始——这解释了 ext 的第一个数据块号通常不是 0。
取证信号:某块组 inode 位图全满而相邻组大量空闲,可能意味着目录被大量创建后删除(inode 耗尽)或 inode 表损坏。
2.4 inode:ext 的元数据核心
每个 inode 是定长结构(长度 = s_inode_size,ext2/ext3 为 128 字节,ext4 通常 256 字节)。基础 inode 布局(ext2/ext3/ext4 的前 128 字节完全一致):
| 偏移 |
长度 |
字段 |
取证意义 |
| 0x00 |
2 |
i_mode |
类型 + 权限(S_IFREG 0x8000 / S_IFDIR 0x4000 / S_IFLNK 0xA000) |
| 0x02 |
2 |
i_uid |
属主 UID(低 16 位) |
| 0x04 |
4 |
i_size_lo |
文件大小低 32 位 |
| 0x08 / 0x0C / 0x10 |
4×3 |
i_atime / i_ctime / i_mtime |
访问 / 状态变更 / 内容修改(秒) |
| 0x14 |
4 |
i_dtime |
删除时间(非 0 = 曾被删除) |
| 0x18 |
2 |
i_gid |
属组 GID(低 16 位) |
| 0x1A |
2 |
i_links_count |
硬链接数(0 = 已删除) |
| 0x1C |
4 |
i_blocks_lo |
占用扇区数(512 字节单位,非文件大小) |
| 0x20 |
4 |
i_flags |
0x00080000 extent;0x10000000 inline_data |
| 0x28 |
60 |
i_block[15] |
传统块指针 / extent 根 |
| 0x68 |
4 |
i_file_acl_lo |
指向扩展属性(xattr)块 |
| 0x6C |
4 |
i_size_high |
大小高 32 位(>16 GB 才用) |
ext4 的 extra inode(i_inode_size > 128 时追加在 0x80 处):0x80 i_extra_isize(附加区长度,常见 0x20)、0x84/0x88/0x8C i_ctime_extra / i_mtime_extra / i_atime_extra(亚秒精度扩展位,与秒字段拼接)、0x90/0x94 i_crtime / i_crtime_extra(**文件"创建时间"**及其扩展位)。
i_crtime 不是 ctime:ext4 在 i_ctime 之外另加了真正的"文件创建时间"。传统 Linux 三时间是 atime / mtime / ctime(ctime 指 inode 元数据变更,不等于创建)。混为一谈会让时间线错位——报告中必须分别标注。
inline_data 位(i_flags 的 0x10000000):文件内容直接存在 inode 结构内、没有数据块。这是几十到一百多字节小文件在 ext4 上的常见存储方式。
符号链接的陷阱:i_size 是目标路径字符串的长度(通常几十字节),不是 0。目标路径就存在其数据块里。
误判"符号链接应该是 0 字节"会漏掉极有价值的原始路径信息。
2.5 i_block:间接块与 extent
i_block 是 15 个 32 位值(60 字节),有两种用法。
(A)传统块间接(ext2/3,或 ext4 未启用 extent 时):i_block[0..11] 是直接指针;i_block[12] 一级间接,指向一个"块号数组"块;i_block[13] 二级间接;i_block[14] 三级间接。问题在于随机访问需多次间接寻址,取证逐层手工下钻非常痛苦。
(B)Extent 映射(ext4 默认):把数据描述成若干条"从第 L 逻辑块开始、连续 N 块、物理上从 P 块开始"的记录。
i_block[0..3] 作为 extent header:
eh_magic = 0xF30A eh_entries = 条目数
eh_max = 最大条目数 eh_depth = 0(叶) / >0(索引,需再下钻)
叶节点每条记录(12 字节):
ee_block(4) 起始逻辑块 | ee_len(2) 块数
ee_start_hi(2) 物理块高 16 位 | ee_start_lo(4) 物理块低 32 位
例:逻辑块 0–99 → 物理块 2000–2099
取逻辑块 50 → 物理块 = 2000 + (50 - 0) = 2050
取证意义:一次解析即得全部映射,无需多层间接;ee_len 小的记录多 = 碎片严重,连续大 extent = 碎片少;映射记录了"应占哪些物理块",与未分配空间比对可发现被覆盖。
关键提醒:eh_depth > 0 时存在索引 extent 节点,叶节点在下一层,只读第一层会漏掉大文件的数据位置。
2.6 xattr:藏在 inode 之外的元数据
ext4 的扩展属性存储在 inode 之外的独立块中(i_file_acl_lo 指向它)。
user.* 命名空间是隐蔽存储的经典位置——不出现在 ls 输出里,不影响文件大小显示,常被快速取证流程完全忽略。发现文件大小与预期不符时,必须检查 xattr。
security.* 记录 SELinux 标签与文件能力,posix_acl_access 记录访问控制列表(权限异常值得关注),trusted.* 需 root 才能读。
三、操作步骤
3.1 第 1 步:定位 ext 分区并确认超级块
mmls /work/DigiForensics.dd # 假设 ext4 分区 Slot 05,起始扇区 1181696
dd if=/work/DigiForensics.dd of=/work/DigiForensics-part3.dd bs=512 skip=1181696 status=none
xxd -s 1024 -l 16 /work/DigiForensics-part3.dd # 期望 53ef —— 关键:偏移是 1024,不是 0
fsstat -o 1181696 /work/DigiForensics.dd # TSK 侧确认(不必切卷)
3.2 第 2 步:用 debugfs 读取超级块
debugfs 是 e2fsprogs 自带的 ext2/3/4 离线检查工具,默认只读、不写盘:
debugfs -R "stats" /work/DigiForensics-part3.dd
printf 'stats\
show_super_stats\
' > /tmp/df.cmds && debugfs -f /tmp/df.cmds /work/DigiForensics-part3.dd
| 输出字段 |
意义 |
Filesystem volume name |
卷标(与委托方描述是否一致) |
Filesystem features |
特性集(has_journal = ext3/4;extent + 64bit = ext4) |
Block size |
常见 1024 / 4096,必须实测 |
Inode size / Inode count / Free inodes |
inode 结构与用量 |
Filesystem state |
clean / clean with errors / not cleanly unmounted |
Last mount time / Filesystem created |
最近挂载时间 / 创建时间 |
Journal inode |
日志 inode 号(0 = 无日志,即 ext2) |
⚠️ 绝对不要使用 debugfs -w,也不要执行 write / rm / zap_block 这类写命令——会直接改写镜像,且不经过任何确认提示。
3.3 第 3 步:遍历已删除 inode