检测到堆栈粉碎


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,2009年

我认为这是溢出错误的代名词。例如,如果您初始化并包含5个元素的数组,则在尝试编写第6个元素或数组边界之外的任何元素时,都会出现此错误。
DorinPopescu 18/09/12

Answers:


349

实际上,此处的堆栈粉碎是由gcc用于检测缓冲区溢出错误的保护机制引起的。例如下面的代码段:

#include <stdio.h>

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

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

编译器(在本例中为gcc)添加了具有已知值的保护变量(称为canaries)。输入字符串的大小大于10会导致此变量损坏,从而导致SIGABRT终止程序。

为了获得一些见识,您可以尝试 -fno-stack-protector 在编译时使用option禁用对gcc的保护 。在这种情况下,您将得到一个不同的错误,很可能是段错误,因为您尝试访问非法的内存位置。请注意,-fstack-protector对于发行版本,应始终将其打开,因为它是一项安全功能。

您可以通过使用调试器运行程序来获取有关溢出点的一些信息。Valgrind不能很好地解决与堆栈相关的错误,但是像调试器一样,它可以帮助您查明崩溃的位置和原因。


3
感谢您的回答!我发现就我而言,我没有初始化要写入的变量
Ted Pennings 2012年

5
Valgrind不能很好地解决与堆栈相关的错误,因为它无法在其中添加红色区域
toasted_flakes 2014年

7
该答案是错误的,并提供了危险的建议。首先,删除堆栈保护器不是正确的解决方案-如果遇到堆栈粉碎错误,则您的代码中可能存在严重的安全漏洞。正确的响应是修复错误的代码。其次,正如grasGendarme指出的那样,尝试使用Valgrind的建议将无效。Valgrind通常不适用于检测对堆栈分配的数据的非法内存访问。
DW 2014年

22
OP为此行为询问可能的原因,我的回答提供了一个示例以及它与合理已知错误的关系。此外,卸下堆栈保护器不是解决方案,这是一种实验,可以使人们对问题有更多的了解。建议实际上是以某种方式修复错误,感谢您指出valgrind,我将编辑答案以反映这一点。
sud03r 2014年

4
@DW应该在发行版中关闭堆栈保护,因为起初- 堆栈检测到的错误消息仅对开发人员有帮助;第二-应用程序可能还没有生存的机会;第三,这是一个很小的优化。
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运行程序,或者core事后检查文件:

gdb -nh -q a.out core

然后,正如我们在程序集上看到的那样,GDB应该将您指向进行canary检查的函数的结尾:

(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)

因此,此函数调用之一很可能会出现问题。

接下来,我们尝试在设置金丝雀之后立即进行一次单步查找,以找出确切的失败呼叫:

  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 = 5i = 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发行版当前包括六个生产质量工具:一个内存错误检测器,两个线程错误检测器,一个缓存和分支预测探查器,一个生成调用图的缓存探查器和一个堆探查器。它还包括两个实验工具: 堆/堆栈/全局数组溢出检测器和SimPoint基本块矢量生成器。它可以在以下平台上运行:X86 / Linux,AMD64 / Linux,PPC32 / Linux,PPC64 / Linux和X86 / Darwin(Mac OS X)。


2
是的,但是Valgrind对于堆栈分配的缓冲区溢出并不能很好地工作,这就是此错误消息所指示的情况。
DW 2014年

4
我们如何使用该堆栈数组溢出检测器?你能详细说明吗?
Craig McQueen 2014年

@CraigMcQueen我试图在一个最小的示例上使用Valgrind的实验启发式SGCheck堆栈粉碎检测器:stackoverflow.com/a/51897264/895245,但它失败了。
西罗Santilli郝海东冠状病六四事件法轮功

4

这意味着您以非法方式写入了堆栈中的某些变量,这很可能是Buffer溢出的结果。


9
堆栈溢出是指堆栈粉碎成其他东西。这是另一回事:某些东西已砸入堆栈。
Peter Mortensen

5
并不是的。它是堆栈的一部分,粉碎成另一部分。因此,这确实是缓冲区溢出,不仅不在堆栈顶部,而且“仅”进入堆栈的另一部分。
Bas Wijnen 2012年

2

可能是什么原因造成的,我该如何纠正?

下例是一种情况:

#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()其中的长度,但是在第二个代码中,您可以通过该长度来检查长度:

revSTR( arr + 2, len );

现在的长度大于您说的实际长度arr + 2

strlen ( arr + 2 )!=的长度strlen ( arr )


1
我喜欢这个示例,因为它不依赖于标准库函数,例如getsscrcpy。我想知道是否可以进一步减少这种情况。我至少会摆脱string.hsize_t len = sizeof( arr );。在gcc 6.4,Ubuntu 16.04上测试。我还将给出失败的示例,arr + 2以最大程度地减少复制粘贴。
西罗Santilli郝海东冠状病六四事件法轮功

1

通常由缓冲区溢出引起的堆栈损坏。您可以通过防御性编程来防御它们。

每当您访问数组时,都要在其前面放置一个断言,以确保访问不会超出范围。例如:

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

这使您考虑数组边界,也使您考虑添加测试以在可能的情况下触发它们。如果这些断言中的一些断言在正常使用中失败,则将它们变成常规断言if


0

我在使用malloc()将一些内存分配给结构*时花了一些调试代码的错误,最后我使用free()函数释放了分配的内存,随后错误消息消失了:)


0

堆栈崩溃的另一个原因是(不正确)使用vfork()代替fork()

我只是调试了一个情况,子进程无法访问execve()目标可执行文件,并返回错误代码,而不是调用_exit()

因为vfork()已经产生了那个孩子,所以它实际上仍在父进程的处理空间中执行时返回,这不仅破坏了父进程的堆栈,而且导致两个不同的诊断集通过“下游”代码打印。

更改vfork()fork()解决两个问题,将孩子的return陈述_exit()改为。

但是由于子代码在execve()调用之前先调用其他例程(在这种情况下设置uid / gid),因此从技术上讲,它不满足的要求vfork(),因此fork()在此处将其更改为正确的用法。

(请注意,有问题的return语句实际上并不是这样编码的-而是调用了一个宏,并且该宏决定了是否基于全局变量_exit()return基于全局变量。因此,立即发现子代代码在vfork()用法上不符合要求并不是很明显。 )

有关更多信息,请参见:

fork(),vfork(),exec()和clone()之间的区别

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.