NTFS代替ストリーム

(2008.1.10)

(2018.11.1) Visual Studio 2017 で再ビルド.

Windowsのファイルシステム NTFSは、UNIXのファイルシステムとは違った機能・特徴があります。その一つ, 代替ストリームについて書いてみます。

代替ストリームを扱うプログラムの書き方。

ストリーム

UNIXでは「ファイル」はバイトの集まりです。ファイルを開いて頭から読めば、すべてのデータを取り出すことができます。まったく当たり前です。

ところが Windows NTFSでは、ファイルは構造を持っています。普通にファイルを開いたときに読み出せるバイト列以外に、複数のバイト列を格納できます。普通に開いたときのバイト列を主ストリーム、それ以外のバイト列を代替ストリームといいます。

例えば, Internet Explorerは、代替ストリームにファイルの出どころを書き込み、これを利用して信頼できる出どころでない実行ファイルを実行する時に警告を出したりします。

エクスプローラでこのファイルの長さを見ても主ストリームの分しか表示されません。実際にはディスク上でもっと大きい領域を占めていることがあります。

WINE, Sambaでの扱い

WINEでは、今のところ、代替ストリームはサポートされていません。代替ストリームは、単純に「:」以降がファイル名に付いた別のファイルになります。ストリームを開いて読むだけであれば問題ありませんが、それ以上のことをしようとするとエラーになってしまいます。

(2018.11 更新)

Samba 4.8 は、代替ストリームを問題なくサポートしています (ただし設定によります).

代替ストリームを有効にしていない場合, Windows で作った代替ストリームを含むファイルを Samba サーバにコピー・移動すると、代替ストリームはエラーなく失われます。Windows のローカルのFATファイルシステムなどにコピーするときには警告メッセージが出ますが、それもありません。注意が必要です。

PowerShell でストリームを読み出す

ストリーム名が分かっていれば、例えば次のようにするだけで、ストリームの中身を表示できます。

> cat '.\test.txt:すとりーむ1'

代替ストリームにアクセスするには、ファイル名に「:ストリーム名」を付けるだけです。

プログラムで代替ストリームを操作

普通にファイルを開くと主ストリームしか読めないので、ファイルをコピーするときに read() したバイト列をそのまま write() しては失敗します (内容が失われる).

C++で代替ストリームを扱うプログラムを書いてみます。

共通の関数

まず、以降のサンプルで使いまわす関数を書いておきます。

C++
[RAW]
  1. #include "pch.h"
  2. #include <tchar.h>
  3. #include <string>
  4. using namespace std;
  5. // エラーメッセージを出力. プログラムを終了する.
  6. void error(const wstring& message)
  7. {
  8. LPTSTR buf = nullptr;
  9. ::FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
  10. FORMAT_MESSAGE_FROM_SYSTEM,
  11. NULL,
  12. GetLastError(),
  13. LANG_USER_DEFAULT,
  14. (LPTSTR) &buf,
  15. 0,
  16. NULL);
  17. MessageBox(NULL, (message + L"\n" + buf).c_str(), NULL,
  18. MB_OK | MB_ICONERROR );
  19. ::LocalFree(buf);
  20. exit(1);
  21. }
  22. // ASCII は文字を表示する.
  23. void dump(const BYTE* buf, size_t len)
  24. {
  25. int i;
  26. for ( i = 0; i < len; i++) {
  27. if (buf[i] >= 0x20 && buf[i] <= 0x7e)
  28. printf("%c ", buf[i]);
  29. else
  30. printf("%02x ", buf[i]);
  31. if ((i % 16) == 15)
  32. printf("\n");
  33. }
  34. if ( (i % 16) != 0 )
  35. printf("\n");
  36. }

FormatMessage() で確保した領域は LocalFree() で解放するのを忘れずに。

ストリームを読み書き

ストリームに書き込んだり、その内容を読み出したりしてみます。

まず, 書き込みモードでストリームを開きます。

C++
[RAW]
  1. #include "pch.h"
  2. #include <stdio.h>
  3. #include "../stream_common.h"
  4. #include <assert.h>
  5. #include <tchar.h>
  6. #include <string>
  7. using namespace std;
  8. // 書き込みモードでストリームを開く
  9. HANDLE open_stream( LPCTSTR fname, LPCTSTR stream )
  10. {
  11. assert(fname);
  12. wstring file;
  13. if (stream && wstring(stream) != L"" )
  14. file = wstring(fname) + L":" + stream;
  15. else
  16. file = fname;
  17. HANDLE hFile = ::CreateFile( file.c_str(),
  18. GENERIC_WRITE,
  19. 0, // dwShareMode
  20. NULL, // lpSecurityAttributes
  21. OPEN_ALWAYS,
  22. FILE_ATTRIBUTE_NORMAL,
  23. NULL); // hTemplateFile
  24. if (hFile == INVALID_HANDLE_VALUE)
  25. error(wstring(L"create failed: ") + fname + L":" + stream );
  26. return hFile;
  27. }

CreateFile() API でストリームを開きます。「ファイル名 ":" ストリーム名」を開くだけです。与えるオプションに変わったものはありません。

書き込むのは, 単に WriteFile() するだけです。

C++
[RAW]
  1. int _tmain( int argc, TCHAR* argv[] )
  2. {
  3. DWORD dwRet;
  4. if (argc != 2) {
  5. _tprintf(L"%s <filename>\n", argv[0]);
  6. exit(1);
  7. }
  8. const TCHAR* fname = argv[1];
  9. // 主ストリームに書き込む
  10. HANDLE hFile = open_stream( fname, nullptr );
  11. const char* s = "This is a test file.\n";
  12. ::WriteFile(hFile, s, strlen(s), &dwRet, NULL);
  13. ::CloseHandle(hFile);
  14. // 代替ストリームに書き込む
  15. HANDLE hStream = open_stream( fname, L"すとりーむ1" );
  16. s = "This is a test file's stream.\nSTREAM STREAM STREAM\n";
  17. ::WriteFile(hStream, s, strlen(s), &dwRet, NULL);
  18. ::CloseHandle(hStream);
  19. // 主ストリームは ":$DATA" という別名がある。コロンに注意. WINE では失敗する。
  20. read_stream( fname, L"すとりーむ1" );
  21. read_stream( fname, L":$DATA" );
  22. return 0;
  23. }

主ストリームはファイル名だけてもアクセスできますし、:$DATA という別名でもアクセスできます。: (コロン) は、区切りではなく, ストリーム名の先頭文字です。:$DATA でのアクセスは WINEではエラーになります。

(2018.11) 厳密には, :$DATA は stream type で, 主ストリームのストリーム名は無名です。

読み込むときも、普通にファイルを開くだけです。

C++
[RAW]
  1. // ストリームを開き、読み込み、表示する。
  2. void read_stream( LPCTSTR fname, LPCTSTR stream )
  3. {
  4. assert(fname);
  5. assert(stream);
  6. HANDLE hRead = ::CreateFile((wstring(fname) + L":" + stream).c_str(),
  7. GENERIC_READ,
  8. FILE_SHARE_READ,
  9. NULL,
  10. OPEN_EXISTING,
  11. 0,
  12. NULL);
  13. if (hRead == INVALID_HANDLE_VALUE)
  14. error(wstring(L"open for read failed: ") + fname + L":" + stream);
  15. BYTE buf[100];
  16. DWORD read_bytes = 0;
  17. ::ReadFile(hRead, buf, sizeof(buf) - 1, &read_bytes, NULL);
  18. buf[read_bytes] = '\0';
  19. printf("data = '%s'\n", buf);
  20. ::CloseHandle(hRead);
  21. }

ReadFile() でバイト列を読み込みます。

実行結果:

> .\alt_stream1.exe test.txt
data = 'This is a test file's stream.
STREAM STREAM STREAM
'
data = 'This is a test file.
'

すべてのストリームを読み出す

ストリーム名を指定すれば特定のストリームにアクセスできます。では、すべてのストリームのデータを読み込むにはどうしたらいいのでしょうか。

読み込みは, ファイルのバックアップのためのAPI, BackupRead() を用います。代替ストリームも含めて自分でコピーする場合は BackupWrite() を使います。

(2018.11) Windows Vista 以降に限れば, 新しいAPI, FindFirstStreamW() および FindNextStreamW() を使うこともできます。

まず, 以降で作る関数を呼び出す main() です。

C++
[RAW]
  1. #include "pch.h"
  2. #include <tchar.h>
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include "../stream_common.h"
  6. #include <string>
  7. using namespace std;
  8. extern void enum_stream_headers(LPCTSTR fname);
  9. extern void dump_all_streams(LPCTSTR fname);
  10. int _tmain( int argc, TCHAR* argv[] )
  11. {
  12. // __TEXT() は <winnt.h> で, _T() は <tchar.h> で定義される.
  13. _tsetlocale(LC_ALL, _T("")); // printf()に必要.
  14. if (argc != 2) {
  15. _tprintf(L"%s <filename>\n", argv[0]);
  16. exit(1);
  17. }
  18. dump_all_streams(argv[1]);
  19. enum_stream_headers(argv[1]);
  20. return 0;
  21. }

内容も含めて表示

CreateFile() で, FILE_FLAG_BACKUP_SEMANTICS を指定するのがポイントです。

こうやって開いてから BackupRead() でファイルを読み込むと、

ストリームヘッダ 内容 ストリームヘッダ 内容 ...
というようなバイト列が取り出せます。

ストリームをダンプします。

C++
[RAW]
  1. #include "pch.h"
  2. #include <stdio.h>
  3. #include <string.h>
  4. #include "../stream_common.h"
  5. #include <assert.h>
  6. #include <tchar.h>
  7. #include <string>
  8. using namespace std;
  9. void dump_all_streams(LPCTSTR fname)
  10. {
  11. HANDLE hFile = CreateFile(fname,
  12. GENERIC_READ,
  13. FILE_SHARE_READ,
  14. NULL,
  15. OPEN_EXISTING,
  16. FILE_FLAG_BACKUP_SEMANTICS, // これがポイント
  17. NULL);
  18. if (hFile == INVALID_HANDLE_VALUE)
  19. error(wstring(L"open file error: ") + fname);
  20. BYTE buf[10000];
  21. size_t bufsiz = sizeof(buf);
  22. memset(buf, 0, bufsiz);
  23. LPVOID context = NULL; // a pointer to an internal data structure
  24. DWORD ret_bytes = 0;
  25. BOOL r = ::BackupRead(hFile, // the file or directory to be backed up.
  26. buf,
  27. bufsiz,
  28. &ret_bytes, // lpNumberOfBytesRead
  29. FALSE,
  30. FALSE,
  31. &context);
  32. printf("bytes read = %ld\n", ret_bytes);
  33. if (!r)
  34. error(L"BackupRead() failed");
  35. // 表示する
  36. dump(buf, ret_bytes);
  37. // 最初のストリームだけ, ヘッダを表示する
  38. WIN32_STREAM_ID* sid = (WIN32_STREAM_ID*)buf;
  39. printf("dwStreamId = %ld, dwStreamAttributes = %ld, Size = %lld, dwStreamNameSize = %ld\n",
  40. sid->dwStreamId,
  41. sid->dwStreamAttributes,
  42. *(long long*)&sid->Size, // LARGE_INTEGER 型
  43. sid->dwStreamNameSize);
  44. // context を解放する (必須)
  45. r = BackupRead(hFile,
  46. NULL, 0,
  47. NULL,
  48. TRUE, // bAbort = TRUE で解放
  49. FALSE,
  50. &context);
  51. CloseHandle(hFile);
  52. }

すごい不思議なAPIです。context で読み込み位置など (?) を保持します。使い終わったら、必ず解放しなければなりません。

解放は, 同じく BackupRead()bAbort = TRUE で呼び出します。

このサンプルを実行すると、次のように表示されます (前半):

> .\all_streams.exe test.txt
bytes read = 138
01 00 00 00 00 00 00 00 15 00 00 00 00 00 00 00
00 00 00 00 T  h  i  s     i  s     a     t  e
s  t     f  i  l  e  .  0a 04 00 00 00 00 00 00
00 3  00 00 00 00 00 00 00 1a 00 00 00 :  00 Y
0  h  0  8a 0  fc 0  80 0  1  00 :  00 $  00 D
00 A  00 T  00 A  00 T  h  i  s     i  s     a
   t  e  s  t     f  i  l  e  '  s     s  t  r
e  a  m  .  0a S  T  R  E  A  M     S  T  R  E
A  M     S  T  R  E  A  M  0a
dwStreamId = 1, dwStreamAttributes = 0, Size = 21, dwStreamNameSize = 0

代替ストリーム名が「:streamname:$DATA」になっています。実際, test.txt:streamname でも, test.txt:streamname:$DATA でも同じ内容にアクセスできます。

(2018.11) 上述のとおり, :$DATA は stream type です。ストリームタイプの一覧はこちら; File Streams | Microsoft Docs

ストリーム名を列挙する

複数のストリームを含むファイルをバイト列に展開できたので、今度はストリーム名を一覧 (列挙) してみましょう。

ストリームヘッダは WIN32_STREAM_ID 構造体です。<WinBase.h> で, 次のように定義されています。

C++
[RAW]
  1. typedef struct _WIN32_STREAM_ID {
  2. DWORD dwStreamId ;
  3. DWORD dwStreamAttributes ;
  4. LARGE_INTEGER Size ; // 内容の長さ (バイト数)
  5. DWORD dwStreamNameSize ; // ストリーム名の長さ (文字数ではなくバイト数)
  6. WCHAR cStreamName[ ANYSIZE_ARRAY ] ; // dwStreamNameSizeが非0のときのみ
  7. } WIN32_STREAM_ID, *LPWIN32_STREAM_ID ;

dwStreamNameSize が文字数ではなくバイト数なのに注意です。適宜 sizeof(WCHAR) で割ってやります。

主ストリームは、dwStreamNameSize が 0で、cStreamName が1バイトも確保されていません。そのため、dwStreamNameSize までを読み込み、ストリーム名がある場合のみ読み進めるようにします。

まず、一つのストリームの名前を表示し, 内容をスキップするコードを作ります.

C++
[RAW]
  1. constexpr size_t header_size = offsetof(WIN32_STREAM_ID, cStreamName);
  2. // ストリームヘッダを一つ読み、表示する。内容をスキップする
  3. // @return 終端のとき false
  4. bool read_header(HANDLE hFile, LPVOID* context)
  5. {
  6. WIN32_STREAM_ID sid;
  7. memset(&sid, 0, sizeof(sid));
  8. // BackupRead() は、ファイルに含まれるストリームを結合したバイト列を読み込む
  9. // sid, ストリーム1, sid, ストリーム2, ...
  10. DWORD ret_bytes = 0;
  11. BOOL r = BackupRead(hFile,
  12. (BYTE*) &sid, header_size,
  13. &ret_bytes,
  14. FALSE,
  15. FALSE,
  16. context);
  17. if (!r)
  18. error(L"BackupRead() failed");
  19. // ストリームの終端でさらにBackupRead()すると、正常に終了して、ret_bytes = 0
  20. printf("ret_bytes = %ld\n", ret_bytes);
  21. if (!ret_bytes)
  22. return false;
  23. printf("dwStreamId = %ld, dwStreamAttributes = %ld, Size = %lld, dwStreamNameSize = %ld\n",
  24. sid.dwStreamId,
  25. sid.dwStreamAttributes,
  26. *(long long*) &sid.Size, // LARGE_INTEGER 型
  27. sid.dwStreamNameSize);
  28. // 主ストリームはストリーム名がない
  29. if (!sid.dwStreamNameSize)
  30. printf("stream name: (none)\n");
  31. else {
  32. // ストリーム名を読む。ストリーム名は MAX_PATH より長いことがある。
  33. // dwStreamNameSize は, 文字数ではなくバイト数.
  34. WCHAR* name = (WCHAR*) malloc(sid.dwStreamNameSize + sizeof(WCHAR));
  35. ret_bytes = 0;
  36. r = ::BackupRead(hFile, (BYTE*) name, sid.dwStreamNameSize,
  37. &ret_bytes, FALSE, FALSE, context);
  38. if (!r)
  39. error(L"read stream-name");
  40. name[ret_bytes / sizeof(WCHAR)] = '\0';
  41. // 表示
  42. _tprintf(L"%s\n", name );
  43. free(name);
  44. }
  45. // 次のストリームまでスキップする.
  46. if (sid.Size.LowPart || sid.Size.HighPart) {
  47. if (sid.dwStreamId == BACKUP_SPARSE_BLOCK) {
  48. // おそらく、どれぐらい穴を空けるか、の情報: 0x1 0008 (65,544) bytes 固定?
  49. BYTE* sparse_buf = (LPBYTE)GlobalAlloc(GPTR, sid.Size.LowPart);
  50. DWORD dwRead = 0;
  51. ::BackupRead(hFile, sparse_buf, sid.Size.LowPart, &dwRead,
  52. FALSE, TRUE, context);
  53. ::GlobalFree(sparse_buf);
  54. }
  55. else {
  56. DWORD dw1, dw2;
  57. r = BackupSeek(hFile,
  58. sid.Size.LowPart, sid.Size.HighPart,
  59. &dw1, &dw2,
  60. context);
  61. if (!r)
  62. error(L"BackupSeek() failed");
  63. }
  64. }
  65. return true;
  66. }

offsetof マクロで, 構造体内部の位置を取れます。

BackupRead() は、ストリームの終端でさらに読み出そうとすると、呼び出しに成功したうえで, ret_bytes = 0 になります。

代替ストリームのときは、ストリーム名が格納できるメモリを動的に確保します。NTFSのファイル名は非常に長くできるので、静的に確保してはいけません。

ストリームの内容があるとき (長さ1バイト以上) は、BackupSeek() API でファイルを読み進めます。BackupSeek() には相対位置を渡します。

BACKUP_SPARSE_BLOCK の場合, BackupSeek() は失敗します。workaround として BackupRead() で進めます。

以上を踏まえて, 列挙します。

C++
[RAW]
  1. void enum_stream_headers( LPCTSTR fname )
  2. {
  3. HANDLE hFile = CreateFile( fname,
  4. GENERIC_READ,
  5. FILE_SHARE_READ,
  6. NULL,
  7. OPEN_EXISTING,
  8. FILE_FLAG_BACKUP_SEMANTICS,
  9. NULL);
  10. if (hFile == INVALID_HANDLE_VALUE)
  11. error(wstring(L"open file error: ") + fname);
  12. LPVOID context = NULL;
  13. bool has_next;
  14. do {
  15. has_next = read_header(hFile, &context);
  16. } while (has_next);
  17. // リソースを解放する (必須)
  18. BOOL r = BackupRead(hFile,
  19. NULL, 0,
  20. NULL,
  21. TRUE, // bAbort
  22. FALSE,
  23. &context);
  24. if (!r)
  25. error(L"BackupRead() for release failed");
  26. CloseHandle(hFile);
  27. }

BackupRead() を使い終わったら, リソース (context) を解放します。

リソースの解放も BackupRead() を呼び出します。close...などの関数が別にあるわけではありません。

実行結果 (後半):

ret_bytes = 20
dwStreamId = 1, dwStreamAttributes = 0, Size = 21, dwStreamNameSize = 0
stream name: (none)
ret_bytes = 20
dwStreamId = 4, dwStreamAttributes = 0, Size = 51, dwStreamNameSize = 26
:すとりーむ1:$DATA
ret_bytes = 0

ストリーム名その2

BackupRead(), BackupSeek() はドキュメント化された方法ですが、まどろっこしいです。undocumented ですが、ntdll.dll の NtQueryInformationFile() を呼び出す方法もあります。

まず、構造体などを宣言します。

C++
[RAW]
  1. #include <windows.h>
  2. #include <ntdef.h>
  3. #include <stdio.h>
  4. #include "stream_common.cc"
  5. // <ddk/winddk.h>
  6. typedef struct _IO_STATUS_BLOCK {
  7. _ANONYMOUS_UNION union {
  8. NTSTATUS Status;
  9. PVOID Pointer;
  10. } DUMMYUNIONNAME;
  11. ULONG_PTR Information;
  12. } IO_STATUS_BLOCK;
  13. // <ddk/winddk.h>
  14. typedef enum _FILE_INFORMATION_CLASS {
  15. FileStreamInformation = 22,
  16. } FILE_INFORMATION_CLASS, *PFILE_INFORMATION_CLASS;
  17. // <ddk/ntifs.h>
  18. typedef struct _FILE_STREAM_INFORMATION {
  19. ULONG NextEntryOffset;
  20. ULONG StreamNameLength;
  21. LARGE_INTEGER StreamSize;
  22. LARGE_INTEGER StreamAllocationSize;
  23. WCHAR StreamName[1]; // 可変長
  24. } FILE_STREAM_INFORMATION, *PFILE_STREAM_INFORMATION;
  25. typedef NTSTATUS (*NtQueryInformationFileFunc)(
  26. /* IN */ HANDLE hFile,
  27. /* OUT */ IO_STATUS_BLOCK* io,
  28. /* OUT */ void* ptr,
  29. /* IN */ LONG len,
  30. /* IN */ FILE_INFORMATION_CLASS information_class);

GetProcAddress()NtQueryInformationFile 関数のアドレスを取得します。

C++
[RAW]
  1. NtQueryInformationFileFunc NtQueryInformationFile = NULL;
  2. void load_dll() {
  3. HINSTANCE dll = LoadLibrary("ntdll.dll");
  4. if (!dll)
  5. error("load ntdll");
  6. NtQueryInformationFile = (NtQueryInformationFileFunc)
  7. GetProcAddress(dll, "NtQueryInformationFile");
  8. if (!NtQueryInformationFile)
  9. error("get address");
  10. }

一撃でストリームの情報を取得できます。

C++
[RAW]
  1. int main() {
  2. load_dll();
  3. HANDLE hFile = CreateFile("test.txt",
  4. GENERIC_READ,
  5. FILE_SHARE_READ,
  6. NULL,
  7. OPEN_EXISTING,
  8. 0,
  9. NULL);
  10. if (hFile == INVALID_HANDLE_VALUE)
  11. error("open error");
  12. BYTE buf[10000]; // 固定長で確保すると不味そうだが?
  13. size_t bufsiz = sizeof(buf);
  14. memset(buf, 0, bufsiz);
  15. IO_STATUS_BLOCK io_status;
  16. memset(&io_status, 0, sizeof(io_status));
  17. NTSTATUS r = NtQueryInformationFile(hFile, &io_status, buf, bufsiz,
  18. FileStreamInformation);
  19. if (!NT_SUCCESS(r))
  20. error("query");
  21. dump((BYTE*) &io_status, sizeof(io_status));
  22. dump(buf, sizeof(FILE_STREAM_INFORMATION) * 3);
  23. BYTE* p = buf;
  24. while (p + ((FILE_STREAM_INFORMATION*) p)->NextEntryOffset <= buf + bufsiz) {
  25. FILE_STREAM_INFORMATION* info = (FILE_STREAM_INFORMATION*) p;
  26. string r;
  27. w2a(info->StreamName, info->StreamNameLength / sizeof(WCHAR), r);
  28. printf("name = %s¥n", r.c_str());
  29. if (!info->NextEntryOffset)
  30. break;
  31. p += info->NextEntryOffset;
  32. }
  33. return 0;
  34. }

実行結果はこうなります。

00 00 00 00 Z  00 00 00 
(  00 00 00 0e 00 00 00 15 00 00 00 00 00 00 00 
18 00 00 00 00 00 00 00 :  00 :  00 $  00 D  00 
A  00 T  00 A  00 00 00 00 00 00 00 1a 00 00 00 
3  00 00 00 00 00 00 00 8  00 00 00 00 00 00 00 
:  00 s  00 t  00 r  00 e  00 a  00 m  00 :  00 
$  00 D  00 A  00 T  00 A  00 00 00 00 00 00 00 

name = ::$DATA
name = :stream:$DATA

外部リンク

NTFS Alternate Streams: What, When, and How To
非常に詳しい解説。ストリームの削除、コピー, FindFirstStreamW() を使った列挙方法もある。