堆栈溢出检测到

365

我正在执行我的 a.out 文件。执行后,程序运行一段时间后会退出,并显示以下消息:

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

这可能有哪些可能的原因,我该如何纠正它?


3
你能否确定是代码的哪些部分导致了堆栈溢出,并将其发布出来吗?然后我们可能就能够准确地指出为什么会发生这种情况以及如何进行修正。 - Bjarke Freund-Hansen
3
我认为它与溢出错误同义。例如,如果您初始化一个包含5个元素的数组,在尝试写入第6个元素或任何超出数组边界的元素时,将出现此错误。 - DorinPopescu
10个回答

472

栈溢出实际上是由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 禁用gcc的此保护。在这种情况下,您将获得不同的错误,很可能是分段错误,因为您尝试访问非法内存位置。请注意,-fstack-protector应始终打开以进行发布版本构建,因为它是一个安全功能。
您可以通过使用调试器运行程序来获取有关溢出点的一些信息。Valgrind在处理堆栈相关错误方面效果不佳,但像调试器一样,它可能会帮助您确定崩溃的位置和原因。

6
谢谢您的回复!我发现在我的情况下,我没有初始化我想要写入的变量。 - Ted Pennings
5
Valgrind 在处理与栈相关的错误时效果不佳,因为它无法在栈上添加红色区域。 - toasted_flakes
9
这个答案是不正确的,并且提供了危险的建议。首先,移除堆栈保护并不是正确的解决方案——如果你收到“stack smashing error”(栈溢出)错误,那么你的代码可能存在严重的安全漏洞。正确的做法是修复有缺陷的代码。其次,就像grasGendarme指出的那样,尝试使用Valgrind不会产生有效的结果。Valgrind通常无法检测到对基于栈分配的数据进行的非法内存访问。 - D.W.
46
OP询问这种行为可能的原因,我的回答提供了一个例子,并解释了它与一个相对知名的错误的关联。此外,移除栈保护并不是一个解决方案,而是一种可以进行的实验,以便更深入地了解问题。建议实际上是要修复错误,感谢您指出valgrind,我将编辑我的答案以反映这一点。 - sud03r
4
在发布版本中应该关闭堆栈保护,因为首先,“stack smashing detected”消息仅对开发人员有帮助;其次,应用程序可能还有生存的机会;第三,这是一项微小的优化。 - Hi-Angel
显示剩余8条评论

90

带有反汇编分析的最小复现示例

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); /* Cause smashing by writing one byte too many. */
    return 0;
}

GitHub源码.

编译并运行:

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

符合预期的失败:

*** stack smashing detected ***: terminated
Aborted (core dumped)

在Ubuntu 20.04、GCC 10.2.0上进行测试。

在Ubuntu 16.04、GCC 6.4.0上,我可以通过使用-fstack-protector代替-fstack-protector-all来重现此问题,但当我按照Geng Jiawen的评论中所述在GCC 10.2.0上进行测试时,程序不再崩溃。根据man gcc的说明,正如选项名称建议的那样,-all版本会更积极地添加检查,因此可能会导致更大的性能损失:

-fstack-protector

发出额外的代码以检查缓冲区溢出,例如堆栈破坏攻击。这是通过向具有易受攻击对象的函数添加保护变量来完成的。这包括调用"alloca"的函数和具有大于或等于8字节的缓冲区的函数。保护变量在进入函数时初始化,然后在函数退出时进行检查。如果保护检查失败,则打印错误消息并退出程序。只考虑实际分配在堆栈上的变量,优化掉的变量或分配在寄存器中的变量不计入其中。

-fstack-protector-all

与-fstack-protector相似,但所有函数都受到保护。

反汇编

现在我们来看一下反汇编:

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的地址无害化器(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 missing error

GDB

一个重要的观察是,如果你通过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

所以这可能会破坏堆栈并阻止您查看跟踪。

此外,该方法需要知道canary检查函数的最后一次调用是什么,否则您将得到错误的结果,这并不总是可行的,除非您使用反向调试


1
在像gcc 10这样的更高版本中,您可能需要使用-fstack-protector-all。演示:https://godbolt.org/z/n1cqPn - Geng Jiawen
补充上面的评论:-fstack-protector-strong 也可以起作用。 - valiano
1
由于这里没有 "inb4 2022",所以我想提醒大家,自 2022 年起,使用人工智能来评论汇编语言并可能在 C 中反编译它 已经成为现实(尽管我仍然在浏览 Stack Overflow)。 - dteod
1
@dteod 哈哈,好的没问题!!! - Ciro Santilli OurBigBook.com
2
哇,你真是天使啊。这个答案会让任何C开发者向前迈进好几英里呢。 - Özgür

19
请看下面的情况:
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"时应该会出现错误。

因此,回答您上面的问题,消息“** stack smashing detected : xxx”显示是因为您的堆栈保护器处于活动状态,并发现程序中存在堆栈溢出。

只需找出发生这种情况的位置并进行修复即可。


8
您可以尝试使用Valgrind来调试问题:

Valgrind发行版目前包括六个生产质量的工具:一个内存错误检测器,两个线程错误检测器,一个缓存和分支预测分析器,一个生成调用图的缓存分析器以及一个堆分析器。它还包括两个实验性工具:一个堆/栈/全局数组溢出检测器和一个SimPoint基本块向量生成器。它适用于以下平台:X86/Linux、AMD64/Linux、PPC32/Linux、PPC64/Linux和X86/Darwin(Mac OS X)。


2
是的,但Valgrind对于堆栈分配缓冲区的溢出情况效果不佳,而这正是此错误消息所指示的情况。 - D.W.
4
我们如何使用那个栈数组越界检测器?你能详细说明一下吗? - Craig McQueen
1
@CraigMcQueen 我曾尝试在一个最小的示例上使用Valgrind的实验性启发式SGCheck堆栈破坏检测器:https://dev59.com/CnM_5IYBdhLWcg3wgzhl#51897264,但失败了。 - Ciro Santilli OurBigBook.com

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

9
堆栈溢出是堆栈砸向其他东西。但在这里,情况恰好相反:某些东西已经砸进了堆栈。 - Peter Mortensen
6
不完全是。它只是堆栈的一部分撞上了另一部分。因此,它确实是缓冲区溢出,但不是在堆栈的顶部,而是“仅仅”溢出到堆栈的另一部分。 - Bas Wijnen

4
可能的原因是什么,如何纠正这个问题?一个可能的情况是以下示例:
#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)

这是由于在第一个代码中,在 revSTR() 中检查了 arr 的长度,这是可以的。但在第二个代码中,您传递了长度:

revSTR( arr + 2, len );

长度现在比你输入时说的 arr + 2 的实际长度要长。

strlen ( arr + 2 ) 的长度不等于 strlen ( arr )

1
我喜欢这个例子,因为它不依赖于标准库函数,如getsscrcpy。我想知道我们是否可以进一步将其最小化。至少我会用size_t len = sizeof(arr);来摆脱string.h。在gcc 6.4,Ubuntu 16.04上测试过。我还会给出失败的例子,使用arr + 2来最小化复制粘贴。 - Ciro Santilli OurBigBook.com

4

堆栈破坏通常由于缓冲区溢出引起。您可以通过进行防御性编程来防范它们。

每当您访问一个数组时,请在其前面放置一个assert来确保访问不越界。例如:

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

这会让你想到数组边界,并考虑在可能的情况下添加触发它们的测试。如果某些断言在正常使用中可能会失败,则将它们转换为常规的if语句。


0
在使用malloc()为一个struct *分配内存时,我遇到了这个错误。经过一番调试代码后,我最终使用了free()函数来释放已分配的内存,随后错误消息消失了 :)

0

栈溢出的另一个来源是使用vfork()而不是fork()(不正确地)。

我刚刚调试了这样一个案例,其中子进程无法execve()目标可执行文件,并返回错误代码而不是调用_exit()

因为vfork()已经生成了该子进程,所以它在实际上仍在父进程的进程空间中执行时返回,不仅破坏了父进程的堆栈,还导致“下游”代码打印了两组不同的诊断信息。

vfork()更改为fork()解决了这两个问题,将子进程的return语句更改为_exit()也可以解决。

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

(请注意,存在问题的return语句实际上并不是以这种方式编码的 - 而是调用了一个宏,该宏根据全局变量决定是否使用_exit()return。因此,孩子代码的vfork()使用不符合规范并不明显。)

更多信息,请参见:

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


0

当我编辑结构体时,遇到了这个问题,但没有重新编译使用该结构体的库。在某个大型项目中,我向结构体添加了新字段,然后在lib_struct中从json解析,这个库稍后用于小部件以显示已解析的内容。我的make文件没有涵盖依赖项,因此在编辑结构体后,库没有重新编译。我的解决方案是重新编译所有使用该结构体的内容。


这并没有真正回答问题。如果您有不同的问题,可以通过点击提问来提出。如果您想在此问题获得新的答案时得到通知,您可以关注此问题。一旦您拥有足够的声望,您还可以添加悬赏以吸引更多关注此问题的人。- 来自审核 - Sangeerththan Balachandran
1
@SangeerththanBalachandran 我认为它回答了问题,即“可能的原因是什么,我该如何纠正它?”。我提供了一个在答案列表中没有看到的原因,并添加了解决方案,这解决了我的问题。 - ygroeg
这并不是OP遇到的问题,你所面临的问题是与你所参与的项目的makefile有关。 - Sangeerththan Balachandran
@SangeerththanBalachandran 我认为,如果同一个问题有不同的原因,为什么不能发布不同解决方案和思考过程的路径呢?被标记为正确的解决方案将无法解决makefile问题。事实上,OP没有遇到这个问题,并不意味着所有遇到这个错误的人都能像OP一样解决它。很多人在他们的项目中使用makefiles,其中很多人可能会犯错误。 - ygroeg
在这种情况下,进一步提供具体发生了什么样的错误将会很有用。 - Sangeerththan Balachandran
@SangeerththanBalachandran 如果您想为此答案提供有用的具体示例,请随意在下面发布。我将引用您的回复并添加到答案中。我指出了错误和这种情况的根本原因,而没有涉及我的项目规范。解决方案非常简单,但示例可能不同,例如make、cmake、qmake等。 - ygroeg

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接