0%

Windows文件系统概述(三)ADS

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
驱动器 C 中的卷是 Windows
卷的序列号是 CC7A-4265

C:\Test\ADS 的目录

2023/03/28 21:42 <DIR> .
2023/03/07 22:47 <DIR> ..
2023/03/28 21:44 26 hello.txt
1 个文件 26 字节
2 个目录 67,740,217,344 可用字节

如果用notepad.exe hello.txt打开hello.txt会发现内容依然是”HelloWorld”,而用notepad.exe hello.txt:ads1.txt打开hello.txt:ads1.txt文件则会发现”ThisIsADS1”这个内容已经被成功写入了,如果删除hello.txthello.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提供FindFirstStreamWFindNextStreamW来获取文件/目录拥有的所有数据流。

  1. 首先用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;
    }
  2. 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;
    }
    }
  3. 最后用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.txtads2.txt所有目录,都没有主数据流,但可能包含ADS。例如上文中C:\Test\ADS\dir1可以拿到如下打印结果:
:dirADS1.txt:$DATA
:dirADS2.txt:$DATA

无论目录还是文件的ADS都可以看作是文件。对于ADS的读取、创建、写入都可以直接用之前几章节提到的文件操作API CreateFileWReadFileWriteFile等。传入的路径可以用<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);

根据该句柄执行ReadFileWriteFile就可以和读写常规文件一样读写ADS了。

需要注意的是,对ADS的读写不光会改变ADS的内容,也会改变宿主文件的部分属性(例如AccessTime、ModifyTime),且ADS只和宿主在数据流内容和大小上相互区分,在元数据上他们和宿主是共享的。用获得的宿主文件句柄和ADS句柄可以获得一样的文件元数据信息(Index、设备号,时间,Attribute)。

相关API文档:

扩展资料

Disqus评论区没有正常加载,请使用科学上网