界: Input Validation and Representation

入力の検証や表現の問題は、メタキャラクター、代替エンコーディング、数値表現などによって引き起こされます。セキュリティの問題は、入力を信頼することに起因します。この問題に含まれるのは、「Buffer Overflow」、「Cross-Site Scripting」攻撃、「SQL Injection」などです。

Buffer Overflow

Abstract
割り当てられたメモリブロックの境界外に書き込まれると、データが破損したりプログラムがクラッシュする可能性があり、悪意あるコードが実行される原因になることもあります。
Explanation
Buffer Overflow は、ソフトウェアセキュリティの脆弱性の中で最も有名な形態でしょう。Buffer Overflow の脆弱性については大半のソフトウェア開発者に知られているにも関わらず、依然として Buffer Overflow は新旧を問わずアプリケーションに対して最も多く見られる攻撃です。これは、Buffer Overflow には多種多様の発生形態があることや、この攻撃を阻止するために使用される手法が誤りやすいものであることによるものです。

古典的な Buffer Overflow の悪用では、攻撃者がプログラムに送信したデータが、それよりも小さいサイズのスタックバッファに格納されます。その結果、コールスタックにある情報、特に関数の戻りポインタが上書きされます。このデータがセットした戻りポインタの値に関数が戻ると、攻撃者のデータに含まれる悪意あるコードに制御が移ります。

このタイプのスタック Buffer Overflow は一部のプラットフォームや開発コミュニティでは今でも一般的ですが、ほかにもヒープ Buffer Overflow や「一つ違い」エラーなどのさまざまな Buffer Overflow があります。Buffer Overflow 攻撃の仕組みを解説した書籍には、Building Secure Software [1]、Writing Secure Code [2]、The Shellcoder's Handbook [3] など優れた本が数多くあります。

コードのレベルでは、Buffer Overflow 脆弱性には通常、プログラマの想定外のことが含まれます。C および C++ のメモリ操作関数の多くは境界チェックを行わないため、動作しているバッファに対して割り当てられた境界を簡単に上書きすることができます。strncpy() など境界が定められた関数の場合も、不正に使用されると脆弱性の原因になります。メモリの操作と、データのサイズや構成に関する誤った想定が同時に発生することが、大部分の Buffer Overflow の根本的な原因です。

Buffer Overflow の脆弱性は通常、以下のようなコードで発生します。

- 外部データがコードの動作を制御することを許容している。

- コードの直接のスコープの外部で強制されているデータのプロパティに依存している。

- コードが複雑すぎてプログラマにも動作が正確に予測できない。



これら 3 つのシナリオを以下の例で説明します。

例 1.a: 次のサンプルコードは単純な Buffer Overflow の例です。多くの場合、コードの動作制御を外部データに任せるという 1 つ目のシナリオによって引き起こされます。このコードは gets() 関数を使用して、任意の量のデータをスタックバッファに読み込みます。この関数により読み込まれるデータ量を制限する方法がないので、このコードが安全かどうかは、ユーザーが常に BUFSIZE 字以下しか入力しないということにかかっています。


...
char buf[BUFSIZE];
gets(buf);
...
例 1.b: この例は、>> 演算子を使用して入力を char[] 文字列に読み込むことによって C++ の gets() 関数の危険な動作を簡単に模倣できることを示しています。


...
char buf[BUFSIZE];
cin >> (buf);
...
例 2: これもコードの動作の制御がユーザー入力にかかっているコードの例ですが、境界のあるメモリコピー関数 memcpy() の使用により、さらに間接的になっています。この関数は、コピー先バッファ、コピー元バッファ、およびコピーするバイト数を受け取ります。入力バッファには read() への境界のあるコールが入りますが、memcpy() がコピーするバイト数はユーザーが指定します。


...
char buf[64], in[MAX_SIZE];
printf("Enter buffer contents:\n");
read(0, in, MAX_SIZE-1);
printf("Bytes to copy:\n");
scanf("%d", &bytes);
memcpy(buf, in, bytes);
...


注:このタイプの Buffer Overflow 脆弱性 (プログラムはデータを読み取った後、残りのデータに対する以降のメモリ操作でデータから送られる値を信頼する) は、一定の頻度で画像やオーディオなどのファイルを処理するライブラリに発生します。

例 3: この 2 つ目のシナリオの例では、ローカルで検証されていないデータのプロパティにコードが依存しています。この例では、lccopy() という名の関数で文字列をその引数として取り、その文字列の大文字をすべて小文字に変換した上で、文字列のヒープに割り当てられたコピーを返します。この関数では、str は常に BUFSIZE より小さいと想定されているため、入力に対して境界チェックが行われません。lccopy() をコールするコードで攻撃者がチェックを回避する場合や、コードが変更された結果 str のサイズに関する想定が正しくなくなった場合、lccopy()strcpy() への境界のないコールで buf を オーバーフローさせます。


char *lccopy(const char *str) {
char buf[BUFSIZE];
char *p;

strcpy(buf, str);
for (p = buf; *p; p++) {
if (isupper(*p)) {
*p = tolower(*p);
}
}
return strdup(buf);
}
例 4: 次のコードは、コードが非常に複雑なので動作を簡単に予測できない 3 番目のシナリオを示しています。このコードは、さまざまなアプリケーションで使用されている、人気の libPNG 画像デコーダーからのものです。

このコードは変数の長さをチェックしているので、安全に境界チェックを実行しているように見えます (この変数長は、後で png_crc_read(). によってコピーされたデータ量を制御するために使用されます)。しかし、長さをテストする直前で、このコードは png_ptr->mode のチェックを実行しており、このチェックが失敗した場合は警告を出して処理を継続します。lengthelse if ブロックでテストされているので、1 回目のチェックが失敗すると length はテストされません。このため png_crc_read() に対するコールで無条件に使用されることになり、スタック Buffer Overflow が発生する可能性があります。

この例のコードは最も複雑な部類に属するものではありませんが、メモリ操作を実行するコードの複雑性を最小限に抑えなければならない理由がよくわかります。


if (!(png_ptr->mode & PNG_HAVE_PLTE)) {
/* Should be an error, but we can cope with it */
png_warning(png_ptr, "Missing PLTE before tRNS");
}
else if (length > (png_uint_32)png_ptr->num_palette) {
png_warning(png_ptr, "Incorrect tRNS chunk length");
png_crc_finish(png_ptr, length);
return;
}
...
png_crc_read(png_ptr, readbuf, (png_size_t)length);
例 5: これも第 3 のシナリオの例で、プログラムが複雑なために Buffer Overflow の危険が生じています。この場合、危険の原因は、前の例のようにコードの構造にあるのではなく、いずれかの関数のインターフェイスが多義的であることにあります。

getUserInfo() 関数は、マルチバイト文字列として指定されたユーザー名およびユーザー情報を格納する構造体へのポインタを受け取り、ユーザーに関する情報を構造体に代入します。Windows Authentication ではユーザー名に Unicode が使用されるため、username 引数はまずマルチバイト文字列から Unicode 文字列に変換されます。次に、この関数は unicodeUser のサイズを、文字数ではなく、誤ってバイト単位で渡しています。このため、MultiByteToWideChar() のコールは最大で (UNLEN+1)*sizeof(WCHAR) のワイド文字、
つまり (UNLEN+1)*sizeof(WCHAR)*sizeof(WCHAR) バイトを unicodeUser 配列に書き込む可能性があります。これに対し、この配列には (UNLEN+1)*sizeof(WCHAR) バイトしか割り当てられていません。username 文字列に含まれる文字数が UNLEN 字を超えている場合、MultiByteToWideChar() のコールにより、バッファ unicodeUser はオーバーフローします。


void getUserInfo(char *username, struct _USER_INFO_2 info){
WCHAR unicodeUser[UNLEN+1];
MultiByteToWideChar(CP_ACP, 0, username, -1,
unicodeUser, sizeof(unicodeUser));
NetUserGetInfo(NULL, unicodeUser, 2, (LPBYTE *)&info);
}
References
[1] J. Viega, G. McGraw Building Secure Software Addison-Wesley
[2] M. Howard, D. LeBlanc Writing Secure Code, Second Edition Microsoft Press
[3] J. Koziol et al. The Shellcoder's Handbook: Discovering and Exploiting Security Holes John Wiley & Sons
[4] About Strsafe.h Microsoft
desc.dataflow.cpp.buffer_overflow