strcpy_sとかmemcpy_s

職場で勤務時間のいくらかは学習時間に充てろという話があって、

社内の研修プログラムの受講とかで時間を費やすなら話は簡単だが、

そういうわけでもないなら何かネタをみつけてやらないといけない。

それで最近はセキュアコーディングというところでC言語の勉強をしている。

ソースコードの解析でこういうのは脆弱性の温床だという指摘はいろいろ見てきたが、

背景や打開策などわかっていない部分もあったので真面目に勉強するかと。


よく知られた話なのだがC標準ライブラリのstrcpy関数はけっこうこわい関数である。

C言語では文字列は文字の配列をNULL文字で終端することで表すのが通常で、

strcpyはNULL文字を検出するまで文字を右から左にコピーする関数である。

この関数が怖いのはNULL文字が出てくるまで際限なく転送してしまうことで、

転送先の配列のサイズなんて考えないわけである。

この結果、想定外の入力が与えられるとメモリ上の他のデータを破壊してしまうことがある。

それが脆弱性の原因となることがしばしばあるのである。


とはいえ、こういう文字列関係の関数は仕事ではほぼ使わないですね。

なのであまり興味はなかったのだが、この問題を緩和するための関数が導入されていたらしい。

C11(ISO/IEC 9899:2011)のAnnex Kで規定された”Bounds-checking interfaces”である。

strcpyに代表される危険な関数に境界チェック機能を追加したものを規定していて、

strcpyに対して strcpy_s のように “_s” を付けた関数名を使う。

NULL文字での終端を前提とした文字列関係の関数は基本的にあるわけだが、

memcpy_s というものも存在するらしい。

memcpyは指定されたサイズのデータを右から左にコピーする関数である。

strcpyと違って指定されたサイズを転送するので、あまりリスクはないと思っていた。

でも境界チェック機能付きのものがあるんですね。


そもそもこの境界チェック付きの関数とはどういうものか、strcpy_sの使用例を見てみる。

char buf[128];
errno_t err;

strcpy(buf, msg); err = strcpy_s(buf, sizeof(buf), msg); if(err != 0){ /* エラー処理 */ }

strcpyとstrcpy_sの使用上の差は2つある。

1つは転送先(上記では”buf”)とともに転送先のサイズ(上記では”sizeof(buf)”)を引数加えること。

もう1つは結果としてエラーコードが返ってくること。

従来のstrcpyは失敗することは基本的に想定しなくてよかったのだが、

境界チェックでひっかかると転送失敗ということになる。

その成否を表す値が返り値として渡されるわけである。

それをエラー処理につなげる必要があるかは設計によるが。


memcpy_sも同じような考えで転送先のサイズを渡すわけである。

memcpy(buf, rcv, len);
err = memcpy_s(buf, sizeof(buf), rcv, len);
if(err != 0){ /* エラー処理 */ }

単純に転送先のサイズより転送サイズが大きければエラーにするだけである。

この転送サイズを示すlenというのは、例えばデータ構造のヘッダに格納されているデータだと、

もしかすると想定外の値が入っていて転送先を破壊する危険がある。

本来はそういうことを意識してチェックする必要がある。

通常のmemcpyでもこういう書き方をすれば、上記のmemcpy_sはほぼ同じであろう。

if(len > sizeof(buf)){
   //エラー処理 }else{
   memcpy(buf, rcv, len); }

ただ、こういうことを個別に考えずとも徹底的に点検できるようにmemcpy_s というのがあるのだろう。


ところでこの”Bounds-checking interfaces”だが、C11ではオプション機能と位置づけられている。

必ずしも対応しているわけではなく、対応しているコンパイラでも

__STDC_WANT_LIB_EXT1__という定数を1にセットしなければならないという。

どうもこれはMicrosoftの提案でC11の規格書に掲載された経緯があるらしい。

STR07-C. 境界チェックインタフェースを使用し、文字列操作を行う既存のコードの脅威を緩和する (JPCERT)

そういえばVisual Studioで_s付きの関数を使えという警告が出てきたことがあったっけ。

元々はMicrosoft独自拡張だったが、規格書に掲載されて一応は標準規格にはなったが、

この拡張について懐疑的に思っているベンダーもあるらしい。


とはいえ、この関数は単なる境界チェックだけでもないんですね。

strcpy_s で転送先のサイズを超過する場合、転送先にはNULL文字を1文字だけ書き込む。

すなわち、転送に失敗した場合は転送先を空文字にするわけですね。

strcpyがNULL文字が出現するまで際限なく転送するという問題に対して、

昔からある標準ライブラリ関数のstrncpyを使うことで最大の転送数が決められるのだが、

これは最大の転送数に達するとNULL終端せずに終わってしまうので、後の処理で問題を引き起こす危険がある。

そういう問題を引き起こしにくいので使いやすいのではないか。


正直、NULL文字で終端されたchar型の配列というのは、

文字列を格納する手段としてはあまり使いやすいものでもないような気はする。

そういう環境で文字列を扱うことの是非は当然ある。

ただ、それが必要な場合にstrcpy_sのような関数を使うのは効果的に思う。

一方でmemcpyは外側でしっかり対策する方が効くのではないかなと思う。

でも徹底的にチェックするという意味でmemcpy_sを推奨したいという考えを持っている人もいるということだろう。


じゃあ、どうやると強固なプログラムを作れるのか?

それはプログラムの性質によるところもあるでしょう。

こうするのが安全と単純に言える話でもないのかなと思った。