質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
C

C言語は、1972年にAT&Tベル研究所の、デニス・リッチーが主体となって作成したプログラミング言語です。 B言語の後継言語として開発されたことからC言語と命名。そのため、表記法などはB言語やALGOLに近いとされています。 Cの拡張版であるC++言語とともに、現在世界中でもっとも普及されているプログラミング言語です。

セキュリティー

このタグは、コンピューターシステムの安全性やデータの機密性に関連したトピックの為に使われます。

C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

Q&A

解決済

4回答

30552閲覧

C/C++ 危険な書き方について

strike1217

総合スコア651

C

C言語は、1972年にAT&Tベル研究所の、デニス・リッチーが主体となって作成したプログラミング言語です。 B言語の後継言語として開発されたことからC言語と命名。そのため、表記法などはB言語やALGOLに近いとされています。 Cの拡張版であるC++言語とともに、現在世界中でもっとも普及されているプログラミング言語です。

セキュリティー

このタグは、コンピューターシステムの安全性やデータの機密性に関連したトピックの為に使われます。

C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

4グッド

8クリップ

投稿2016/11/27 21:35

「C言語は脆弱性を作り出しやすい」というような一行をとあるサイトで見たことがあります。

C言語だと
strcpy() や gets()など
printf(buf);

このような書き方は非常に危険かと思います。

これ以外にも「危険!これはやってはいけない!」というような”書き方”はありますか??
有名なもので結構ですので、ぜひ教えてください。

またC++の方がC言語に比べれば脆弱性を生みにくい気がします。

std::cout << buf << std::endl;

などの書き方は問題ないはずです。
C++でも「これはダメ!」といった書き方があれば教えてください!!

LLman, sawayakatoast, yohhoy, suittizihou👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答4

0

ベストアンサー

C/C++の危険性の一つにメモリ安全では無い事があります。メモリ安全性とは何かを知るには次の記事を参考になります。

What is memory safety? - The PL Enthusiast

記事ではSoKの論文Eternal War in Memoryであげられたメモリ安全ではないもの、いわゆるメモリアクセスエラーの原因として、次の5つを上げています。

  1. buffer overflow バッファーオーバーフロー
  2. null pointer dereference ヌルポインタ参照
  3. use after free 解放後の使用
  4. use of uninitialized memory 未初期化メモリの使用
  5. illegal free (of an already-freed pointer, or a non-malloced pointer) (既に解放済みのポインタ、または、未確保のポインタに対する) 不正な解放

これが含まれていなければメモリ安全であるというのは、受動的な定義です。そこで記事では、メモリ安全性を定義し、この5つをどのようにして排除するのかを述べています。(定義の詳細は長くなるので省きます。)

さて、この記事では大きなヒントを与えてくれました。上の5つが起こるような書き方は全て危険と言うことです。

###1. バッファーオーバーフロー

代表的なのはgetsでしょう。getsはバッファーオーバーフローを防ぐ方法がありません。他にも、strcpyscanfも危険と言われています。では、strcpyではなくstrncpyを使えば安全かというとそうではありません。指定のサイズが間違ってコピー先の領域よりも大きかったら、やはりバッファーオーバーフローが発生します。strcpy_sstrncpy_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が解放された後、yxが使っていた領域を再利用してしまいます。そのあとの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

raccy

総合スコア21735

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

strike1217

2016/11/28 23:37

返事が遅れました。 ありがとうございます
strike1217

2016/11/28 23:47

メモリ管理が難しそうですね! もっとC言語を勉強いたします。
strike1217

2016/11/28 23:54

strncpy、strcpy_sやstrncpy_sなどの新しい関数群も必ず、安全とは言い切れないんですね! 色々実験してみます。
strike1217

2016/11/29 01:12

ん? What is memory safety を見てみました。 /* Program 3 */ struct foo { int x; int y; char *pc; }; struct foo *pf = malloc(...); pf->x = 5; pf->y = 256; pf->pc = "before"; pf->pc += 3; int *px = &pf->x; /* Program 4 */ int x; int *p = &x; int y = (int)p; int *q = (int *)y *q = 5; プログラム3,4はどこがいけないのですか? 構造体にあまり慣れていないので、よくわからないのですが・・・
strike1217

2016/11/29 01:16 編集

プログラム4は4行目に問題がありそうな気がするんですが・・・
raccy

2016/11/29 09:39

Program 3とProgram 4は何か問題があるというコードの例では無く、メモリ安全性を保つのに必要なPointers as capabilities(機能付きポインター)の説明として用いています。ポインタの動きの解説用なので、意味があるわけではありません。
strike1217

2016/11/29 14:01

そうですか!ありがとうございます
guest

0

これ以外にも「危険!これはやってはいけない!」というような”書き方”はありますか??

C/C++の安全なプログラミングについては、JPCERTウェブサイトに情報がまとまっています。

英語版CERTでは下記ページが対応します。(上記JPCERTは英語版より翻訳されたものです)

投稿2016/11/28 06:33

yohhoy

総合スコア6191

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

strike1217

2016/11/28 23:36

ありがとうございます。
strike1217

2016/11/28 23:56

セキュアコーディングの本買ってみます! 面白そうです
guest

0

「危険な実装」と「脆弱性」には相関はありますが同じ意味ではありませんので区別しておいたほうがよいと思います。一般的に「危険な実装」はそれが「悪意のある第三者にセキュリティー上の攻撃手段を与え得る」ものであった場合に「脆弱性」と呼ばれると思います。

それはさておき...

  • バッファーオーバーラン

例に挙げられた関数はいずれもバッファーオーバーランを起こす可能性がありますね。このような関数はどのくらいあるかというと「無数にある」というのが自分の認識です。自分の考えは「どの関数が危険か覚える」のは有効な手段でなく、「自分が使う全ての関数の動作と仕様を正確に知る」姿勢と実践が有効だと思います。バッファーオーバーランの脆弱性についてはよくおわかりだと思いますが、ポインターを用いる関数はその仕様をきちんとおさえていない限り全て脆弱性を持つ実装になり得ると考えても過言ではないと思いませんか?
ポインターを直接扱えるというC,C++の特徴は効率上の大きな利点ですがバグで簡単にメモリーを破壊できてしまう点およびどのように破壊されるかが予想できる点(C/C++のランタイムはブラックボックスではない)が脆弱性に繋がりやすくなることから諸刃の剣といえると思います。これはどの関数が危険かというより「全てのポインターを用いる実装において不当なアドレスを参照しないように作られなければならない」ぐらいに認識しておくべきのことだと自分は思います。脆弱性に繋がるかどうかの前の段階の話(バグを無くすという話)ですが。

  • バッファーオーバーランは全て脆弱性なのか

全てのオーバーランのバグが脆弱性になるわけではないですね?第三者が特定の計算機に外部から攻撃する際に通過しなければならないソフトウェア(実装が公開されているOS,言語,フレームワーク)にバッファーオーバーランになるような実装が存在していたときは直ちにそれが脆弱性になると思います。しかしそうでないもの(ソースが公開されていない,外部から入力が与えられるようなものでない,etc.)はバグではありますが脆弱性となる危険度は下がると思います。

  • 脆弱性はバッファーオーバーランだけ気にすればいいのか

バッファーオーバーランはインターネット初期からあった脆弱性であり大変有名なものですが氷山の一角でしかないというのが自分の認識です。それ以外に多くの脆弱性となりえるものが色々あります。クロスサイトスクリプティングとか有名ですね。そういった問題はC/C++がメモリーを破壊しやすいので危険でJavaのようなものはその危険が少ないといった単純に考えられるようなものではなく、安全を求める立場からいえば特定の言語やOSやフレームワークだけに限らず広く「こんな危険がある」という知識を必要とするものだと思います。

(かく言う私は脆弱性について知識があるとは全然思っていません。実務上気を付けるべき色々な例を挙げてくださる方々の回答を自分も期待します。)

投稿2016/11/28 00:53

編集2016/11/28 01:45
KSwordOfHaste

総合スコア18394

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

strike1217

2016/11/28 23:36

ありがとうございます!
guest

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

PineMatsu

総合スコア3579

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

strike1217

2016/11/28 23:37

ありがとうございます
strike1217

2016/11/28 23:55

プログラミングするときに「例外処理はいらないか〜めんどくさいし」と思って、よく手抜きをするのですが、例外処理とはそんなに重要なんですか?
PineMatsu

2016/11/29 09:33

例外で落ちても良いようなアプリなら良いのでしょうけど(本当は良くないはず。例外ということは不正なことをしているはずだから、何らかの影響は出るはず)、通常、勝手に落ちてしまうのはマズイです。特に、産業用途でPCを使ったモニタリング装置や条件の設定装置などを作っている場合、勝手に落ちてしまうとユーザーは困ったことになってしまいます。なので、絶対に例外で異常終了させてはいけない。例外が発生したことを記録し、リカバリできるのならそのまま続行し、どうしても出来ない場合は、終了する旨とログを送ってほしいなどのメッセージを残してから終了させます。
strike1217

2016/11/29 14:06

なるほどです! ありがとうございます
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問