ext4 结构与 inode

NTFS 把所有元数据集中在一张定长表($MFT)里,ext 系相反:元数据分散在成百上千个 inode 和索引节点中。

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

关键词:ext4、superblock、块组、inode、extent、xattr、debugfs、journal 难度:进阶 前置知识:Linux 文件系统通识、磁盘分区结构、十六进制分析 相关文章:文件系统取证总览、文件头与文件签名

一、概述

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 步:遍