プログラム検証手法は、ハートブリードのジャンルのバグの発生を防ぐことができますか?


9

ハートブリードバグの問題について、ブルースシュナイアーは4月15日の暗号グラムで次のように書いています。「破滅的」は正しい言葉です。1から10のスケールで、これは11です。私は数年前に、特定のオペレーティングシステムのカーネルが最新のプログラム検証システムで厳密に検証されていることを読みました。したがって、今日のプログラム検証手法を適用することで、Heartbleedのジャンルのバグが発生するのを防ぐことができますか、それとも、これはまだ非現実的か、または基本的に不可能でしょうか。


2
J. Regehrによるこの質問の興味深い分析を次に示します。
Martin Berger 14

回答:


6

最も簡潔な方法で質問に答えるために-はい、このバグは正式な検証ツールによって検出された可能性があります。実際、「送信されたハートビートのサイズよりも大きいブロックを送信しない」というプロパティは、ほとんどの仕様言語(LTLなど)で形式化するのがかなり簡単です。

問題(これは形式的な方法に対する一般的な批判です)は、使用する仕様が人間によって記述されていることです。確かに、正式な方法は、バグを探す課題を、バグを見つけることから、バグとは何かを定義することへと変えるだけです。これは難しい仕事です。

また、状態爆発の問題により、ソフトウェアの正式な検証は非常に困難です。この場合は特に重要です。州の爆発を回避するために何度も境界を抽象化するためです。たとえば、「すべてのリクエストの後に100000ステップ以内で許可が続く」と言いたい場合、非常に長い式が必要なので、「すべてのリクエストの後に最終的に許可が続く」という式に抽象化します。

したがって、ハートブリードの場合、要件を形式化しようとしても、問題の境界が抽象化され、同じ動作が発生する可能性があります。

要約すると、このバグは形式的な方法を使用することで回避できた可能性がありますが、このプロパティを事前に人間が指定する必要があったでしょう。


5

KlocworkやCoverityなどの商用プログラムチェッカーは、比較的簡単に「境界チェックエラーを実行するのを忘れた」ので、チェックするように設計された主要な問題の1つであるため、Heartbleedを見つけることができた可能性があります。しかし、はるかに単純な方法があります。バッファオーバーランが発生しないことが十分にテストされている不透明な抽象データ型を使用します。

Cプログラミングで使用できる「安全な文字列」の抽象データ型は多数あります。私が最もよく知っているのはVstrです。著者のJames Antillは、独自のコンストラクター/ファクトリーメソッドを備えた文字列抽象データ型と、Cの他の文字列抽象データ型のリストが必要な理由について素晴らしい議論をしています


2
CoverityはHeartbleedを検出しません。JohnRegehr によるこの分析を参照してください。
Martin Berger 14

いいリンクだ!これは、ストーリーの真の道徳を示しています。プログラムの検証では、設計が不十分な(または存在しない)抽象化を補うことはできません。
Wandering Logic

2
それはあなたがプログラム検証によって何を意味するかによる。静的分析を意味する場合は、そうです、ライスの定理の直接の結果として、それは常に近似です。対話型の定理プローサーで完全な動作を検証すると、プログラムがその仕様を満たしているという鋳鉄製の保証が得られますが、これは非常に面倒です。そして、あなたはまだあなたの仕様が間違っているかもしれないという問題に直面しています(例えば、Ariane 5の爆発を見てください)。
マーティンバーガー

1
@MartinBerger:Coverityが今すぐ見つけます
モニカの復活-M.シュレーダー2014

4

ランタイムバウンドチェックとファジングの組み合わせを「プログラム検証手法 」として数える場合  、はい、この特定のバグが検出された可能性があります

適切なファジングを行うと、悪名高いmemcpy(bp, pl, payload);メモリブロックがpl属するメモリブロックの制限を超えて読み取られます。実行時のバウンドチェックは、原則としてそのようなアクセスをキャッチできます。実際には、この特定のケースでは、デバッグバージョンでmallocも、パラメータをバウンドチェックしmemcpyてジョブを実行したはずです(ここでMMUをいじる必要はありません)。 。問題は、各種類のネットワークパケットに対してファジングテストを実行するのに手間がかかることです。


1
一般的にはIIRCですが、OpenSSLの場合、作成者は独自の内部メモリ管理を実装したためmemcpy、システムから最初に要求された(大きな)領域の実際の境界にぶつかる可能性ははるかに低くなりましたmalloc
ウィリアム価格

はい、バグ発生時のmemcpy(bp, pl, payload)OpenSSLのmalloc場合は、システムではなく、OpenSSLの代わりに使用される境界に対してチェックする必要がありましたmalloc。これにより、バイナリレベルでの自動境界チェックが除外されます(少なくともmalloc置換についての深い知識がない場合)。トークンを置き換えるCマクロなど、mallocOpenSSLが使用する代替のものを使用して、ソースレベルのウィザードで再コンパイルする必要があります。memcpy非常に巧妙なMMUのトリックを除いて、同じことが必要なようです。
fgrieu 14

4

より厳密な言語を使用しても、目標の投稿が実装の正しいものから仕様の正しいものに移動するだけではありません。非常に間違っているが論理的に一貫しているものを作るのは難しいです。これが、コンパイラが非常に多くのバグをキャッチする理由です。

タイプシステムは実際に想定されていることを意味しないため、通常は定式化されているポインター演算は適切ではありません。ガベージコレクションされた言語で作業することで、この問題を完全に回避できます(抽象化の費用もかかる通常のアプローチ)。または、使用しているポインターの種類についてより具体的にすることもできます。これにより、コンパイラーは、矛盾するものや、記述されたとおりに正しいことが証明できないものをすべて拒否できます。これは、Rustのような一部の言語のアプローチです。

構成された型は証明に相当するので、これを忘れる型システムを作成すると、あらゆる種類のことがうまくいきません。しばらくの間、型を宣言するとき、実際には変数の内容について真実を主張していると仮定します。

  • int * x; //偽のアサーション。xが存在し、intを指していない
  • int * y = z; // zがintを指すことが証明されている場合にのみtrue
  • *(x + 3)= 5; //(x + 3)がxと同じ配列のintを指している場合にのみtrue
  • int c = a / b; // bがゼロ以外の場合のみtrueのようになります: "nonzero int b = ...;"
  • nullable int * z = NULL; // nullable int *はint *と同じではありません
  • int d = * z; // zはnull可能であるため、偽のアサーション
  • if(z!= NULL){int * e = z; } // zがnullではないため、OK
  • free(y); int w = * y; //偽のアサーション、yはwに存在しないため

この世界では、ポインターをnullにすることはできません。NullPointerの逆参照は存在せず、ポインターのnullがどこにあるかをチェックする必要はありません。代わりに、「nullable int *」は、その値をnullまたはポインタに抽出できる別の型です。これは、null以外の仮定が開始した時点で、例外をログに記録するか、nullブランチに進むことを意味します。

この世界では、範囲外エラーの配列も存在しません。コンパイラが境界内にあることを証明できない場合は、コンパイラが証明できるように書き直してください。できない場合は、その場所で仮定を手動で入力する必要があります。コンパイラは後でそれと矛盾するかもしれません。

また、初期化されていないポインターを使用できない場合、初期化されていないメモリへのポインターはありません。解放されたメモリへのポインタがある場合、コンパイラによって拒否されます。Rustには、これらの種類の証明を期待するのに妥当なものにするためのさまざまなポインタータイプがあります。排他的に所有されるポインタ(つまり、エイリアスなし)、深く不変の構造へのポインタがあります。デフォルトのストレージタイプは不変などです。

また、プロトコル(インターフェースメンバーを含む)に実際に明確に定義された文法を適用して、入力表面積を予想されるものに正確に制限するという問題もあります。「正確さ」についてのことは、1)未定義の状態をすべて取り除く2)論理的な一貫性を確保する。そこにたどり着くことの難しさは、(正確性の観点から)非常に悪いツールを使用することと多くの関係があります。

これがまさに、2つの最悪の慣行がグローバル変数と後藤である理由です。これらの事柄は、事前/事後/不変条件を何にも付けないようにします。また、型が非常に効果的である理由でもあります。型が強くなると(最終的には依存型を使用して実際の値を考慮に入れる)、それらはそれ自体が建設的な正当性の証明になることに近づきます。一貫性のないプログラムを作成すると、コンパイルが失敗します。

それは愚かな間違いだけではないことを覚えておいてください。それはまた、巧妙な侵入者からコードベースを守ることでもあります。「正式に指定されたプロトコルに従う」などの重要なプロパティの説得力のある機械生成の証拠なしに提出を拒否する必要がある場合があります。



1

自動化された/正式なソフトウェア検証は有用であり、場合によっては役立ちますが、他の人が指摘したように、それは特効薬ではありません。OpenSSLは、オープンソースでありながら商用および業界全体で使用され、広く使用されており、リリース前に厳密にピアレビューされていないという脆弱性があることを指摘することができます(プロジェクトに有料の開発者がいるのかどうか疑問に思います)。欠陥は基本的にはリリース後のコードレビューを通じて発見され、コードは明らかにリリース前にレビューされました(おそらく内部のコードレビューを誰が行ったかを追跡する方法はありませんが)。ハートブリード(他の多くの中で)を伴う「教えられる瞬間」は、非常に機密性の高いコードのespをリリースする前に、基本的にはより良いコードレビューであり、おそらくより追跡されます。たぶん、OpenSSLはもっと精査されるでしょう。

その起源を詳述したメディアからのより多くのbkg:

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.