a.outファイルを実行しています。実行後、プログラムはしばらく実行され、次のメッセージで終了します。
**** stack smashing detected ***: ./a.out terminated*
*======= Backtrace: =========*
*/lib/tls/i686/cmov/libc.so.6(__fortify_fail+0x48)Aborted*
これの考えられる理由は何ですか?それをどのように修正しますか?
a.outファイルを実行しています。実行後、プログラムはしばらく実行され、次のメッセージで終了します。
**** stack smashing detected ***: ./a.out terminated*
*======= Backtrace: =========*
*/lib/tls/i686/cmov/libc.so.6(__fortify_fail+0x48)Aborted*
これの考えられる理由は何ですか?それをどのように修正しますか?
回答:
ここでのスタックスマッシングは、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はスタック関連のエラーではうまく機能しませんが、デバッガーのように、クラッシュの場所と理由を特定するのに役立ちます。
分解解析を使用した最小限の再現例
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;
}
コンパイルして実行:
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
そのため、スタックが破損し、トレースが表示されなくなる可能性があります。
また、この方法では、カナリアチェック関数の最後の呼び出しが何であるかを知る必要があります。そうでない場合は、誤検知が発生します。これは、リバースデバッグを使用しない限り、常に実行できるとは限りません。
次の状況を見てください。
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」というメッセージが表示されました。これは、スタックスマッシングプロテクターがアクティブで、プログラムにスタックオーバーフローがあることが判明したためです。
それが発生する場所を見つけて修正します。
valgrindを使用して問題をデバッグしようとすることができます:
現在、Valgrindディストリビューションには、メモリエラー検出器、2つのスレッドエラー検出器、キャッシュおよびブランチ予測プロファイラー、コールグラフ生成キャッシュプロファイラー、およびヒーププロファイラーという、6つの製品品質のツールが含まれています。また、ヒープ/スタック/グローバルアレイオーバーラン検出器とSimPoint基本ブロックベクトルジェネレーターの2つの実験ツールも含まれてい ます。X86 / Linux、AMD64 / Linux、PPC32 / Linux、PPC64 / Linux、およびX86 / Darwin(Mac OS X)で動作します。
これは、おそらくバッファオーバーフローの結果として、スタック上のいくつかの変数に不正な方法で書き込んだことを意味します。
これの考えられる理由は何ですか?それをどのように修正しますか?
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 )
。
gets
andのような標準ライブラリ関数に依存しないため、この例が好きscrcpy
です。それ以上なら最小化できるかな。私は、少なくとも取り除くだろうstring.h
とsize_t len = sizeof( arr );
。gcc 6.4、Ubuntu 16.04でテスト済み。また、arr + 2
コピーの貼り付けを最小限に抑えるために失敗する例を示します。
通常はバッファオーバーフローが原因で発生するスタックの破損。あなたは防御的にプログラミングすることによってそれらから守ることができます。
配列にアクセスするときは常に、その前にアサートを置き、アクセスが範囲外にならないようにします。例えば:
assert(i + 1 < N);
assert(i < N);
a[i + 1] = a[i];
これにより、配列の境界について考えることができ、可能であればそれらをトリガーするテストを追加することについても考えることができます。これらのアサートの一部が通常の使用中に失敗する可能性がある場合は、通常の状態に変更してくださいif
。
スタックスマッシングのもう1つの原因は、のvfork()
代わりに(不正な)を使用することですfork()
。
子プロセスがexecve()
ターゲットの実行可能ファイルにアクセスできず、呼び出しではなくエラーコードを返す、このケースをデバッグしたところです_exit()
です。
vfork()
はその子を生成したため、親のプロセススペース内で実際に実行中に戻り、親のスタックを破壊するだけでなく、2つの異なる診断セットを「ダウンストリーム」コードによって出力させました。
子供の声明を代わりに変更したのと同様に、変更vfork()
するとfork()
両方の問題が修正さreturn
れ_exit()
ました。
ただし、子コードはexecve()
呼び出しの前に他のルーチン(この特定のケースではuid / gidを設定するため)への呼び出しの前にあるため、技術的にはの要件を満たしていないため、ここでvfork()
使用するように変更することfork()
が正しいです。
問題のあること(注return
代わりに、マクロが呼び出された、そしてそのマクロがするかどうかを決めた-文は、実際のようなコード化されていなかった_exit()
か、return
グローバル変数に基づいて、子コードがために不適合たことがすぐに明らかではなかったので。vfork()
使用方法。 )
詳細については、以下を参照してください。