【C言語】strcpy関数/strncpy関数【詳解|危険性と注意点 strlcpyの脆弱性】

strcpy関数とstrncpy関数の仕様と注意点について解説します。

strcpy

strcpy関数は文字列のコピーを実現する関数です。

#include <string.h>
char *strcpy(char *dst, const char *src);
// dst: 書き込み先の配列
// src: 新たに追加する文字列(終端ナル文字を含む)
// 戻り値: `dst`を返す

strcpy関数は、srcが指す文字列をdstが指す配列にコピーします。srcが指す文字列はナル文字('\0')で終端されている必要があります。またdstに書き込まれる文字列にはナル文字も含まれます。

戻り値にはdstの値が返されます。dstsrcの指す領域が重なり合う場合の動作は未定義です。

char s[4] = {'-', '-', '-', '-'};
char a[3] = {'a', 'b', '\0'};

strcpy(s, a);     // s == {'a', 'b', '\0', '-'}
strcpy(s, "abc"); // s == {'a', 'b', 'c', '\0'}
strcpy(s, "");    // s == {'\0', 'b', 'c', '\0'}

// バッファオーバーランが発生(危険)
strcpy(s, "abcde"); // s == {'a', 'b', 'c', 'd', 'e', '\0'}

strcpy関数とバッファオーバーラン

strcpy関数にはバッファオーバーランの危険性があります。コピー先の配列のバッファサイズを超えた過剰な書き込みが発生してしまう恐れがあります。

char s[3];
strcpy(s, "abcd");
// s == {'a', 'b', 'c', 'd', '\0'} // 2バイト分余計に書き込まれてしまう

この問題の最も安全な回避策としては、書き込み可能なバッファサイズとコピー元の文字列長を事前にチェックし、安全な場合にのみコピーを行うようにする事です。

char s[3];
const char *a = "ab";

if (strlen(a) < 3) {
   strcpy(s, a);
}

他にも、長さ指定が可能な# strncpy関数や、より安全性の高い# strlcpy関数を用いる方法もあります。

strncpy

#include <string.h>
char *strncpy(char *dst, const char *src, size_t len);
// dst: 書き込み先の配列
// src: コピーしたい文字列
// len: コピーしたい長さ
// 戻り値: `dst`を返す

strncpy関数は、srcが指す文字列をdstが指す配列にコピーします。コピーされる文字の長さは、文字列srcの長さとlenのいずれか小さい方となります。コピー後はdst全体でlen文字分の書き込みが行われるまでナル文字('\0')が付加されます。

戻り値にはdstの値が返されます。dstsrcの指す領域が重なり合う場合の動作は未定義です。

char s[4];

strncpy(s, "abc", 4);
// s ≒ {'a', 'b', 'c', '\0'}

strncpy(s, "abcd", 4); // ①
// s ≒ {'a', 'b', 'c', 'd'}

strncpy(s, "ab", 4);   // ②
// s ≒ {'a', 'b', '\0', '\0'}
srcの長さがlen以上の場合には、ナル文字が付加されないため注意が必要です。
srcの長さがlenよりも小さい場合には、残りの全ての領域にナル文字が付加されます。
char s[4] = {'-', '-', '-', '-'};
strncpy(s, "a"  , 3); // s: {'a', '\0', '\0', '-'}(②余分な長さの場合は残りをナル文字で埋める)
strncpy(s, "ab" , 3); // s: {'a', 'b' , '\0', '-'}
strncpy(s, "abc", 3); // s: {'a', 'b' , 'c' , '-'}(①ナル文字による終端処理が行われない)
strncpy(s, "ABC", 2); // s: {'A', 'B' , 'c' , '-'}(指定の長さを超えた過剰な書き込みは行われない)
strncpy(s, "a"  , 0); // s: {'A', 'B' , 'c' , '-'}(書き込みが行われない)
strncpy(s, ""   , 1); // s: {'\0','B' , 'c' , '-'}(不足分はナル文字で埋める)

strncpy関数の問題点と脆弱性

strncpy関数では終端ナル文字('\0')が書き込まれないケースが存在するため注意が必要です。書き込み元の文字列の長さが第三引数に指定された長さ以上の場合には、書き込み先の配列にはナル文字が付加されません。

char s[4] = {'-', '-', '-', '-'};
strncpy(s, "abc", 3); // s == {'a', 'b', 'c', '-'}

strncpy関数そのものにはバッファオーバーランの危険性は無いものの、上記のようなナル終端されていない文字列を生み出す危険性があります。このような不完全な文字列がヌル終端文字列として扱われた場合、プログラムの不具合の発生や意図しないバッファオーバーランに繋がってしまう危険性があります。

/* バッファオーバーフローに繋がる例 */
char s[3];
strncpy(s, "abc", 3);
// s: {'a', 'b', 'c', '%', '&', '\0'}(4文字目以降の領域は不定な値)

memset(s, '0', strlen(s)); // ナル文字が出現するまでゼロ埋めする処理
// s: {'0', '0', '0', '0', '0', '\0'}(4文字目以降の領域も書き換わってしまった)

他にも、ナル終端されていない文字列によって、後続の不定な領域を誤って利用してしまう危険性を生み出します。この脆弱性にはインジェクション攻撃に繋がる危険性もあります。

/* インジェクション攻撃の脆弱性に繋がるコードの例 */
char s[7]; // s ≒ {'%', '&', '#', '^', ' ', 'b', '\0'}(ゴミデータ)
const char *t = "rm a";   // ファイル`a`を削除するコマンド
strncpy(s, t, strlen(t)); // ナル文字は書き込まれない
puts(s); // "rm a b"      // ファイル`a`とファイル`b`を削除するコマンド

strncpy関数の安全な利用

strncpy関数を安全に利用するためには、手動で終端ナル文字を書き込む必要があります。もっとも簡潔な方法としては、第三引数に対して常に書き込み先の配列のバッファサイズを指定し、コピー後は配列の末尾にナル文字を書き込むようにすることです。

char s[4] = {'-', '-', '-', '-'};
strncpy(s, "abcd", 4); // s ≒ {'a', 'b', 'c', 'd'}
s[3] = '\0';           // s ≒ {'a', 'b', 'c', '\0'}

ただし、超過した分の文字列は暗黙的に切り捨てられることになる点に注意が必要です。「# strlcpy関数の問題点と危険性」で紹介する例のように、文字列の暗黙的な切り詰めは別の新たな問題を引き起こすことに繋がる可能性もあります。

よって、この場合のもっとも堅実な対策としては、書き込み元の文字列の長さが書き込み先の配列バッファサイズよりも小さい場合にのみ、実際のコピー処理を行うようにすることです。

char s[4];
const char *t = "abc";

if (strlen(t) < 4) {
   strncpy(s, t, 4);
}

strlcpy

利用できる環境は限られますが、より安全なstrlcpy関数やstrcpy_s関数を用いる方法もあります。第三引数には書き込み先の配列のバッファサイズを指定することができます。指定されたバッファサイズを超える書き込みは行われず、書き込み先の配列は常にナル文字によって終端処理されます。

#include<string.h>
size_t strlcpy(char *dst, const char *src, size_t size);
// dst: 書き込み先の配列
// src: コピーしたい文字列
// size: 書き込む長さ
// 戻り値: `src`の長さ(終端ナル文字はカウントされない)

srcが指す文字列をdstの指す配列へコピーします。コピーは最大でsize - 1文字分だけ行われ、コピー後の末尾にはナル文字が書き込まれます。戻り値には文字列srcの長さが返されます。

char s[4] = {'-', '-', '-', '-'};
strlcpy(s, "abc", 3); // s: {'a', 'b', '\0', '-'}(文字列の末尾にはナル文字が付加される)
strncpy(s, "abc", 3); // s: {'a', 'b', 'c', '-'}
strlcpy(s, "a"  , 3); // s: {'a', '\0', 'c', '-'}(残りのバッファはヌル文字で埋めない)

strlcpy関数の問題点と危険性

strlcpy関数やsnprintf関数による書き込み制限や、「# strncpy関数の安全な利用」等の終端ナル文字を手動で付加する対処は、あくまで万が一の場合の予防策に過ぎません。超過した分の文字列は暗黙の内に切り捨てられる事になるため、このことが逆に別の新たな問題やバグを引き起こすことに繋がる危険性もあります。

const char *cmd = "rm *.txt"; // テキストファイルのみを削除するコマンド
char s[5];
strlcpy(s, cmd, 5);
puts(s); // "rm *" // すべてのファイルを削除するコマンド

この場合、「# strcpy関数とバッファオーバーラン」で紹介した例のように、書き込み先のバッファサイズとコピー元の文字列の長さを事前にチェックすること等が求められます。サイズが一致しないケースは明らかなバグや想定外の仕様であり、ソフトウェアの不具合にも繋がる危険性があるため、本来であれば何かしらのエラー処理や例外処理が必要となります。

なおstrlcpy関数はコピー元の文字列の長さを返すため、この値を書き込み時に指定したサイズと比較することで、コピー時に行われた暗黙的な切り詰めを検知することもできます。戻り値が指定したサイズ以上の場合には切り詰めが行われた事になります。

const char *cmd = "rm *.txt";
char s[5];
if (strlcpy(s, cmd, 5) >= 5) {
   puts("コピー後の文字列は切り詰められています");
   puts("配列変数`s`は不完全な文字列を指しています");
}
広告