Windows の Alternative data stream(ADS) について

昼過ぎに起きて、何気なく NTFSAlternative Data Stream について気になった。
そういえばどうやったら列挙できるんだろう?知らないな!知りたいな!と思ったら…いつの間にか日付が変わってた orz
というわけで、休日を一日つぶした結果をここで発表。

#include <windows.h>
#include <stdio.h>

void EnumerateNTFSStream(HANDLE hTargetFile, bool& bFailed)
{
	bFailed = true;
	
	void* pContext = NULL;
	
	const int STREAMID_SIZE = 20; //sizeof(streamID) - (sizeof(WCHAR) * ANYSIZE_ARRAY);
	
	bool bContinue = true;
	while (bContinue)
	{
		// ----------------------------------------------------------------------------------------------------------------
		//  Step.1 - Read a stream header.
		// ----------------------------------------------------------------------------------------------------------------
		WIN32_STREAM_ID streamID;
		memset(&streamID, 0x00, sizeof(streamID));
		
		DWORD numberOfBytesRead = 0;
		
		HANDLE  hFile                = hTargetFile;
		LPBYTE  lpBuffer             = (LPBYTE)&streamID;
		DWORD   nNumberOfBytesToRead = STREAMID_SIZE; 
		LPDWORD lpNumberOfBytesRead  = &numberOfBytesRead;
		BOOL    bAbort               = FALSE;
		BOOL    bProcessSecurity     = FALSE;
		LPVOID* lpContext            = &pContext;
		BOOL retBackupRead = BackupRead(hFile, lpBuffer, nNumberOfBytesToRead, lpNumberOfBytesRead, bAbort, bProcessSecurity, lpContext);
		if (retBackupRead == (BOOL)0)
		{
			fprintf(stderr, "BackupRead() Failed\n");
			bFailed = true;
			break;
		}
		
		// ----------------------------------------------------------------------------------------------------------------
		//  Break loop if the stream ended.
		// ----------------------------------------------------------------------------------------------------------------
		// printf("numberOfBytesRead:%d\n", numberOfBytesRead);
		if (numberOfBytesRead == 0)
		{
			// Success
			bFailed = false;
			break;
		}
		
		// ----------------------------------------------------------------------------------------------------------------
		//  Step.2 - Read and show stream name.
		// ----------------------------------------------------------------------------------------------------------------
		if (streamID.dwStreamNameSize == 0)
		{
			printf("        StreamName: None\n");
		}
		else
		{
			size_t bufferSize = streamID.dwStreamNameSize + (sizeof(WCHAR) * 1);
			WCHAR* pStreamName = (WCHAR*)malloc(bufferSize);
			memset(pStreamName, 0x00, bufferSize);
			
			BackupRead(hFile, (BYTE*)pStreamName, streamID.dwStreamNameSize, lpNumberOfBytesRead, bAbort, bProcessSecurity, lpContext);
			printf("        StreamName: \"%S\"\n", pStreamName);
			
			free(pStreamName);
		}
		printf("        dwStreamId: %d\n",   streamID.dwStreamId);
		printf("dwStreamAttributes: %d\n",   streamID.dwStreamAttributes);
		printf("              Size: %lld\n", streamID.Size.QuadPart);
		printf("  dwStreamNameSize: %d\n",   streamID.dwStreamNameSize);
		printf("\n");
		
		// ----------------------------------------------------------------------------------------------------------------
		//  Step.3 - Skip main content of this stream if needed.
		// ----------------------------------------------------------------------------------------------------------------
		if (streamID.Size.QuadPart > 0)
		{
			DWORD dwLowByteSeeked = 0;
			DWORD dwHighByteSeeked = 0;
			
			DWORD   dwLowBytesToSeek   = streamID.Size.LowPart;
			DWORD   dwHighBytesToSeek  = streamID.Size.HighPart;
			LPDWORD lpdwLowByteSeeked  = &dwLowByteSeeked;
			LPDWORD lpdwHighByteSeeked = &dwHighByteSeeked;
			BOOL retBackupSeek = BackupSeek(hFile, dwLowBytesToSeek, dwHighBytesToSeek, lpdwLowByteSeeked, lpdwHighByteSeeked, lpContext);
			if (retBackupSeek == (BOOL)0)
			{
				fprintf(stderr, "BackupSeek() Failed\n");
				fprintf(stderr, "    required:(%d %d) skipped:(%d %d)\n", dwLowBytesToSeek, dwHighBytesToSeek, dwLowByteSeeked, dwHighByteSeeked);
				bFailed = true;
				break;
			}
			
			if ((dwLowByteSeeked == 0) && (dwHighByteSeeked == 0))
			{
				fprintf(stderr, "BackupSeek() Can't skipped\n");
				bFailed = true;
				break;
			}
		}
	}
	
	bFailed = false;
}

int main(int argc, char** argv)
{
	for (int i = 1; i < argc; ++i)
	{
		printf("==================================================\n");
		printf(" %s\n", argv[i]);
		printf("==================================================\n");
		LPCTSTR               lpFileName            = argv[i];
		DWORD                 dwDesiredAccess       = GENERIC_READ;
		DWORD                 dwShareMode           = FILE_SHARE_READ;
		LPSECURITY_ATTRIBUTES lpSecurityAttributes  = NULL;
		DWORD                 dwCreationDisposition = OPEN_EXISTING;
		DWORD                 dwFlagsAndAttributes  = FILE_ATTRIBUTE_NORMAL;
		HANDLE                hTemplateFile         = NULL;
		HANDLE hTargetFile = CreateFile(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile);
		if (hTargetFile == INVALID_HANDLE_VALUE)
		{
			fprintf(stderr, "CreateFile() Failed\n");
			return 1;
		}
		
		bool bFailed = false;
		EnumerateNTFSStream(hTargetFile, bFailed);
		CloseHandle(hTargetFile);
		
		if (bFailed)
		{
			fprintf(stderr, "Enumeration failed\n");
			return 1;
		}
	}
	
	return 0;
}


こんな感じで BackupRead() と BackupSeek() を使えば列挙できるようだ。
なんでこんな名前の関数なのかを推測すると…


バックアップをとるための関数以外に NTFS-Stream の情報にアクセスする手段がないんだろうなw
例外的に、バックアップをとる関数だけは、ちゃんと丸ごと保存できなきゃいけないからアクセスできる、と。


この NTFS-Stream 名の列挙テストを書いていてハマった点をいくつか。
g++ をつかった為なのか、それとも Platform SDK のヘッダでもなるのかは確かめてないけど、
バックアップのストリームのヘッダ部分を読み込むとき、
WIN32_STREAM_ID 構造体から末尾の cStreamName[ANYSIZE_ARRAY] の分を取り除いたサイズを指定したけど 2 バイトくらいずれた(パディングが入ってた)。
もしかしたら Platform SDK のヘッダだと WIN32_STREAM_ID 構造体をちゃんと pack してあるのかもしれない。
回避策としては決め打ちで 20 バイト読むことにした。本当は unsigned char の配列に読んでから WIN32_STREAM_ID にコピーした方がよさそうだけど、
とりあえず今は直接 WIN32_STREAM_ID を指定してサイズだけ 20 バイト指定で上手く行ったようだ。


あと、BackupRead() と BackupSeek() に指定するコンテキストなのだけど、
MSDN 見ても開放する関数が書いてない。
このコンテキストを NULL にして呼び出すと今まで読み込み/シークした結果が無視されるので、
普通に考えれば開放しないとリークするような気がするんだけど?
ファイルハンドルを閉じるときに一緒に閉じてくれるんだろうか?


そして、テストに使ったファイルなのだけど、Explorer でプロパティから概要を適当に埋め尽くした結果、
メインのストリーム以外に無名のストリームが出来てしまったように見えるファイルがある…。
これってどういうことだろう? dwStreamID は 7 でサイズは 64 となってるんだけど…。