Alternative Data Streams (ADS)是Windows文件系统中引入的一项功能,可以为文件关联额外的数据流。这项功能的诞生原因是为了提高文件系统的兼容性和功能性,特别是与Macintosh和Unix系统创建的文件进行交互时。
在Macintosh和Unix系统中,文件可以具有多个数据流,而Windows文件系统中最初只支持一个数据流。为了使Windows文件系统与其他操作系统更兼容,Microsoft引入了ADS功能,使Windows文件系统能够处理多个数据流。这项功能可以用于存储文件的元数据,如作者、创建日期、修改日期等,还可以用于存储图像和音频文件的缩略图、元数据等。
ADS功能可以提供重要的功能和灵活性,但也带来了一些安全风险。因为ADS可以隐藏在文件中,不容易被普通的文件系统API或一些防病毒软件所发现,因此,它也可能被恶意软件利用来隐藏其存在,使得系统的安全受到威胁。
尽管如此,ADS仍然被保留在Windows文件系统中,因为它在某些应用程序和使用情况下提供了重要的功能。但是,用户和组织需要意识到ADS所带来的安全风险,并采取措施来减轻这些风险,例如使用专门设计用于检测ADS中隐藏的恶意软件的防病毒软件等。本文将介绍ADS的创建和检测,以及如何用Win32 API读写ADS。
创建ADS
NTFS交换数据流(Alternate Data Streams,简称ADS)是NTFS磁盘格式的一个特性。在NTFS文件系统下,每个文件都可以存在多个数据流。通俗的理解,就是其它文件可以“寄宿”在某个文件身上。
首先来看个例子,打开cmd.exe
,在C:\Test\ADS
下创建一个hello.txt
文件C:\Test\ADS>ECHO HelloWorld > hello.txt
用DIR
可以看到该目录下已经有一个hello.txt
文件,可以用notepad.exe hello.txt
查看其内容,会发现”HelloWorld”已经被写入。
接着继续指向如下命令:C:\Test\ADS>echo ThisIsADS1 > hello.txt:ads1.txt
这条命令在cmd.exe
下可以执行成功,看起来是向hello.txt:ads1.txt
这个文件写入了”ThisIsADS1”的内容,但是此时用DIR
列出该目录下所有文件,会发现依然只有hello.txt
:
C:\Test\ADS>DIR |
如果用notepad.exe hello.txt
打开hello.txt
会发现内容依然是”HelloWorld”,而用notepad.exe hello.txt:ads1.txt
打开hello.txt:ads1.txt
文件则会发现”ThisIsADS1”这个内容已经被成功写入了,如果删除hello.txt
则hello.txt:ads.txt
也无法打开。如果直接echo ThisIsADS1 > hello.txt:ads1.txt
创建hello.txt:ads1.txt
则实际上会创建一个空的hello.txt
文件。以上这些现象说明hello.txt:ads1.txt
在当前文件系统是真实存在的而又不可见,看似就是依附于hello.txt
的子文件。
hello.txt:ads.txt
这类文件就是指定了宿主的ADS文件,hello.txt
就是它的宿主。宿主的删除会导致依附的ADS文件也被删除
要查看目录下的ADS文件可以用DIR /R
C:\Test\ADS>DIR /R
驱动器 C 中的卷是 Windows
卷的序列号是 CC7A-4265
C:\Test\ADS 的目录
2023/03/28 21:55 <DIR> .
2023/03/07 22:47 <DIR> ..
2023/03/28 21:55 0 hello.txt
26 hello.txt:ads1.txt:$DATA
1 个文件 0 字节
2 个目录 67,761,876,992 可用字节
可以看见hello.txt
有时间信息,而hello.txt:ads.txt
并没有。这是因为ADS文件和宿主共享相同的文件元数据。ADS文件的大小也不会反应在宿主文件的大小上,宿主文件的大小属性只描述其自身数据的大小。
ADS文件不仅可以依附于文件,也可以依附于目录。创建依附目录的ADS文件同理:C:\Test\ADS>mkdir dir1
C:\Test\ADS>echo ThisIsDirADS1 > dir1:dirADS1.txt
C:\Test\ADS>DIR /R
驱动器 C 中的卷是 Windows
卷的序列号是 CC7A-4265
C:\Test\ADS 的目录
2023/03/28 22:09 <DIR> .
2023/03/07 22:47 <DIR> ..
2023/03/28 22:09 <DIR> dir1
16 dir1:dirADS1.txt:$DATA
2023/03/28 21:55 0 hello.txt
26 hello.txt:ads1.txt:$DATA
1 个文件 0 字节
3 个目录 67,760,422,912 可用字节
也可以创建不指定宿主的ADS文件:C:\Test\ADS>echo ThisIsDirADS2 > :dirADS2.txt
C:\Test\ADS>DIR /R
驱动器 C 中的卷是 Windows
卷的序列号是 CC7A-4265
C:\Test\ADS 的目录
2023/03/28 22:13 <DIR> .
16 .:dirADS2.txt:$DATA
2023/03/07 22:47 <DIR> ..
2023/03/28 22:09 <DIR> dir1
16 dir1:dirADS1.txt:$DATA
2023/03/28 21:55 0 hello.txt
26 hello.txt:ads1.txt:$DATA
1 个文件 0 字节
3 个目录 67,745,931,264 可用字节
C:\Test\ADS>cd ..
C:\Test>DIR /R
驱动器 C 中的卷是 Windows
卷的序列号是 CC7A-4265
C:\Test 的目录
2023/03/07 22:47 <DIR> .
2023/03/28 22:13 <DIR> ADS
16 ADS:dirADS2.txt:$DATA
这时可以观察到实际上创建的没有宿主的ADS会把当前目录作为自己的宿主
除了上述用echo
命令向ADS文件写入数据外,cmd.exe
还提供了一个type
命令用于将指定文件写入ADS文件。例如:C:\Test\ADS>type "C:\Program Files\Bandizip\Bandizip.exe" > hello.txt:Bandzip.exe
C:\Test\ADS>dir /R
驱动器 C 中的卷是 Windows
卷的序列号是 CC7A-4265
C:\Test\ADS 的目录
2023/03/28 22:13 <DIR> .
16 .:dirADS2.txt:$DATA
2023/03/07 22:47 <DIR> ..
2023/03/28 22:09 <DIR> dir1
16 dir1:dirADS1.txt:$DATA
2023/03/28 22:32 0 hello.txt
26 hello.txt:ads1.txt:$DATA
3,211,240 hello.txt:Bandzip.exe:$DATA
1 个文件 0 字节
3 个目录 67,742,556,160 可用字节
将一个可执行程序Bandizip.exe
附加在了hello.txt:Bandzip.exe
上,该过程是拷贝的,即hello.txt:Bandzip.exe
占用独立的磁盘空间。由于ADS不可见的特性,ADS很容易被用于将木马文件隐藏在常规文件中以躲避操查杀。要删除ADS可以用特殊的工具,也可以将文件拷入不支持ADS特性的FAT32分区。
ADS Win32 API
Windows提供FindFirstStreamW
和FindNextStreamW
来获取文件/目录拥有的所有数据流。
首先用
FindFirstStreamW
查找文件对应的第一个hStream
句柄。传入文件路径,第二参数固定为_STREAM_INFO_LEVELS::FindStreamInfoStandard
,第四个参数固定为0。如果成功将把查找到的流的信息读入WIN32_FIND_STREAM_DATA
结构,如果失败会返回INVALID_HANDLE_VALUE
。std::wstring wPath = LR"(C:\Test\ADS\hello.txt)";
WIN32_FIND_STREAM_DATA findStreamData{};
HANDLE hStream = ::FindFirstStreamW(
wPath.c_str(),
_STREAM_INFO_LEVELS::FindStreamInfoStandard,
&findStreamData,
0
);
if (hStream == INVALID_HANDLE_VALUE) {
/* get stream handle failed */
return;
}用
FindNextStreamW
结合之前获取的hStream
继续查找下一个流的信息。这个过程和遍历文件用到的FindNextFileW
很类似。当查找完成FindNextStreamW
会失败,GetLastError()
将会返回ERROR_HANDLE_EOF
。查找成功下一个流的信息将会覆写入WIN32_FIND_STREAM_DATA findStreamData
中。while (true) {
std::cout << findStreamData.cStreamName << std::endl;
if (!::FindNextStreamW(hStream, &findStreamData)) {
if (::GetLastError() != ERROR_HANDLE_EOF) {
/* error occured */
std::cerr << "error happened" << std::endl;
}
break;
}
}最后用
FindClose
关闭句柄,释放资源。::FindClose(hStream);
hStream = INVALID_HANDLE_VALUE;
对于之前的hello.txt
文件,会打印出这样的结果:::$DATA
:ads1.txt:$DATA
:ads2.txt:$DATA
形式可以看成是:<stream name>:$DATA
。其中::$DATA
表示streamName为空,这表示一个主数据流,所有文件(非目录),无论是否包含ADS,都必定有一个主数据流。:ads1.txt:$DATA
和:ads2.txt:$DATA
表明hello.txt
还包含两个ADS,分别为ads1.txt
和ads2.txt
。所有目录,都没有主数据流,但可能包含ADS。例如上文中C:\Test\ADS\dir1
可以拿到如下打印结果::dirADS1.txt:$DATA
:dirADS2.txt:$DATA
无论目录还是文件的ADS都可以看作是文件。对于ADS的读取、创建、写入都可以直接用之前几章节提到的文件操作API CreateFileW
、ReadFile
、WriteFile
等。传入的路径可以用<filePath>:<streamName>
表示。打开一个ADS的句柄可以表示为:std::wstring wPath = LR"(C:\Test\ADS\hello.txt:ads1.txt)";
HANDLE hFile = ::CreateFileW(
wPath.c_str(),
GENERIC_READ,
FILE_SHARE_READ,
nullptr,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
0);
根据该句柄执行ReadFile
和WriteFile
就可以和读写常规文件一样读写ADS了。
需要注意的是,对ADS的读写不光会改变ADS的内容,也会改变宿主文件的部分属性(例如AccessTime、ModifyTime),且ADS只和宿主在数据流内容和大小上相互区分,在元数据上他们和宿主是共享的。用获得的宿主文件句柄和ADS句柄可以获得一样的文件元数据信息(Index、设备号,时间,Attribute)。