(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++
- #include "pch.h"
- #include <tchar.h>
- #include <string>
- using namespace std;
-
-
- void error(const wstring& message)
- {
- LPTSTR buf = nullptr;
- ::FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
- FORMAT_MESSAGE_FROM_SYSTEM,
- NULL,
- GetLastError(),
- LANG_USER_DEFAULT,
- (LPTSTR) &buf,
- 0,
- NULL);
- MessageBox(NULL, (message + L"\n" + buf).c_str(), NULL,
- MB_OK | MB_ICONERROR );
- ::LocalFree(buf);
-
- exit(1);
- }
-
-
- void dump(const BYTE* buf, size_t len)
- {
- int i;
- for ( i = 0; i < len; i++) {
- if (buf[i] >= 0x20 && buf[i] <= 0x7e)
- printf("%c ", buf[i]);
- else
- printf("%02x ", buf[i]);
- if ((i % 16) == 15)
- printf("\n");
- }
- if ( (i % 16) != 0 )
- printf("\n");
- }
FormatMessage()
で確保した領域は LocalFree()
で解放するのを忘れずに。
ストリームを読み書き
ストリームに書き込んだり、その内容を読み出したりしてみます。
まず, 書き込みモードでストリームを開きます。
C++
- #include "pch.h"
- #include <stdio.h>
- #include "../stream_common.h"
- #include <assert.h>
- #include <tchar.h>
- #include <string>
- using namespace std;
-
-
- HANDLE open_stream( LPCTSTR fname, LPCTSTR stream )
- {
- assert(fname);
- wstring file;
- if (stream && wstring(stream) != L"" )
- file = wstring(fname) + L":" + stream;
- else
- file = fname;
-
- HANDLE hFile = ::CreateFile( file.c_str(),
- GENERIC_WRITE,
- 0,
- NULL,
- OPEN_ALWAYS,
- FILE_ATTRIBUTE_NORMAL,
- NULL);
- if (hFile == INVALID_HANDLE_VALUE)
- error(wstring(L"create failed: ") + fname + L":" + stream );
- return hFile;
- }
CreateFile()
API でストリームを開きます。「ファイル名 ":
" ストリーム名」を開くだけです。与えるオプションに変わったものはありません。
書き込むのは, 単に WriteFile()
するだけです。
C++
- int _tmain( int argc, TCHAR* argv[] )
- {
- DWORD dwRet;
-
- if (argc != 2) {
- _tprintf(L"%s <filename>\n", argv[0]);
- exit(1);
- }
-
- const TCHAR* fname = argv[1];
-
-
- HANDLE hFile = open_stream( fname, nullptr );
- const char* s = "This is a test file.\n";
- ::WriteFile(hFile, s, strlen(s), &dwRet, NULL);
- ::CloseHandle(hFile);
-
-
- HANDLE hStream = open_stream( fname, L"すとりーむ1" );
- s = "This is a test file's stream.\nSTREAM STREAM STREAM\n";
- ::WriteFile(hStream, s, strlen(s), &dwRet, NULL);
- ::CloseHandle(hStream);
-
-
- read_stream( fname, L"すとりーむ1" );
- read_stream( fname, L":$DATA" );
-
- return 0;
- }
主ストリームはファイル名だけてもアクセスできますし、:$DATA
という別名でもアクセスできます。:
(コロン) は、区切りではなく, ストリーム名の先頭文字です。:$DATA
でのアクセスは WINEではエラーになります。
(2018.11) 厳密には, :$DATA
は stream type で, 主ストリームのストリーム名は無名です。
読み込むときも、普通にファイルを開くだけです。
C++
-
- void read_stream( LPCTSTR fname, LPCTSTR stream )
- {
- assert(fname);
- assert(stream);
-
- HANDLE hRead = ::CreateFile((wstring(fname) + L":" + stream).c_str(),
- GENERIC_READ,
- FILE_SHARE_READ,
- NULL,
- OPEN_EXISTING,
- 0,
- NULL);
- if (hRead == INVALID_HANDLE_VALUE)
- error(wstring(L"open for read failed: ") + fname + L":" + stream);
-
- BYTE buf[100];
- DWORD read_bytes = 0;
- ::ReadFile(hRead, buf, sizeof(buf) - 1, &read_bytes, NULL);
- buf[read_bytes] = '\0';
- printf("data = '%s'\n", buf);
-
- ::CloseHandle(hRead);
- }
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++
- #include "pch.h"
- #include <tchar.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include "../stream_common.h"
- #include <string>
- using namespace std;
-
-
- extern void enum_stream_headers(LPCTSTR fname);
- extern void dump_all_streams(LPCTSTR fname);
-
- int _tmain( int argc, TCHAR* argv[] )
- {
-
- _tsetlocale(LC_ALL, _T(""));
-
- if (argc != 2) {
- _tprintf(L"%s <filename>\n", argv[0]);
- exit(1);
- }
-
- dump_all_streams(argv[1]);
- enum_stream_headers(argv[1]);
-
- return 0;
- }
内容も含めて表示
CreateFile()
で, FILE_FLAG_BACKUP_SEMANTICS
を指定するのがポイントです。
こうやって開いてから BackupRead()
でファイルを読み込むと、
ストリームヘッダ 内容 ストリームヘッダ 内容 ...
というようなバイト列が取り出せます。
ストリームをダンプします。
C++
- #include "pch.h"
- #include <stdio.h>
- #include <string.h>
- #include "../stream_common.h"
- #include <assert.h>
- #include <tchar.h>
- #include <string>
- using namespace std;
-
- void dump_all_streams(LPCTSTR fname)
- {
- HANDLE hFile = CreateFile(fname,
- GENERIC_READ,
- FILE_SHARE_READ,
- NULL,
- OPEN_EXISTING,
- FILE_FLAG_BACKUP_SEMANTICS,
- NULL);
- if (hFile == INVALID_HANDLE_VALUE)
- error(wstring(L"open file error: ") + fname);
-
- BYTE buf[10000];
- size_t bufsiz = sizeof(buf);
- memset(buf, 0, bufsiz);
-
- LPVOID context = NULL;
- DWORD ret_bytes = 0;
- BOOL r = ::BackupRead(hFile,
- buf,
- bufsiz,
- &ret_bytes,
- FALSE,
- FALSE,
- &context);
- printf("bytes read = %ld\n", ret_bytes);
- if (!r)
- error(L"BackupRead() failed");
-
-
- dump(buf, ret_bytes);
-
-
- WIN32_STREAM_ID* sid = (WIN32_STREAM_ID*)buf;
- printf("dwStreamId = %ld, dwStreamAttributes = %ld, Size = %lld, dwStreamNameSize = %ld\n",
- sid->dwStreamId,
- sid->dwStreamAttributes,
- *(long long*)&sid->Size,
- sid->dwStreamNameSize);
-
-
- r = BackupRead(hFile,
- NULL, 0,
- NULL,
- TRUE,
- FALSE,
- &context);
- CloseHandle(hFile);
- }
すごい不思議な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++
- typedef struct _WIN32_STREAM_ID {
- DWORD dwStreamId ;
- DWORD dwStreamAttributes ;
- LARGE_INTEGER Size ;
- DWORD dwStreamNameSize ;
- WCHAR cStreamName[ ANYSIZE_ARRAY ] ;
- } WIN32_STREAM_ID, *LPWIN32_STREAM_ID ;
dwStreamNameSize
が文字数ではなくバイト数なのに注意です。適宜 sizeof(WCHAR)
で割ってやります。
主ストリームは、dwStreamNameSize
が 0で、cStreamName
が1バイトも確保されていません。そのため、dwStreamNameSize
までを読み込み、ストリーム名がある場合のみ読み進めるようにします。
まず、一つのストリームの名前を表示し, 内容をスキップするコードを作ります.
C++
- constexpr size_t header_size = offsetof(WIN32_STREAM_ID, cStreamName);
-
-
-
- bool read_header(HANDLE hFile, LPVOID* context)
- {
- WIN32_STREAM_ID sid;
- memset(&sid, 0, sizeof(sid));
-
-
-
- DWORD ret_bytes = 0;
- BOOL r = BackupRead(hFile,
- (BYTE*) &sid, header_size,
- &ret_bytes,
- FALSE,
- FALSE,
- context);
- if (!r)
- error(L"BackupRead() failed");
-
- printf("ret_bytes = %ld\n", ret_bytes);
- if (!ret_bytes)
- return false;
-
- printf("dwStreamId = %ld, dwStreamAttributes = %ld, Size = %lld, dwStreamNameSize = %ld\n",
- sid.dwStreamId,
- sid.dwStreamAttributes,
- *(long long*) &sid.Size,
- sid.dwStreamNameSize);
-
-
- if (!sid.dwStreamNameSize)
- printf("stream name: (none)\n");
- else {
-
-
- WCHAR* name = (WCHAR*) malloc(sid.dwStreamNameSize + sizeof(WCHAR));
- ret_bytes = 0;
- r = ::BackupRead(hFile, (BYTE*) name, sid.dwStreamNameSize,
- &ret_bytes, FALSE, FALSE, context);
- if (!r)
- error(L"read stream-name");
- name[ret_bytes / sizeof(WCHAR)] = '\0';
-
-
- _tprintf(L"%s\n", name );
- free(name);
- }
-
-
- if (sid.Size.LowPart || sid.Size.HighPart) {
- if (sid.dwStreamId == BACKUP_SPARSE_BLOCK) {
-
- BYTE* sparse_buf = (LPBYTE)GlobalAlloc(GPTR, sid.Size.LowPart);
- DWORD dwRead = 0;
- ::BackupRead(hFile, sparse_buf, sid.Size.LowPart, &dwRead,
- FALSE, TRUE, context);
- ::GlobalFree(sparse_buf);
- }
- else {
- DWORD dw1, dw2;
- r = BackupSeek(hFile,
- sid.Size.LowPart, sid.Size.HighPart,
- &dw1, &dw2,
- context);
- if (!r)
- error(L"BackupSeek() failed");
- }
- }
-
- return true;
- }
offsetof
マクロで, 構造体内部の位置を取れます。
BackupRead()
は、ストリームの終端でさらに読み出そうとすると、呼び出しに成功したうえで, ret_bytes = 0
になります。
代替ストリームのときは、ストリーム名が格納できるメモリを動的に確保します。NTFSのファイル名は非常に長くできるので、静的に確保してはいけません。
ストリームの内容があるとき (長さ1バイト以上) は、BackupSeek()
API でファイルを読み進めます。BackupSeek()
には相対位置を渡します。
BACKUP_SPARSE_BLOCK
の場合, BackupSeek()
は失敗します。workaround として BackupRead()
で進めます。
以上を踏まえて, 列挙します。
C++
- void enum_stream_headers( LPCTSTR fname )
- {
- HANDLE hFile = CreateFile( fname,
- GENERIC_READ,
- FILE_SHARE_READ,
- NULL,
- OPEN_EXISTING,
- FILE_FLAG_BACKUP_SEMANTICS,
- NULL);
- if (hFile == INVALID_HANDLE_VALUE)
- error(wstring(L"open file error: ") + fname);
-
- LPVOID context = NULL;
- bool has_next;
- do {
- has_next = read_header(hFile, &context);
- } while (has_next);
-
-
- BOOL r = BackupRead(hFile,
- NULL, 0,
- NULL,
- TRUE,
- FALSE,
- &context);
- if (!r)
- error(L"BackupRead() for release failed");
- CloseHandle(hFile);
- }
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++
- #include <windows.h>
- #include <ntdef.h>
- #include <stdio.h>
- #include "stream_common.cc"
-
-
- typedef struct _IO_STATUS_BLOCK {
- _ANONYMOUS_UNION union {
- NTSTATUS Status;
- PVOID Pointer;
- } DUMMYUNIONNAME;
- ULONG_PTR Information;
- } IO_STATUS_BLOCK;
-
-
- typedef enum _FILE_INFORMATION_CLASS {
- FileStreamInformation = 22,
- } FILE_INFORMATION_CLASS, *PFILE_INFORMATION_CLASS;
-
-
- typedef struct _FILE_STREAM_INFORMATION {
- ULONG NextEntryOffset;
- ULONG StreamNameLength;
- LARGE_INTEGER StreamSize;
- LARGE_INTEGER StreamAllocationSize;
- WCHAR StreamName[1];
- } FILE_STREAM_INFORMATION, *PFILE_STREAM_INFORMATION;
-
- typedef NTSTATUS (*NtQueryInformationFileFunc)(
- HANDLE hFile,
- IO_STATUS_BLOCK* io,
- void* ptr,
- LONG len,
- FILE_INFORMATION_CLASS information_class);
GetProcAddress()
で NtQueryInformationFile
関数のアドレスを取得します。
C++
- NtQueryInformationFileFunc NtQueryInformationFile = NULL;
-
- void load_dll() {
- HINSTANCE dll = LoadLibrary("ntdll.dll");
- if (!dll)
- error("load ntdll");
-
- NtQueryInformationFile = (NtQueryInformationFileFunc)
- GetProcAddress(dll, "NtQueryInformationFile");
- if (!NtQueryInformationFile)
- error("get address");
- }
一撃でストリームの情報を取得できます。
C++
- int main() {
- load_dll();
-
- HANDLE hFile = CreateFile("test.txt",
- GENERIC_READ,
- FILE_SHARE_READ,
- NULL,
- OPEN_EXISTING,
- 0,
- NULL);
- if (hFile == INVALID_HANDLE_VALUE)
- error("open error");
-
- BYTE buf[10000];
- size_t bufsiz = sizeof(buf);
- memset(buf, 0, bufsiz);
- IO_STATUS_BLOCK io_status;
- memset(&io_status, 0, sizeof(io_status));
-
- NTSTATUS r = NtQueryInformationFile(hFile, &io_status, buf, bufsiz,
- FileStreamInformation);
- if (!NT_SUCCESS(r))
- error("query");
-
- dump((BYTE*) &io_status, sizeof(io_status));
- dump(buf, sizeof(FILE_STREAM_INFORMATION) * 3);
-
- BYTE* p = buf;
- while (p + ((FILE_STREAM_INFORMATION*) p)->NextEntryOffset <= buf + bufsiz) {
- FILE_STREAM_INFORMATION* info = (FILE_STREAM_INFORMATION*) p;
- string r;
- w2a(info->StreamName, info->StreamNameLength / sizeof(WCHAR), r);
- printf("name = %s¥n", r.c_str());
- if (!info->NextEntryOffset)
- break;
- p += info->NextEntryOffset;
- }
-
- return 0;
- }
実行結果はこうなります。
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()
を使った列挙方法もある。