JPCERT コーディネーションセンター

FIO32-C. 通常ファイルに対してのみ行われるべき操作をデバイスファイルに対して行わない

Windows や UNIX などさまざまなオペレーティングシステムにおいて、デバイスに対応する特殊ファイルへのアクセスをファイル名を使って行うことがある。Microsoft Windows における予約されたデバイス名の例としては、AUXCONPRNCOM1LPT1 や、\\.\ で始まるデバイス名前空間のパスなどがある。UNIX システムでは、デバイスに対するアクセス制御をデバイスファイルを通じて行ったり、適切なデバイスドライバへの指令をデバイスファイルに対する操作を通じて行ったりする。

対象として通常のテキストファイルやバイナリファイルを想定している操作をデバイスファイルに対して行うと、システムがクラッシュしたり、サービス運用妨害(DoS)攻撃が引き起こされる可能性がある。たとえば Windows では、デバイスファイル名をファイルリソースとして解釈しようとすると、不正なリソースアクセスが行われ、通常はクラッシュの原因となる[Howard 2002]。

UNIX において、攻撃者が不正にデバイスファイルにアクセスできる場合、セキュリティ上のリスクとなり得る。たとえば攻撃者が /dev/kmem デバイスを読み書きできる場合、プロセスの優先順位、UID、その他の属性を変更したり、単純にシステムをクラッシュさせる可能性がある。同様に、ディスクデバイス、テープデバイス、ネットワークデバイス、および他のユーザが使用中のターミナルデバイスへのアクセスも問題につながる可能性がある[Garfinkel 1996]。

Linux では、通常ファイルの代わりにデバイスファイルを開かせることによって、特定のアプリケーションをロックすることができる。以下にデバイスファイルの例を列挙する。

/dev/mouse
/dev/console
/dev/tty0
/dev/zero

これらのデバイスファイルをチェックしない Web ブラウザでは、<IMG src="file:///dev/mouse"> のような画像タグを含む Web サイトにアクセスすることで、ユーザのマウスをロックすることができる。

違反コード

以下の違反コード例では、ユーザはデバイスファイルや FIFO (first-in, first-out) ファイルを指定することができ、プログラムは fopen() を呼び出したところで停止する。

#include <stdio.h>

void func(const char *file_name) {
  FILE *file;
  if ((file = fopen(file_name, "wb")) == NULL) {
    /* エラー処理 */
  }

  /* ファイルを操作する */

  if (fclose(file) == EOF) {
    /* エラー処理 */
  }
}
適合コード (POSIX)

POSIX では、open()O_NONBLOCK フラグを指定することで、プログラムが停止しないことを保証している[IEEE Std 1003.1:2013]。

O_RDONLY または O_WRONLY を指定して FIFO ファイルをオープンする場合:

  • O_NONBLOCK を指定している場合、読み取り専用の open() は遅延することなく戻る。読み取り用にファイルをオープンしているプロセスが存在しない状態で、書き込み専用の open() を呼び出すと、open() はエラーを返す。
  • O_NONBLOCK が指定されていない場合、読み取り専用の open() は、ファイルが書き込み用にオープンされるまで呼び出しスレッドをブロックする。書き込み専用の open() は、ファイルが読み取り用にオープンされるまで呼び出しスレッドをブロックする。

ノンブロックモードをサポートするブロック特殊ファイルやキャラクタ特殊ファイルを開く場合:

  • O_NONBLOCK が指定されている場合、open() 関数は、デバイスが準備完了または利用可能になるまでブロックせずに戻る。その後のデバイスの動作は、デバイス依存である。
  • O_NONBLOCK が指定されていない場合、open() 関数は、デバイスが準備完了または利用可能になるまで呼び出し側のスレッドをブロックする。デバイスが準備完了または利用可能になったら open() は戻る。

それ以外の場合、O_NONBLOCK の挙動は不定である。

オープンしたファイルに対して、プログラマは、POSIX の lstat()fstat() 関数を使ってファイルに関する情報を取得したり、S_ISREG() マクロを使ってそのファイルが通常のファイルであるかどうかを調べることができる。

ファイルをオープンしたあとの read() あるいは write() 呼び出しにおける O_NONBLOCK の動作は不定なため、対象のファイルが特殊デバイスではないと確認できた場合は、このフラグを無効にするほうが良い。

利用できるのであれば(たとえば Linux 2.1.126+、FreeBSD、Solaris 10、POSIX.1-2008)、O_NOFOLLOW も使用すべきである。(「POS01-C. ファイルを操作するときにはリンクかどうかを確認する」を参照)。O_NOFOLLOW が利用できない場合、シンボリックリンクのチェックには「POS35-C. シンボリックリンクの存在をチェックするときには競合状態を避ける」にある手法を使用すべきである。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#ifdef O_NOFOLLOW
  #define OPEN_FLAGS O_NOFOLLOW | O_NONBLOCK
#else
  #define OPEN_FLAGS O_NONBLOCK
#endif

void func(const char *file_name) {
  struct stat orig_st;
  struct stat open_st;
  int fd;
  int flags;

  if ((lstat(file_name, &orig_st) != 0) ||
      (!S_ISREG(orig_st.st_mode))) {
    /* エラー処理 */
  }

  /* 競合ウィンドー */

  fd = open(file_name, OPEN_FLAGS | O_WRONLY);
  if (fd == -1) {
    /* エラー処理 */
  }

  if (fstat(fd, &open_st) != 0) {
    /* エラー処理 */
  }

  if ((orig_st.st_mode != open_st.st_mode) ||
      (orig_st.st_ino  != open_st.st_ino) ||
      (orig_st.st_dev  != open_st.st_dev)) {
    /* ファイルはすり替えられている */
  }

  /*
   * 問題ないファイルであることが確認できたので O_NONBLOCK
   * を無効にする (省略可能)
   */
  if ((flags = fcntl(fd, F_GETFL)) == -1) {
    /* エラー処理 */
  }

  if (fcntl(fd, F_SETFL, flags & ~O_NONBLOCK) == -1) {
    /* エラー処理 */
  }

  /* ファイルを操作する */

  if (close(fd) == -1) {
    /* エラー処理 */
  }
}

このコードには厄介な TOCTOU (time-of-check, time-of-use) 競合状態がある。攻撃者は、lstat() の呼び出しのあと、open() の呼び出しの前に file_name が参照するファイルを変更することができる。ファイルのすり替えはファイルをオープンした後で検出できるが、ファイルのオープン自体が望ましくない動作を引き起こす場合にこの動作を防止できない(TOCTOU 競合状態について詳しくは FIO45-C. Avoid TOCTOU race conditions while accessing files を参照)。

攻撃者がファイルを置き換えた場合、置き換えられたファイルの種類に応じて次の表にあるような影響が想定される。

ファイルの種類と影響

ファイルの種類

影響

別の通常ファイル

fstat() 検証によりファイルをすり替えられたことがわかる。

FIFO

open()-1 を返し errnoENXIO に設定する。または open() は成功するが fstat() 検証でファイルをすり替えられたことがわかる。

シンボリックリンク

O_NOFOLLOW を使用している場合、open()-1 を返す。そうでない場合、fstat() 検証によりファイルをすり替えられたことがわかる。

デバイスファイル

通常は fstat() 検証で st_mode の値からファイルをすり替えられたことがわかる。単純にオープンやクローズするだけで問題が起こるようなデバイスファイルにすり替えられた場合はどうしようもない。st_mode が同じ値であった場合、デバイスは(オープンした後)通常ファイルのように見える。その後 fstat() 検証によって st_devst_ino の値からファイルをすり替えられたことがわかる(Solaris の /dev/fd/* を使った場合のように同じファイルにすり替えられるのでないかぎり。ただしこのようなすり替えであれば問題はないだろう)。

本ルールに適合し、この TOCTOU 競合状態を防ぐためには、file_name はセキュアディレクトリにあるファイルを指していなければならない (FIO15-C. ファイル操作はセキュアディレクトリで行う を参照)。

違反コード (Windows)

次の違反コードでは、通常のファイル以外をオープンするのを GetFileType() 関数を使って避けようとしている。

#include <Windows.h>

void func(const TCHAR *file_name) {
  HANDLE hFile = CreateFile(
    file_name,
    GENERIC_READ | GENERIC_WRITE, 0, 
    NULL, OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL, NULL
  );
  if (hFile == INVALID_HANDLE_VALUE) {
    /* エラー処理 */
  } else if (GetFileType(hFile) != FILE_TYPE_DISK) {
    /* エラー処理 */
    CloseHandle(hFile);
  } else {
    /* ファイルを操作する */
    CloseHandle(hFile);
  }
}

一見よさそうに見えるが、この場合 Win32 GetFileType() 関数を使うのは危険である。渡されたファイル名が名前付きパイプで読み出しリクエストでブロックしている場合、GetFileType() 呼出しは読み出しリクエストが完了するまでブロックするため、DoS 攻撃に悪用できる。さらにファイルハンドルをオープンする動作が悪影響を及ぼす可能性もある。例えば、シリアルデバイスをオープンするなど。

適合コード (Windows)

Microsoft は Windows で使われる予約デバイス名やデバイス名前空間のリストを提供している[MSDN]。次の適合コードでは、与えられたパスがデバイスを指しているかどうかを判別する isReservedName() 関数を定義している。パスを isReservedName() 関数で調べ、そのパスに対して操作を行うという一連の処理を行う際には、TOCTOU 競合状態を避けること。

#include <ctype.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

static bool isReservedName(const char *path) {
  /* MSDN から拾ってきた予約名一覧 */
  static const char *reserved[] = {
    "nul", "con", "prn", "aux", "com1", "com2", "com3",
    "com4", "com5", "com6", "com7", "com8", "com9",
    "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6",
    "lpt7", "lpt8", "lpt9"
  };
  bool ret = false;

/*
 * まずデバイス名前空間かどうかを確認する。
 * デバイス名前空間は通常のファイルパスとは異なり、
 * 必ず \\.\ で始まる。
 */

  if (!path || 0 == strncmp(path, "\\\\.\\", 4)) {
    return true;
  }

  /* 昔の予約名との比較 */
  for (size_t i = 0; !ret &&
       i < sizeof(reserved) / sizeof(*reserved); ++i) {
   /*
    * Windows のファイルシステムでは文字の大小は区別されないので
    * 小文字で比較する。Note: このコードでは多言語環境は考慮していない。
    * 扱う文字は全て ASCII 文字と想定している。
    */
    if (0 == _stricmp(path, reserved[i])) {
      ret = true;
    }
  }
  return ret;
}
リスク評価

通常ファイルに対してのみ行われるべき操作をデバイスファイルに対して行わせることが可能な場合、サービス運用妨害(DoS)攻撃や、プラットフォームによっては他のより深刻な攻撃に悪用される可能性がある。

ルール

深刻度

可能性

修正コスト

優先度

レベル

FIO32-C

P4

L3

自動検出

ツール

バージョン

チェッカー

説明

Compass/ROSE

 

 

このルールへの違反の一部を検出することができる。ルールが適用されるのは信頼できないファイル名文字列だけであるが、ROSE はどの文字列が「信頼できる」か判断することはできない。ヒューリスティックな方法としては、fopen() 呼び出しの前後にファイル名検証が行われているかどうかを調べるのがよいだろう。何らかの検証が存在するならば、lstat()を呼び出してからファイルをオープンし、その後で fstat() を呼び出しているはずである。このような動作をしているからといってルールに完全に準拠していることにはならないが、少なくともプログラマが lstat-fopen-fstat イディオムを知っていることを示唆している。

Fortify SCA

5.0

 

 

Related Guidelines
CERT C コーディングスタンダード FIO05-C. 複数のファイル属性を使用してファイルを特定する
FIO15-C. ファイル操作はセキュアディレクトリで行う
POS01-C. ファイルを操作するときにはリンクかどうかを確認する
POS35-C. シンボリックリンクの存在をチェックするときには競合状態を避ける
CERT C++ Secure Coding Standard VOID FIO32-CPP. Do not perform operations on devices that are only appropriate for files
Java セキュアコーディングスタンダード CERT/Oracle 版 FIO00-J. 共有ディレクトリにあるファイルを操作しない
MITRE CWE CWE-67, Improper Handling of Windows Device Names
参考資料
[Garfinkel 1996] Section 5.6, "Device Files"
[Howard 2002] Chapter 11, "Canonical Representation Issues"
[IEEE Std 1003.1:2013] XSH, System Interfaces, open
[MSDN]  
翻訳元

これは以下のページを翻訳したものです。

FIO32-C. Do not perform operations on devices that are only appropriate for files (revision 108)

Top へ

Topへ
最新情報(RSSメーリングリストTwitter