NTFS 替代数据流

替代数据流(alternate data stream)从 Windows 3.1 就开始正式使用了。这是 NTFS 的一项特性,ReFS 貌似有有限的支持(我没测试过)。这个功能是 Windows 识别的文件系统的可选功能,不过(常见的)实现可以认为就一种。关于它的简要介绍,微软的文档已经写得比较清晰了。

有意思的是,早期它会被用于攻击。当然,直到现在,如果没有合适的处理,你任然有可能掉到坑里——虽然绝大多数时候也不会遇上。

因为替代数据流也是一个文件系统对象,所以通用的文件操作(CreateFile()ReadFile()WriteFile()DeleteFile(),省略了 ANSI/Unicode 后缀)都是支持的,只需要指定正确的对象名称就行。所谓“正确的对象名称”可以参照上面的文档链接。

怎么进行查询/枚举呢?查询的方法和标准文件是一样的,CreateFile() 就可以了。枚举,可以使用 FindFirstStreamW()FindNextStreamW()。由于替代数据流和主文件共享属性,所以它只有名称和大小,没有额外的属性、权限信息。

如果你细心一点的话可以看到,FindFirstStreamW() 是从 Windows Vista 开始提供的。虽然那也是17年之前了而且早就被淘汰了,但是如果我想在更老的系统上去访问这些信息,行不行呢?答案是可以的,Windows XP 开始提供了 BackupRead()BackupSeek()。更早的系统可能就真的需要人工读 MFT 了。

示例代码如下:

// altdstrm.h

#include <Windows.h>

#include <stdint.h>
#include <stdbool.h>

#include "ll.h" // linked list implementation, omitted

typedef struct tagAltDataStreamMetadata
{
    wchar_t fileName[MAX_PATH];
    wchar_t streamName[MAX_PATH + 36];
    uint64_t size;
} AltDataStreamMetadata;

typedef struct tagAltDataStreamEnumError
{
    wchar_t message[1024];
} AltDataStreamEnumError;

bool EnumerateAltDataStreams(const wchar_t* fileName, LinkedList* result, AltDataStreamEnumError* error);
// altdstrm.c

#include <wchar.h>

#include "altdstrm.h"

bool EnumerateAltDataStreams(const wchar_t* fileName, LinkedList* result, AltDataStreamEnumError* error)
{
    memset(error->message, 0, sizeof(error->message));

    HANDLE hFile = CreateFileW(fileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

    if (hFile == NULL || hFile == INVALID_HANDLE_VALUE)
    {
        GetLastErrorMessage(GetLastError(), error->message, ARRAYSIZE(error->message));
        return false;
    }

    bool hasError = false;
    do
    {
        LPVOID backupContext = NULL;

        BOOL shouldReadNextEntry = TRUE;
        bool endOfFileReached = false;

        while (shouldReadNextEntry)
        {
            AltDataStreamMetadata metadata;
            bool gotMetadata = false;

            shouldReadNextEntry = ReadSingleStreamMetadata(hFile, &metadata, &backupContext, &gotMetadata, &endOfFileReached);

            if (shouldReadNextEntry && gotMetadata)
            {
                wcsncpy_s(metadata.fileName, MAX_PATH, fileName, MAX_PATH);
                llAppend(result, &metadata, sizeof(metadata));
            }
        }

        if (!endOfFileReached)
        {
            DWORD code = GetLastError();

            if (code != ERROR_HANDLE_EOF)
            {
                GetLastErrorMessage(code, error->message, ARRAYSIZE(error->message));
                hasError = true;
            }
        }

        // Call BackupRead with bAbort=TRUE (see docs) to finish reading
        {
            WIN32_STREAM_ID dummy;
            DWORD n;
            BackupRead(hFile, (LPBYTE)(&dummy), 0, &n, TRUE, FALSE, &backupContext);
        }
    }
    while (false);

    CloseHandle(hFile);
    return !hasError;
}

BOOL ReadSingleStreamMetadata(HANDLE hFile, AltDataStreamMetadata* metadata, LPVOID* context, bool* gotMetadata, bool* endOfFileReached)
{
    *endOfFileReached = false;
    *gotMetadata = false;

    // Windows expects the size of WIN32_STREAM_ID to be 20, but it is actually 24 on x64 machines (incorrect packing)
    const size_t WIN32_STREAM_ID_SIZE = 20;
    const size_t WRONG_WIN32_STREAM_ID_SIZE = sizeof(WIN32_STREAM_ID); // 24
    // Extra bytes size is copied from WIN32_FIND_STREAM_DATA.
    const size_t WIN32_STREAM_ID_BUFFER_SIZE = WIN32_STREAM_ID_SIZE + sizeof(wchar_t) * (MAX_PATH + 36);
    LPBYTE dataBuffer = (LPBYTE)malloc(WIN32_STREAM_ID_BUFFER_SIZE);
    memset(dataBuffer, 0, WIN32_STREAM_ID_BUFFER_SIZE);

    BOOL continueReadingNextEntry = FALSE;

    do
    {
        DWORD numBytesRead = 0;
        BOOL readSomeData = BackupRead(hFile, dataBuffer, WIN32_STREAM_ID_SIZE, &numBytesRead, FALSE, FALSE, context);

        if (!readSomeData)
        {
            break;
        }

        if (numBytesRead == 0)
        {
            *endOfFileReached = true;
            break;
        }

        LARGE_INTEGER toSeek = {0};

        const WIN32_STREAM_ID* streamId = (const WIN32_STREAM_ID*)dataBuffer;

        if (streamId->dwStreamId == BACKUP_DATA || streamId->dwStreamId == BACKUP_ALTERNATE_DATA)
        {
            readSomeData = BackupRead(hFile, dataBuffer + WIN32_STREAM_ID_SIZE, streamId->dwStreamNameSize, &numBytesRead, FALSE, FALSE, context);

            if (!readSomeData)
            {
                break;
            }

            wcsncpy_s(metadata->streamName, MAX_PATH + 36, (LPCWSTR)(dataBuffer + WIN32_STREAM_ID_SIZE), MAX_PATH + 36);
            metadata->size = streamId->Size.QuadPart;

            *gotMetadata = true;
        }

        toSeek.QuadPart += streamId->Size.QuadPart;

        LARGE_INTEGER sought = {0};

        if (toSeek.QuadPart > 0)
        {
            BackupSeek(hFile, toSeek.LowPart, toSeek.HighPart, &sought.LowPart, (DWORD*)&sought.HighPart, context);
        }

        if (sought.QuadPart == toSeek.QuadPart)
        {
            continueReadingNextEntry = TRUE;
        }
    }
    while (false);

    free(dataBuffer);
    return continueReadingNextEntry;
}

另外一些有趣的链接:

分享到 评论