检测到堆栈粉碎

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

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

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

652305 次浏览

你可以尝试使用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终止程序。

为了获得一些见解,你可以尝试在编译时使用选项 -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。Out wepassssssssssssssssssss "

所以回答你上面的问题,消息“**堆栈粉碎检测到: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 );

Length现在比你说arr + 2时传递的实际长度要长。

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

堆栈损坏通常是由缓冲区溢出引起的。

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

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

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

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

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

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

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

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

(注意,有问题的return语句实际上并不是这样编码的——相反,调用了一个宏,该宏根据一个全局变量决定是_exit()还是return。因此,不能立即看出子代码不符合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-protector-all进行复制,但当我根据耿家文评论道在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启用谷歌的地址消毒器(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应该将您指向执行金丝雀检查的函数的末尾:

(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

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

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

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