【C言語】
バッファオーバーフローの
傾向と対策
(Buffer overflow)

buffer overflow メモリ破壊

warning: iteration 1024 invokes undefined behavior

警告:ループカウンタ 1024 で未定義動作
[-Waggressive-loop-optimizations]

[gcc -fsanitize=addres]
[gcc -Wall -Wextra -O3]


■バッファオーバーフローとは

バッファオーバーフロー(以下BOF)とは、
簡単に言うと
バッファ(主に配列)をはみ出した領域を
参照(読み書き)する事です。

■strcpyでBOF

#include <stdio.h>
#include <string.h>
//gcc -Wall -O
//warning: ‘__builtin___strcpy_chk’ writing 94 bytes 
//into a region of size 16 overflows the destination 
//[-Wstringop-overflow=]
int main(void) {
    char    buf[16];
    char    *jugemu = "寿限無寿限無五劫のすり切れ海砂利水魚の水行末雲来末風来末~~~";
    strcpy(buf, jugemu);
    puts(buf);
}

この問題は
gcc -Wall -Wextra -Oで検出できます。  
※ーOで最適化しないと検出できません。

参考:

汝が
“foo”とタイプするところで
“supercalifragilisticexpialidocious”とタイプする者が、
いつの日か必ずいるからである


■strncpyでBOF

#include <stdio.h>
#include <string.h>
//clang -Wall 
//warning: 'strncpy' size argument is too large; 
//destination buffer has size 4, 
//but size argument is 1024 
//[-Wfortify-source]
int main(void) {
    char    buf[4];
    strncpy(buf,"abc",1024);
    puts(buf);
}

4byte領域に”abc”3byteと\0をコピーするので
問題なさそうですが、
第三引数の1024がくせ者です。

■strcatでBOF

#include <stdio.h>
#include <string.h>
//gcc strcat.c -Wall  -O2
// warning: ‘__builtin___strcat_chk’ forming offset [4, 6] is 
//out of the bounds
//[0, 4] of object ‘buf1’ with type ‘char[4]’ 
//[-Warray-bounds]
int main(void){
    char    buf1[] = "abc";
    char    buf2[] = "def";
    strcat(buf1,buf2);
    puts(buf1);
}

第一引数に第二引数を加える領域がありません。

■memcpyでBOF

#include <stdio.h>
#include <string.h>
//gcc -Wall -Wextra  -O -fsanitize=address
//warning: ‘__builtin_memcpy’ forming offset [4, 1023] is 
//out of the bounds [0, 4] 
//[-Warray-bounds]
int main(void){
    char buf[1024];
    //OK:"abc"の後ろは0で埋まる
    strncpy(buf,"abc",sizeof(buf));
    puts(buf);

    //NG:"def"の後ろを領域外参照する
    memcpy(buf,"def",sizeof(buf));
    puts(buf);
}

文字列定数”def”の後ろを参照してしまいます。

■sprintfでBOF

#include <stdio.h>
//gcc -fsanitize=address
int main(void){
    
    char     input[4] = "abcd";//終端文字が無い
    char    output[5];
    
    sprintf(output,"%s",input);
    puts(output);
}

inputに終端文字\0が無いので
暴走して
\0を見つけるまでoutputに書き込みします。

■scanfでBOF

#include <stdio.h>
//gcc -fsanitize=address
int main(void){  
    printf("4桁の数字を入れてね");

    char    buf[4];//終端文字を考慮していない
    scanf("%s",buf);
    printf("%sが入力されました\n",buf);
}

4桁の数字文字列を収容するには終端文字\0を入れて5byte必要です。

■fgetsでBOF

#include <stdio.h>
//warning: 
//‘fgets’ writing 8192 bytes into a region of size 4 
//overflows the destination
//[-Wstringop-overflow=]
int main(void){  
    int     num;
    char    buf[4];

    printf("4桁の数字を入れてね");
    fgets(buf,BUFSIZ,stdin);
    sscanf(buf,"%d",&num);
    printf("%dが入力されました\n",num);
}

4桁の数字文字列を収容するには終端文字\0を入れて5byte必要です。

■for文で<と<=を間違える

static int  ary[1024];
int main(void){
    for(int i = 0 ; i <= 1024 ;i++){//★NG
        ary[i] = 0 ;
    }
}

➡修正例1.<に変更する

static int  ary[1024];
int main(void){
    for(int i = 0 ; i < 1024 ;i++){
        ary[i] = 0 ;
    }
}

■要素数とサイズを間違える

#include <stdint.h>
static char aryC[1024];
static int  aryI[1024];
int main(void){
    //たまたま上手くいく例
    for(uint32_t i = 0 ; i < sizeof(aryC) ;i++){
        aryC[i] = 0 ;
    }
    //配列の要素数とサイズは違う
    for(uint32_t i = 0 ; i < sizeof(aryI) ;i++){//★NG
        aryI[i] = 0 ;
    }
}

要素数はサイズではありません。
sizeof(aryI) != 1024
sizeof(aryI) == sizeof(int)*1024 
です。

このコードではLP64モードの場合、4096回転するので
配列のメモリ破壊が発生します。

➡修正例2.

#include <stdint.h>
static char aryC[1024];
static int  aryI[1024];
int main(void){
    const uint32_t maxC = sizeof(aryC)/sizeof(aryC[0]);
    for(uint32_t i = 0 ; i < maxC ;i++){
        aryC[i] = 0 ;
    }
    
    const uint32_t maxI = sizeof(aryI)/sizeof(aryI[0]);
    for(uint32_t i = 0 ; i < maxI ;i++){
        aryI[i] = 0 ;
    }
}

配列の要素数を求める定石 sizeof(配列)/sizeof(配列[0])を使います。