C言語の標準関数を自作する:番外編 n系関数によるセキュアコーディングとstrncpy()による「0埋め」の謎
目次
はじめに
ここまでで、文字列処理に使う関数はだいたい揃いました。
もう文字列処理は我々の思うがままです。
せっかくなので、以前作った「簡易メールアドレスチェッカー」を、もう少し実用的にしてみましょう。
前回は、入力するメールアドレスをコードに直接書いていました。
え?
何、入力文字列を勝手に決めてハードコーディングしているんですか?
そんなんじゃ、みんな同じメールアドレスになっちゃうじゃないですか。
……ということで、今回はシステムコールのread()を利用して、ユーザーからメールアドレスを入力してもらいます。
1. read()で入力する
read()を使えば、標準入力からデータを受け取れます。
read(0, lv_Buffer, sizeof(lv_Buffer));
簡単ですね。
今回は、メールアドレスを入力したあと、確認のためにもう一度入力してもらうようにしましょう。
一致していればOK。
よくあるシステムですね。
これだけでは、1つのメールアドレスしかチェックできないため、「exit」が入力されるまで上記の処理を繰り返すことにしましょう。
つまり、以下のようなイメージです。
A君
↓
メールアドレス入力
↓
確認用入力
↓
一致チェック
↓
メールアドレス入力待機
B君
↓
メールアドレス入力
↓
確認用入力
↓
一致チェック
↓
メールアドレス入力待機
C君
↓
「exit」と入力
↓
プログラム終了
では、作ってみましょう。
2. とりあえず作ってみる
まずは、一旦何も考えずに実装します。
#include <unistd.h>
#include "wrapper/string/my_strlen.h"
#include "wrapper/string/my_strcmp.h"
#include "wrapper/string/my_strcpy.h"
#include "wrapper/string/my_strchr.h"
int main(void)
{
char lv_Input[128] = ""; // 今回はあえて他のバッファより大きく確保します.
char lv_Email[64] = "";
char lv_Check[64] = "";
char *lp_Newline;
const char *lp_Message;
ssize_t lv_Res;
while (1) {
lp_Message = "メールアドレスを入力してください(終了: exit): ";
lv_Res = write(1, lp_Message, my_strlen(lp_Message));
lv_Res = read(0, lv_Input, sizeof(lv_Input) - 1);
if(lv_Res < 0){
// 読取に失敗した場合は最初からやり直し.
continue;
}
// readによる読取文字列の末尾に終端文字を設定.
// 今回は改行コードを置き換える形式とする.
lp_Newline = my_strchr(lv_Input, '\n');
if (NULL != lp_Newline) {
*lp_Newline = '\0';
} else {
lv_Input[lv_Res] = '\0';
}
if (0 == my_strcmp(lv_Input, "exit")) {
// 「exit」が入力されたため終了.
break;
}
my_strcpy(lv_Email, lv_Input);
lv_Email[sizeof(lv_Email) - 1] = '\0';
lp_Message = "確認のため、もう一度入力してください: ";
lv_Res = write(1, lp_Message, my_strlen(lp_Message));
lv_Res = read(0, lv_Input, sizeof(lv_Input) - 1);
if(lv_Res < 0){
// 読取に失敗した場合は最初からやり直し.
continue;
}
lp_Newline = my_strchr(lv_Input, '\n');
if (NULL != lp_Newline) {
*lp_Newline = '\0';
} else {
lv_Input[lv_Res] = '\0';
}
my_strcpy(lv_Check, lv_Input);
lv_Check[sizeof(lv_Check) - 1] = '\0';
if (0 == my_strcmp(lv_Email, lv_Check)) {
lp_Message = "メールアドレスが一致しました。\n";
} else {
lp_Message = "メールアドレスが一致しません。\n";
}
lv_Res = write(1, lp_Message, my_strlen(lp_Message));
lp_Message = "入力されたメールアドレス: ";
lv_Res = write(1, lp_Message, my_strlen(lp_Message));
lv_Res = write(1, lv_Email, my_strlen(lv_Email));
lp_Message = "\n";
lv_Res = write(1, lp_Message, my_strlen(lp_Message));
/*
* デバッグ用:バッファ全体を確認する.
* 実際のアプリケーションでは不要.
*/
lp_Message = "--- デバッグ表示 ---\n";
lv_Res = write(1, lp_Message, my_strlen(lp_Message));
lp_Message = "Emailバッファ : ";
lv_Res = write(1, lp_Message, my_strlen(lp_Message));
lv_Res = write(1, lv_Email, sizeof(lv_Email));
lv_Res = write(1, "\n", 1);
lp_Message = "Checkバッファ : ";
lv_Res = write(1, lp_Message, my_strlen(lp_Message));
lv_Res = write(1, lv_Check, sizeof(lv_Check));
lv_Res = write(1, "\n\n", 2);
// 本来は必要だが、エラー処理は省略.
(void)lv_Res;
}
return 0;
}
それっぽいですね。
ただし、このコードは危険です。
今回は、後述するコードとの比較用にあえて作成しました。
3. 野生の入力が現われた!
さて、ここからが前回との大きな違いです。
前回は我々が入力を決めていました。
今回は違います。
ユーザーが入力します。
え?
ユーザーが普通にメールアドレスを入力する?
いつからメールアドレスが入力されると錯覚していた!
野生の入力です。
これらはとても凶暴なのです。
例えば、
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
こんな入力が飛んできたらどうでしょう。
我々のバッファは、
64バイト
しかありません。
そこへ、
64バイトを超える入力
がやってきます。
当然、バッファには入りきりません。
さらに、我々はmy_strcpy()も使っています。
つまり、
野生の入力
↓
my_strcpy()
↓
バッファオーバーフロー
です。
我々のバッファが噛み切られてしまいます。
これはいけません。
猛獣(バッファロー)に立ち向かうために、我々のコードにも防具を付けなければいけません。
それがセキュアコーディングです。
今回は、n系関数で防御しましょう。
4. いけ!n系関数!
今回までに作ってきたn系関数は、
my_strncmp()
my_strncpy()
my_strncat()
です。
これらの関数には共通点があります。
それは、
「どこまで処理するか」
を指定できることです。
例えば、
my_strncpy(lv_Email, lv_Input, sizeof(lv_Email) - 1);
とすれば、コピーする量に上限を付けられます。
つまり、
無制限に来い!
から、
ここまでだ!
に変わります。
猛獣に対して、
「お前はここまでだ」
と言えるわけです。
5. n系関数、装着!
では、n系関数を使って武装してみましょう。
#include <unistd.h>
#include "wrapper/string/my_strlen.h"
#include "wrapper/string/my_strcmp.h"
#include "wrapper/string/my_strncmp.h"
#include "wrapper/string/my_strncpy.h"
#include "wrapper/string/my_strchr.h"
int main(void)
{
char lv_Input[128] = ""; // 今回はあえて他のバッファより大きく確保します.
char lv_Email[64] = "";
char lv_Check[64] = "";
char *lp_Newline;
const char *lp_Message;
ssize_t lv_Res;
while (1) {
lp_Message = "メールアドレスを入力してください(終了: exit): ";
lv_Res = write(1, lp_Message, my_strlen(lp_Message));
lv_Res = read(0, lv_Input, sizeof(lv_Input) - 1);
if(lv_Res < 0){
// 読取に失敗した場合は最初からやり直し.
continue;
}
// readによる読取文字列の末尾に終端文字を設定.
// 今回は改行コードを置き換える形式とする.
lp_Newline = my_strchr(lv_Input, '\n');
if (NULL != lp_Newline) {
*lp_Newline = '\0';
} else {
lv_Input[lv_Res] = '\0';
}
if (0 == my_strcmp(lv_Input, "exit")) {
// 「exit」が入力されたため終了.
break;
}
my_strncpy(lv_Email, lv_Input, sizeof(lv_Email) - 1);
lv_Email[sizeof(lv_Email) - 1] = '\0';
lp_Message = "確認のため、もう一度入力してください: ";
lv_Res = write(1, lp_Message, my_strlen(lp_Message));
lv_Res = read(0, lv_Input, sizeof(lv_Input) - 1);
if(lv_Res < 0){
// 読取に失敗した場合は最初からやり直し.
continue;
}
lp_Newline = my_strchr(lv_Input, '\n');
if (NULL != lp_Newline) {
*lp_Newline = '\0';
} else {
lv_Input[lv_Res] = '\0';
}
my_strncpy(lv_Check, lv_Input, sizeof(lv_Check) - 1);
lv_Check[sizeof(lv_Check) - 1] = '\0';
if (0 == my_strncmp(lv_Email,
lv_Check,
sizeof(lv_Email))) {
lp_Message = "メールアドレスが一致しました。\n";
} else {
lp_Message = "メールアドレスが一致しません。\n";
}
lv_Res = write(1, lp_Message, my_strlen(lp_Message));
lp_Message = "入力されたメールアドレス: ";
lv_Res = write(1, lp_Message, my_strlen(lp_Message));
lv_Res = write(1, lv_Email, my_strlen(lv_Email));
lp_Message = "\n";
lv_Res = write(1, lp_Message, my_strlen(lp_Message));
/*
* デバッグ用:バッファ全体を確認する.
* 実際のアプリケーションでは不要.
*/
lp_Message = "--- デバッグ表示 ---\n";
lv_Res = write(1, lp_Message, my_strlen(lp_Message));
lp_Message = "Emailバッファ : ";
lv_Res = write(1, lp_Message, my_strlen(lp_Message));
lv_Res = write(1, lv_Email, sizeof(lv_Email));
lv_Res = write(1, "\n", 1);
lp_Message = "Checkバッファ : ";
lv_Res = write(1, lp_Message, my_strlen(lp_Message));
lv_Res = write(1, lv_Check, sizeof(lv_Check));
lv_Res = write(1, "\n\n", 2);
// 本来は必要だが、エラー処理は省略.
(void)lv_Res;
}
return 0;
}
これで、
read()
↓
my_strchr()
↓
my_strncpy()
↓
my_strncmp()
↓
write()
と、これまで作ってきた関数が勢揃いしました。
これで、
野生の入力
を手懐けられます。
そして、そのためにn系関数という防具を装備しました。
まぁ、本来はもっと検証が必要です。
例えば、
・@マークの検出
・ドメイン名の有効性確認 etc...
ただ今回は棚に上げます。
6. 実行してみる
では、実際に実行してみましょう。
A君に入力してもらいます。
メールアドレスを入力してください(終了: exit): taro19900101@example.com
確認のため、もう一度入力してください: taro19900101@example.com
メールアドレスが一致しました。
入力されたメールアドレス: taro19900101@example.com
続いてB君。
メールアドレスを入力してください(終了: exit): jiro@example.com
確認のため、もう一度入力してください: jiro@example.com
メールアドレスが一致しました。
入力されたメールアドレス: jiro@example.com
こちらも問題なさそうです。
ところで。
コードの最後に、こんなものがありましたね。
write(1, lv_Email, sizeof(lv_Email));
デバッグ用に、バッファ全体を表示しています。
先ほどの2人の実行結果。
何か違うところに気付きませんでしたか?
7. 「0」がいっぱいある
A君の入力。
taro19900101@example.com
B君の入力。
jiro@example.com
B君の方が短いですね。
では、バッファ全体を見てみます。
n系関数を使わない場合、
jiro@example.com\0xample.com\0...
のように、後ろにA君の入力の残骸が残ることがあります。
一方、n系関数を使った場合は、
jiro@example.com\0\0\0\0\0\0...
となります。
後ろが'\0'で埋まっています。
なるほど。
strncpy()のおかげで、古いデータが残らなくなりました。
……ところで。
なぜstrncpy()は、わざわざ0埋めするのでしょうか?
8. strncpy()による「0埋め」の謎
普通に考えれば、
ABC
をコピーしたら、
A B C
で終わってもよさそうです。
ところがstrncpy()は、コピー元が短い場合、
A B C \0 \0 \0 \0 \0
のように残りを'\0'で埋めます。
なぜでしょう。
実は、これには歴史があります。
strncpy()は、単なる「安全なstrcpy()」として生まれたわけではありません。
古いUnixでは、固定長のデータ領域を扱う場面がありました。
例えば、ファイルシステムではファイル名が14バイトの固定長文字列として設計されていました。
つまり、
[新しいデータ][残りの領域]
という領域です。
ここで、新しいファイル名が以前のファイル名より短かったらどうなるでしょう。
残りの領域に古いデータが残ります。
[新しいデータ][古いデータ]
これは困ります。
そこで、
[新しいデータ][0][0][0][0]...
と埋めてしまう。
こうして、古いデータを残さないようにします。
つまり、
「なんでこんなに0を入れるんだ?」
と思っていた仕様には、
古いデータを残さない
という理由があったわけです。
半世紀以上前のUnixの世界から、現在まで生き残っている仕様です。
9. ただし、n系関数は無敵ではない
ここまで来ると、
n系関数を使えば全部安全!
と思いたくなります。
残念ながら、そうではありません。
例えば、
char lv_Buffer[8];
my_strncpy(lv_Buffer, "123456789", 8);
なら、
12345678
となり、'\0'はありません。
また、
my_strncpy(lv_Buffer, "123456789", 100);
なら、そもそもコピー先の領域が足りません。
つまり、n系関数は防具です。
無敵の鎧ではありません。
どこまで処理するのか。
コピー先にどれだけの容量があるのか。
これらは、結局のところ利用側が適切に管理する必要があります。
10. まとめ
今回は、ユーザー入力に対応した簡易メールアドレスチェッカーを作成してみました。
前回は、
我々が用意した文字列
を処理しました。
今回は、
野生の入力
です。
そこでread()を使って入力を受け付け、n系関数を使ってセキュアコーディングを意識したコードへ改良しました。
そして、デバッグ用にバッファ全体を覗いてみると、短い文字列を入力したときに古いデータが残るという問題も見えてきました。
そこで登場したのが、strncpy()の0埋めです。
さらに調べてみると、その一見不思議な仕様には、古いUnixにおける固定長データ処理という歴史がありました。
まさか、今回作った関数の「0埋め」が、半世紀以上前のUnixから続いていたとは。
幸い、今回はメールアドレスでした。
もしこれがパスワードだったら……。
ひえっ。
みなさんも、野生の入力には気を付けましょう。
🔗 関連記事・関連リンク
この記事の最新アップデートや、このシリーズの関連記事は以下のリンクからご覧いただけます。
内容は基本的に同じですので、お好みのサイトでお読みください。
🟢 Zenn
-
この記事の最新版はこちら。
-
https://zenn.dev/malloc/articles/11_extra03_securecoding_buffoverflow
-
「標準関数自作」Topicの関連記事はこちら。
🔵 Qiita
-
「標準関数自作」タグの関連記事はこちら。
🧡 note
-
このシリーズの記事はこちら。