第10章 著名な脆弱性対策
フォーマット文字列攻撃対策

フォーマット文字列攻撃は、領域をあふれさせることなくバッファオーバーフロー攻撃と同様の被害を及ぼすことのできる攻撃手口である。攻撃文字列のしくみと対策を解説する。

フォーマット文字列攻撃

フォーマット文字列攻撃は、printf() や syslog() 等のライブラリ関数がもつ書式編集機能を悪用し、実行中のプログラムのメモリに悪意の機械語コードを送り込んで実行させる攻撃である。

フォーマット文字列攻撃は、printf() 等の関数の書式引数に外部からの入力データがそのまま渡るようになっている場合に起こり得る。例えば、次の message の値が外部から与えられるようなケースである。

char* message;
printf(message);

攻撃が成功すれば最悪の場合、コンピュータの制御を奪われる。そうでなくてもプロセスが異常終了する等の障害が起こり得る。

攻撃は、例えば、次のような具合に進行する。

  1. 外部から与えられた悪意の書式文字列が printf() 関数によって解釈実行される
  2. %n 書式によって関数のリターンアドレスが書き変わる
  3. 関数からのリターン時、侵害コードへのジャンプが起こる
フォーマット文字列攻撃
図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);

これを利用して、悪意の書式文字列は、例えば、次の要素から構成される。

これらにはそれぞれ次のような値が含まれる。

フォーマット文字列攻撃対策

フォーマット文字列攻撃の対策には次を行う。

(1) 外部入力値の不使用

printf()、syslog()等の書式引数には、外部から入力したデータを用いない。やむを得ず外部から書式文字列を受け入れる必要のある場合は、許される文字列パターンを明確に定め、そのパターンに合致していることを確認してから使用する。その際決して %n 書式の使用を許すべきではない。

書式引数を与える際に注意が必要なprintf系のライブラリ関数のうち C11仕様が対策したものには次のものがある。

書式引数を与える際に C11仕様においてもなお注意が必要なprintf系ならびにsyslog系のライブラリ関数には次のものがある。

(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%完璧ではあり得ないことにご注意いただきたい。)

まとめ

フォーマット文字列攻撃は、バッファオーバーフロー攻撃と同様に、動作中のプログラムのメモリ中に機械語プログラムを送り込んで実行させる攻撃である。注意すべきは、領域あふれを起こさずに攻撃が成功してしまうことである。外部からの入力を printf()系および syslog()系関数の書式引数に与えないようにすると共に、他の対策も組み合わせて対処する。