1. 从一次“0字节却有内容”的排查说起
1.1 一次反直觉的文件系统异常
大概是两年前,我接手一批从旧服务器迁移出来的文件。同事很慌,说有个“报价清单.txt”,右键属性显示0字节,双击用文本编辑器打开,里面却整整齐齐写了上百行数据。第一反应是磁盘出了问题,但chkdsk跑完没报错,文件的修改时间也正常,哈希算了一遍也稳定。
后来我查了那个文件的十六进制和目录结构,才发现问题根本不在文件内容,而在于Windows资源管理器给我看的“大小”这一列,只统计了文件的主数据流。真正的内容被存放在同一个文件“外壳”下的另一条数据流里,也就是NTFS的备用数据流(Alternate Data Streams,简称ADS)。中文安全社区里有些人习惯叫它“交换数据流”,其实是一回事。
1.2 备用数据流要解决什么问题
ADS并不是什么黑客发明的神秘功能,它从NTFS诞生起就存在。最早的设计目标,是让NTFS能完整搬运Mac系统HFS文件里的“资源分叉”——那个年代的Mac文件分数据和资源两部分,如果Windows这边没有对应位置存放资源部分,文件一拷贝过来就要丢东西。NTFS干脆设计成“一个文件条目可以挂多个数据块”,每个数据块就是一条命名流,这样资源分叉进来就有地方放了。
后来Windows自身也开始用它。最典型的是你从浏览器下载一个文件,系统会自动给文件挂一条名为Zone.Identifier的备用流,里面写着“这文件来自Internet”,Office等软件拿到这个标记就会多一重“解除锁定”的限制。至于文件摘要信息、某些应用缓存、反病毒软件的标记文件,都可能藏在ADS里。
1.3 这篇文章适合谁、讲了什么
如果你是安全工程师、系统运维、数字取证人员,或者只是对Windows文件系统有好奇心的开发者,这篇文章可以帮你把备用数据流这件事彻底搞明白。我会从存储原理讲起,然后是命令行到底怎么创建、查看、删除流,再讲恶意利用和排查盲区,最后给一套可以直接抄作业的检测与清理流程。
读完之后,你至少能做到三件事:不看文件“大小”也能发现隐藏流;用PowerShell把整个目录的ADS枚举出来;知道什么时候该删、什么时候不该删。
2. NTFS备用数据流的存储原理与设计初衷
2.1 文件不是“一个字节串”,而是“一组命名数据流”
在NTFS卷上,文件在底层并不是一段连续的字节串,而是一组“命名数据流”的集合。每个文件至少有一个未命名的主数据流,也就是我们平时读到的文件内容;在此基础上,可以叠加任意数量的备用数据流,每条流都有自己的名字。
引用格式很简单:文件路径:流名称。比如demo.txt:secret.txt,意思就是路径为demo.txt的文件里,有一条名为secret.txt的备用流。文件系统在目录项中记录的是宿主文件的基本信息,比如文件名、大小、时间戳,但这些信息默认只对应主数据流。
打个比方,普通文件就像一个只有一只抽屉的柜子。ADS相当于在柜子侧面多做了一个暗格,外观、锁孔、说明书都和原来一模一样,但暗格里的东西只有知道机关的人才能取出来。Windows图形界面默认不画暗格,所以你看“属性”“大小”“占用空间”,永远只看得到主抽屉。
需要特别强调的是,备用流不能独立存在,它必须依附于一个宿主文件或宿主目录。宿主被删除,所有流一起消失;宿主被重命名,流跟着保留;宿主被移动到另一个NTFS分区,流的命运则取决于复制工具。
2.2 为什么NTFS要设计这种结构
NTFS的早期版本在设计时高度兼容别的系统已经是一件很重要的事。当年的Windows NT要和Mac系统、POSIX子系统互通文件,而不同文件系统对“一个文件到底有几部分内容”的认知差异很大。Mac的HFS文件有data fork和resource fork两个分支,如果NTFS只认一个数据块,从Mac拷过来的文件在Windows上读起来就是残缺的。多流设计天然适合这种场景。
这个设计还有一个副产品:文件系统层面的“边车文件”能力。一个文件可以顺便带一些附属数据,而不需要再造一个新文件。后来Windows很多组件都看中了这个特性,比如Exchange Server早期把邮件附件拆成多条流存放,IIS也用流存过索引元数据。再比如编译器生成的“作者”“标题”“备注”等摘要信息,很多就存在SummaryInformation这条流里。
这解释了为什么ADS在系统中如此普遍:它本来就是系统机制的一部分,不是“非正常文件”的代名词。判断一个流是否可疑,不能只看“存在性”,还要看流名称、宿主文件位置、内容形态和来源。
2.3 使用ADS需要满足哪些条件
ADS只在NTFS文件系统上生效,FAT32、exFAT、ReFS之外的大多数文件系统都不支持。想确认当前分区类型,可以在命令行执行:
fsutil fsinfo volumeinfo C:输出中能看到“文件系统名称:NTFS”之类的信息。如果分区不是NTFS,后面所有操作都不适用,这也意味着“把文件复制到U盘(FAT32)再拷回来”是一种粗暴但有效的清理手段,当然这样做会丢失原始证据,做取证时不能这么干。
流名的命名规则和Windows文件名基本一致,不能包含/ \ : * ? " < > |这些字符,长度也受限。注意冒号本身是路径和流名的分隔符,任何一级都不能再出现。流的内容没有格式限制,文本、二进制、PE文件、压缩包,什么都能放。
还有一个容易忽略的点:目录也能挂备用流。给文件夹加一条流完全合法,echo x > somefolder:mystream就能做到,只是资源管理器和大部分命令行工具根本不展示。检测脚本如果只遍历文件、不遍历目录,就会漏掉这一块。
3. 创建与查看:把备用数据流玩明白的完整操作
3.1 最小用例:一条命令创建一个隐藏流
在NTFS分区上打开CMD,随便建一个文件,然后用重定向语法加一条流:
echo 主内容 > demo.txt echo 这是备用内容 > demo.txt:secret.txt type demo.txt more < demo.txt:secret.txt执行结果:type demo.txt只输出“主内容”,more < demo.txt:secret.txt能读出一行“这是备用内容”。文件在资源管理器里大小还是那几个字节,不会因为多了一条流就膨胀。
这里就出现了一个值得品味的事实:重定向符号本身就是创建ADS最常用的方式。很多软件在把日志、配置、临时数据写到磁盘时,只要路径里不小心带上了冒号,就可能把数据写进一条命名流。恶意代码更是只需要一行命令就能完成隐蔽存储。
3.2 查看和读取的几种姿势
查看当前目录下的所有备用流,CMD自带一个参数:
dir /r输出里会把流名也列出来,形如demo.txt:secret.txt。这是最原始、最直接的检查方式,适合快速验证,但不适合写自动化脚本。
在PowerShell里,查看单条流的完整信息可以用:
Get-Item -Path .\demo.txt -Stream *每条流会显示Stream名称和Length。过滤主数据流后,剩下的就是备用流。读取流内容的专门参数也很顺手:
Get-Content -Path .\demo.txt -Stream secret.txtCMD环境里也可以用more读取,或者用文本编辑器直接打开demo.txt:secret.txt,注意旧版Windows的记事本还能打开流路径,新版记事本对这类路径的支持取决于系统版本,不建议依赖它。
用Sysinternals工具也可以快速盘点。工具名就叫Streams,直接扔到目录里执行:
streams.exe C:\work\demo.txt streams.exe -s C:\work-s参数会递归扫描子目录。这类工具的好处是命令行短、输出清晰,适合在应急排查的早期阶段用。
3.3 删除、复制、管理:实际操作中的命令组合
删除一条指定的流:
Remove-Item -Path .\demo.txt -Stream secret.txt删除之后,dir /r里不再能看到demo.txt:secret.txt,但宿主文件的主内容完全不受影响。
把流从A文件复制到B文件,CMD的type配合重定向就能做:
type source.txt:hidden > target.txt:hidden2注意这条命令是在CMD里执行的,PowerShell的重定向语法对流的支持不如CMD这样直观,我更建议用PowerShell读取后落盘的方式做复制,因为这样你同时对内容有掌控权。
批量删流要更谨慎。Sysinternals的Streams工具提供了-d参数:
streams.exe -d C:\work它会列出所有流并询问是否删除,适合在已知流都可丢弃的场景下使用。如果只想清点不想动手,去掉-d即可。删除前务必确认白名单,系统自己生成的Zone.Identifier、摘要信息、某些备份软件的自定义流,都有可能是“正常流”。
4. 恶意利用与排查盲区:安全视角的必修课
4.1 从“文件隐藏”到“零字节武器库”
ADS最让安全人员头疼的地方在于:它的隐藏成本太低。把攻击载荷放进备用流的语法,本质上和把一句话写进文件一样简单。宿主文件可以是一张照片、一个配置文件、甚至一个只有几十字节的文本,从资源管理器看,它就是“一个正常的小文件”。
恶意代码为什么喜欢往这里藏?因为常规的扫描、审计、备份流程,默认都只处理主数据流。你把目录下所有文件哈希一遍,算出来的是主数据流的值;你打包压缩整个目录,压缩工具读的也是主数据流;你把文件复制到其他地方,复制工具大概率也只带走主数据流。于是藏得深的内容就从绝大多数监控视野里消失了。我不展开具体执行手段,但你要知道,一个安全团队如果完全没有ADS的检测能力,就等于看不到一块很大的攻击面。
4.2 为什么杀毒软件和常规监控容易漏掉
很多反病毒软件的实时防护,关注点是进程创建、文件打开、payload落地这些行为,对ADS的写入操作并不敏感。原因很简单:写入ADS本质上就是对宿主文件的一段扩展写操作,难以从“写文件”这个行为里区分出来。而扫描引擎在枚举文件时,很多实现只遍历到主数据流,根本不枚举命名流。
更麻烦的是,有些安全产品自身也用ADS做标记,比如给文件挂上某个自定义流表示“已检查”。这就导致“看到流就报警”的策略根本不可行,误报率会高得没法看。正确做法是先把当前环境里的ADS基线摸清楚,识别哪些流是系统、业务软件、安全产品自己创建的,然后把基线之外的可疑流作为重点排查对象。
4.3 绝不能一刀切的“正常流量”
下面这些流你在机器上看到,很大概率是正常的:
| 流名称 | 常见宿主 | 来源 |
|---|---|---|
| Zone.Identifier | 从浏览器/下载工具获取的文件 | 系统自动写入,标记文件来自Internet |
| SummaryInformation | Office文档 | 文件摘要属性,可能由Office或编制工具写入 |
| DocumentSummaryInformation | Office文档 | 同上 |
| WofCompressedData | Windows系统文件 | 系统压缩相关机制产生 |
判断一条流是否可疑,我习惯按这个顺序来:先看流名称是不是已经见过、能不能解释来源;再看宿主文件出现在什么目录,系统目录、业务目录、还是临时目录;接着看流的大小,几KB的系统标记和几十MB的隐藏压缩包是两种完全不同的量级;最后才决定是保留、导出分析还是直接删除。遇到叫“photo.zip”“update.log”“temp.dat”的流名,反而要格外小心,因为恶意样本特别喜欢伪装成不起眼的扩展名。
5. 取证视角的攻防复盘:发现、提取、清除的完整链路
5.1 第一步:确认文件系统与证据保全
在动手前一分钟,先确认目标分区确实是NTFS:
fsutil fsinfo volumeinfo C:如果是FAT32或exFAT,ADS理论上是不会存在的,排查可以转向其他方向。确认是NTFS后,进入正式流程前建议先做证据保全:把可疑宿主文件连同主数据流一起做哈希,有条件的话整盘镜像更好。因为你后面做任何清理动作都是不可逆的,流一旦删除,里面存的东西就再也找不回来了。
5.2 第二步:全目录枚举备用数据流
下面这段PowerShell脚本,是我在服务器巡检和取证排查中一直用的底稿。它递归扫描指定目录下所有文件和目录的备用流,过滤掉主数据流后输出路径、流名、大小。
$target = "D:\work" Get-ChildItem -Path $target -Recurse -Force -ErrorAction SilentlyContinue | ForEach-Object { Get-Item -Path $_.FullName -Stream * -ErrorAction SilentlyContinue | Where-Object { $_.Stream -notlike '*$DATA' } | Select-Object @{n='Path';e={$_.FileName}}, @{n='StreamName';e={$_.Stream}}, Length } | Format-Table -AutoSize如果你要处理的是几千个文件,建议在最后加一句Export-Csv -Path ads_report.csv -NoTypeInformation,导出成表格慢慢看。脚本里-notlike '*$DATA'的作用是排除主数据流,这个过滤条件在不同PowerShell版本上输出格式略有差异,但这段匹配逻辑在5.1和7.x下都稳定。
单看一个文件也很简单:
Get-Item -Path .\report.doc -Stream * | Format-List Stream, Length如果Stream列只有主数据流,那这个文件就没有备用流。这一步可以用来快速验证批量扫描的结果。
5.3 第三步:提取可疑内容并完成清除
确认一条流可疑之后,先别急着删,把内容导出来再动手。导出二进制流用PowerShell的字节读取方式:
$data = Get-Content -Path .\report.doc -Stream hidden -Encoding Byte [IO.File]::WriteAllBytes("D:\evidence\extracted.bin", $data)注意,Windows PowerShell 5.1里用-Encoding Byte,到了PowerShell 7.x这个参数改名为-AsByteStream,写脚本时看准版本。如果是文本流,直接Get-Content -Stream hidden就能看内容。
确认内容已经没有保留价值后,删掉流:
Remove-Item -Path .\report.doc -Stream hidden清完之后再跑一遍枚举脚本,确认流数量已经恢复基线。删除整个流不影响宿主文件主内容,但如果主内容本身也是恶意的,那要走的就不是“删流”流程,而是整文件隔离处置了。
5.4 一次真实排查的复盘记录
有一次做例行检查,我发现某个业务目录下的几个文档文件大小都只有几KB,但枚举脚本显示每个文件后面都挂着一条名为“photo.zip”的流,单个大小几十MB,合起来塞了将近一个G的数据。从流名看非常像恶意样本,导出后发现里面其实是一组图片缓存,是一个影像处理软件的错误写法,把临时缓存写到了文档流里。
这次经历给我两个印象:一是ADS在现实环境里的使用范围比我预想的更广,很多正经软件也会因为“顺手”把数据写进流里;二是不能只凭“有流”就下结论,必须导出、分析、结合宿主文件来源做判断。安全工作的严谨性恰恰体现在这种地方:你可以快速怀疑,但不能仅凭怀疑就处置。
5.5 防御侧的三个习惯
第一,关键服务器和办公终端上定期枚举ADS,把结果保存成基线文件,以后每次巡检和基线比对,流数量突然增加就是值得查的信号。第二,迁移和备份优先选择支持ADS的工具,比如robocopy加/COPYALL,否则数据在你看不见的地方悄悄丢失。第三,在EDR或主机审计产品能力允许的前提下,把“新增命名流”“命名流大小异常”这类事件纳入告警设计,哪怕前期只观察不处理,也比完全没有可见性好得多。
6. 移动、复制、压缩、备份场景下的“流去向”总表
6.1 一张表看明白各种传输场景
ADS依附于宿主文件,但它命运完全取决于承载它的文件系统和传输工具。我整理了日常最常遇到的几个场景,直接给结论:
| 场景 | 行为 | 备用流去向 |
|---|---|---|
| 同一NTFS卷内移动/重命名 | 文件系统内部更新目录项 | 保留全部流 |
| 不同NTFS卷之间手工复制 | 资源管理器默认复制文件内容 | 大多数情况下不复制ADS |
| NTFS复制到FAT32/exFAT | 目标文件系统不支持命名流 | 备用流被丢弃 |
| robocopy普通复制 | 默认复制数据、属性、时间戳 | 不复制ADS,需加/COPYALL才保留 |
| 打包成zip/rar | 压缩格式不识别NTFS流 | 备用流被丢弃 |
| HTTP/FTP上传下载 | 协议层只传输文件实体 | 备用流被丢弃 |
| 常见备份软件 | 多数只备份主数据流 | 备用流被丢弃 |
| SMB网络共享复制 | 取决于客户端实现 | 多数工具不复制ADS |
这张表的内容看似简单,实际影响很大:它决定了你会不会在迁移后弄丢关键数据,也决定了“把文件复制出去再复制回来”能不能作为一种粗暴清流手段。
6.2 备份与迁移的坑:备份成功但数据缺失
很多人知道robocopy是好用的复制工具,但未必知道它的默认策略不复制ADS。我见过不止一次这样的场景:运维把整个文件服务器从旧机器迁到新机器,robocopy日志全是“成功”,对账文件数量一致,但第二年有人需要某个文件里藏着的扩展属性时,才发现那些附加数据早就在迁移中无声无息地丢了。
避免这个坑的办法很笨但很有效:迁移前后各跑一次ADS枚举,对比流数量和总大小。没有基线的迁移,从文件系统的角度看是不完整的迁移。如果确实没有保留ADS的需求,那也应该是“知道它会丢”的前提下主动接受丢失,而不是毫不知情地被动丢失。
6.3 反过来用:利用“流去向”完成物理级清除
流在复制过程中容易丢失,这也可以反过来变成处置手段。当确认某个文件的所有备用流都不需要保留,而且你又没有时间逐条删除流时,把它复制到FAT32分区或直接打包压缩,再拷回来,流就会自然消失。这个方法在应急场景下快速有效,但不适合取证流程,因为这会破坏原始证据,正确做法仍然是对原文件逐条清理或整文件隔离。
7. 常见误区与容易被忽略的细节
7.1 目录也有备用数据流
文件能挂流,目录一样能挂。给目录添加ADS的语法一模一样,检测手段也一样是dir /r或PowerShell枚举。很多利用场景恰恰选择目录当宿主,因为目录在资源管理器里的“大小”概念更模糊,被检查的概率更低。枚举脚本如果只遍历文件而忽略目录,等于主动放弃了半块排查区域。我在5.2节的脚本是用Get-ChildItem -Recurse同时覆盖文件和目录的,这一点不用改。
7.2 名称、大小与文件类型的限制
ADS的流名不能包含/ \ : * ? " < > |这些字符,也不能使用保留名,所以你在文件系统里能看到的合法文件名规则,在流名里基本同样适用。流的大小可以很大,没有“只能放小体积文本”的限制,二进制、压缩包、可执行文件都能作为流内容存在。宿主文件被删除时所有流一并删除,宿主文件被改名时流全部保留,这两个基本事实经常被忽略。
还有一个很容易踩的细节:用编辑器或脚本打开流路径时,如果路径包含空格,一定要用引号把整个路径包起来,否则空格会被解释成参数分隔符,最终打开的根本不是目标流。命令行操作ADS时遇到莫名其妙的报错,十次里有八次是路径引号问题。
7.3 不要神话它,也不要无视它
ADS说到底只是NTFS的一个文件组织特性,它的“隐藏”属性来自默认工具的认知盲区,而不是什么加密或高深技术。真正有经验的人不会对“机器上有ADS”这件事本身大惊小怪,但也不会在检测体系里漏掉它。
我个人的做法其实很简单:平时不刻意去翻每一台机器的流,但在每次重要巡检清单里固定加一个枚举步骤,让流数量和基线可比。这个习惯救过我很多次——有一次就靠它发现了一个应用把临时数据全部堆到文档流里的反常现象,那次要不是有基线,我根本不会注意到几个文件的大小变化有什么问题。对待ADS,最好的心态就是把它当成一件正常存在的文件系统能力:你可以不用它,但你不能不知道它,更不能在需要排查的时候才发现自己从来没给监控体系留过它的位置。