Skip to main content

C/C++ の脆弱性のダークアーツの怖くない入門書

著者:
Aviad Hahami
Aviad Hahami
wordpress-sync/feature-c-vulnerabilities-orange

2022年4月15日

0 分で読めます

ルーモス、光よ。

Synk がアンマネージド依存ライブラリ (主に C/C++ ライブラリ) のサポートを発表したため、C 環境に潜む一般的なリスクの高い危険について、C 以外のコミュニティに知らせることが有益だと考えました (理解できましたか?)。C や C++ の脆弱性がどのようなもので、どのような問題を引き起こし、どのように修正するかについての「初心者向けガイド」と考えることができます。

重大な脆弱性とその発見場所

C および C++ (この記事の残りの部分では、両方まとめて「C」と呼びます) は、低レベルのプログラミング言語と見なされています。低レベルの言語と高レベルの言語 (JS、Python など) を比較すると、マシンのメモリ管理プロセスに主な違いがあることがわかります。高レベルの言語ではメモリの割り当て、消費、および解放が管理されるのに対し、C ではメモリ管理の責任が開発者に委ねられています。これにより、開発者はルーチンのパフォーマンスと実装の最適化を正確に行えますが、この言語領域に固有のさまざまな問題が発生する可能性もあります。

バッファオーバーフロー (CWE-121) および境界外書き込み (CWE-787)

バッファオーバーフローは、おそらくメモリ関連の脆弱性の中で最も悪名高いものです。バッファオーバーフローの悪用は複雑になる可能性がありますが、脆弱性自体は単純です。つまり、割り当てられたバッファをオーバーフローさせるだけです。定義上、「バッファオーバーフロー」は通常、脆弱性の実際の攻撃を指して用いられますが、「スタックベースのバッファオーバーフロー」と「境界外書き込み」は本質的に同じ弱点を指しています (したがって、これらをまとめて説明します)。

オーバーフローしてもスタック (またはヒープ) に常に「コードを書き込む」ことができるとは限らないため、攻撃の行為は難しいかもしれません。 時間の経過とともに、この問題を強化したり、軽減したりする試みが行われ、防御メカニズムはほとんど導入されていません。これらの防御メカニズムは多くの要因に依存し、コードを防御する際に OS、カーネル機能、コンパイラなどを考慮する場合があります。いくつか例を挙げると、ASLR (アドレス空間レイアウトのランダム化)、スタックカナリア、DEP (データ実行防止) などのメカニズムがあります。これらはすべて、バッファオーバーフローなど、メモリ破損バグの防止を目的としています。ランタイムでこれらのメカニズムのいずれかが失敗すると、OS が実行を停止して SEGFAULT をスローし、攻撃プロセス全体が複雑化します。

そのような脆弱性と攻撃の一例をご覧ください。以下の C プログラムについて考えてみましょう。

1#include <stdlib.h>
2#include <unistd.h>
3#include <stdio.h>
4
5int main(int argc, char **argv)
6  volatile int modified;
7  char buffer[64];
8
9  modified = 0;
10  gets(buffer);
11
12  if(modified != 0) {
13    printf("you have changed the 'modified' variable\n");
14  } else {
15    printf("Try again?\n");
16  }
17}

上記の例は 0xRick氏のブログ記事に掲載されているものです。

コードを見ると (C 言語の知識がなくても)、buffer が 64 文字の長さのバッファとして割り当てられ、modified は整数であることがわかります。いいですね。

また、9 行目の modified に 0 の値が設定されていることを確認し、12 行目でその値が 0 かどうかを検証しています。ご存知だと思いますが、ここでの目標は 0 にしないことです。10 行目では、gets 関数を使用して、stdin (=ユーザー入力) から buffer 変数に読み取ります。gets をよく知らない人のために説明すると、stdin から指定されたバッファに改行文字 (\\n) が読み込まれるまで読み込むというものです。

YouTube の動画をご覧になっておわかりいただけたように、スタックが下位アドレスに成長し、スタックデータ構造により、modified データはメモリ内のbuffer の「下位」に存在するため (64 文字を超えて buffer に書き込むと) modified の値のオーバーライドが開始されます。そして、それが不思議なバッファオーバーフローという現象です。

この具体例は、(デモなので) あまり害はありません。ただし、パスワード変数やマシンがターゲットとする URL の値を上書きしたらどうなるか、想像してみてください。対策はシンプルです。fgets 関数の使用が推奨されており、これにより「シーケンス終了文字」の存在だけでなく、入力の長さもチェックされます。

解放済みメモリ使用 (CWE-416)

解放済みメモリ使用の脆弱性は、適切に自己記述されています。変数の参照を開放された後に使用すると発生します。この脆弱性は、変数がクリアされたに使用された場合に、ソフトウェアフローに関するメモリ管理エラーが原因で発生します。これにより、予期しないアクションが実行されたり、アプリケーションに予期しない影響が残ったりします。

この脆弱性と攻撃の様子を実際に見るには、LiveOverflow の動画がお勧めです。UAF の脆弱性の攻撃にチャレンジした様子が撮影されています。

整数オーバーフロー/アンダーフロー (CWE-190 & CW-191)

整数のオーバーフローとアンダーフローは、コンピューターの数値表現が原因で発生する 2 種類の類似のバグ (および後の脆弱性) です。

ここでは、変数タイプやコンピューターで数字がどのように表現されているかについては触れませんが、数字の表現方法には大きく分けて符号付き符号なしの 2 種類があることに触れておきます。符号付き変数が負の数と正の数を表現できるのに対し、符号なし変数には正の数のみが保存されます。

整数オーバーフローは、マシンに保存を要求した値が、保存可能な最大値より大きいことを意味します。整数アンダーフローは、マシンに保存を要求した値が、最小値より小さいことを意味します (たとえば、符号なし整数で負の数を保存するように要求した場合)。

どちらの場合も、結果は同様です。値は「ラップアラウンド」され (つまり、保存可能な範囲の最初または最後から始まる)、値が変更されます。オーバーフローの場合はラップアラウンド値は 0 から始まり、アンダーフローの場合は保存可能な最大値から始まります (つまり、8 ビット符号なし整数は 256 \[10 進数] までラップアラウンドされます)。

この図は変数タイプと変数が保持できる値を示しています。

wordpress-sync/blog-cworld-variabletypes

バッファオーバーフローを引き起こす整数オーバーフローの攻撃の例は、OpenSSH v3.3 で発見されています (CVE-2002-0639)。

以下のようなスニペットについて考えてみましょう。

1nresp = packet_get_int();
2if (nresp > 0) {
3 response = xmalloc(nresp*sizeof(char*));
4 for (i = 0; i < nresp; i++)
5  response[i] = packet_get_string(NULL);
6}

nresp が 1073741824sizeof(char\*) が 4 (一般的なポインターサイズ) だとすると、nresp*sizeof(char*) はオーバーフローを引き起こします (ラップアラウンドして値が 0 になるため)。したがって、xmalloc() は 0 バイトのバッファを受け取って割り当てます。次のループは、未割り当てのメモリ位置に書き込むときにヒープバッファオーバーフローを引き起こします。これは、ハッカーが何らかのコードを実行するために悪用する可能性があります。

NULL ポインターデリファレンス (CWE-467)

デリファレンスとは、アドレスの値に対してアクションを実行することです。 この脆弱性についてわかりやすく説明するため、例を見てみましょう。

1#include <stddef.h>
2
3void main(){
4        int *x = NULL;
5        *x = 1;
6}

C 標準によると、上記のコードを実行すると「未定義の動作」が発生する可能性があります。ただし、ほとんどの実装では SEGFAULT でパニックが発生します。これは、ソフトウェアがメモリ制限領域にアクセスしようとしたことを意味します (そのため、メモリアクセス違反が発生します)。これにより、ソフトウェアがオペレーティングシステムによって終了されます。

境界外読み取り (CWE-125)

「指定された場所/バッファの外部を読み取った」場合、境界外読み取りが発生します。このような脆弱性の結果として、最善の場合でもシステムクラッシュが発生し、アプリ内の情報 (つまり、他ユーザーのパスワード) の漏洩が発生する可能性がありますが、望ましくありません。

このような脆弱性の例として、PureFTPd アプリケーションのスニペットを以下に示します。17 行目を見てみましょう。s1 の長さが s2 の長さより大きい場合、8 行目は s1 - の長さだけ繰り返されるため、10 行目でアクセスする情報は s2 の境界を超えてしまいます。これにより、境界外読み取りが発生します。

1int pure_memcmp(const void *const b1_, const void *const b2_, size_t len)
2  {
3   const unsigned char *b1 = (const unsigned char *) b1_;
4   const unsigned char *b2 = (const unsigned char *) b2_;
5   size_t i;
6   unsigned char d = (unsigned char) 0 U;
7   for (i = 0 U; i < len; i++)
8   {
9     d |= b1[i] ^ b2[i];
10   }
11   return (int)((1 &((d - 1) >> 8)) - 1);
12}
13
14int pure_strcmp(const char *const s1, const char *const s2)
15{
16  return pure_memcmp(s1, s2, strlen(s1) + 1 U);
17}

このバグは CVE-2020-9365 を受け取ります。こちらからレポートを読むことができます。

結論と次のステップ

ここまでで、C/C++ の脆弱性がどのようなもので、通常どこに存在し、どのような形態で存在するかについて、(大まかに) ご理解いただけたかと思います。これらの攻撃のいくつかは、最初は複雑に思えるかもしれませんが、それらを深く理解することで、ソフトウェア内部の構造に関する全体的な理解が深まり、重大なバグを回避したり、防止したりするのに役立つことがあります。

上記の整数オーバーフローの説明に示されているように、このような脆弱性は互いに連鎖することがあり、脆弱な連鎖により、悪意のある攻撃にさらされる可能性が高まります。

これまで Snyk Open Sourceを使用する C/C++ について説明してきました。ここからは、C および C++ の脆弱性を見つけ、攻撃し、修正する方法について説明するトピックの追加のコンテンツを共有します。