Cがバッファオーバーフローを起こしにくくするのはなぜ難しいのですか?


23

私は大学でコースを行っています。ラボの1つでは、提供されたコードに対してバッファオーバーフローエクスプロイトを実行しています。これは、スタック上の関数の戻りアドレスを変更して別の関数に戻るなどの単純な悪用から、プログラムのレジスタ/メモリ状態を変更してから呼び出した関数に戻るコードまでの範囲です。あなたが呼び出した関数は、エクスプロイトを完全に忘れています。

私はこれについていくつか調査を行いましたが、この種のエクスプロイトは、Wiihomebrewを実行したり、iOS 4.3.1のテザーなしのジェイルブレイクを実行したりするなど、今でもどこでも使用されています。

私の質問は、なぜこの問題を修正するのがとても難しいのですか?これが何百ものものをハッキングするために使用される主要なエクスプロイトの1つであることは明らかですが、許可された長さを超えて入力を切り捨て、取得したすべての入力を単純にサニタイズするだけで修正するのは非常に簡単だと思われます。

編集:私は考慮すべき答えが欲しい別の視点-なぜCの作成者はライブラリを再実装することでこれらの問題を修正しないのですか?

回答:


35

彼らはライブラリを修正しました。

現代の任意のC標準ライブラリは、より安全な亜種が含まれstrcpystrcatsprintf、などを。

最ものUnixである- - C99システムではあなたのような名前でこれらを見つけるstrncatsnprintf、「n」は、それがバッファのサイズやコピーへの要素の最大数です引数を取ることを示しています。

これらの関数は、多くの操作をより安全に処理するために使用できますが、振り返ってみると、それらの操作性は大きくありません。たとえば、一部のsnprintf実装では、バッファーがnullで終了することを保証しません。strncatコピーするためにいくつかの要素を取りますが、多くの人が誤ってdestバッファのサイズを渡します。

Windowsでは、1はしばしば見つけstrcat_ssprintf_s「安全」を示す、「_s」サフィックスを。これらもC11のC標準ライブラリに組み込まれ、オーバーフローが発生した場合に発生する処理(切り捨てとアサートなど)をより細かく制御できます。

多くのベンダーasprintfは、GNU libcのようにさらに適切なサイズのバッファーを自動的に割り当てる非標準の代替手段を提供しています。

「ただCを修正する」ことができるという考えは誤解です。Cを修正することは問題ではなく、すでに行われています。問題は、無知な、疲れた、または急いでいるプログラマーによって書かれた何十年ものCコード、またはセキュリティが重要でないコンテキストからセキュリティが重要なコンテキストに移植されたコードを修正することです。標準のライブラリを変更してもこのコードを修正することはできませんが、新しいコンパイラや標準のライブラリに移行すると、問題を自動的に特定できる場合があります。


11
+1は、言語ではなく、プログラマーの問題を狙ったものです。
ニコルボーラス

8
@Nicol:「問題はプログラマーにある」と言うのは不当な還元主義者です。問題は、何年も(何十年も)Cが安全なコードよりも安全でないコードを書くことを容易にしていたことです。特に「安全」の定義はどの言語標準よりも速く進化し、そのコードはまだ存在しています。これを単一の名詞に減らしたい場合、問題は「プログラマー」ではなく「1970-1999 libc」です。

1
これらの問題を解決するために現在持っているツールを使用することは、プログラマーの責任です。半日かそこらを取り、これらのことのためにソースコードを通していくつかのgreppingを行います。
ニコルボーラス

1
@Nicol:潜在的なバッファオーバーフローを検出するのは簡単ですが、実際の脅威であることを確認するのは簡単ではないことが多く、バッファがオーバーフローした場合にどうなるかを判断するのはそれほど簡単ではありません。エラー処理は考慮されていない場合が多く、予期しない方法でモジュールの動作を変更できるため、「迅速に」改善を実装することはできません。私たちはこれを数百万行のレガシーコードベースで実行しましたが、実行する価値はありますが、多くの時間(およびお金)がかかりました。
マッテンツ

4
@NicolBolas:どんな種類の店働いているのかわからないが、本番用にCを書いた最後の場所では、詳細な設計ドキュメントの修正、レビュー、コードの変更、テスト計画の修正、テスト計画のレビュー、完全な実行が必要でしたシステムテスト、テスト結果の確認、顧客のサイトでのシステムの再認証。これは、もう存在しない会社のために書かれた、異なる大陸のテレコムシステム用です。前回私は、ソースがQICテープ上のRCSアーカイブにあった、知っていなければならないあなたは、適切なテープドライブを見つけることができれば、まだ、読み取り可能。
TMN

19

Cが設計上「エラーを起こしやすい」と言っても、実際には不正確ではありません。のようないくつかの重大な間違いは別getsとして、C言語は、そもそも人々をCに引き寄せる主要な機能を失うことなく、他の方法にはなり得ません。

Cは、一種の「ポータブルアセンブリ」として機能するシステム言語として設計されました。C言語の主要な機能は、高レベル言語とは異なり、Cコードが実際のマシンコードに非常に密接にマッピングされることが多いことです。言い換えれば、++i通常は単なるinc命令であり、多くの場合、Cコードを調べることで、実行時にプロセッサが何を行うかについての一般的なアイデアを得ることができます。

しかし、暗黙的な境界チェックを追加すると、余分なオーバーヘッドが多く追加されます。オーバーヘッドはプログラマが要求しなかったものであり、望ましくない場合があります。このオーバーヘッドは、各アレイの長さを保存するために必要な追加のストレージ、またはすべてのアレイアクセスでアレイの境界をチェックするための追加の命令をはるかに超えています。ポインター演算はどうですか?または、ポインターを受け取る関数がある場合はどうなりますか?ランタイム環境には、そのポインターが正当に割り当てられたメモリブロックの境界内にあるかどうかを知る方法がありません。これを追跡するには、現在割り当てられているメモリブロックのテーブルに対して各ポインターをチェックできる本格的なランタイムアーキテクチャが必要です。この時点で、すでにJava / C#スタイルの管理されたランタイム領域に到達しています。


12
正直なところ、なぜCが「安全」ではないのかと人々が尋ねると、アセンブリが「安全」ではないと文句を言うのではないかと思うようになります。
ベンブロッカ

5
C言語は、Digital Equipment Corporation PDP-11マシンのポータブルアセンブリによく似ています。彼らは右にプログラムを取得するには本当に簡単だったので、同時にバローズのマシンは、CPUにチェック配列の境界を持っていたアレイのチェックをハードウェアの生活の中で(主に航空機に使用される。)ロックウェル・コリンズのハードウェアでは上。
ティムWilliscroft

15

私は本当の問題は、これらの種類のバグを修正するのが難しいということではなく、それらを作るのがとても簡単だと思います:を使用する場合strcpysprintfそして友人が(一見)最も簡単な方法で動作するなら、あなたはおそらくバッファオーバーフローのドアを開けました。そして、誰かがそれを悪用するまで誰も気付かないでしょう(あなたが非常に良いコードレビューを持っている場合を除いて)。ここで、多くの平凡なプログラマーがいて、ほとんどの場合、時間的なプレッシャーにさらされているという事実を追加します。そして、バッファーオーバーフローに悩まされるコードのためのレシピがあり、単にすべてを修正するのは難しいでしょうそれらの多くと彼らはとてもよく隠れています。


3
「非常に良いコードレビュー」は本当に必要ありません。禁止sprintfを必要とするか、sizeof()とポインターのサイズのエラーなどを使用するものにsprintfを再定義する必要があります。コードのレビューも不要で、SCMコミットでこのようなことができますフックとgrep。

1
@JoeWreschnig:sizeof(ptr)一般的に4または8です。これは、Cのもう1つの制限です。配列へのポインターだけでは、配列の長さを決定する方法はありません。
MSalters

@MSalters:はい、int [1]またはchar [4]の配列、または誤検知の可能性のあるものですが、実際にはそれらの関数でそのサイズのバッファーを処理することはありません。(ここでは理論的に話をしていません-このアプローチを使用して4年間大規模なCコードベースで作業しました。char[4]へのスプリントの制限に達することはありませんでした。)

5
@BlackJack:ほとんどのプログラマーはバカではありません-サイズを強制的に渡すと、正しいサイズを渡すことになります。また、強制しない限り、サイズを渡すことはほとんどありません。静的または自動サイズの配列の長さを返すマクロを作成できますが、ポインターを指定するとエラーになります。次に、#define sprintfを再定義して、サイズを指定したマクロでsnprintfを呼び出します。これで、既知のサイズの配列でのみ動作するバージョンのsprintfがあり、プログラマーは手動で指定されたサイズでsnprintfを強制的に呼び出せます。

1
このようなマクロの1つの簡単な例は#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]) / (sizeof(a) != sizeof(void *))、コンパイル時のゼロ除算をトリガーします。私がChromiumで最初に見たもう1つの巧妙な方法は#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]) / !(sizeof(a) % sizeof((a)[0]))、少数の誤検知をいくつかの誤検知と交換することです。blogs.msdn.com/b/ce_base/archive/2007/05/08/…など、さまざまなコンパイラ拡張機能を使用して、さらに信頼性を高めることができます。

7

Cは、問題に対処するための有用なツールを事実上提供しないため、バッファオーバーフローを修正することは困難です。これは、ネイティブのバッファが何の保護を提供しないことを基本的な言語の欠陥だ C ++がでやったようにそれは、優れた製品とそれらを置き換えるために、事実上完全でない、不可能だstd::vectorstd::arrayそしてそれがバッファオーバーフローを見つけるためにも、デバッグモードでは難しいです。


13
「言語の欠陥」は非常に偏った主張です。ライブラリが境界チェックを提供しなかったことは欠陥でした。言語がオーバーヘッドを回避するための意識的な選択ではなかったこと。この選択は、高レベルの構造をstd::vector効率的に実装できるようにするものの一部です。そしてvector::operator[]、安全性よりも速度についても同じ選択をします。安全性はvector、サイズに合わせて簡単にカートに入れることができるようにすることで実現します。これは、最新のCライブラリが採用しているのと同じアプローチです。

1
@Charles:「Cは、標準ライブラリの一部として動的に拡張するバッファを一切提供していません。」いいえ、これはそれとは何の関係もありません。まず、Cはそれらを提供しますrealloc(C99では、自動変数を使用して、ランタイムで決定された一定のサイズを使用してスタック配列のサイズを設定することもできますchar buf[1024]。第二に、問題はバッファーの拡張とは関係ありません。バッファーがサイズを保持しているかどうかに関係し、アクセス時にそのサイズをチェックします。

5
@ジョー:問題はそれほど多くないので、ネイティブ配列が壊れています。交換することは不可能です。最初に、vector::operator[]デバッグモードで境界チェックを行います-ネイティブ配列ではできないこと-そして、Cには、境界チェック行うことができるものとネイティブ配列タイプを交換する方法がありません。テンプレートも演算子もないためです。過負荷。あなたから移動したい場合はC ++では、T[]std::array、あなたは事実上だけのtypedefを交換することができます。Cでは、それを実現する方法はなく、同等の機能を備えたクラスを作成する方法はなく、インターフェイスはありません。
DeadMG

3
@Joe:静的なサイズにすることはできず、汎用にすることはできません。C ++ std::vector<T>と同じ役割をするライブラリをCで記述することは不可能std::array<T, N>です。これを行うことができるライブラリを設計および指定する方法はありません。標準ライブラリでさえもです。
DeadMG

1
「静的なサイズにすることはできません」という意味がわかりません。この用語を使用するstd::vectorように、静的なサイズにすることもできません。ジェネリックに関しては、良いCが必要とするのと同じくらいジェネリックにすることができます-void *(追加、削除、サイズ変更)の少数の基本的な操作と他に特別に書かれたすべて。CにはC ++スタイルのジェネリックがないと文句を言うのであれば、それは安全なバッファ処理の範囲外です。

7

問題はC 言語にはありません。

IMO、克服すべき唯一の主要な障害は、Cがただひどく教えられているということです。何十年もの悪い習慣や間違った情報がリファレンスマニュアルや講義ノートに制度化され、新しい世代のプログラマーの頭を最初から傷つけていました。学生には、gets1scanfなどの「簡単な」I / O機能の簡単な説明が与えられ、その後、自分のデバイスに任されます。それらは、それらのツールが失敗する可能性のある場所や方法、またはそれらの失敗を防ぐ方法を教えられていません。これらは、使用について語られていないfgetsstrtol/strtodこれらは「高度な」ツールと見なされているためです。その後、彼らは彼らの大混乱をもたらすために、プロの世界で解き放たれます。同じ脳損傷を受けた教育を受けたため、経験豊富なプログラマーの多くがこれ以上良く知っているわけではありません。それは気違いです。ここやStack Overflowや他のサイトでは、質問をしている人が自分が話していることを知らないだけの人に教えられていることが明らかになっているので、もちろんたくさんの質問があります。「教授は間違っています」彼は教授であり、あなたはインターネット上の単なる男だからです。

そして、あなたは「まあ、言語標準に従って...」で始まる答えを軽disする群衆を持っています。なぜなら彼らは現実の世界で働いており、彼らによれば標準は現実の世界には適用されないからです。私はただ悪い教育を受けた人に対処することができますが、無知であることを主張する人誰でも業界での苦境です。

安全なコードを書くことに重点を置いて言語を正しく教えれば、バッファオーバーフローの問題はありません。それは「ハード」ではなく、「高度」ではなく、ただ注意しているだけです。

はい、これは暴言でした。


1ありがたいことに、言語仕様から最終的に削除されましたが、40年分のレガシーコードには永遠に潜んでいます。


1
私はほとんどあなたに同意しますが、あなたはまだ少し不公平だと思います。私たちが「安全」だと考えるのは時間の関数でもあります(そして、私はあなたが私よりもずっと長い間プロのソフトウェア開発者だったので、あなたはこれに精通していると確信しています)。今から10年後、2012年の誰もがDoS対応ハッシュテーブルの実装を使用した理由について誰かがこの同じ会話をすることになりますが、セキュリティについては何も知りませんでしたか?教育に問題がある場合、「ベスト」プラクティスを教えることに集中しすぎて、ベストプラクティス自体が進化するのではないという問題です。

1
そして正直になりましょう。だけで安全なコードを書くことができますsprintf、それは言語に欠陥がないという意味ではありません。Cに欠陥があり、他の言語と同様に)欠陥があります。これらの欠陥を認めて、引き続き修正できるようにすることが重要です。

@JoeWreschnig-より大きなポイントには同意しますが、DoS可能なハッシュテーブルの実装とバッファオーバーランには質的な違いがあると思います。前者はあなたを取り巻く状況に起因する可能性がありますが、2番目は言い訳はありません。バッファオーバーランは、コーディングエラー、期間です。はい、Cにはブレードガードがなく、不注意な場合は切断されます。それが言語の欠陥かどうかを議論することができます。それは非常に少数の学生が与えられているという事実に直交だ任意の彼らは言語を学んでいる時に、安全指示を。
ジョンボード

5

この問題は、プログラマーの無能さよりも管理上の近視の問題の1つです。90,000行のアプリケーションでは、完全に安全ではない安全でない操作が1つだけ必要です。基本的に安全でない文字列処理の上に記述されたアプリケーションが100%完璧になる可能性の領域をほぼ超えています-つまり、安全でないことを意味します。

問題は、安全でないことの費用が正しい受取人に請求されないこと(アプリを販売する会社が購入価格を払い戻す必要がほとんどないこと)、または決定が下された時点ではっきりと見えないことです(「出荷する必要があります3月になっても!」)。長期的なコストと企業の利益ではなくユーザーのコストを考慮した場合、Cまたは関連言語で書くのははるかに高価であり、おそらく非常に高価であるため、多くの人にとって明らかに間違った選択であると確信しています今日の慣習的な知恵がそれが必要であると言う分野。しかし、より厳しいソフトウェア責任が導入されない限り、それは変わりません-業界の誰も望んでいません。


-1:すべての悪の根源として管理を非難することは特に建設的ではありません。歴史を無視することは少し少なくなります。答えは最後の文でほぼ引き換えられます。
マッテンツ

セキュリティに関心があり、その代価を支払おうとするユーザーによって、より厳しいソフトウェア責任が導入される可能性があります。ほぼ間違いなく、セキュリティ侵害に対して厳しい罰則を課すことで導入される可能性があります。ユーザーがセキュリティの代価を支払おうとするなら、市場ベースのソリューションは機能しますが、そうではありません。
デビッドソーンリー

4

Cを使用する大きな利点の1つは、Cを使用すると、適切と思われる方法でメモリを操作できることです。

Cを使用することの大きな弱点の1つは、Cが適切と思われる方法でメモリを操作できることです。

安全でない機能には安全なバージョンがあります。ただし、プログラマーとコンパイラーは厳密にその使用を強制しません。


2

Cの作成者がライブラリを再実装してこれらの問題を修正しないのはなぜですか?

おそらくC ++がすでにこれを行っており、Cコードとの下位互換性があるためです。したがって、Cコードで安全な文字列型が必要な場合は、std :: stringを使用し、C ++コンパイラを使用してCコードを記述します。

基礎となるメモリサブシステムは、ガードブロックとそれらの有効性チェックを導入することにより、バッファオーバーフローを防ぐのに役立ちます。したがって、すべての割り当てに4バイトの「fefefefe」が追加されます。メモリへの書き込みを防ぐことは保証されていませんが、何かが間違っており、修正する必要があることを示します。

問題は、古いstrcpyなどのルーチンがまだ存在していることだと思います。それらがstrncpyなどを支持して削除された場合、それは助けになります。


1
strcpyなどを完全に削除すると、インクリメンタルアップグレードパスがさらに難しくなり、アップグレードがまったく行われなくなります。現在の方法では、C11コンパイラに切り替えてから、_sバリアントの使用を開始し、_s以外のバリアントを禁止し、既存の使用法を修正できます。

-2

オーバーフローの問題が修正されない理由を理解するのは簡単です。Cはいくつかの領域で欠陥がありました。当時、これらの欠陥は許容できるものであるか、機能であるとさえ考えられていました。数十年後、これらの欠陥は修正不可能になりました。

プログラミングコミュニティの一部は、これらの穴を塞ぐことを望んでいません。文字列、配列、ポインター、ガベージコレクションから始まるすべてのフレーム戦争を見てください...


5
笑、ひどい、間違った答え。
ヒースハニカット

1
これが悪い答えである理由を説明すると、Cには確かに多くの欠陥がありますが、バッファオーバーフローなどを許可することはそれらとはほとんど関係がありませんが、基本的な言語要件があります。Cの仕事をするための言語を設計し、バッファオーバーフローを許可することはできません。コミュニティの一部は、多くの場合正当な理由で、Cが許可する機能を放棄したくありません。これらの問題のいくつかを回避する方法についても意見の相違があり、プログラミング言語の設計を完全に理解していないことを示しています。
デビッドソーンリー

1
@DavidThornley:Cの仕事をする言語を設計することはできますが、通常の慣用的な方法で少なくともコンパイラーがバッファオーバーフローを合理的に効率的にチェックできるようにするために、コンパイラーがそうすることを選択できます。memcpy()アレイセグメントを効率的にコピーするための標準的な手段であるということと、それを標準的な手段にすることとの間には大きな違いがあります。
supercat 14
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.