アーカイブ
第10章 6.フォーマット文字列攻撃対策
公開日:2007年9月26日
独立行政法人情報処理推進機構
セキュリティセンター
本ページの情報は2007年9月時点のものです。
記載の資料は資料公開当時のもので、現在は公開されていないものも含みます。
フォーマット文字列攻撃は、領域をあふれさせることなくバッファオーバーフロー攻撃と同様の被害を及ぼすことのできる攻撃手口である。攻撃文字列のしくみと対策を解説する。
フォーマット文字列攻撃
フォーマット文字列攻撃は、printf() や syslog() 等のライブラリ関数がもつ書式編集機能を悪用し、実行中のプログラムのメモリに悪意の機械語コードを送り込んで実行させる攻撃である。
フォーマット文字列攻撃は、printf() 等の関数の書式引数に外部からの入力データがそのまま渡るようになっている場合に起こり得る。例えば、次の message の値が外部から与えられるようなケースである。
char* message;
printf(message);
攻撃が成功すれば最悪の場合、コンピュータの制御を奪われる。そうでなくてもプロセスが異常終了する等の障害が起こり得る。
攻撃は、例えば、次のような具合に進行する。
- 外部から与えられた悪意の書式文字列が printf() 関数によって解釈実行される
- %n 書式によって関数のリターンアドレスが書き変わる
- 関数からのリターン時、侵害コードへのジャンプが起こる
-
図10-9: フォーマット文字列攻撃
%n書式と攻撃文字列のしくみ
フォーマット文字列攻撃は、printf() 等が解釈実行する %n 書式の悪用によって起こる。
- 注:%n 書式は、C11 仕様において廃止された。(「C11:2011年以降のC言語仕様」参照)
%n は、これまでの書式編集出力で何バイトのデータが書き出されたかの値を整数変数に書き戻すことを指示する書式である。例えば、次のような場合、変数 count には値 7 が入る。
int count;
printf("%d%n", 1234567, &count)
%n より前に行われる書式編集出力の桁数が十分に大きければ、アドレスのような値を表すこともできる。次の例では、変数 address に値 0x79ABCDE0 が入る。
int address;
printf("%2041302496c%n", 0, &address);
これを利用して、悪意の書式文字列は、例えば、次の要素から構成される。
- 4バイトのポインタ値
- 長い桁数が指定された%c書式
- 対応する引数の番号が指定された%n書式
- 侵害コード(機械語プログラム、シェルコード)
これらにはそれぞれ次のような値が含まれる。
- 4バイトのポインタ値
-
書き変えたいメモリ上のワードのアドレス。これは、例えば、次のようなもののアドレスである:関数リターンアドレスの格納場所、例外ハンドラアドレスの格納場所、関数ポインタの格納場所等。
- 長い桁数が指定された%c書式
-
これは%cのみならず、%d、%p、%x等整数値を編集するものであれば他の書式であってもよい。この書式の桁数には、書き変えたいメモリ上のワードに書き込む値を十進数で表現する。ただし、値を4減じておく。これは先行する「4バイトのポインタ値」の分である。
- 対応する引数の番号が指定された%n書式
-
%n書式を使ってメモリへの書き込みを起こす際、%n書式が参照する何らかのポインタ値をprintf()関数の引数として供給する必要がある。printf()関数は、プログラマが実際に引数を指定していなくても、書式を解釈してスタック上の該当する位置から値を拾うようになっている。そこで、この書式文字列の先頭に置いた「4バイトのポインタ値」をこの%n書式に拾わせるようにしたい。そのために、書式文字列自身がprintf()の引数であるとしたら何番目の引数に該当するかの番号をnn$の形式で付加し、例えば、%14$nのように指定する。
- 侵害コード
-
実行させたい機械語プログラム
フォーマット文字列攻撃対策
フォーマット文字列攻撃の対策には次を行う。
(1) 外部入力値の不使用
printf()、syslog()等の書式引数には、外部から入力したデータを用いない。やむを得ず外部から書式文字列を受け入れる必要のある場合は、許される文字列パターンを明確に定め、そのパターンに合致していることを確認してから使用する。その際決して %n 書式の使用を許すべきではない。
書式引数を与える際に注意が必要なprintf系のライブラリ関数のうち C11仕様が対策したものには次のものがある。
- ファイルに出力する関数
-
printf, vprintf, fprintf, vfprintf, dprintf, vdprintf,wprintf, vwprintf, fwprintf, vfwprintf
- 既存の文字列領域に出力する関数、領域長引数なし(使用は推奨されない)
-
sprintf, vsprintf
- 既存の文字列領域に出力する関数、領域長引数あり
-
snprintf, vsnprintf, swprintf, vswprintf
書式引数を与える際に C11仕様においてもなお注意が必要なprintf系ならびにsyslog系のライブラリ関数には次のものがある。
- 文字列領域を新たに割り当てて出力する関数(使用後freeを忘れずに)
-
asprintf, vasprintf
- システムロガーにメッセージを送る関数
-
syslog, vsyslog
(2) GCC の -Wformat=2 オプション
GCC 4 には、フォーマット文字列攻撃脆弱性のおそれのある箇所を指摘してくれるコンパイルオプションがある。それは、-Wformat=2 である。このオプションを指定すると、ライブラリ関数の書式引数に文字列リテラル("xxx" のように記述する文字列値そのもの)以外が指定されていることを警告してくれる。
[コード例 書式引数にポインタ変数を与えている]
1 #inlcude <stdio.h>
2 #include <syslog.h>
3 foo () {
4 char* message = "hello";
5 printf(message);
6 syslog(LOG_USER, message);
7 }
[コンパイル時の警告メッセージ(多少日本語が変ではあるが)]
$ gcc -Wformat=2 foo.c
format_test.c: In function ‘foo’:
format_test.c:5: 警告: フォーマットは非文字列リテラルで、かつフォーマット引数をもちません
format_test.c:6: 警告: フォーマットは非文字列リテラルで、かつフォーマット引数をもちません
$
(3) GCCの -D_FORTIFY_SOURCE=2 オプション
-D_FORTIFY_SOURCE=n オプション(n=1 または 2)は、glibc(GNU C Library)が内部に備えている「ランタイム関数の堅固なバージョン」を使うよう指示する GCC のオプションである。これを指定すると、プログラム実行時に strcpy() 等のランタイム関数が変数領域のオーバーフローを検知して SIGABRT シグナルが発生するようになる。
-D_FORTIFY_SOURCE=2 は、そのバリエーションであり、printf() 等の書式編集関数に %n 書式が与えられたことをエラーとして検知するよう指示するものである。プラットフォームによっては、-01 のような最適化オプションと共に用いることが必要となる場合がある。
すべての %n 書式がエラーとして扱われるわけではない。エラーとなるのは、書き込み可能メモリセグメント上にある書式文字列に %n が含まれている場合のみである。すなわち、プログラム外部から入って来た可能性のあるデータが対象となる。プログラマが文字列定数("xxx" の形の文字列リテラルや const 宣言された静的文字配列)としてソースコードに記述した書式文字列は、コンパイラによってリードオンリーのメモリセグメント上に置かれ、そこに含まれる %n はエラーとならない。プログラマ自身が %n 書式を使う道が残されている。
[コード例 bar.c (定数中の %n と配列の中の %n)]
1 #include <signal.h>
2 #include <stdio.h>
3 #include <stdlib.h>
4 #include <string.h>
5 #include <syslog.h>
6
7 void on_sigabrt(int signum) {
8 fprintf(stderr, "<<< SIGABRT >>>\n");
9 exit(2);
10 }
11
12 int main (int argc, char** argv) {
13 char* format1 = "format1:%n\n"; // 値はリードオンリー
14 char format2[20] = "format2:%n\n"; // 値は書き変え可能
15 int count;
16
17 signal(SIGABRT, on_sigabrt);
18
19 printf(format1, &count);
20 printf("count=%d\n", count);
21
22 syslog(LOG_USER, format2, &count);
23 syslog(LOG_USER, "count=%d\n", count);
24
25 return 0;
26 }
[実行例]
$ gcc -O1 -D_FORTIFY_SOURCE=2 bar.c -o bar
$ ./bar
format1:
count=8
*** %n in writable segment detected ***
<<< SIGABRT >>>
$
(4) バッファオーバーフローと共通の対策
バッファオーバーフロー攻撃と共通の、次の対策をフォーマット文字列攻撃対策に用いることができる。(ただし、どの対策も100%完璧ではあり得ないことにご注意いただきたい。)
- 厳密な入力検査
-
侵害コードを外部から入力されないために予定された入力値以外は受け入れないように厳密な入力検査を行う。
- スタックセグメントの位置のランダム化
-
プラットフォームがもつ、アドレス空間レイアウトのランダム化機能(ASLR)を利用できる場合、固定のアドレスの書き変えや固定のアドレスへのジャンプを狙う攻撃を不成功に終わらせることができる。これが利用できるのは、例えば、次のような環境である。
・Exec-Shield(Linuxカーネルパッチ)が適用されているGNU/Linuxシステム
・Windows Vista の ASLR - データ領域におけるコード実行防止
-
プラットフォームがもつ、データ実行防止機能を利用できる場合、送り込まれた侵害コードの実行を未然に防ぐことができる。これが利用できるのは、例えば、次のような環境である。
・Exec-Shield(Linuxカーネルパッチ)が適用されているGNU/Linuxシステム
・Windows XP および Windows Vista
・OpenBSD の W^X 機能
まとめ
フォーマット文字列攻撃は、バッファオーバーフロー攻撃と同様に、動作中のプログラムのメモリ中に機械語プログラムを送り込んで実行させる攻撃である。注意すべきは、領域あふれを起こさずに攻撃が成功してしまうことである。外部からの入力を printf()系および syslog()系関数の書式引数に与えないようにすると共に、他の対策も組み合わせて対処する。