web-dev-qa-db-ja.com

CおよびC ++で書かれたプログラムがオーバーフロー攻撃に対して脆弱であることが多いのはなぜですか?

実装に関連する過去数年のエクスプロイトを見ると、それらの多くはCまたはC++からのものであり、それらの多くはオーバーフロー攻撃であることがわかります。

  • HeartbleedはOpenSSLのバッファオーバーフローでした。
  • 最近、glibcにバグが見つかり、DNS解決中にバッファオーバーフローが発生する可能性がありました。

それは私が今考えることができるものだけですが、これらがA)CまたはC++で記述されたソフトウェア用であり、B)バッファオーバーフローに基づいている唯一のものであるとは思えません。

特にglibcのバグに関しては、これがCではなくJavaScriptで発生した場合、問題はなかったというコメントを読みました。コードがcompiledからJavascriptへの変換であっても、問題にはなりませんでした。

CとC++はなぜオーバーフロー攻撃に対して脆弱なのですか?

133
Nzall

CおよびC++は、他のほとんどの言語とは異なり、伝統的にオーバーフローをチェックしません。ソースコードが85バイトのバッファに120バイトを置くように言っている場合、CPUは喜んでそうします。これは、CおよびC++にはarrayの概念がありますが、この概念はコンパイル時のみであるという事実に関連しています。実行時にはポインタのみが存在するため、その配列の概念的な長さに関して配列アクセスをチェックするランタイムメソッドはありません。

対照的に、他のほとんどの言語には、実行時に存続する配列の概念があるため、すべての配列アクセスを実行時システムによって体系的にチェックできます。これはオーバーフローを排除しません。ソースコードが長さ85の配列に120バイトを書き込むなど、意味のない何かを要求する場合でも、意味がありません。ただし、これにより、通常の実行が中断されてコードが続行されない内部エラー条件(JavaのArrayIndexOutOfBoundExceptionなどの「例外」)が自動的にトリガーされます。これは実行を妨害し、多くの場合、完全な処理の停止を意味します(スレッドが停止する)が、通常は単純なサービス拒否を超えて悪用されることを防ぎます。

基本的に、バッファオーバーフローの悪用には、オーバーフロー(アクセスされたバッファの境界を超えた読み取りまたは書き込み)を行うコードが必要ですandオーバーフローを超えて処理を続行します。 CやC++(およびForthやAssemblyなどの他のいくつかの言語)とは異なり、最近のほとんどの言語では、オーバーフローを実際に発生させず、代わりに攻撃者を撃ちます。セキュリティの観点からは、これははるかに優れています。

172
Thomas Pornin

セキュリティに関する問題は、CおよびC++に頻繁にリンクされています。しかし、どれだけがこれらの言語の固有の弱点によるものであり、どれだけがそれらが単にコンピュータインフラストラクチャのほとんどが書かれているに含まれる言語なのかによるものですか?


Cは、「アセンブラーから1ステップ上に」なることを意図しています。システムから最後のクロックサイクルをスクイーズするために、自分で実装したもの以外に境界チェックはありません。

C++は、Cに比べてさまざまな改善を提供します。コンテナークラス(<vector>および<string>)などのセキュリティに最も関連します。C++ 11以降、スマートポインター。メモリも手動で処理します。ただし、完全に新しい言語ではなくCのevolutionであるため、それでもalsoはCの手動のメモリ管理メカニズムを提供します。したがって、足、C++はあなたをそれから遠ざけるために何もしません。


では、なぜSSL、バインド、OSカーネルなどがまだこれらの言語で記述されているのでしょうか。

これらの言語canはメモリを直接変更するため、特定のタイプの高性能、低レベルのアプリケーション(暗号化、DNSテーブルルックアップ、ハードウェアドライバーなど)またはJava VM、さらに言えば;-))。

したがって、セキュリティ関連のソフトウェアが侵害された場合、CまたはC++で記述されているchanceは高くなります。これは、ほとんどのセキュリティ関連のソフトウェアisがCまたはC++、通常は歴史的および/またはパフォーマンス上の理由から。そして、それがC/C++で書かれている場合、主な攻撃ベクトルはバッファオーバーランです。

別の言語であれば、別の攻撃ベクトルになりますが、セキュリティ違反も同様に発生すると思います。


C/C++ソフトウェアの悪用は、Javaソフトウェアの悪用よりも簡単です。 Windowsシステムを利用するのと同じように、Linuxシステムを利用するよりも簡単です。前者は至る所に存在し、よく理解されています(つまり、よく知られている攻撃ベクトル、それらを見つける方法と利用する方法)。多くの人がlooking報酬/努力の比率が高いエクスプロイトの場合。

これは、後者が本質的に安全であることを意味しません(saf er、おそらくsafeではありません)。つまり、Bad Boysは、利益が少なくて難しいターゲットであるため、まだそれほど多くの時間を費やしていないということです。

58
DevSolar

実際、 "heartbleed"実際にはバッファオーバーフローではありませんでした。物事をより「効率的に」するために、彼らは多くの小さなバッファを1つの大きなバッファに入れました。大きなバッファには、さまざまなクライアントからのデータが含まれていました。このバグでは、本来読み取るべきではないバイトが読み取られましたが、実際にはその大きなバッファーの外のデータは読み取られませんでした。バッファオーバーフローをチェックする言語はこれを妨げませんでした。誰かが邪魔をしたか、そのようなチェックで問題を見つけられなかったからです。

37
gnasher729

最初に、他の人が述べたように、C/C++は、栄光のあるマクロアセンブラとして特徴付けられることがあります。これは、システムレベルのプログラミング用の言語として、「鉄に近い」ことを意味します。

したがって、たとえば、この言語では、実際にはデータパケットの可変長セクションまたはメモリ内の可変長領域の先頭を表す可能性がある場合に、長さ0の配列をプレースホルダーとして宣言できます。ハードウェアと通信します。

残念ながら、これはC/C++が悪意のある人にとって危険であることも意味します。プログラマーが10個の要素の配列を宣言してから要素101に書き込む場合、コンパイラーはそれを喜んでコンパイルし、コードは喜んで実行され、そのメモリー位置にあるもの(コード、データ、スタック、知っている人)を破棄します。

次に、C/C++は特異です。良い例は、基本的に文字配列である文字列です。ただし、各文字列定数には、追加の不可視の終了文字が含まれています。これは無数のエラーの原因でした。(特に、しかし排他的ではありません)初心者プログラマーは、終端のnullに必要な追加のバイトの割り当てに失敗することが多いためです。

第三に、C/C++は実際にはかなり古いものです。この言語は、ソフトウェアシステムへの外部からの攻撃が基本的に存在しなかったときに生まれました。ユーザーの目標はプログラムを機能させることであり、プログラムをクラッシュさせることではなかったため、敵対的ではなく信頼され協力的であることが期待されていました。

標準C/C++ライブラリに本質的に安全ではない多くの関数が含まれているのはこのためです。たとえば、strcpy()を取ります。それは終端のヌル文字まで何でも喜んでコピーします。終端のnull文字が見つからない場合、重要なものを上書きしてプログラムがクラッシュするまで、地獄がフリーズするまで、または可能性が高いまでコピーを続けます。これは古き良き時代の問題ではありませんでしたが、ユーザーは、たとえば郵便番号、16000文字のガベージ文字に続いて、特別に構築された一連のバイトを実行するために予約されたフィールドに入力することを想定していませんでした。スタックが破棄され、プロセッサが間違ったアドレスで実行を再開した後。

確かに、C/C++は唯一の特異な言語ではありません。他のシステムは異なる特異な振る舞いをしますが、それは同様に悪い場合があります。 PHPのようなバックエンドのプログラミング言語を取り上げ、SQLインジェクションを可能にするコードを簡単に作成できるようにします。

結局、プログラマが仕事をするのに必要な強力なツールを提供したとしても、適切なトレーニングとセキュリティ環境の認識がなければ、どのプログラミング言語を使用しても、悪いことが起こります。

25
Viktor Toth

私はおそらく他の回答のいくつかがすでに述べたいくつかのことに触れますが、質問自体は誤りであり、「脆弱」です。

尋ねられたように、問題は根本的な問題を理解せずに多くを想定しています。 C/C++は他の言語よりも「脆弱性が高い」わけではありません。むしろ、それらは、コンピューティングデバイスの能力とその能力を使用する責任をプログラマーの手に直接委ねます。したがって、状況の現実は、多くのプログラマーが悪用に対して脆弱なコードを記述していることです。C/ C++は、一部の言語のようにプログラマーを自分自身から保護するために長続きしないため、コードはより脆弱です。これはC/C++の問題ではありません。たとえば、アセンブリ言語で記述されたプログラムでも同じ問題が発生するためです。

このような低レベルのプログラミングcanが非常に脆弱である理由は、配列/バッファの境界チェックのようなことを行うと、計算コストが高くなる可能性があるためです。たとえば、主要な検索エンジンのコードを書いているとします。これは、数兆ものデータベースレコードを瞬時に処理する必要があるため、「ページの読み込み中...」の間にエンドユーザーが退屈したりイライラしたりすることはありません。表示されています。あなたのコードがループを通して毎回配列/バッファの境界をチェックし続けるのは望ましくありません。そのようなチェックを行うのにナノ秒かかる場合がありますが、これは10件のレコードを処理しているだけの場合は簡単ですが、何十億、何兆ものレコードをループする場合、合計で数秒または数分かかる可能性があります。

そのため、代わりに、データソース(たとえば、Webサイトをスキャンしてデータベースにデータを配置する「Webボット」)がすでにデータをチェックしていることを「信頼」します。これは不当な仮定であってはなりません。典型的なプログラムでは、データをinputでチェックする必要があるため、processesというコードでデータを最大速度で処理できます。多くのコードライブラリもこのアプローチを採用しています。プログラマーがライブラリー関数を呼び出してデータを操作する前に、データをチェック済みであることを期待していることを文書化する人もいます。

ただし、残念ながら、多くのプログラマーは防御的なプログラミングを行わず、データが有効で安全な境界/パラメーター内にある必要があると想定しています。そして、これは攻撃者によって悪用されるものです。

一部のプログラミング言語は、生成されたプログラムに追加のチェックを自動的に挿入して、プログラマがコードに明示的に書き込みを行わないようにすることで、このような不適切なプログラミングからプログラマを保護するように設計されています。この場合も、コードを数百回以下ループするだけの場合は問題ありません。しかし、数十億回または数兆回の反復を実行している場合、データ処理に長い遅延が生じ、許容できない可能性があります。したがって、特定のコードに使用する言語を選択する場合、およびデータ内の潜在的に危険な/悪用可能な条件を確認する頻度と場所を選択する場合は、トレードオフになります。

4
C. M.

基本的にプログラマーは怠惰な人々(私も含む)です。彼らはfgets()の代わりにgets()を使用し、スタックでI/Oバッファーを定義し、メモリーが意図せずに上書きされる可能性を十分に調べていません(プログラマーにとっては意図せず、ハッカーにとっては意図的に)。

2
Bing Bang

バッファへのチェックされていない書き込みを行う既存のCコードが大量にあります。これのいくつかはライブラリにあります。このコードは、外部状態によって書き込まれた長さが変更される可能性がある場合は悪用可能であり、そうでない場合は非常に危険です。

バッファへの制限付き書き込みを行う既存のCコードが大量にあります。上記のコードのユーザーが数学エラーを実行し、必要以上に書き込む場合、これは上記と同様に悪用可能です。計算が正しく行われるというコンパイル時の保証はありません。

また、メモリ内のオフセットに基づいて読み取りを行う既存のCコードが大量にあります。オフセットが有効であると確認されていないと、情報が漏洩する可能性があります。

C++コードはCとの相互運用のための高水準言語としてよく使用されるため、Cの多くの概念が守られており、C APIとの通信によるバグが一般的です。

このようなオーバーランを防止するC++プログラミングスタイルは存在しますが、それらが発生するのを許すのはたった1つの間違いです。

さらに、メモリリソースが再利用され、ポインタが元のメモリとは異なる有効期間/構造を持つメモリをポイントするダングリングポインタの問題により、ある種の悪用や情報漏えいが発生する可能性があります。

これらの種類のエラー(「フェンスポスト」エラー、「ダングリングポインター」エラー)は非常に一般的であり、完全に排除するのが非常に難しいため、システムで開発された多くの言語は、明示的にが発生しないように設計されています。

当然のことながら、これらのエラーを排除するように設計された言語では、これらのエラーはほとんど発生しません。それらはまだ発生します:言語を実行するエンジンに問題があるか、C/C++ケースの環境に一致する手動の状況が設定されています(プールでオブジェクトを再利用する、コンシューマーによって細分化された共通の大きな共通バッファーを使用する、など) )。しかし、これらの使用法はまれであるため、問題が発生する頻度は低くなります。

C/C++でのすべての動的割り当て、すべてのバッファーの使用は、これらのリスクを伴います。そして、完璧であることは達成できません。

1
Yakk

最も一般的に使用される言語(JavaやRubyなど)は、VMで実行されるコードにコンパイルされます。 VMは、マシンコード、データ、および通常はスタックを分離するように設計されています。これは、通常の言語操作では、コードを変更したり、制御のフローをリダイレクトしたりできないことを意味します(実行できる特別なAPIがある場合があります)これ、たとえばデバッグ用)。

CおよびC++は通常、CPUのネイティブマシン言語に直接コンパイルされます。これにより、パフォーマンスと柔軟性の利点が得られますが、誤ったコードがプログラムメモリまたはスタックを上書きし、元のプログラムにない命令を実行する可能性があります。

これは通常、C++でバッファーが(意図的に)オーバーランしたときに発生します。対照的に、JavaまたはRubyでは、バッファオーバーランはすぐに例外を発生させ、(VMバグを除いて)コードを上書きしたり制御フローを変更したりすることはできません。

0
Rich