スタック破壊が検出されました


246

a.outファイルを実行しています。実行後、プログラムはしばらく実行され、次のメッセージで終了します。

**** stack smashing detected ***: ./a.out terminated*
*======= Backtrace: =========*
*/lib/tls/i686/cmov/libc.so.6(__fortify_fail+0x48)Aborted*

これの考えられる理由は何ですか?それをどのように修正しますか?


2
あなたのコードのどの部分がスタック破壊を引き起こしているのかを特定して投稿できますか?その後、おそらくそれが発生する理由とそれを修正する方法を正確に指摘することができるでしょう。
Bjarke Freund-Hansen

オーバーフローエラーと同義だと思います。たとえば、5つの要素の配列を初期化して配列した場合、6番目の要素、または配列の境界外の要素を書き込もうとすると、このエラーが表示されます。
DorinPopescu 2018

回答:


349

ここでのスタックスマッシングは、gccがバッファオーバーフローエラーを検出するために使用する保護メカニズムが原因で発生します。たとえば、次のスニペットでは:

#include <stdio.h>

void func()
{
    char array[10];
    gets(array);
}

int main(int argc, char **argv)
{
    func();
}

コンパイラー(この場合はgcc)は、既知の値を持つ保護変数(カナリアと呼ばれる)を追加します。サイズが10より大きい入力文字列は、この変数の破損を引き起こし、SIGABRTがプログラムを終了させます。

ある程度の洞察を得るために -fno-stack-protector 、コンパイル中にoption を使用してgccのこの保護を無効にしてみてください 。その場合、違ったメモリの場所にアクセスしようとすると、別のエラーが発生します。おそらくセグメンテーション違反です。-fstack-protectorこれはセキュリティ機能であるため、リリースビルドでは常にオンにする必要があります。

デバッガーでプログラムを実行すると、オーバーフローのポイントに関する情報を取得できます。Valgrindはスタック関連のエラーではうまく機能しませんが、デバッガーのように、クラッシュの場所と理由を特定するのに役立ちます。


3
この答えをありがとう!私の場合、書き込もうとしていた変数を初期化していないことが
わかり

5
Valgrindは、そこに赤いゾーンを追加できないため、スタック関連のエラーに対して適切に機能しません
toasted_flakes

7
この答えは正しくなく、危険なアドバイスを提供します。まず、スタックプロテクターを削除することは適切な解決策ではありません。スタックスマッシングエラーが発生する場合は、コードに重大なセキュリティの脆弱性がある可能性があります。正しい応答は、バグのあるコード修正することです。次に、grasGendarmeが指摘しているように、Valgrindを試すという推奨は効果的ではありません。Valgrindは通常、スタックに割り当てられたデータへの不正なメモリアクセスを検出するために機能しません。
DW

22
OPは、この動作の考えられる理由を尋ねます。私の答えは、例と、それが合理的に既知のエラーとどのように関連するかを提供します。その上、スタックプロテクターを取り除くことは解決策ではありません、それは問題についてより多くの洞察を得るために行うことができる一種の実験です。実際のアドバイスは、なんとかしてエラーを修正することです。valgrindについて指摘してくれてありがとう。これを反映するように回答を編集します。
sud03r 2014

4
@DWリリースバージョンでは、スタック保護をオフにする必要があります。最初は- スタックスマッシング検出メッセージは開発者のみのヘルプです。2番目に、アプリケーションはまだ存続する可能性があります。そして3番目に、これは小さな最適化です。
Hi-Angel

33

分解解析を使用した最小限の再現例

main.c

void myfunc(char *const src, int len) {
    int i;
    for (i = 0; i < len; ++i) {
        src[i] = 42;
    }
}

int main(void) {
    char arr[] = {'a', 'b', 'c', 'd'};
    int len = sizeof(arr);
    myfunc(arr, len + 1);
    return 0;
}

GitHubアップストリーム

コンパイルして実行:

gcc -fstack-protector -g -O0 -std=c99 main.c
ulimit -c unlimited && rm -f core
./a.out

必要に応じて失敗します:

*** stack smashing detected ***: ./a.out terminated
Aborted (core dumped)

Ubuntu 16.04、GCC 6.4.0でテスト済み。

分解

次に、逆アセンブリを見てみましょう。

objdump -D a.out

を含む:

int main (void){
  400579:       55                      push   %rbp
  40057a:       48 89 e5                mov    %rsp,%rbp

  # Allocate 0x10 of stack space.
  40057d:       48 83 ec 10             sub    $0x10,%rsp

  # Put the 8 byte canary from %fs:0x28 to -0x8(%rbp),
  # which is right at the bottom of the stack.
  400581:       64 48 8b 04 25 28 00    mov    %fs:0x28,%rax
  400588:       00 00 
  40058a:       48 89 45 f8             mov    %rax,-0x8(%rbp)

  40058e:       31 c0                   xor    %eax,%eax
    char arr[] = {'a', 'b', 'c', 'd'};
  400590:       c6 45 f4 61             movb   $0x61,-0xc(%rbp)
  400594:       c6 45 f5 62             movb   $0x62,-0xb(%rbp)
  400598:       c6 45 f6 63             movb   $0x63,-0xa(%rbp)
  40059c:       c6 45 f7 64             movb   $0x64,-0x9(%rbp)
    int len = sizeof(arr);
  4005a0:       c7 45 f0 04 00 00 00    movl   $0x4,-0x10(%rbp)
    myfunc(arr, len + 1);
  4005a7:       8b 45 f0                mov    -0x10(%rbp),%eax
  4005aa:       8d 50 01                lea    0x1(%rax),%edx
  4005ad:       48 8d 45 f4             lea    -0xc(%rbp),%rax
  4005b1:       89 d6                   mov    %edx,%esi
  4005b3:       48 89 c7                mov    %rax,%rdi
  4005b6:       e8 8b ff ff ff          callq  400546 <myfunc>
    return 0;
  4005bb:       b8 00 00 00 00          mov    $0x0,%eax
}
  # Check that the canary at -0x8(%rbp) hasn't changed after calling myfunc.
  # If it has, jump to the failure point __stack_chk_fail.
  4005c0:       48 8b 4d f8             mov    -0x8(%rbp),%rcx
  4005c4:       64 48 33 0c 25 28 00    xor    %fs:0x28,%rcx
  4005cb:       00 00 
  4005cd:       74 05                   je     4005d4 <main+0x5b>
  4005cf:       e8 4c fe ff ff          callq  400420 <__stack_chk_fail@plt>

  # Otherwise, exit normally.
  4005d4:       c9                      leaveq 
  4005d5:       c3                      retq   
  4005d6:       66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
  4005dd:       00 00 00 

objdump人工知能モジュールによって自動的に追加された便利なコメントに注目してください。

このプログラムをGDBで複数回実行すると、次のようになります。

  • カナリアは毎回異なるランダムな値を取得します
  • の最後のループはmyfuncカナリアのアドレスを変更するものです

で設定することでランダム化されたカナリア%fs:0x28。これには、以下で説明されているランダムな値が含まれています。

デバッグ試行

これから、コードを変更します。

    myfunc(arr, len + 1);

代わりに:

    myfunc(arr, len);
    myfunc(arr, len + 1); /* line 12 */
    myfunc(arr, len);

もっと面白くなります。

次に+ 1、ソースコード全体を読んで理解するだけでなく、より自動化された方法で原因の呼び出しを特定できるかどうかを確認します。

gcc -fsanitize=address GoogleのAddress Sanitizer(ASan)を有効にする

このフラグで再コンパイルしてプログラムを実行すると、次のように出力されます。

#0 0x4008bf in myfunc /home/ciro/test/main.c:4
#1 0x40099b in main /home/ciro/test/main.c:12
#2 0x7fcd2e13d82f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)
#3 0x400798 in _start (/home/ciro/test/a.out+0x40079

その後、さらに色分けされた出力が続きます。

これは明らかに問題のあるライン12を示しています。

これのソースコードはhttps://github.com/google/sanitizersにありますが、例からわかるように、すでにGCCにアップストリームされています。

ASanは、メモリーリークなどの他のメモリー問題も検出できます。C++コード/プロジェクトでメモリーリークを見つける方法は?

Valgrind SGCheck

他の人が言及した、Valgrindのは、この種の問題を解決するのが得意ではありません。

それはSGCheckと呼ばれる実験的なツールを持っています:

SGCheckは、スタックおよびグローバル配列のオーバーランを見つけるためのツールです。これは、スタックおよびグローバル配列アクセスのありそうな形式についての観察から導き出されたヒューリスティックなアプローチを使用して機能します。

したがって、エラーが見つからなかったとしても、それほど驚いてはいません。

valgrind --tool=exp-sgcheck ./a.out

エラーメッセージは次のようになります。Valgrind行方不明エラー

GDB

重要な観察は、GDBを介してプログラムを実行するか、core事後にファイルを調べる場合です。

gdb -nh -q a.out core

次に、アセンブリで見たように、GDBはカナリアチェックを実行した関数の最後をポイントする必要があります。

(gdb) bt
#0  0x00007f0f66e20428 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:54
#1  0x00007f0f66e2202a in __GI_abort () at abort.c:89
#2  0x00007f0f66e627ea in __libc_message (do_abort=do_abort@entry=1, fmt=fmt@entry=0x7f0f66f7a49f "*** %s ***: %s terminated\n") at ../sysdeps/posix/libc_fatal.c:175
#3  0x00007f0f66f0415c in __GI___fortify_fail (msg=<optimized out>, msg@entry=0x7f0f66f7a481 "stack smashing detected") at fortify_fail.c:37
#4  0x00007f0f66f04100 in __stack_chk_fail () at stack_chk_fail.c:28
#5  0x00000000004005f6 in main () at main.c:15
(gdb) f 5
#5  0x00000000004005f6 in main () at main.c:15
15      }
(gdb)

したがって、この関数が行った呼び出しの1つに問題がある可能性があります。

次に、カナリアが設定された直後に最初のシングルステップアップによって、失敗した呼び出しを正確に特定しようとします。

  400581:       64 48 8b 04 25 28 00    mov    %fs:0x28,%rax
  400588:       00 00 
  40058a:       48 89 45 f8             mov    %rax,-0x8(%rbp)

とアドレスを見て:

(gdb) p $rbp - 0x8
$1 = (void *) 0x7fffffffcf18
(gdb) watch 0x7fffffffcf18
Hardware watchpoint 2: *0x7fffffffcf18
(gdb) c
Continuing.

Hardware watchpoint 2: *0x7fffffffcf18

Old value = 1800814336
New value = 1800814378
myfunc (src=0x7fffffffcf14 "*****?Vk\266", <incomplete sequence \355\216>, len=5) at main.c:3
3           for (i = 0; i < len; ++i) {
(gdb) p len
$2 = 5
(gdb) p i
$3 = 4
(gdb) bt
#0  myfunc (src=0x7fffffffcf14 "*****?Vk\266", <incomplete sequence \355\216>, len=5) at main.c:3
#1  0x00000000004005cc in main () at main.c:12

さて、これは正しい問題の指示に私たちを残します:len = 5そしてi = 4、そしてこの特定のケースでは、犯人の12行目を私たちに示しました。

ただし、バックトレースは破損しており、ゴミが含まれています。正しいバックトレースは次のようになります。

#0  myfunc (src=0x7fffffffcf14 "abcd", len=4) at main.c:3
#1  0x00000000004005b8 in main () at main.c:11

そのため、スタックが破損し、トレースが表示されなくなる可能性があります。

また、この方法では、カナリアチェック関数の最後の呼び出しが何であるかを知る必要があります。そうでない場合は、誤検知が発生します。これは、リバースデバッグ使用しない限り、常に実行できるとは限りません。


16

次の状況を見てください。

ab@cd-x:$ cat test_overflow.c 
#include <stdio.h>
#include <string.h>

int check_password(char *password){
    int flag = 0;
    char buffer[20];
    strcpy(buffer, password);

    if(strcmp(buffer, "mypass") == 0){
        flag = 1;
    }
    if(strcmp(buffer, "yourpass") == 0){
        flag = 1;
    }
    return flag;
}

int main(int argc, char *argv[]){
    if(argc >= 2){
        if(check_password(argv[1])){
            printf("%s", "Access granted\n");
        }else{
            printf("%s", "Access denied\n");
        }
    }else{
        printf("%s", "Please enter password!\n");
    }
}
ab@cd-x:$ gcc -g -fno-stack-protector test_overflow.c 
ab@cd-x:$ ./a.out mypass
Access granted
ab@cd-x:$ ./a.out yourpass
Access granted
ab@cd-x:$ ./a.out wepass
Access denied
ab@cd-x:$ ./a.out wepassssssssssssssssss
Access granted

ab@cd-x:$ gcc -g -fstack-protector test_overflow.c 
ab@cd-x:$ ./a.out wepass
Access denied
ab@cd-x:$ ./a.out mypass
Access granted
ab@cd-x:$ ./a.out yourpass
Access granted
ab@cd-x:$ ./a.out wepassssssssssssssssss
*** stack smashing detected ***: ./a.out terminated
======= Backtrace: =========
/lib/tls/i686/cmov/libc.so.6(__fortify_fail+0x48)[0xce0ed8]
/lib/tls/i686/cmov/libc.so.6(__fortify_fail+0x0)[0xce0e90]
./a.out[0x8048524]
./a.out[0x8048545]
/lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe6)[0xc16b56]
./a.out[0x8048411]
======= Memory map: ========
007d9000-007f5000 r-xp 00000000 08:06 5776       /lib/libgcc_s.so.1
007f5000-007f6000 r--p 0001b000 08:06 5776       /lib/libgcc_s.so.1
007f6000-007f7000 rw-p 0001c000 08:06 5776       /lib/libgcc_s.so.1
0090a000-0090b000 r-xp 00000000 00:00 0          [vdso]
00c00000-00d3e000 r-xp 00000000 08:06 1183       /lib/tls/i686/cmov/libc-2.10.1.so
00d3e000-00d3f000 ---p 0013e000 08:06 1183       /lib/tls/i686/cmov/libc-2.10.1.so
00d3f000-00d41000 r--p 0013e000 08:06 1183       /lib/tls/i686/cmov/libc-2.10.1.so
00d41000-00d42000 rw-p 00140000 08:06 1183       /lib/tls/i686/cmov/libc-2.10.1.so
00d42000-00d45000 rw-p 00000000 00:00 0 
00e0c000-00e27000 r-xp 00000000 08:06 4213       /lib/ld-2.10.1.so
00e27000-00e28000 r--p 0001a000 08:06 4213       /lib/ld-2.10.1.so
00e28000-00e29000 rw-p 0001b000 08:06 4213       /lib/ld-2.10.1.so
08048000-08049000 r-xp 00000000 08:05 1056811    /dos/hacking/test/a.out
08049000-0804a000 r--p 00000000 08:05 1056811    /dos/hacking/test/a.out
0804a000-0804b000 rw-p 00001000 08:05 1056811    /dos/hacking/test/a.out
08675000-08696000 rw-p 00000000 00:00 0          [heap]
b76fe000-b76ff000 rw-p 00000000 00:00 0 
b7717000-b7719000 rw-p 00000000 00:00 0 
bfc1c000-bfc31000 rw-p 00000000 00:00 0          [stack]
Aborted
ab@cd-x:$ 

スタックスマッシングプロテクターを無効にしても、「./ a.out wepasssssssssssssssssss」を使用したときに発生するはずのエラーは検出されませんでした。

したがって、上記の質問に答えるために、「**スタックスマッシングが検出されました:xxx」というメッセージが表示されました。これは、スタックスマッシングプロテクターがアクティブで、プログラムにスタックオーバーフローがあることが判明したためです。

それが発生する場所を見つけて修正します。


7

valgrindを使用して問題をデバッグしようとすることができます:

現在、Valgrindディストリビューションには、メモリエラー検出器、2つのスレッドエラー検出器、キャッシュおよびブランチ予測プロファイラー、コールグラフ生成キャッシュプロファイラー、およびヒーププロファイラーという、6つの製品品質のツールが含まれています。また、ヒープ/スタック/グローバルアレイオーバーラン検出器とSimPoint基本ブロックベクトルジェネレーターの2つの実験ツールも含まれてい ます。X86 / Linux、AMD64 / Linux、PPC32 / Linux、PPC64 / Linux、およびX86 / Darwin(Mac OS X)で動作します。


2
ええ、でもValgrindはスタック割り当てバッファのオーバーフローに対してうまく機能しません。これは、このエラーメッセージが示す状況です。
DW

4
スタックアレイオーバーラン検出器をどのように使用できますか?詳しく説明できますか?
クレイグマックイーン

@CraigMcQueen最小限の例でValgrindの実験的ヒューリスティックSGCheckスタックスマッシング検出器を使用しようとしましたが、stackoverflow.com / a / 51897264/895245 が失敗しました。
Ciro Santilli郝海东冠状病六四事件法轮功

4

これは、おそらくバッファオーバーフローの結果として、スタック上のいくつかの変数に不正な方法で書き込んだことを意味します。


9
スタックオーバーフローとは、スタックが他の何かに衝突することです。ここでそれは逆です:何かがスタックに破壊されました。
Peter Mortensen

5
あんまり。これは、スタックの一部が別の部分に破壊されます。したがって、これは実際にはバッファオーバーフローであり、スタックのトップではなく、スタックの別の部分への「のみ」です。
Bas Wijnen、2012年

2

これの考えられる理由は何ですか?それをどのように修正しますか?

1つのシナリオは、次の例のようになります。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void swap ( char *a , char *b );
void revSTR ( char *const src );

int main ( void ){
    char arr[] = "A-B-C-D-E";

    revSTR( arr );
    printf("ARR = %s\n", arr );
}

void swap ( char *a , char *b ){
    char tmp = *a;
    *a = *b;
    *b = tmp;
}

void revSTR ( char *const src ){
    char *start = src;
    char *end   = start + ( strlen( src ) - 1 );

    while ( start < end ){
        swap( &( *start ) , &( *end ) );
        start++;
        end--;
    }
}

このプログラムでは、たとえばreverse()次のように呼び出した場合、文字列または文字列の一部を逆にすることができます。

reverse( arr + 2 );

次のように配列の長さを渡すことにした場合:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void swap ( char *a , char *b );
void revSTR ( char *const src, size_t len );

int main ( void ){
    char arr[] = "A-B-C-D-E";
    size_t len = strlen( arr );

    revSTR( arr, len );
    printf("ARR = %s\n", arr );
}

void swap ( char *a , char *b ){
    char tmp = *a;
    *a = *b;
    *b = tmp;
}

void revSTR ( char *const src, size_t len ){
    char *start = src;
    char *end   = start + ( len - 1 );

    while ( start < end ){
        swap( &( *start ) , &( *end ) );
        start++;
        end--;
    }
}

うまくいきます。

しかし、これを行うと:

revSTR( arr + 2, len );

あなたは得る:

==7125== Command: ./program
==7125== 
ARR = A-
*** stack smashing detected ***: ./program terminated
==7125== 
==7125== Process terminating with default action of signal 6 (SIGABRT)
==7125==    at 0x4E6F428: raise (raise.c:54)
==7125==    by 0x4E71029: abort (abort.c:89)
==7125==    by 0x4EB17E9: __libc_message (libc_fatal.c:175)
==7125==    by 0x4F5311B: __fortify_fail (fortify_fail.c:37)
==7125==    by 0x4F530BF: __stack_chk_fail (stack_chk_fail.c:28)
==7125==    by 0x400637: main (program.c:14)

そして、これは最初のコードで、 arrがチェックますrevSTR()が、2番目のコードでは長さを渡します。

revSTR( arr + 2, len );

長さは、言うときに実際に渡す長さよりも長くなりましたarr + 2

strlen ( arr + 2 )!=の長さstrlen ( arr )


1
getsandのような標準ライブラリ関数に依存しないため、この例が好きscrcpyです。それ以上なら最小化できるかな。私は、少なくとも取り除くだろうstring.hsize_t len = sizeof( arr );。gcc 6.4、Ubuntu 16.04でテスト済み。また、arr + 2コピーの貼り付けを最小限に抑えるために失敗する例を示します。
Ciro Santilli郝海东冠状病六四事件法轮功

1

通常はバッファオーバーフローが原因で発生するスタックの破損。あなたは防御的にプログラミングすることによってそれらから守ることができます。

配列にアクセスするときは常に、その前にアサートを置き、アクセスが範囲外にならないようにします。例えば:

assert(i + 1 < N);
assert(i < N);
a[i + 1] = a[i];

これにより、配列の境界について考えることができ、可能であればそれらをトリガーするテストを追加することについても考えることができます。これらのアサートの一部が通常の使用中に失敗する可能性がある場合は、通常の状態に変更してくださいif


0

malloc()を使用して構造体にメモリを割り当てるときにこのエラーが発生しました*このコードのデバッグに費やした後、最終的にfree()関数を使用して割り当てられたメモリを解放し、その後エラーメッセージが消えました:)


0

スタックスマッシングのもう1つの原因は、のvfork()代わりに(不正な)を使用することですfork()

子プロセスがexecve()ターゲットの実行可能ファイルにアクセスできず、呼び出しではなくエラーコードを返す、このケースをデバッグしたところです_exit()です。

vfork()はその子を生成したため、親のプロセススペース内で実際に実行中に戻り、親のスタックを破壊するだけでなく、2つの異なる診断セットを「ダウンストリーム」コードによって出力させました。

子供の声明を代わりに変更したのと同様に、変更vfork()するとfork()両方の問題が修正さreturn_exit()ました。

ただし、子コードはexecve()呼び出しの前に他のルーチン(この特定のケースではuid / gidを設定するため)への呼び出しの前にあるため、技術的にはの要件を満たしていないため、ここでvfork()使用するように変更することfork()が正しいです。

問題のあること(注return代わりに、マクロが呼び出された、そしてそのマクロがするかどうかを決めた-文は、実際のようなコード化されていなかった_exit()か、returnグローバル変数に基づいて、子コードがために不適合たことがすぐに明らかではなかったので。vfork()使用方法。 )

詳細については、以下を参照してください。

fork()、vfork()、exec()およびclone()の違い

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.