追記(2015/2/6)
大垣さんから訂正依頼のコメントを頂いておりますので合わせてお読みください。徳丸としては特に訂正の必要は感じませんでしたので、本文はそのままにしています。そう思う理由はコメントとして追記いたしました。 (追記終わり)大垣さんのブログエントリ「GHOSTを使って攻撃できるケース」を読んだところ、以下のようなことが書いてありました。
1. ユーザー入力のIPアドレス(ネットワーク層のIPアドレスではない)に攻撃用データを送る。この記事には以下の問題があると感じました。
2. バリデーション無しで攻撃用の不正なIPアドレスをgethostbyname()に渡される。
3. ヒープオーバーフローでヒープ領域のメモリ管理用の空きサイズを改竄する。
【中略】
どんなソフトウェアが危ないのか?
【中略】
- ユーザー入力のIPアドレスをバリデーションしないでgethostbyname()を使用している。
- インタラクティブな動作を行っている。(攻撃者からの入力に対してレスポンスがある)
- ソフトウェアが持つ実行機能が利用できる。
どうすれば守れたのか?
EximはメールサーバーなのでIPアドレスはネットワーク層からだけでなく、ユーザー入力としても処理しています。IPアドレスとしてあり得ないデータをアプリでエラーにしていれば攻撃はできませんでした。入力データとしてあり得ないデータをライブラリに直接渡すとリスクが伴います。入力バリデーションを行っていれば問題となりませんでした。
glibcの脆弱性の有無に関わらず、これだけしていれば任意コマンドの実行という最悪の事態は防げました。
- 入力バリデーションを行い、おかしな入力は拒否する
- 攻撃経路となる入力値はIPアドレスではなくホスト名である
- 攻撃を防げるとするバリデーションの仕様が明記されていない
- Eximは本当にバリデーションをしていないのか
攻撃経路となる入力値はIPアドレスではなくホスト名である
Qualysの発表したEximに対する攻撃手法によると、攻撃はSMTPのHELOコマンドを経由して、数字とドットのみの「ホスト名」をgethostbyname()に送ることが行われます。GHOST脆弱性の性格から、攻撃に用いる文字は数字とドットのみですが、HELOコマンドが受け取るのはホスト名ですから、IPアドレスとしてバリデーションするわけにはいきません。また、gethostbyname()が受け取る引数もbynameとあるようにホスト名です。大垣さんは、攻撃文字列が数字のみなのでこれをIPv4形式のIPアドレスと誤認されたようですが、これは間違いということになります。
攻撃を防げるとするバリデーションの仕様が明記されていない
前項と関連しますが、大垣さんは、攻撃が防げるとするバリデーションの要件を明示していません。おそらく、IPv4としてのバリデーションを想定しておられたのでしょうが、これは間違いですので、前提条件が崩れたことになります。それでは、「大垣さんだったらホスト名に対してどんなバリデーションをするだろうか」と考えてみましたが、私が勝手に考えても失礼にあたると思いますので、大垣さんの著書「Webアプリセキュリティ対策入門 ~あなたのサイトは大丈夫?」から、似て非なる例としてメールアドレスのバリデーション関数を下記に引用します。
function validate_email($str, $check_dns=true, $mode=V_EXIT) {
// preg_matchもバイナリセーフ
$error = !preg_match ('/^(([a-zA-Z0-9])+([a-zA-Z0-9\._-])*@([a-zA-Z0-9_-])+([a-zA-Z0-9\._-]+)+)$/', $str);
list(,$domain) = split('@',$str);
// checkdnsrr()はWindowsでは実装されていません。
if (!$error && !strstr(PHP_OS,'WIN') && !checkdnsrr($domain,'MX')) {
$error=false;
}
if ($mode == V_RETURN) {
return $error;
}
if ($error) {
trigger_error('不正なメール形式を検出しました。');
}
}
preg_matchの中の赤字の部分がドメイン名のチェックです。正規表現がRFC準拠でないことや、ドメインパートにアンダースコアを許容していることや、量化子「+」がネストしていることが気になる人もいるでしょうが、本題ではないので流します。この関数は、メールアドレスを正規表現でチェックした後、PHPのcheckdnsrr関数を用いて、ドメイン名に対するMXレコードが存在することをチェックしています。これは、大垣さんの主張であるgethostbyname()を呼ぶ前にバリデーションを行うようにという処理とよく似ていますし、ホスト名やドメイン名のチェックを外部APIを呼び出して行う点もよく似ていますので、ここでは上記の正規表現が、「大垣流のドメイン名(ホスト名)バリデーション」と想定して議論を行います。
Qualysのアドバイザリによると、GHOST攻撃に用いる文字列は数字とドットだけからなる長い(1Kバイトを超える(後述))文字列ですが、上記の正規表現はこの攻撃文字列を許容します。すなわち、上記のバリデーションではGHOST攻撃を防げません。
したがって、「入力値をバリデーションすれば攻撃を防げる」というものではなく、当然のことながら、バリデーションの仕方によって防御の効果は変わってきます。
Eximは本当にバリデーションをしていないのか
大垣さんは、「バリデーション無しで攻撃用の不正なIPアドレスをgethostbyname()に渡される」と、Eximはgethostbyname()に渡す文字列をバリデーションしていないと断定していますが、本当にそうでしょうか。Eximのソースコードおよびデバッグログを用いて確認したところ、Eximはgethostbyname()を呼ぶ前に、smtp_in.c内のcheck_helo()関数でホスト名をチェックしています。
static BOOL
check_helo(uschar *s)
{
// 中略
if (*s == '[')
/* 中略 IPv6形式のIPアドレスの処理 */
else if (*s != 0)
{
yield = TRUE;
while (*s != 0)
{
if (!isalnum(*s) && *s != '.' && *s != '-' &&
Ustrchr(helo_allow_chars, *s) == NULL)
{
yield = FALSE;
break;
}
s++;
}
}
}
// 後略
赤字で示したように、HELOコマンドのパラメータ(ホスト名)として、英数字、ドット、ハイフン、helo_allow_chars配列の文字(デフォルトは空だが設定により変更可能)のみを許可しています。上記は、細かい違いはあるものの、大垣さんのvalidate_email()関数のドメインパートのバリデーションロジックと概ね同じです。
このように、Eximはホスト名をバリデーションすることなくgethostbyname()に渡しているとする大垣さんの記述は間違いです。おそらく、前述のように、gethostbyname()に渡すパラメータをホスト名ではなくIPアドレスと勘違いされていたので、「IPアドレスとしてのバリデーションをしていない」と推測されたものと思いますが、ソフトウェアのソースコードや動作を確認することなく「Eximはバリデーションしていない」と断定したことは軽率な行為であると考えます。
どうすればよかったか
Qualysのアドバイザリでは、攻撃に用いる文字列(gethostbynameの引数)の要件として下記の記述があります。It must be long enough to overflow the buffer. For example, the non-reentrant gethostbyname*() functions initially allocate their buffer with a call to malloc(1024) (the "1-KB" requirement).Eximが呼び出しているのは、まさに the non-reentrant gethostbyname*() functions ですので、攻撃には1KB以上の長い文字列が必要ということになります。
一方、ホスト名の長さについては、RFC1123には以下の記述があります。
Host software MUST handle host names of up to 63 characters and SHOULD handle host names of up to 255 characters.試訳
ホストソフトウェアは63文字までのホスト名を扱わなければならず(MUST)、255文字までのホスト名を扱うべき(SHOULD)である。すなわち、ホスト名の上限は255文字としていればRFC上問題はないため、積極的に255文字を超えるホスト名をエラーにしていれば、GHOST脆弱性の影響は受けないことになります。
とは言うものの、RFC1123の要件としては、256文字以上のホスト名を扱ってもとくに問題はないように思えます。すなわち、ホスト名の上限の仕様には以下の選択肢があることになります。
- 63文字(MUST)
- 255文字(SHOULD)
- 256文字以上の有限値(任意)
- 無制限(任意、Eximが該当)
現実問題として、255文字を超えるホスト名を扱うことはまずないでしょうから、長さの制限を設けておけばよかったのになぁとは思います。
まとめ
EximがGHOST脆弱性の影響を受ける問題とバリデーションとの関係について説明しました。Eximは、まったくバリデーションをしていなかったわけではなく、ホスト名の文字種制限(バリデーション)はしていましたが、長さの制限はしていません。そして、長さを適当に(255文字以下、あるいは1023文字以下)制限していれば、GHOST脆弱性の影響は受けませんでした。では、バリデーションはどのように考えればよいでしょうか。従来から言っていることですが以下を推奨します。
- バリデーションの基準はアプリケーションの仕様である
- 仕様を満たさない入力値は再入力を促すナビゲーションを行う
- アプリケーション仕様として、すべての入力パラメータの以下項目を定義しておく
- 文字種
- 文字列長の最小・最大値
- 数値の場合は最小値・最大値
- メールアドレス等は書式
バリデーションのセキュリティ上の効果については以下のように考えます。
- バリデーションの基準はアプリケーション仕様(再掲)
- Eximの場合のように、バリデーションの仕方によっては脆弱性が顕在化しなかったという例は結構ある(参考: Drupalの例)
- しかし、バリデーションは確実な対策ではないので、脆弱性対策をおろそかにしてはいけない
- そのため開発者の心構えとしては、バリデーションによる防御効果はないものとして脆弱性対策をすること
- バリデーションはしておこう。それがセーフティネットになる場合もある
この問題の根本的な原因は文字種バリデーションの有無ではなく、最大長バリデーションの有無です。
返信削除オーバーフローに"異常"に長い文字列が必要であるのは明白で、最大長バリデーションをしていなかったかRFC定義にくらべ異常に長い最大長を許可していたのは明らかです。
「Eximはバリデーションしていない」と断定したことは軽率な行為であると考えます」は誤りです。
「おそらく、IPv4としてのバリデーションを想定しておられたのでしょうが、これは間違いです」これも誤りです。RFCではホスト名はホスト名形式またはIPアドレス形式と明示しています。区別する意味はありません。
これらの訂正をお願いします。
Eximがバリデーションをしている・いないについては、「現状のEximのソフトウェア仕様に基づいたバリデーションをしている」と考えます。Eximは、HELOコマンドに与えるホスト名として英数字、ドット、ハイフンを許容しているので、その文字種のチェックをしています。ホスト名長については仕様として特に制限をしていないので、チェックもしていません。これは一種のバリデーションです。文字列長を制限していればよかったのに、というのは徳丸も同意で記事にも書きました。これは、仕様として、安全上の配慮が不足しているということになりそうです。しかし、仮に255バイト(あるいは253バイト)にホスト名を制限していたとしても、gethostbyname関数の実装によっては以下の様な問題が発生し得たと考えます。
返信削除それは、「オーバーフローに"異常"に長い文字列が必要であるのは明白」とは言えないからです。現に、GHOST脆弱性のうち、リエントラント版の関数については、わずか数十バイト程度でバッファオーバーフローが起こることを確認しています。Qualysが紹介しているPoCはリエントラント版のgethostbyname_rを用いたものですが、元々1000となっているバッファ長をさまざまな数字に変更することにより確認可能です。つまり、gethostbyname(非リエントラント版)が1KB以上の攻撃文字列を要するのは実装上の偶然であって、「"異常"に長い文字列が必要であるのは明白」とは言えません。
ともかく、仕様上の課題はあるものの、Eximはバリデーションをしていることになります。
また、『RFCではホスト名はホスト名形式またはIPアドレス形式と明示しています。区別する意味はありません』とのことですが、元の記事で『バリデーション無しで攻撃用の不正なIPアドレスを』、『バリデーション無しで攻撃用の不正なIPアドレスを』、『IPアドレスとしてあり得ないデータをアプリでエラーにしていれば攻撃はできませんでした』等と列挙していながら、実はホスト名も含んでいたのだという理屈は通らないと考えます。