「C言語は脆弱性を作り出しやすい」というような一行をとあるサイトで見たことがあります。
C言語だと
strcpy() や gets()など
printf(buf);
このような書き方は非常に危険かと思います。
これ以外にも「危険!これはやってはいけない!」というような”書き方”はありますか??
有名なもので結構ですので、ぜひ教えてください。
またC++の方がC言語に比べれば脆弱性を生みにくい気がします。
std::cout << buf << std::endl;
などの書き方は問題ないはずです。
C++でも「これはダメ!」といった書き方があれば教えてください!!
気になる質問をクリップする
クリップした質問は、後からいつでもMYページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。

回答4件
0
ベストアンサー
C/C++の危険性の一つにメモリ安全では無い事があります。メモリ安全性とは何かを知るには次の記事を参考になります。
What is memory safety? - The PL Enthusiast
記事ではSoKの論文Eternal War in Memoryであげられたメモリ安全ではないもの、いわゆるメモリアクセスエラーの原因として、次の5つを上げています。
- buffer overflow バッファーオーバーフロー
- null pointer dereference ヌルポインタ参照
- use after free 解放後の使用
- use of uninitialized memory 未初期化メモリの使用
- illegal free (of an already-freed pointer, or a non-malloced pointer) (既に解放済みのポインタ、または、未確保のポインタに対する) 不正な解放
これが含まれていなければメモリ安全であるというのは、受動的な定義です。そこで記事では、メモリ安全性を定義し、この5つをどのようにして排除するのかを述べています。(定義の詳細は長くなるので省きます。)
さて、この記事では大きなヒントを与えてくれました。上の5つが起こるような書き方は全て危険と言うことです。
###1. バッファーオーバーフロー
代表的なのはgets
でしょう。gets
はバッファーオーバーフローを防ぐ方法がありません。他にも、strcpy
やscanf
も危険と言われています。では、strcpy
ではなくstrncpy
を使えば安全かというとそうではありません。指定のサイズが間違ってコピー先の領域よりも大きかったら、やはりバッファーオーバーフローが発生します。strcpy_s
やstrncpy_s
であっても結局は同じ事なのです。下記のコードはx
の値が書き換わってしまう可能性があります。(コンパイラや環境によっては、aのサイズチェックを実施し、SIGABRTシグナルで落ちる場合があります)
C
1int x = -1; 2char a[4]; 3char *b = "abcdef"; 4strncpy(a, b, 5);
それに、もっと簡単なアクセス、それこそ配列への添字アクセスですら危険です。記事の方に例がのっていましたので、引用しましょう。
C
1/* Program 1 */ 2int x; 3int buf[4]; 4buf[5] = 3; /* overwrites x */
これを防ぐにはどうすれば良いのでしょうか?それは添字アクセスが配列の長さを超えないことを保証するようにプログラミングする以外ありません。上の例では0〜3しか駄目なことは自明ですが、実際はmalloc
で動的に確保された領域へのアクセスだった場合、どれだけのサイズを確保したのか、アクセスするときにそのサイズを超えていないのか、それを確認し、保証しなければなりません。これはプログラマーの仕事であり、そこに絶対的な安全な書き方など無いのです。
逆に言いますと、唯一の例外であるgets
を除けば、事前条件を保証し、正しい方法であればどれも安全に使用できます。strcpy
でも、コピー元の長さが十分に入る領域がコピー先に確保されていることが確認できていれば、安全です。むしろ、そのような場合でもチェックの分だけ遅くなるstrncpy
を使う事は無駄であるとすら言えます。
###2. ヌルポインタ参照
ヌルポインタを参照した場合、ほとんどの場合はエラーでプログラムが終了するでしょう。終了する分だけ、まだ安全と言えます。
###3. 解放後の使用
C
1int *a = (int *)malloc(sizeof(int)); 2*a = 10; 3free(a); 4int *b = (int *)malloc(sizeof(int)); 5*b = 20; 6*a = 30; 7printf("%d\n", *b);
さて、上のプログラムはいくつを出力するでしょうか?答えはわからないです。手元の環境では-O0でコンパイルすると30、-O2でコンパイルすると20でした。
バッファオーバーフローは関数と使い方等が問題でした。しかし、これはfree
の位置の問題です。もし、free(a)
が*a = 30
より後であれば、何も問題は起きません。しかし、実際はfree
のあとにa
を使用しているため問題が起こってしまいます。
メモリをいつ解放するかは重要な問題です。むしろ、脆弱性を防ぐためには、ずっと解放しないでメモリリークでプログラムが終了した方がましとさえ言えます。しかし、実際はプログラムが安定して動作するために、free
はどこかでしなければなりません。そこにプログラマーのジレンマがあります。
###4. 未初期化メモリの使用
C
1void f(void) 2{ 3 int x = 10; 4} 5void g(void) 6{ 7 int y; 8 printf("%d\n", y); 9} 10int main(void) 11{ 12 f(); 13 g(); 14}
最適化無し(-O0)でコンパイルすると、きっと10が表示されると思います(コンパイラによります)。y
は未初期化です。y
が何であるのかさっぱりわかりませんし、0であるなど期待をしてもいけません。もし、これがポインタに対する処理だったら…もう、何が起きてもおかしくありません。
###5. 不正な解放
free
は2回呼んでも大丈夫…ということはありません。free
直後にNULL
を代入して、free(NULL)
になるのは安全です。二重解放とはそのことではありません。
C
1int *x = (int *)malloc(sizeof(int)); 2*x = 10; 3free(x); 4int *y = (int *)malloc(sizeof(int)); 5*y = 20; 6free(x); 7int *z = (int *)malloc(sizeof(int)); 8*z = 30; 9printf("%d\n", *y);
これも最適化無し(-O0)であれば30を出すでしょう。free
は領域の開放です。開放された領域は再利用されます。いつどこにどのように再利用されるかわかりません。このコードではx
しか解放していないように見えますが、x
が解放された後、y
はx
が使っていた領域を再利用してしまいます。そのあとのfree(x)
ではx
を解放しているように見えて、実質y
も解放しています。そしてz
でもまた再利用されて、あらためてy
を見に行ったら、z
の値になっていたと言うことです。(実際にこうなるかは、コンパイラや最適化によって変わります)
さて、メモリにまつわるところを見てきましたが、添字アクセスからfree
まで安全と言えるような物はCにはないように思えます。そう、Cは常に危険に満ちたデンジャラスな世界なのです。常に、メモリ領域を意識しないとCは書けません。安心安全なCなんて無いのです。
C++はどうでしょう。C++はCのスーパーセットです。**Cとほぼ同じ事ができます。**つまり、C++はCと同じぐらい危険なことをできてしまうと言うことです。
ただ、C++にはCより安全な手法が用意されています。例えばスマートポインタを使えば、free
にまつわる危険性を悉く防ぐことができるでしょう。スマートポインタを使う最大の利点は解放し忘れによるメモリリークを防ぐことでは無く、間違った解放によるメモリアクセス違反を防ぐことです。しかし、スマートポインタを使っていれば完全に安全とは言えません。Cのライブラリとの関係で、どうしてもスマートポインタから生ポインタをとりだして使わなくてはならないときがあるでしょう。取り出した生ポインタがまだ使われているのに、スマートポインタの機能で自動的にdeleteされたら…結局はCの時と同じ事が起きてしまうと言うことです。
他にもSTLのstd::stringやstd::vectorを使えば、Cの配列よりもいささか安全かもしれません。しかし、std::vecotr::at
は長さを超えるとエラーになってくれます(エラーになることはむしろ安全であるということです)が、std::vector::operator[]
は長さを超えてもエラーになると限らず、何が起きるかわかりません。結局そこは配列への添字アクセスと同じで、プログラマーが責任を持って範囲に収まることを保証しなければなりません。
C++もCと同じく道具の使いようです。Cと比べるとより安全になる手法が多く用意されていますが、メモリ領域を意識しないといけないことは同じです。正しく使わなければ、メモリが破壊され、脆弱性を産むことになるでしょう。
他の言語はメモリ安全性を確保するために、GCの導入、配列の常時範囲チェック、領域の自動拡張などを実装しました。しかし、これられの機能は、純粋な操作に余計な処理がくっつくことになるため、速度低下に繋がります。C/C++が高級言語では未だに最速であるのは、こういった機能をプログラマーの仕事に押しつけることで、必要最低限のチェックや領域の確保・開放を行うことができるからです。これが良い事であるとも悪い事であるとも言えません。
これらのことは、危険性の一つにしか過ぎません。これだけを対策すれば脆弱性が無くなるわけではありません。しかし、(おそらくメモリ安全であると思われる)他言語に比べてC/C++において最も危険なのこのメモリ安全性が無い事だと思います。
投稿2016/11/28 12:10
総合スコア21751
0
これ以外にも「危険!これはやってはいけない!」というような”書き方”はありますか??
C/C++の安全なプログラミングについては、JPCERTウェブサイトに情報がまとまっています。
英語版CERTでは下記ページが対応します。(上記JPCERTは英語版より翻訳されたものです)
投稿2016/11/28 06:33
総合スコア6191
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
0
「危険な実装」と「脆弱性」には相関はありますが同じ意味ではありませんので区別しておいたほうがよいと思います。一般的に「危険な実装」はそれが「悪意のある第三者にセキュリティー上の攻撃手段を与え得る」ものであった場合に「脆弱性」と呼ばれると思います。
それはさておき...
- バッファーオーバーラン
例に挙げられた関数はいずれもバッファーオーバーランを起こす可能性がありますね。このような関数はどのくらいあるかというと「無数にある」というのが自分の認識です。自分の考えは「どの関数が危険か覚える」のは有効な手段でなく、「自分が使う全ての関数の動作と仕様を正確に知る」姿勢と実践が有効だと思います。バッファーオーバーランの脆弱性についてはよくおわかりだと思いますが、ポインターを用いる関数はその仕様をきちんとおさえていない限り全て脆弱性を持つ実装になり得ると考えても過言ではないと思いませんか?
ポインターを直接扱えるというC,C++の特徴は効率上の大きな利点ですがバグで簡単にメモリーを破壊できてしまう点およびどのように破壊されるかが予想できる点(C/C++のランタイムはブラックボックスではない)が脆弱性に繋がりやすくなることから諸刃の剣といえると思います。これはどの関数が危険かというより「全てのポインターを用いる実装において不当なアドレスを参照しないように作られなければならない」ぐらいに認識しておくべきのことだと自分は思います。脆弱性に繋がるかどうかの前の段階の話(バグを無くすという話)ですが。
- バッファーオーバーランは全て脆弱性なのか
全てのオーバーランのバグが脆弱性になるわけではないですね?第三者が特定の計算機に外部から攻撃する際に通過しなければならないソフトウェア(実装が公開されているOS,言語,フレームワーク)にバッファーオーバーランになるような実装が存在していたときは直ちにそれが脆弱性になると思います。しかしそうでないもの(ソースが公開されていない,外部から入力が与えられるようなものでない,etc.)はバグではありますが脆弱性となる危険度は下がると思います。
- 脆弱性はバッファーオーバーランだけ気にすればいいのか
バッファーオーバーランはインターネット初期からあった脆弱性であり大変有名なものですが氷山の一角でしかないというのが自分の認識です。それ以外に多くの脆弱性となりえるものが色々あります。クロスサイトスクリプティングとか有名ですね。そういった問題はC/C++がメモリーを破壊しやすいので危険でJavaのようなものはその危険が少ないといった単純に考えられるようなものではなく、安全を求める立場からいえば特定の言語やOSやフレームワークだけに限らず広く「こんな危険がある」という知識を必要とするものだと思います。
(かく言う私は脆弱性について知識があるとは全然思っていません。実務上気を付けるべき色々な例を挙げてくださる方々の回答を自分も期待します。)
投稿2016/11/28 00:53
編集2016/11/28 01:45総合スコア18404
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
0
C++もC言語のようにポインターを扱えますし、Cのライブラリ関数を使うことは禁じてないので、やはり書き方に依存すると思います。危険なことをしようと思えばどんなことでも出来てしまいます。(そのつもりがなくてもついついやってしまうこともあります。)
メモリ確保(new)のdelete忘れはよく有りがちなケースです。すぐには動作への影響は出ませんが、メモリーリークを起こすため、処理によってはある一定時間以上動かすと、メモリー不足を起こしたりします。
例外が発生する関数などを使った時に、例外処理を入れて無くてアプリが突然落ちてしまう、ということはよく経験してます^^;。まあ、想定していた入力条件以外のものが入ってきたとか、そういう異常処理をどこまで施しているかになってきます。
ただ、C++はクラスや参照などを使って、それらの危険を未然に防ぐような処理をある程度施すことが出来ます。スマートポインターなどを使えばdelete忘れも少なくなるでしょう。(クラス化したら安全になるということではありません。安全になるような設計をしないともちろん駄目です)
配列も、vectorやarrayなどのコンテナライブラリを使うことで、配列サイズを超えてアクセスする危険を未然に防ぐ事ができますが、速度などとのトレードオフになります。(便利さを考えるとコンテナを使うメリットは大きいと思いますが)
また、std::cout << buf << std::endl;
ですが、もし buf が std::string ならまあ安全かもしれませんが、char* の場合、文字列の終端がNULLで終わってなかったら、バッファを超えてNULLが見つかるまでアクセスすることになります。(読み出しなのでそれほど危険じゃないかもしれませんが、一般保護例外が出る可能性はあります)
投稿2016/11/28 08:32
総合スコア3579
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。

あなたの回答
tips
太字
斜体
打ち消し線
見出し
引用テキストの挿入
コードの挿入
リンクの挿入
リストの挿入
番号リストの挿入
表の挿入
水平線の挿入
プレビュー
質問の解決につながる回答をしましょう。 サンプルコードなど、より具体的な説明があると質問者の理解の助けになります。 また、読む側のことを考えた、分かりやすい文章を心がけましょう。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2016/11/28 23:37
2016/11/28 23:47
2016/11/28 23:54
2016/11/29 01:12
2016/11/29 01:16 編集
2016/11/29 09:39
2016/11/29 14:01