我正在执行我的a.out文件。执行后,程序运行一段时间,然后带着消息退出:

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

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


您可以尝试使用valgrind调试问题:

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


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


这里的Stack Smashing实际上是由gcc用于检测缓冲区溢出错误的保护机制引起的。例如,在下面的代码片段中:

#include <stdio.h>

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

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

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

你可以尝试在编译时使用option -fno-stack-protector禁用gcc的这种保护。在这种情况下,你会得到一个不同的错误,最有可能的分割错误,因为你试图访问一个非法的内存位置。注意-fstack-protector应该在发布版本中始终打开,因为它是一个安全特性。

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


请看下面的情况:

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。wepassssssssssssssssss”

所以回答你上面的问题,消息“**堆栈粉碎检测到:xxx”显示,因为你的堆栈粉碎保护者是活跃的,发现有堆栈溢出在你的程序。

只要找到发生的地方,然后修复它。


我得到了这个错误,而使用malloc()分配一些内存到一个结构*后,花了一些这个调试代码,我最终使用free()函数来释放分配的内存,随后错误消息消失了:)


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

一个场景是下面的例子:

#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)。


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

无论何时访问数组,都要在数组前面加上断言,以确保访问没有越界。例如:

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

这使您考虑数组边界,并使您考虑在可能的情况下添加测试来触发它们。如果其中一些断言在正常使用期间失败,则将它们转换为常规If。


另一个堆栈破坏的来源是(不正确)使用vfork()而不是fork()。

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

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

将vfork()更改为fork()修复了这两个问题,将子函数的return语句改为_exit()也解决了这两个问题。

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

(请注意,有问题的return语句实际上并不是这样编码的——相反,调用了一个宏,该宏根据一个全局变量决定是_exit()还是返回。因此,子代码不符合vfork()的用法并不是很明显。

有关更多信息,请参见:

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


最小复制实例与拆卸分析

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-protect -all进行复制,但当我在GCC 10.2.0上测试时,它停止了崩溃,正如Geng Jiawen的评论所述。Man GCC澄清,正如选项名称所暗示的那样,-all版本更积极地添加检查,因此可能会导致更大的性能损失:

-fstack-protector Emit extra code to check for buffer overflows, such as stack smashing attacks. This is done by adding a guard variable to functions with vulnerable objects. This includes functions that call "alloca", and functions with buffers larger than or equal to 8 bytes. The guards are initialized when a function is entered and then checked when the function exits. If a guard check fails, an error message is printed and the program exits. Only variables that are actually allocated on the stack are considered, optimized away variables or variables allocated in registers don't count. -fstack-protector-all Like -fstack-protector except that all functions are protected.

拆卸

现在我们来看分解:

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来随机化,它包含一个随机值,如在下面解释的那样:

https://unix.stackexchange.com/questions/453749/what-sets-fs0x28-stack-canary 为什么这个内存地址%fs:0x28 (fs[0x28])有一个随机值?

调试的尝试

从现在开始,我们修改代码:

    myfunc(arr, len + 1);

取而代之:

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

为了更有趣。

然后,我们将尝试使用比阅读和理解整个源代码更自动化的方法来确定罪魁祸首+ 1调用。

gcc -fsanitize=address启用谷歌的地址消毒器(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运行程序,或者在事后检查核心文件:

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)

因此问题可能出在这个函数的某个调用中。

接下来,我们试图通过在金丝雀设置后的第一个单一步骤来精确定位失败的调用:

  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

因此,这可能会破坏堆栈并阻止您看到跟踪。

此外,该方法需要知道金丝雀检查函数的最后一次调用是什么,否则会出现假阳性,除非使用反向调试,否则这并不总是可行的。


我在编辑该结构时遇到了这个问题,但没有重新编译使用该结构的库。在一些大项目中,我添加了新的字段到struct,后来从json在lib_struct中解析,这个库后来在小部件中使用,以显示被解析的内容。我的make文件没有覆盖依赖项,所以库在编辑结构后没有重新编译。我的解决方案是重新编译所有使用该结构的东西。