web-dev-qa-db-ja.com

C / C ++配列でNULL終了の余分なバイトを無視することのセキュリティへの影響

ご検討ください:英語が私の第二言語です。


Security Now!ポッドキャスト episode 518HORNET:A Fix for TOR?)で、 27:51のマークスティーブギブソンは、C/C++の脆弱なコードの例を引用しています。

「[...]それらの1つ[脆弱なコードの問題]が特定のサイズの新しい配列を作成しています[...]。そして、修正は「特定のサイズ+ 1」のものです。したがって、[...] [脆弱なコード]は1バイトが短すぎました。おそらくNULLターミネータなので、配列にサイズオブジェクトを入力すると、NULLの終了を保証する1バイトのNULLが追加され、それはその文字列がオーバーランするのを防ぎますしかし、それはコーダーがやったことではありません:彼らは '+ 1'を忘れていました[...]」

私は彼の意味を理解しています。配列を作成するとき、NULL終了バイト用に1バイト余分に許可する必要があります。この投稿で私が達成したいのは、最後のバイトがバイトターミネータではない配列を持つことの影響をさらに調査するためのポインタを取得することです。私はそのような過失の完全な意味と、これがどのように悪用につながるかを理解していません。 NULL終了があると彼が言ったとき

"その文字列がオーバーランするのを防ぎます"、

私の質問は、「NULL終了文字が無視されている場合、どのようにオーバーランするか」です。

これは大きなトピックであることを理解しているため、コミュニティに過度に包括的な回答を押し付けないでください。しかし、誰かがさらに読むためのいくつかの指針を提供するのに十分親切であることができれば、私は非常に感謝し、自分で研究を行って喜んでいます。

21
user82100

文字列終了の脆弱性


これについてもっと考えると、strncpy()を使用することはおそらくnull終了エラーを作成する可能性がある(私が考えることができる)最も一般的な方法です。一般に、人々はバッファの長さを_\0_を含まないと考えています。したがって、次のようなものが表示されます。

strncpy(a, "0123456789abcdef", sizeof(a));

aが_char a[16]_で初期化されていると仮定すると、a文字列はnullで終了しません。では、なぜこれが問題になるのでしょうか。さて、メモリには次のようなものがあります:

_30 31 32 33 34 35 36 37 38 39 61 62 63 64 65 66 
e0 f3 3f 5a 9f 1c ff 94 49 8a 9e f5 3a 5b 64 8e
_

Nullターミネータがないと、標準の文字列関数はバッファの長さを認識できません。たとえば、strlen(a)は、_0x00_バイトに達するまでカウントを続けます。それはいつですか、誰が知っていますか?しかし、それが見つかると、バッファよりもはるかに長い長さを返します。 78としましょう。例を見てみましょう。

_int main(int argc, char **argv) {
    char a[16];

    strncpy(a, "0123456789abcdef", sizeof(a));

    ... lots of code passes, functions are called...
    ... we finally come back to array a ...

    do_something_with_a(a);
}

void do_something_with_a(char *a) {
    int a_len = 0;
    char new_array[16];

    // Don't know what the length of the 'a' string is, but it's a string so lets use strlen()!
    a_len = strlen(a);

    // Gonna munge the 'a' string, so lets copy it first into new_array
    strncpy(new_array, a, a_len);
}
_

これで、16バイトのみが割り当てられている変数に78バイトが書き込まれました。

バッファオーバーフロー


バッファオーバーフローは、そのバッファに割り当てられているよりも多くのデータがバッファに書き込まれると発生します。これは文字列の場合と同じですが、_string.h_関数の多くはこのnullバイトに依存して文字列の終わりを示します。上で見たように。

この例では、16バイトにのみ割り当てられるバッファに78バイトを書き込みました。それだけでなく、ローカル変数でもあります。これは、バッファがスタックに割り当てられたことを意味します。書き込まれた最後の66バイトは、スタックの66バイトを上書きしただけです。

そのバッファーの終わりを超えて十分なデータを書き込んだ場合、他のローカル変数_a_len_(後で使用する場合も同様)、スタックに保存されたスタックフレームポインターを上書きしてから、関数のアドレス。今、あなたは本当に行って、物事を台無しにしました。なぜなら、現在、戻りアドレスは完全に間違っているからです。 do_something_with_a()の終わりに達すると、悪いことが起こります。

上記の例にさらに追加できます。

_void do_something_with_a(char *a, char *new_a) {
    int a_len = 0;
    char new_array[16];

    // Don't know what the length of the 'a' string is, but it's a string so
    // lets use strlen()!
    a_len = strlen(a);

    // 
    // By the way, copying anything based on a length that's not what you
    // initialized the array with is horrible horrible coding.  But it's
    // just an example.
    //
    // Gonna munge the 'a' string, so lets copy it first into new_array
    strncpy(new_array, a, a_len);

    // 'a_len' was on the stack, that we just blew away by writing 66 extra 
    // bytes to the 'new_array' buffer.  So now the first 4 bytes after 16
    // has now been written into a_len.  This can still be interpreted as
    // a signed int.  So if you use the example memory, a_len is now 0xe0f33f5a
    //
    // ... did some more munging ...
    //
    // Now I want to return the new munged string in the *new_a variable
    strncpy(new_a, new_array, a_len);

    // Everything burns

}
_

私のコメントはほとんどすべてを説明していると思います。しかし、最終的には、アレイに大量のデータを書き込んだことになります。おそらく、16バイトしか書き込んでいないと考えています。この脆弱性がどのように現れるかによって、リモートコード実行による悪用につながる可能性があります。

これは貧弱なコーディングの非常に不自然な例ですが、メモリの操作やデータのコピーを慎重に行わないと、事態が急速にエスカレートする様子がわかります。ほとんどの場合、脆弱性はこれほど明白ではありません。大規模なプログラムでは、非常に多くのことが行われているため、この脆弱性は簡単に特定できず、複数の関数呼び出しを離れたコードによって引き起こされる可能性があります。

バッファオーバーフローのしくみ の詳細。

そして、誰もそれを言及する前に、単純化のためにメモリを参照するときはエンディアンを無視しました


参考文献

脆弱性の完全な説明
Common Weakness Enumeration(CWE)エントリ
Secure Coding Strings Presentation(PDF自動ダウンロード)
ピッツバーグ大学-セキュアコーディングC/C++:文字列の脆弱性(PDF)

22
RoraΖ

私はさらに別の答えを追加することによって冗長になるリスクがありますが、既存の答えはあなたが求めていることを完全には扱っていないかもしれません。従来のバッファオーバーフローの脆弱性(特にスタックベースの多様性)では、現在の関数が戻ろうとしたときに実行コードがエクスプロイトコードにジャンプするように、スタックのフレームポインターを上書きしようとします。

明らかに、それはあなた(攻撃者)がプログラムにバッファの終わりを超えて書き込むことができる唯一のものがゼロバイトである場合には機能しません。潜在的に、無効なアドレスにジャンプしようとすることにより、プログラムをこのようにクラッシュさせる可能性がありますが、これは単なるDoSであり、リモートコードの実行ではありません。

ただし、プログラムが16バイトの長さの文字列を "A"と呼ぶ16バイトのバッファーに書き込むようにして、nullバイトがオーバーランすることを考慮してください。次に、プログラムにそのヌルバイトを\ 0以外のもので上書きさせるため、文字列Aはヌルで終了しません。次に、Aのコンテンツを送信するプログラムを取得すると、Aの終わりを超えて読み取られ、あらゆる種類の秘密情報にアクセスできる可能性があります。 Heartbleedはこの種の情報開示を使用して秘密鍵を盗みましたが、これはかなり深刻です。

この時点で、文字列Aは実際にはプログラマーが予期したよりも長くなっています。 Aが16バイトの文字列であることに依存してそれを別の場所にコピーし、他のバッファが1バイトをはるかに超えてオーバーフローする可能性があると想像するのはそれほど難しくありません。これは、任意のコードを実行するために使用できます。

3
Lexelby

応答で示したように、プログラマーがNULLバイトの文字列を終了しない場合、バッファオーバーフローは脆弱性の可能性があります。その理由は、ほとんどの文字列関数がこれを前提としており、ゼロに遭遇するまで続くからです。運が良ければ、開発の初期段階でセグメンテーションフォールトエラーが発生するほどのエラーであるため、問題をデバッグして修正できます。ただし、多くのバグでは、この明らかな障害は特別な条件下でのみ発生します。多くの場合、攻撃者はプログラムの動作を利用し、脆弱性の詳細に応じて、隠すべきメモリの内容を読み取るために悪用したり、ユーザー入力からユーザーが意図していないメモリ領域にデータをコピーしたりしますバッファがスタックに配置されている場合、後者のエクスプロイトを使用してコードを挿入し、スタックのより高いアドレス(x86)にあるスタックフレームに格納されている戻りアドレスを上書きできます。一部のオペレーティングシステムには、実行不可能なメモリセグメントなどの保護機能がありますが、これは、プログラマーが依存したくないものです。

Cのような言語でプログラミングするのが初めてのプログラマーは、これらの問題を回避するためのプラクティスを困難またはエラーが発生しやすいと感じるかもしれませんが、結局それは間違いを犯す可能性はありますが、第二の性質になります。できる限り練習するだけで、私はCで約7年間プログラミングを行っていますが、それでも時々間違いを修正する必要があります。

練習する良い方法は、文字配列を割り当て、0以外の印刷不可能なASCII文字以外の文字で配列全体をmemsetすることです。選択した標準ライブラリ関数を使用して文字列をコピーしてください明らかに、それがプログラムをクラッシュさせる場合は、配列に追加します。それ以外の場合は、基本的なforループを使用して配列のすべての要素を反復処理し、数値を出力します。0があるべき場所にあることを確認してください。 printfを使用すると、文字列のその時点で停止します。これは、strcat、strncat、strcpy、strlcpy、strlcpy、strlcat、sprintなどの関数間の違いを見つけるための実験に適した方法です。 strlcpyおよびstrlcatは、ほとんどの場合、strncpyおよびstrncatよりもはるかに簡単で、エラーが発生しにくくなります。

別のヒントとして、頭の中でアルゴリズムを検証することが難しいと思われる場合は、非常に小さな入力で同じことを行っていると想像してください。文字列の場合、1文字とNULLバイトの余地がある文字列に対して操作を実行していると想像してください。これにより、他の方法ではより多くの脳の作業が必要になる文字列の多くのプロパティを簡単に確認できます。たとえば、1つの文字を格納する必要がある場合でも、2つの要素を持つ配列を割り当てる必要があります。 2番目の要素str [1]は明らかに0である必要があります。strlenは文字列の長さが1であることを報告します。これで快適に一般化して、strlen(str)が常にNULLバイトのインデックスであることを知ることができます(これを前提としています)。もちろんNULLで終了します:)、同様にstrlen(str)-1ここで、> 0は常に文字列の最後の文字のインデックスです。文字列とNULLバイトに必要なストレージの量は常にstrlen(str)+ 1です。

最後に一つだけ。 NULLで終了する文字列は慣例にすぎないことに注意することが重要です。多くの可能な代替案が存在し、現在も存在します。これは、NULLバイトがメモリ内の処理を停止するポイントを示すと想定する関数を使用する場合にのみ必要です。これは、libcの文字列関数の場合です。文字列の先頭に付加される長さを格納する独自の文字列関数を作成できます。 255文字を超える長さの文字列に必要な型パンニングによって導入されるいくつかの追加の複雑さを犠牲にして、文字列が更新されるたびにこの数を維持するこの方法には、O(1)時間。文字列ポインタと長さの値を構造体に格納することもできますが、これはヒープ上にない文字列にきちんと一般化することはできません。ほとんどのプログラマはおそらくほとんどの場合、標準の文字列表現に固執する必要があることを伝えてください。それらはおそらく正しいです。しかし、それがあなた自身のコードである場合、誰が何をすべきかを指示するのは、普遍的な計算機です(少なくとも有限近似)。計算のランドスケープをサンドボックスにして、楽しんでください!

1
user3259161

これは上記の回答に浮かんでいますが、明示的にする必要があると思います。 C/C++の文字配列処理には、1つずれる可能性のある多くの潜在的な危険があります...例:

""  // a zero length string requiring one byte of storage
    // in memory:  00

"Hi."  // a length 3 string requiring four bytes of storage
       // in memory:  48 69 2e 00
"Hi."[3]  // is the 00, the characters in a string and a string array are indexed starting at 0, to wit
"Hi."[0]  // is the 'H'.

char foo[3]  // a length three character array requiring three bytes of storage
     bar[4]  // a length four character array requiring four bytes of storage

strncpy(foo, "Hi.", 3)  // copies three characters from a length three string to a length three character array.  
                        // The result is not a string because the null is not copied.

strcpy(foo, "Hi.")  // copies four characters from a length three string to a length three character array
                    // This causes overrun of the array.
                    // It writes 00 on whatever (if anything) is allocated next in storage.

strcpy(bar, "Hi.")  // copies four characters from a length three string to a length four character array.
                    // This works/is safe (enough).

そう

  • 長さ3のストリングには4文字が含まれます。
  • 長さ3の文字列が長さ3の文字配列に収まらない
  • 長さ3の文字列から3文字をコピーしても、文字列はコピーされません
  • mystringが長さである場合n文字列、mystring [n]は終了00です。したがって、- n-番目の文字は00をコピーします。

または、要約すると、これは最大で1つずれたエラーを引き起こすように設計されています。

1
Eric Towers