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
を使用してこの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
このフラグを使用して再コンパイルしてプログラムを実行すると、次が出力されます。
#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が明確に特定されます。
Valgrind SGCheck
他の人が言及 のように、Valgrindはこの種の問題を解決するのが得意ではありません。
実験ツールがあります SGCheckと呼ばれます :
SGCheckは、スタックとグローバル配列のオーバーランを見つけるためのツールです。スタックおよびグローバル配列アクセスの可能性のある形式についての観察から導出された発見的アプローチを使用して機能します。
だから、エラーを見つけられなかったとき、私はあまり驚きませんでした:
valgrind --tool=exp-sgcheck ./a.out
エラーメッセージは明らかに次のようになります。 Valgrind missing error
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 wepassssssssssssssssss」を使用したときに発生していたはずです。
上記の質問に答えるために、スタック破壊プロテクタがアクティブで、プログラムにスタックオーバーフローがあることが判明したため、メッセージ「**スタック破壊が検出されました:xxx」が表示されました。
それがどこで発生しているのかを見つけて修正してください。
valgrind を使って問題をデバッグすることができます。
Valgrindディストリビューションには現在、6つのプロダクション品質ツールが含まれています。1つはメモリエラー検出器、2つはスレッドエラー検出器、1つはキャッシュおよび分岐予測プロファイラ、もう1つはコールグラフ生成キャッシュプロファイラ、およびヒーププロファイラです。それはまた2つの実験的なツールを含んでいます:ヒープ/スタック/グローバル配列オーバーラン検出器、およびSimPoint基本ブロックベクトルジェネレータ。 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 )
の長さ。
通常、バッファオーバーフローによって引き起こされるスタック破損。あなたは防御的にプログラミングすることによって彼らに対して防御することができます。
配列にアクセスするときはいつでも、アクセスが範囲外にならないようにするためにその前にアサートを入れてください。例えば:
assert(i + 1 < N);
assert(i < N);
a[i + 1] = a[i];
これにより、配列の境界について考えることができ、また可能であればそれらをトリガーするテストを追加することについて考えることもできます。これらのアサートのいくつかが通常の使用中に失敗する可能性がある場合は、それらを通常のif
に変換してください。
コードのデバッグにいくらか費やした後、malloc()を使用して構造体*にメモリを割り当てる際にこのエラーが発生し、最後にfree()関数を使用して割り当てられたメモリを解放し、その後エラーメッセージが消えました。
スタックスマッシングのもう1つの原因は、(誤った)vfork()
ではなくfork()
の使用です。
この場合をデバッグしたところです。子プロセスはターゲット実行可能ファイルをexecve()
できず、_exit()
を呼び出すのではなくエラーコードを返しました。
vfork()
がその子を生成したので、それは実際にはまだ親のプロセス空間内で実行されている間に戻り、親のスタックを破壊するだけでなく、2つの異なる診断セットを「ダウンストリーム」コードで表示しました。
vfork()
をfork()
に変更すると、両方の問題が修正され、代わりに子のreturn
ステートメントを_exit()
に変更しました。
しかし、子コードはexecve()
呼び出しの前に(この場合はuid/gidを設定するために)呼び出されるので、技術的にはvfork()
の要件を満たしていないので、ここではfork()
を使用するように変更します。
(問題のあるreturn
ステートメントは実際にはそのようにコーディングされていないことに注意してください。代わりにマクロが呼び出され、そのマクロはグローバル変数に基づいて_exit()
またはreturn
のどちらを決定しました。 vfork()
の使用法)
詳しくは、以下を参照してください。