替代数据流(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;
}
另外一些有趣的链接: