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 了。

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 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);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
// 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;
}

另外一些有趣的链接:

分享到 评论