NTFS 数据流(ADS)与隐藏数据

NTFS 上"一个文件"并不等于"一份内容"。同一个文件对象可以挂载多个独立的 $DATA 流:一个是匿名流(文件名能看到的那份),其余全是命名流(Named Stream),通过 文件名:流名 访问。

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

关键词:ADS、备用数据流、命名流、$DATA、$I30、索引目录、Slack space、未分配空间、隐写术
难度:进阶
前置知识:NTFS MFT 结构、簇与簇链、十六进制分析
相关文章:NTFS 结构与 MFT 解析、文件头与文件签名


一、概述

NTFS 上"一个文件"并不等于"一份内容"。同一个文件对象可以挂载多个独立的 $DATA 流:一个是匿名流(文件名能看到的那份),其余全是命名流(Named Stream),通过 文件名:流名 访问。

由此产生一个反直觉的事实:dir 看到的文件清单与磁盘上实际存在的数据流数量完全不对应。一个 0 字节的 readme.txt 可以在磁盘上带着 2 MB 的 readme.txt:hidden 流——资源管理器、多数取证工具的默认视图、ls 全都看不见它。

更麻烦的是三条:命名流大小上限等于卷大小(可远超主文件)、各流互不共享数据(删主文件不会删流)、默认流可以是空的(内容挪进命名流即可规避"按大小"的内容检查)。

这一层挖掘在分析环节的扩展挖掘阶段,位置是 NTFS 结构与 MFT 解析 之后——那里读的是 $DATA 属性的正常形态,这里读的是它能承载的东西。往下的空间是四个逐层加深的藏匿层次,从文件系统原生特性一路走到格式内部:

层次 机制 隐蔽性 检出难度
命名流(ADS) 文件系统原生特性 ★★★ ★★ istat / fls 可查
$I30 索引目录 目录项的排序结构 ★★★ ★★★ 需解析 INDX
Slack / 未分配空间 簇内 / 空闲簇残留 ★★★★ ★★★ 需按块提取 + 搜索
隐写术 合法格式内嵌密文 ★★★★★ ★★★★★ 需格式感知

这个顺序本身就是检出成本递增的顺序。前两层是确定性的(枚举即可),第三层需要提取加搜索,第四层需要格式知识。报告里把这四层的覆盖情况分别写清,比笼统说一句"已全面检查"有用得多。

上游是元数据判读。下游两个方向:找到载体之后的内容提取与验证,以及——如果检出的是恶意载荷,如何在不做在线分析的前提下完成定性(这属于内容边界,这里只讲检出与提取)。

落到具体证据上:

  • 文件名:流名 的访问语法 —— 命名流的标识方式。流名几乎可含任意字符,历史上存在 file.txt:..\..\dir 形式的路径穿越用法,不要假设流名是干净的
  • file.txt:Zone.Identifier —— 浏览器下载文件的系统标记,含 ZoneId 与来源 URL。这是正常现象,不是恶意证据。判断 ADS 是否恶意必须提取内容并验证
  • file.txt:$DATA —— 等价于默认流,某些工具的显式写法
  • $I30 索引与 INDX 缓冲区 —— 目录项的排序结构。INDX 不是 MFT 记录(istat 里 MagNum 字段区分),这是NTFS 结构与 MFT 解析2.2 那张魔数表里最容易误判的一项
  • Slack space —— 文件长度不是簇大小整数倍时尾部剩余的字节。在已分配簇内部,blkls 不含此项,需按簇读取后自行截取
  • 未分配空间 —— 空闲簇与空闲簇链,blkls 直接输出。与 slack 的区别很重要:前者在文件系统外部,后者在内
  • DigiForensics.dd 的 blkls -o 2048 输出 —— 未分配块提取。注意卷层的未分配与文件系统层的未分配是两回事
  • 隐写载体 —— PNG 的 IEND 之后、JPEG 的 FFD9(EOI)之后、GIF 的 0x3B 之后、PDF 的 %%EOF 之后、BMP 声明尺寸小于实际大小、OLE 复合文档的未使用流。它们的共同点是文件本身完全正常,双击能打开,校验和也正确

走完这一层能回答的是这些:一个文件对象到底带几个数据流、目录里哪些条目是"幽灵文件"、slack 与未分配空间该分别怎么提取、以及为什么"文件被覆盖"不等于"旧版本彻底消失"(slack 里往往还残留更早版本的内容)。

最后一条是从容性往回追的一条线索。2.4 讲 slack 为什么有东西:文件系统只把"声明长度"以外的部分当作可复用,读取旧内容前不会清零。同理,删除文件的真实过程有四步,第四步"簇内数据本身不被清零"才是取证的根本前提——内容还在,只是"不再被任何文件引用"。

有几件事不归这一层管:提取出来的内容说明什么,解读要看具体载体类型;这是不是恶意载荷的定性,2.6 那句区分最重要——"PNG 的 IEND 之后有数据"是客观事实;"这是攻击者藏的命令"需要提取、解码、验证,这两层在报告里必须分开写;簇链已经断了怎么恢复文件走文件雕刻路线,见文件雕刻与签名恢复;加密流怎么读看属性标志里的加密位,见EFS 文件系统加密取证。

往下读之前必须先掌握 NTFS 结构与 MFT 解析——这里的每一层都建立在 MFT 属性结构之上:命名流是 $DATA 属性的名称字段、$I30 是目录的 $DATA 内容、slack 依赖 $DATA 声明长度与簇大小。没读那篇就读这篇,会觉得所有名词都是悬空的。

另外需要十六进制查看与搜索的基本功(xxd、strings、grep -aob)——核心操作大量是"提取一块区域然后搜关键字",这个动作本身不难,但偏移算错就读到别的地方去了。

四层机制的检出原理在核心原理,对应的命令链在操作步骤。准备动手前建议先扫一遍常见陷阱——这个领域最贵的错误不是提取不到,而是"提取到了却把它写成了另一种东西",那一章每一条都对应一个具体的定性错误。完整走一遍的参考流程见案例示例,四层命令的直接抄写见命令速查。

二、核心原理

2.1 $DATA 属性:匿名流与命名流

回到 MFT 的 $DATA 属性(类型 0x80),它的名称字段决定了它是哪一个流:

类型 名称长度字段 访问语法 内容
匿名流(默认流) 0 type report.docx 文件的正文,每个文件有且仅有一个
命名流(ADS) > 0,UTF-16LE type report.docx:Zone.Identifier 附加内容,可为任意大小

关键点:

  1. 命名流名几乎可含任意字符(历史上存在 file.txt:..\..\dir 形式的路径穿越用法),不要假设流名是干净的。
  2. 命名流大小上限 = 卷大小,可远超主文件;各流互不共享数据,删主文件不会删流。
  3. 默认流可以是空的(0 字节)——内容挪进命名流即可规避"按大小"的内容检查。
  4. "备用数据流"是历史遗留叫法:早期服务 Macintosh 双机互联,现被大量滥用为隐藏载体。
流名约定 常见用途
file.txt:$DATA 等价于默认流,某些工具的显式写法
file.txt:Zone.Identifier 浏览器下载文件的系统标记,含 ZoneId 与来源 URL(正常现象)
file.txt:$J / 其他自定义名 恶意载荷、配置、加密容器

注意:Zone.Identifier 是正常现象,不是恶意证据。判断 ADS 是否恶意必须提取内容并验证,不能仅凭"存在 ADS"定性。

2.2 命名流对系统工具的可见性

哪些工具能看到 ADS,哪些看不到——这决定了"用常规方法查过"到底意味着什么:

工具/接口 默认流 命名流
Windows 资源管理器 ✅ ❌ 完全不可见
dir(cmd 默认) ✅ ❌(需 /r 参数)
PowerShell Get-Content ✅ ✅ 用 -Stream 参数(PS 5.0+)
Python open() ✅ ✅ 传 file:stream 路径
fls / istat(TSK) ✅ ✅ 列出 file.txt$DATA 形式条目
Autopsy / FTK Imager ✅ ✅ 需切到数据流视图
多数"快速取证"脚本 ✅ ❌ 常漏
# Windows 原生枚举 ADS(PowerShell 5.0+)
Get-Item -Path 'C:\inetpub\wwwroot\default.aspx' -Stream *
Get-Content -Path 'C:\inetpub\wwwroot\default.aspx' -Stream 'hidden'

取证含义:"常规文件遍历没发现异常"不能作为"无隐藏数据"的结论。 判定必须基于 istat 的属性列表(见操作步骤 3.2),而不是文件管理器视图。

2.3 $I30:把目录变成排序索引

NTFS 的目录不只是"文件名列表",它同时是一个 B+ 树索引,索引内容存在名为 $I30 的命名 $DATA 流里(这是一个例外:通常 $DATA 存内容,但目录的 $DATA 存的是 $I30 索引)。

索引记录(INDX 缓冲区)的关键点:

项目 说明
魔数 INDX(49 4E 44 58)
作用 让 dir 能按名字排序、让查找 O(log n)
存储位置 目录 MFT 记录的 $DATA 命名流 $I30
条目内容 文件名(8.3 名 + Win32 名)、MFT 引用、时间戳

取证价值与风险:

  • 价值:删文件时 $MFT 条目被标记删除,但** $I30 索引条目可能仍残留**——它提供了"目录里曾经有过哪些文件名"的独立线索。
  • 风险:索引与 $MFT 可能不一致。工具改名后只改 $MFT 不改索引(反取证常见做法),会出现"目录树里没有、但索引里有"的幽灵文件——这正是"文件被隐藏"的检测信号。
  • 必须提取原始 INDX 缓冲区人工比对,因为主流工具通常只呈现 $MFT 视角。
icat -o 2048 /work/DigiForensics.dd 5-0-0 > /tmp/root-i30.bin   # 目录的 $DATA 流就是 $I30
xxd -l 4 /tmp/root-i30.bin     # 期望看到 49 4E 44 58 = "INDX"

2.4 Slack Space:簇内的"边角料"

若文件长度不是簇大小的整数倍,尾部剩余字节就是 slack:

簇 0(4096 字节)
┌────────────────────────────────────────┐
│ 文件数据 3500 字节($DATA 声明的真实大小)│
├────────────────────────────────────────┤ ← slack:596 字节
│ ★ 前一个文件的残影 / 已删文件 / 密钥    │
└────────────────────────────────────────┘

为什么 slack 里有东西:文件系统只把"声明长度"以外的部分当作可复用,读取旧内容前不会清零,所以前一次占用该簇的文件内容会原样留在 slack。检出方法是逐簇提取 slack 做关键字/结构搜索。注意陷阱:文件被覆盖时,slack 里往往还残留更早版本的内容。

Slack 与"未分配空间"的区别很重要:

Slack space 未分配空间(unallocated)
所在 已分配簇内部 空闲簇 / 空闲簇链
提取方式 blkls 不含此项,需按簇读取后自行截取 blkls 直接输出
常见误区 以为"文件内容之外没东西" 以为"只有删掉的文件才在这"

2.5 未分配空间与文件残留

删除一个文件的真实过程(NTFS):① $MFT 条目在用位归零,条目内容通常不擦除;② $I30 索引对应条目被标记无效;③ 数据簇在 $Bitmap 中被标记为空闲;④ 簇内数据本身不被清零。

第 ④ 步是取证的根本前提:内容还在,只是"不再被任何文件引用"。三种提取范围:

blkls -o 2048 /work/DigiForensics.dd > /tmp/unalloc.bin                    # ① 文件系统内未分配块
grep -aob 'target-string' /work/DigiForensics.dd | head                     # ② 全镜像搜索(含分区间隙)
blkls -o 2048 -s 1-2 /work/DigiForensics.dd | head                         # ③ 列出空闲块地址(可自行取 slack)

2.6 隐写术基础:格式内的合法藏身处

前四层都是"文件系统层面的藏匿"。隐写术不同:它把数据藏在文件格式的合法空白处,文件本身完全正常,双击能打开,校验和(若有)也正确。

载体 藏匿位置 典型容量 检出手段
PNG IEND 块之后追加任意数据 无限 file 报 "data after IEND";检查 IEND 是否在文件末尾
JPEG EOI 标记(FFD9)之后追加 无限 搜索 FFD9 是否在文件末尾
JPEG EXIF 段中的厂商私有区域 KB 级 EXIF 字段异常长/异常值
GIF Trailer 0x3B 之后 无限 尾字节检查
PDF %%EOF 之后、增量更新区 无限 %%EOF 后有数据;对象代数递增
BMP 文件头声明尺寸 < 实际文件大小 无限 像素数据量 > 声明值
Office 97-2003 OLE 复合文档未使用的流 不定 用 oletools 逐流枚举
MP3 / 视频 标签区扩展字段 KB 级 帧结构边界检查

重要区别:隐写术的"发现"与"证明"是两件事。"PNG 的 IEND 之后有数据"是客观事实;"这是攻击者藏的命令"需要提取、解码、验证。报告中必须区分这两层。


三、操作步骤

3.1 第 1 步:确认卷与基本参数