なんでもセキュリティ Advent Calendar 2016 19日目の記事です。
セスペH28秋で一悶着あった問題を共有したいと思います。
問題と解答
午後1問2(セキュアプログラミング)は、以下に示すヒープベースBOF脆弱性のあるプログラムYに関する内容でした。
※全文はIPAの過去問ページを参照してください。
#include <iostream>
#include <cstring>
//(省略)
#define UID_SIZE 8 // 利用者IDの文字列の上限値
#define PASS_SIZE 8 // パスワードの文字列の上限値
//(省略)
using namespace std;
void getPass(char *pass, char *uid)
{
//(省略,uidで指定された利用者IDを基に登録済パスワードを取得しpassに格納,利用者IDが存在しない場合は長さ0の文字列をpassに格納)
}
//(省略)
int main(int argc, char **argv)
{
static char *uid;
static char *pass;
//(省略,引数の個数をチェック)
uid = new char[UID_SIZE+1];
pass = new char[PASS_SIZE+1];
getPass(pass, argv[1]);
strcpy(uid, argv[1]); // 23行目
if (strlen(pass) == 0 || strcmp(argv[2], pass) != 0) {
cout << "認証失敗" << endl;
//(省略,uidを出力,認証失敗時の処理)
} else {
cout << "認証成功" << endl;
//(省略,uidを出力)
}
}
このプログラムYに関して、設問3の(2),(3)で以下のようなことが問われました。
(2) 利用者認証を回避される原因となるヒープベースBOF脆弱性の存在箇所を,実際にバッファがオーバーフローするコードの行番号で答えよ。
これは23行目の strcpy(uid, argv[1]); でBOFの可能性があります。
strcpy()はコピー元に終端記号が現れるまでひたすらコピーを続けようとするからです。
(3) (2)で示した行番号の行を差し替えて行う改修案として適切なものを解答群の中から全て選び,記号で答えよ。
解答群
ア memcpy(uid, argv[1], strlen(argv[1]+1);
イ memcpy(uid, argv[1], UID_SIZE+1);
ウ pass = new char[PASS_SIZE+8];
エ strncpy(uid, argv[1], strlen(argv[1])+1);
オ strncpy(uid, argv[1], UID_SIZE+1);
これについて私は、23行目での問題点はコピーする長さを指定していないことなのだから、長さを正しく指定しているイとオが適切だと判断しました。
実際、IPAの解答はイとオだったのですが、一部の人はmemcpy()とstrncpy()の違いに着目して、イは適切ではないと判断したようです。
資格学校の解答速報も答えが割れました。アイテック(PDFファイル)はイとオ。TAC(PDFファイル)はオのみ。
お恥ずかしながら、この2つの関数の違いをよく認識していなかったので調べてみました。
memcpy()とstrncpy()の違い
memcpy()とstrncpy()はどちらも指定したサイズだけコピーする関数ですが、以下のような違いがあります。
-
memcpy()
- 終端記号の有無にかかわらず指定したサイズをすべてコピーする
- 終端記号のチェックをしない分だけ高速
- [strncpy()](strncpy - C++ Reference)
- 指定したサイズに到達する前に終端記号が現れればそこでコピーをやめる
- コピー先の終端記号以降は指定したサイズになるまでゼロで埋める
極端な例を考えると、コピー元の文字列はギリギリでメモリ領域に収まっていても、memcpy()はその先を読み込もうとしてAccess Violationとなる可能性があります。
今回の問いに関して言えば、memcpy()を使っていてもヒープ領域のオーバーフローにはならない、外部から意図的に攻撃できる脆弱性ではない等の理由でイも正答になるということでしょうが、文字列のコピーにはstrncpy()を使う方が適切でしょう。