NTFS代替ストリーム
(2008.1.10)
WindowsのファイルシステムNTFSは、UNIXのファイルシステムとは違った機能・特徴があります。その一つ代替ストリームについて書いてみます。
代替ストリームを扱うプログラムの書き方。
ストリーム
UNIXでは「ファイル」はバイトの集まりです。ファイルを開いて頭から読めば、すべてのデータを取り出すことができます。まったく当たり前です。
ところがNTFSでは、ファイルは構造を持っています。普通にファイルを開いたときに読み出せるバイト列以外に、複数のバイト列を格納できます。普通に開いたときのバイト列を主ストリーム、それ以外のバイト列を代替ストリームといいます。
Internet Explorerは、代替ストリームにファイルの出どころを書き込み、これを利用して信頼できる出どころでない実行ファイルを実行する時に警告を出したりします。
代替ストリームにアクセスするには、ファイル名に「:ストリーム名」を付けてファイルを開くだけです。
● テキストファイルに実行ファイルを足してみる。
エクスプローラでこのファイルの長さを見ても主ストリームの分しか表示されません。しかし、実行することができます。(セキュリティ上のリスク?)
WINE, Sambaでの扱い
WINEでは、今のところ、代替ストリームはサポートされていません。代替ストリームは、単純に「:」以降がファイル名に付いた別のファイルになります。ストリームを開いて読むだけであれば問題ありませんが、それ以上のことをしようとするとエラーになってしまいます。
Windows で作った代替ストリームを含むファイルを Samba サーバにコピー・移動すると、代替ストリームはエラーなく失われます。Windows のローカルのFATファイルシステムなどにコピーするときには警告メッセージが出ますが、それもありません。注意が必要です。
プログラムで代替ストリームを操作
普通にファイルを開くと代替ストリームが読めないので、ファイルをコピーするときに write(read(...)) のようにしては失敗します。(内容が失われる)
C++で代替ストリームを扱うプログラムを書いてみます。
共通の関数
まず、以降のサンプルで使いまわす関数を書いておきます。
- #include <string>
- using namespace std;
- // エラーメッセージを出力
- void error(const char* message) {
- char* buf = NULL;
- FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
- FORMAT_MESSAGE_FROM_SYSTEM,
- NULL,
- GetLastError(),
- LANG_USER_DEFAULT,
- (LPTSTR) &buf,
- 0,
- NULL);
- printf("%s: %s¥n", message, buf);
- LocalFree(buf);
- exit(1);
- }
- void dump(const BYTE* buf, size_t len) {
- for (int 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");
- }
- printf("¥n");
- }
- void w2a(const WCHAR* p,
- int char_count, // 文字数。ナル終端のときは-1
- string& r) {
- int bytes = ::WideCharToMultiByte(CP_THREAD_ACP,
- 0,
- p,
- char_count,
- NULL,
- 0, // バイト数を得る
- NULL,
- NULL);
- if (!bytes)
- error("w2a");
- char* buf = (char*) malloc(bytes + 1);
- bytes = ::WideCharToMultiByte(CP_THREAD_ACP, 0, p, char_count,
- buf, bytes, NULL, NULL);
- buf[bytes] = '¥0';
- r = buf;
- free(buf);
- }
ストリームを読み書き
まずは、ストリームに書き込んだり、その内容を読み出したりしてみます。
- #include <windows.h>
- #include <stdio.h>
- #include <string.h>
- #include "stream_common.cc"
- const char* TEST_FILE = "test.txt";
- // 書き込みモードでストリームを開く
- HANDLE open_stream(const string& fname) {
- HANDLE hFile = CreateFile(fname.c_str(),
- GENERIC_WRITE,
- 0,
- NULL,
- OPEN_ALWAYS,
- FILE_ATTRIBUTE_NORMAL,
- NULL);
- if (hFile == INVALID_HANDLE_VALUE)
- error(fname.c_str());
- return hFile;
- }
CreateFile() API でストリームを開きます。与えるオプションに変わったものはありません。読み込むときも、普通にファイルを開くときと同じです。
- // ストリームから読み込み、表示する。
- void read_stream(const char* name) {
- HANDLE hRead = CreateFile((string(TEST_FILE) + ":" + name).c_str(),
- GENERIC_READ,
- FILE_SHARE_READ,
- NULL,
- OPEN_EXISTING,
- 0,
- NULL);
- if (hRead == INVALID_HANDLE_VALUE)
- error(name);
- char 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);
- }
代替ストリームは「ファイル名 : ストリーム名」になります。主ストリームはファイル名だけてもアクセスできますし、:$DATAという別名でもアクセスできます。:$DATA でのアクセスはWINEではエラーになります。
- int main() {
- DWORD dwRet;
- // 主ストリームに書き込む
- HANDLE hFile = open_stream(TEST_FILE);
- const char* s = "This is a test file.¥n";
- WriteFile(hFile, s, strlen(s), &dwRet, NULL);
- CloseHandle(hFile);
- // 代替ストリームに書き込む
- HANDLE hStream = open_stream(string(TEST_FILE) + ":stream");
- s = "This is a test file's stream.¥nSTREAM STREAM STREAM¥n";
- WriteFile(hStream, s, strlen(s), &dwRet, NULL);
- CloseHandle(hStream);
- // 主ストリームは:$DATAという別名がある。WINE では失敗する。
- read_stream("stream");
- read_stream(":$DATA");
- return 0;
- }
すべてのストリームを読み出す
ストリーム名を指定すれば特定のストリームにアクセスできます。では、すべてのストリームのデータを読み込むにはどうしたらいいのでしょうか。
ファイルのバックアップのためのAPI、BackupRead() を用います。
CreateFile() でファイルを開いたうえで、BackupRead() でファイルを読み込むと、
ストリームヘッダ 内容 ストリームヘッダ 内容 ...というようなバイト列が取り出せます。自分でコピーする場合はBackupWrite()。
contextで読み込み位置など(?)を保持します。
- #include <windows.h>
- #include <stdio.h>
- #include "stream_common.cc"
- int main() {
- HANDLE hFile = CreateFile("test.txt",
- GENERIC_READ,
- FILE_SHARE_READ,
- NULL,
- OPEN_EXISTING,
- FILE_FLAG_BACKUP_SEMANTICS,
- NULL);
- if (hFile == INVALID_HANDLE_VALUE)
- error("open error");
- 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("ret %ld¥n", ret_bytes);
- if (!r)
- error("BackupRead");
- // 表示する
- dump(buf, ret_bytes);
- return 0;
- }
このサンプルを実行すると、次のように表示されます。
ret 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 s 00 t 00 r 00 e 00 a 00 m 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
代替ストリーム名が「:stream:$DATA」になっています。実際、test.txt:stream でも、test.txt:stream:$DATA でも内容にアクセスできます。なんという。
ストリーム名を取り出す
複数のストリームを含むファイルをバイト列に展開できたので、今度はストリーム名を一覧 (列挙) してみましょう。
ストリームヘッダは WIN32_STREAM_ID 構造体で、<winbase.h> で次のように定義されています。dwStreamNameSize はバイト数なので、適宜 sizeof(WCHAR) で割ってやります。
- typedef struct _WIN32_STREAM_ID {
- DWORD dwStreamId;
- DWORD dwStreamAttributes;
- LARGE_INTEGER Size; // 内容の長さ(バイト数)
- DWORD dwStreamNameSize; // ストリーム名の長さ(文字数ではなくバイト数)
- WCHAR cStreamName[ANYSIZE_ARRAY]; // dwStreamNameSizeが非0のときのみ
- } WIN32_STREAM_ID, *LPWIN32_STREAM_ID;
主ストリームは、dwStreamNameSizeが0で、cStreamNameが1バイトも確保されていません。そのため、dwStreamNameSizeまでを読み込み、ストリーム名がある場合のみ読み進めるようにします。
offsetofで構造体内部の位置を取れます。(弱気にassert() していますが。)
- #include <windows.h>
- #include <stdio.h>
- #include <string.h>
- #include "stream_common.cc"
- // ストリームヘッダを読む
- bool read_header(HANDLE hFile, LPVOID* context) {
- WIN32_STREAM_ID sid;
- memset(&sid, 0, sizeof(sid));
- size_t header_size = offsetof(WIN32_STREAM_ID, cStreamName);
- assert(header_size == 20);
- // BackupRead() は、ファイルに含まれるストリームを結合したバイト列を読み込む
- // sid, ストリーム1, sid, ストリーム2, ...
- DWORD ret_bytes = 0;
- BOOL r = BackupRead(hFile,
- (BYTE*) &sid, header_size,
- &ret_bytes,
- FALSE,
- FALSE,
- context);
- if (!r)
- error("BackupRead");
- // ストリームの終端でさらにBackupRead()すると、正常に終了して、ret_bytes = 0
- printf("ret %ld¥n", ret_bytes);
- if (!ret_bytes)
- return false;
- printf("dwStreamId = %ld, dwStreamAttributes = %ld, Size = %ld, dwStreamNameSize = %ld¥n",
- sid.dwStreamId,
- sid.dwStreamAttributes,
- sid.Size.LowPart,
- sid.dwStreamNameSize);
代替ストリームのときは、ストリーム名が格納できるメモリを動的に確保します。NTFSのファイル名は非常に長くできるので、静的に確保してはいけません。
- // 主ストリームはストリーム名がない
- if (!sid.dwStreamNameSize)
- printf("stream name: n/a¥n");
- else {
- // ストリーム名を読む。ストリーム名は MAX_PATH より長いことがある。
- WCHAR* name = (WCHAR*) malloc(sid.dwStreamNameSize + sizeof(WCHAR));
- memset(name, 0, sid.dwStreamNameSize + sizeof(WCHAR));
- r = BackupRead(hFile, (BYTE*) name, sid.dwStreamNameSize,
- &ret_bytes, FALSE, FALSE, context);
- printf("stream name: ret %ld¥n", ret_bytes);
- if (!r)
- error("read stream name");
- string sn;
- w2a(name, sid.dwStreamNameSize / sizeof(WCHAR), sn);
- printf("%s¥n", sn.c_str());
- free(name);
- }
ストリームの内容があるとき(長さ1バイト以上)は、BackupSeek() API でファイルを読み進めます。BackupSeek() には相対位置を渡します。
- // 内容があるとき、次のストリームへ進める
- if (sid.Size.LowPart || sid.Size.HighPart) {
- DWORD dw1, dw2;
- r = BackupSeek(hFile,
- sid.Size.LowPart, sid.Size.HighPart,
- &dw1, &dw2,
- context);
- if (!r)
- error("seek");
- }
- return true;
- }
BackupRead() は、ファイルの最後まで読み込んだ状態で呼び出されると、呼び出しに成功し、かつ読み込めたバイト数0を返します。そのときは、リソース (context) を解放します。
リソースの解放も BackupRead() を呼び出します。close...などの関数が別にあるわけではありません。
- const char* fname = "test.txt";
- int main() {
- HANDLE hFile = CreateFile(fname,
- GENERIC_READ,
- FILE_SHARE_READ,
- NULL,
- OPEN_EXISTING,
- FILE_FLAG_BACKUP_SEMANTICS,
- NULL);
- if (hFile == INVALID_HANDLE_VALUE)
- error("open file");
- LPVOID context = NULL;
- bool has_next;
- do {
- has_next = read_header(hFile, &context);
- } while (has_next);
- // リソースを解放する
- BOOL r = BackupRead(hFile,
- NULL, 0,
- NULL,
- TRUE, // bAbort
- FALSE,
- &context);
- if (!r)
- error("end read");
- CloseHandle(hFile);
- return 0;
- }
ストリーム名その2
BackupRead(), BackupSeek() はドキュメント化された方法ですが、まどろっこしいです。undocumented ですが、ntdll.dll の NtQueryInformationFile() を呼び出す方法もあります。
まず、構造体などを宣言します。
- #include <windows.h>
- #include <ntdef.h>
- #include <stdio.h>
- #include "stream_common.cc"
- // <ddk/winddk.h>
- typedef struct _IO_STATUS_BLOCK {
- _ANONYMOUS_UNION union {
- NTSTATUS Status;
- PVOID Pointer;
- } DUMMYUNIONNAME;
- ULONG_PTR Information;
- } IO_STATUS_BLOCK;
- // <ddk/winddk.h>
- typedef enum _FILE_INFORMATION_CLASS {
- FileStreamInformation = 22,
- } FILE_INFORMATION_CLASS, *PFILE_INFORMATION_CLASS;
- // <ddk/ntifs.h>
- 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)(
- /* IN */ HANDLE hFile,
- /* OUT */ IO_STATUS_BLOCK* io,
- /* OUT */ void* ptr,
- /* IN */ LONG len,
- /* IN */ FILE_INFORMATION_CLASS information_class);
GetProcAddress() で NtQueryInformationFile 関数を取得します。
- 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");
- }
一撃でストリームの情報を取得できます。
- 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 2000 Part1: ストリームとハード リンク
- ストリーム、ハードリンクなどの解説。
- 8-3. NTFS のセキュリティ機能と落とし穴
- ぬるり。: C# と NTFS ストリームの甘くなくもない関係
- COMインターフェイスIPropertySetStorage経由でのストリームへのアクセス。
- NTFS Alternate Streams: What, When, and How To