Comments
Description
Transcript
Windows パス名の落とし穴
ISEC セキュア・プログラミング講座 IPA IPA 8-1. Windowsパス名の落とし穴 第 8 章 セキュア Windows プログラミング [8-1.] Windows パス名の落とし穴 Windows の「パス名」は一筋縄ではいかない「くせ」を持っており,ユー ザに入力させたパス名をプログラム中で使用するときは注意が必要である。 ディレクトリ区切り文字に「¥」と「/」の両方が混在・重複しても許され たり,ロングネームとショートネームが存在するなど,複雑な事情がある。 ● ● ● パス名 コンピュータに保存されているファイルの識別に用いられる名称が「パス名」である。パス名は,ドライブ 文字,ディレクトリパス,ファイル名などを一定の記法に従って連結して表記したものである。Windows で 用いられるパス名は,例えば次のようなものだ。 d:¥InetPub¥wwwroot¥default.htm プログラム中にハード・コーディングされる場合もあるが,プログラムがアクセスするファイルのパス名は 多くの場合パラメタとしてプログラムの外部から与えられる。システムの重要なファイルが読み出されたり 壊されたりしてはならないので,ユーザがプログラムのパラメタに指定できるパス名にはプログラムのロジッ クで一定の制限をかけることも少なくない。たとえば,あるディレクトリ階層以下のファイルにアクセスを 限定する,などである。 Windows のパス名には少々複雑な事情があり,このようなパス名に関する制限のロジックを組むにあたって さまざまな注意が必要である。 文字定数の落とし穴 一つのファイルをオープンする処理は標準のCランタイム関数fopen( )を使って次のように書くことができる。 FILE* pfile; pfile = fopen ("d:¥InetPub¥wwwroot¥default.htm", "r"); 残念ながらここには間違いがある。すでに注意深い読者はお分かりだろう, 「¥」 が1個ずつ足りないのだ。C や C++ の文字列定数の中ではタブ「¥t」,改行「¥n」やナルキャラクタ「¥0」という記法を許しているため, 文字「¥」は何らかの特別な意味を持ってしまう。文字「¥」そのものを文字列定数の中に指定したいときは, 「¥」を2つ並べて「¥¥」のように書く必要がある。従って上のコードは正しくは次のように書かなくてはな らない。 FILE* pfile; pfile = fopen ("c:¥¥InetPub¥¥wwwroot¥¥default.htm", "r"); -1- Copyright Copyright © 2002 IPA, All Rights Reserved. ISEC セキュア・プログラミング講座 IPA IPA 8-1. Windowsパス名の落とし穴 パス名解釈のサービス機能 C言語を使用する場合,Windowsのプログラムがファイルをオープンするのに次の4種類の方法がある。4種 類のそれぞれごとに入出力を行う一群の関数が用意されていて,それらの使い方は少しずつ異なっている。 CreateFile( ) OpenFile( ) / _lcreat( ) _open( ) / _creat( ) fopen ( ) ファイルのオープンにこれら4種類のうちのどれを使っても,仕組みとしては最終的に Win32 API の関数で あるCreateFile( )が呼び出され,そこでパス名の解釈が行われる。他のプログラミング言語を使用する場合も やはり内部で CreateFile( )関数が呼び出されて,そこでパス名の解釈が行われる。 このCreateFile( )で行われるパス名の解釈にはいくつものサービス機能があり,パス名の取り扱いを難しくし ている。 CreateFile( )関数 本稿では話をパス名に集中するため主にfopen( )関数を用いて説明するが,きめ細かなファイルの取り扱いを 指定したい場合は,直接 Win32 API の関数 CreateFile( )を用いてファイルを取り扱うことになるだろう。この 関数は7つの引数をとり,次のように指定する。 HANDLE hfile; hfile = CreateFile (パス名,読み書きの種別,共有の指定,セキュリティ属性, ファイルの生成の要求,ファイルの属性,テンプレートの指定); 上記のd:¥InetPub¥wwwroot¥default.htmを入力でオープンする指定をCreateFile( )関数を用いて書くと,たとえ ば次のようになる。 HANDLE hfile; hfile = CreateFile ("d:¥¥InetPub¥¥wwwroot¥¥default.htm", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL); この関数の名前はCreateFileだが,Win32 APIでは既存のファイルをファイルをオープンするだけのときもこ の関数を使う。ここで「Create」という語が使われているのは,ファイルアクセスを仲介してくれるWindows NT カーネルオブジェクトを「作る」操作であることに由来していると考えられる。 ディレクトリ階層の落とし穴 ユーザがファイルのパス名,あるいはパス名の一部をパラメタとして入力するとそれに応じたファイルアク セスと処理を行うようなプログラムを考えてみる。こうしたプログラムでは,システムのファイルに干渉さ れたり秘密のデータファイルが読み出されないよう,ユーザが指定可能なパス名に制限をかけることになる。 この制限はユーザが入力してきたパス名を特定の文字列やパターンと照合することによって処理を許可した り禁止したりする形で実装することになるだろう。このときに注意が必要なのは,Windows NT/2000 のファ イルのパス名にはとても大きな自由度があるということだ。 特定のディレクトリパスへのアクセスを制限するつもりで, -2- Copyright Copyright © 2002 IPA, All Rights Reserved. ISEC セキュア・プログラミング講座 IPA IPA 8-1. Windowsパス名の落とし穴 if (_strnicmp (pszPathname, "c:¥¥InetPub¥¥wwwroot¥¥secret¥¥", 26) == 0) { // アクセス禁止 else // アクセス許可 といったロジックを書けば,はたして secret ディレクトリ下のファイルへのアクセスを制限できるだろうか。 答えは「ノー」である。(ここで用いている_strnicmp( )は,半角英字の大文字小文字の区別を行わずに指定バ イト数だけ文字列を比較する関数である。) 次のパス名はすべて同じファイルを示すものとして解釈される: 1)d:¥InetPub¥wwwroot¥secret¥data.txt 基本形 2)d:¥inetpub¥WWWROOT¥SECRET¥DATA.TXT 大文字小文字は同一視される 3)d:/InetPub/wwwroot/secret/data.txt ディレクトリの区切りに「/」も使える 4)d:¥¥InetPub¥¥¥wwwroot¥¥¥¥secret¥¥¥¥¥data.txt ディレクトリの区切り文字は幾つか重複しても構わない 5)d:////InetPub///wwwroot//secret/data.txt 「/」も重複できる 6)d:¥InetPub¥.¥wwwroot¥.¥.¥secret¥.¥.¥.¥data.txt 「カレントディレクトリ」を表す . を差し挟むことができる 7)d:¥fake¥fake¥..¥..¥InetPub¥wwwroot¥secret¥data.txt 実在しないディレクトリもあとで「..」で遡れば指定可能 したがって,上記の 1)2)以外のパス名はすべてチェックをすり抜けてしまう。 末尾文字の落とし穴 複数種類のファイルを扱う場面では,ファイルの拡張子に応じて処理を切り替えるといったことが行われる。 例えばファイルの内容を開示するプログラムの中の次のような分岐を考えてみる。 pszPathname = ユーザが入力したパス名 pszExtension = pszPathname の中の最後の「.」以降の文字列 if (_stricmp (pszExtention, ".scr") == 0) { // ファイルをスクリプトとして解釈,実行 ... } else if (_istrcmp (pszExtention, ".pri") == 0) { // プライベートデータのファイル内容は開示しない ... } else { // ファイル内容を開示 pFile = fopen (pszPathname, "r"); ... } (ここで用いている_stricmp( )は,半角英字の大文字小文字の区別を行わずにナル文字の手前までの内容で文 字列を比較する関数である。) ここに,d:¥dir¥file.priというプライベートデータファイルがあったとすると,このパス名を指定されても拡張 子「.pri」をもつファイルはプライベートデータであるとして開示をしないで済む。しかし Win32 のパス名に -3- Copyright Copyright © 2002 IPA, All Rights Reserved. ISEC セキュア・プログラミング講座 IPA IPA 8-1. Windowsパス名の落とし穴 は,末尾に付けられた「.」と半角スペースは何個有っても無視される,というサービス機能があるのである。 したがって,上のロジックに対し次のようなパス名を与えるとこのファイルの内容が漏洩してしまうことに なる。 d:¥dir¥file.pri... すでに述べたとおり,この問題は「.」だけでなく半角スペースが末尾についているときにも起こる。入力欄 からデータを取り込むときには前後の空白の除去が欠かせない。 サービス機能の不活性化 多くの自由度を持つ Win32 のパス名を相手にするには複雑なロジックのプログラムが必要になるが,パス名 の解釈に伴う「サービス機能」の多くを不活性化する特殊な指定方法がある。それはパス名の先頭に ¥¥?¥ と いう4文字を付け加えることである。たとえば, pfile = fopen ("d:¥¥dir¥¥data.txt", "r"); の代わりに pfile = fopen ("¥¥¥¥?¥¥d:¥¥dir¥¥data.txt", "r"); のようにするのである。 ¥¥?¥ から始まるパス名についてはサービス機能が次のようにはたらかなくなる。 1)ディレクトリ区切り文字「/」が使用できない。 2)ディレクトリ区切り文字の重複「¥¥」 (C/C++ の文字列定数内では "¥¥¥¥")が許されない。 3)パス名の途中に ¥.¥ を差し挟めない。 4)パス名の途中に ¥..¥ を差し挟めない。 5)パス名の末尾の「.」や半角スペースが無視されない。 ショートネーム Windows のファイルやディレクトリはその名前としてロングネームとそのロングネームから自動生成された ショートネームの2つを持っており,そのどちらを使ってもファイルにアクセスできる。 たとえば, MyHomePage.html のように 8.3 形式におさまらない名前のファイルには MYHOME~1.HTM のような短い別名がシステムにより自動で付けられている。 ショートネームの自動生成はかつて8.3形式のファイル名しか扱えなかったWindowsのファイルシステムが改 訂されてロングネームが導入されたとき,以前の古いソフトウェアの互換性を保つために導入された機能で ある。 われわれが普段付けるファイル名はショートネームの枠に収まらないことが多いから,たいていはロングネー ムを使っていると言っていい。そして,ファイルにロングネームで名前を付けると,そのファイルには必ず -4- Copyright Copyright © 2002 IPA, All Rights Reserved. ISEC セキュア・プログラミング講座 IPA IPA 8-1. Windowsパス名の落とし穴 ショートネームも付いている。 自動生成されるショートネームは, 6文字の英数字 「~ 」 数字 「.」 3文字以内の拡張子 という一般形をもっている。使われる英字はすべて大文字である。 ショートネーム機能のスイッチ Windows NT/2000 では,次のレジストリ項目を設定しシステムを再起動することによって,その時点以降 ショートネームを自動生成しないようにすることができる。ただし,それ以前に自動生成されたショートネー ムはそのまま存続する。 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\FileSystem\ NtfsDisable8dot3NameCreation = 1 [REG_DWORD] せっかくの機能であるが,多くの場合この方法でショートネームの問題を回避しようとすることには無理が あるかもしれない。マシン管理者であれば別だが,パッケージソフトウェアを供給するといった立場ではユー ザのシステム設定にまで関与できないことが多いからである。 ショートネームの排除 ユーザがある特定のファイルのパス名を入力してきたらそれを拒絶するロジックを書いたとしても,もしロ ングネームしか考慮していなければ,ショートネームが使われるとチェックを迂回されてしまう。 Windows 2000 および Windows 98 以降の OS に限定されるが,Win32 API には GetLongPathName という関数が あり,与えたパス名をすべてロングネームを使ったパス名に変換して返してくれる。 GetLongPathName (何らかのパス名 , 結果のパス名の領域 , 領域長) 戻り値: 結果のパス名のバイト数 この関数が使える環境であるなら,一旦ロングネームによるパス名に変換してからそのパス名の妥当性を検 査することができる。 Winodws NT(4.0 およびそれ以前)や Windows 95 ではこの関数は利用できない。ロングネームのファイル名 およびディレクトリ名には文字「~ 」を使わない,という運用ルールを設定し,ユーザ入力のパス名に「~ 」が 含まれていたらショートネームが混入されていると見なす,といった工夫が必要である。 なお, 「~ 」の文字コードの値は JIS(および ASCII)では 0x7E であり,このバイト値はシフト JIS 漢字の2バ イト目に入り得る。パス名の区切り「¥」(0x5C)を扱うときと同様の考慮が必要である。 ストリーム c: d:などのドライブキャラクタの記述部分を別として,Windows NT/2000のNTFSのパス名では文字コロン 「:」 の取り扱いにも注意を要する。ファイル名中の「:」は NTFS ファイルの「ストリーム」を表す区切り記号と して意味を持つからだ。「:」がファイル名に含まれていると,アプリケーションプログラムから見てコン -5- Copyright Copyright © 2002 IPA, All Rights Reserved. ISEC セキュア・プログラミング講座 IPA IPA 8-1. Windowsパス名の落とし穴 ピュータは一見正常に入出力を行うが,それは通常期待されるものとは異なる動作になるのである。 ストリームについての説明は NTFS に関する記事に譲ることにする。要点は,ファイル名に「:」が含まれて いると具合が悪い,ということである。 予約デバイス名 Windows には予約デバイス名というものがある。次の名前はディレクトリやファイルの名前には使ってはな らないとされているのだ。 AUX CON NUL PRN CLOCK$ COM1 ∼ COM9 LPT1 ∼ LPT9 これらは,MS-DOSの時代に用いられていた,コンソール,シリアル通信ポート,プリンタポートなどを表す 古典的な名称だが,Windows においてもこれらは有効なのである。 特に問題になるのは,c:¥dir¥aux.txt のようにドライブキャラクタやディレクトリ修飾され,拡張子がついたパ ス名の中で用いても,ファイル名としては有効でないことだ。とくにCONはこうした文脈でもデバイス名と して「正常に」はたらいてしまう。ファイルの内容を読み出すプログラムにファイル名として CON を含むパ ス名が与えられると,プログラムはコンソールデバイスを正常にオープンし入力を待ち続け,先に進まなく なる。悪意あるユーザが容易にプログラムを妨害できてしまうことになる。 10 番以降のデバイス 通常の指定の仕方では,COM10やLPT10はデバイスではなくファイル名として解釈される。10番以降をデバ イスとして扱うには先頭に ¥¥.¥ を付けて ¥¥.¥COM10,¥¥.¥LPT10 のように指定する。もちろん,C/C++ の文 字列定数では次のように ¥ を2個ずつ並べて書くのは言うまでもない。 pfile = fopen ("¥¥¥¥.¥¥COM10", "w"); 予約デバイス名の回避 残念ながらファイルのパス名が予約デバイス名と衝突することを簡単に回避する手段は Win32 API には用意 されていない。対策としては,たとえばリスト1(文末に掲載)のような照合用の関数を用意して,ユーザ が入力してきたパス名に予約名が含まれていないかどうか次のような形で確認しつつ使用する必要がある。 if (PathContainsReservedName (pszPathname)) // エラー処理 else // pszPathname をパス名として使用 -6- Copyright Copyright © 2002 IPA, All Rights Reserved. ISEC セキュア・プログラミング講座 IPA IPA 8-1. Windowsパス名の落とし穴 まとめ Windowsには,同一のファイルを表す複数通りのパス名の書き方がある。こうした別名のいくつかは¥¥?¥を 用いて無効にできるが,ショートネームについては別の取り扱いが必要だ。ファイルのパス名の中に含まれ ていると具合が悪いものとしてNTFSのストリーム名や予約デバイス名がある。プログラムが予期しない動作 をしてしまうのでこれらは排除しなくてはならない。 参考文献 『CreateFile 関数,プラットフォーム SDK ファイル入出力』 http://www.microsoft.com/japan/developer/library/jpwinpf/_win32_createfile.htm 『File Name Conventions』(英文) http://msdn.microsoft.com/library/en-us/fileio/fsys_7qwj.asp 『INFO: Types of File I/O Under Win32』(英文) http://support.microsoft.com/support/kb/articles/Q99/1/73.ASP 『INFO: Filenames Ending with Space or Period Not Supported』(英文) http://support.microsoft.com/support/kb/articles/Q115/8/27.asp 『How to Disable Automatic Short File Name Generation (Q210638)』(英文) http://supprt.microsoft.com/support/kb/articles/Q210/6/38.asp 『HOWTO: Use NTFS Alternate Data Streams』(英文) http://support.microsoft.com/support/kb/articles/Q105/7/63.ASP 『INFO: CreateFile() Using CONOUT$ or CONIN$』 (英文) http://support.microsoft.com/support/kb/articles/Q90/0/88.ASP 『HOWTO: Specify Serial Ports Larger than COM9』(英文) http://support.microsoft.com/support/kb/articles/Q115/8/31.asp 『INFO: Accessing a Device on Windows 2000 Terminal Server Through CreateFile()』 (英文) http://support.microsoft.com/support/kb/articles/Q259/1/31.ASP -7- Copyright Copyright © 2002 IPA, All Rights Reserved. ISEC セキュア・プログラミング講座 IPA IPA 8-1. Windowsパス名の落とし穴 リスト1 パス名に予約デバイス名が使われているかどうかを調べる関数 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 #include <windows.h> #include <stdio.h> // 使用するサブルーチン(関数)の宣言。 BOOL TestName (char* pszName, int size); // // PathContainsReservedName - パス名が予約デバイス名を含んでいるかどうかの判定。 // BOOL PathContainsReservedName (char* pszPathname) { BOOL bResult = FALSE; // 判定結果。予約デバイス名が含まれていると TRUE。 // すなわちファイルの識別名として使用できない。 BOOL bShiftJis = FALSE; // シフト JIS 漢字コードの2バイト目であるか否か。 int offsetName = 0; // ディレクトリ名/ファイル名の開始位置。 int offsetStream = 0; // ファイル名の最初の「:」の位置。 int i = 0; // カウンタ。 // 図式 // // d:\dir1\dir2\dir3\con.foo.bar:stream // || -> | -> | -> | | // | | // offsetName offsetStream // ドライブ文字が先頭に指定されているときはそれを飛び越す。 if (isalpha (pszPathname[0]) && pszPathname[1] == ':') { i = 2; offsetName = i; } // ディレクトリ区切り文字「\」および「/」ごとに名前の検査を行なう。 for (; pszPathname[i] != '\0'; i++) { if (bShiftJis) { // シフト JIS 漢字2バイト目のときは何もしない。 bShiftJis = FALSE; } else if (pszPathname[i] >= '\x81' && pszPathname[i] <= '\x9F' || pszPathname[i] >= '\xE0' && pszPathname[i] <= '\xFC') { // シフト JIS 漢字1バイト目なら次は2バイト目 bShiftJis = TRUE; } else if (pszPathname[i] == '\\' || pszPathname[i] == '/') { // ディレクトリ区切り文字。 // その直前の名前を検査する。もし予約デバイス名が含まれていたら結論が出た。 bResult = TestName (pszPathname + offsetName, i - offsetName); if (bResult) return bResult; // 次の階層の名前の先頭位置を記憶しておく。 offsetName = i + 1; } } // ファイル名に続く最初の「:」の位置を求める。「:」の後ろはストリーム名。 // 「:」が含まれていないときは文字列の最後までファイル名。 offsetStream = strlen (pszPathname); for (i = offsetName; pszPathname[i] != '\0'; i++) { -8- Copyright Copyright © 2002 IPA, All Rights Reserved. ISEC セキュア・プログラミング講座 IPA IPA 8-1. Windowsパス名の落とし穴 リスト1 (つづき) 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 if (pszPathname[i] == ':') { offsetStream = i; } } // ファイル名を検査する。(予約デバイス名との照合) bResult = TestName (pszPathname + offsetName, offsetStream - offsetName); // 照合結果を返す。 return bResult; } // // TestName - 名前(パス名ではない)の中に予約デバイス名が含まれているかどうかの照合。 // BOOL TestName (char* pszName, int size) { BOOL bResult = FALSE; // 照合結果。予約デバイス名が含まれていると TRUE。 int offsetDot = size; // 名前の中の最初の「.」の位置。 int i = 0; // カウンタ。 // 名前の中の最初の「.」の位置を求める。「.」の手前までが予約デバイス名との照合対象。 // 「.」が含まれていないときは名前全体が照合対象。 for (i = 0; i < size; i++) { if (pszName[i] == '.') { offsetDot = i; break; } } // 予約デバイス名との照合 if (offsetDot == 3) { if ( _strnicmp (pszName, "AUX", 3) == 0 || _strnicmp (pszName, "CON", 3) == 0 || _strnicmp (pszName, "NUL", 3) == 0 || _strnicmp (pszName, "PRN", 3) == 0) // AUX, CON, NUL, PRN のいずれかに該当。 bResult = TRUE; } else if (offsetDot == 4) { if ( (_strnicmp (pszName, "COM", 3) == 0 || _strnicmp (pszName, "LPT", 3) == 0 ) && pszName[3] >= '1' && pszName[3] <= '9') // COM1 ∼ 9, LPT1 ∼ 9 のいずれかに該当。 bResult = TRUE; } else if (offsetDot == 6) { if (_strnicmp (pszName, "CLOCK$", 6) == 0) // CLOCK$ に該当。 bResult = TRUE; } // 照合結果を返す。 return bResult; } -9- Copyright Copyright © 2002 IPA, All Rights Reserved.