写这篇文章本意是帮助萌新们对栈溢出能够有一个较为直观的理解,可能废话有点多,让各位大佬见笑了,还望不喜勿喷。

 

阅读本文前,建议掌握一定汇编基础

 

接下来我们进入正题。
或许你在平常时有在老师、朋友、或是其他的程序员的口中听过“栈溢出”这个词,那到底什么是栈溢出呢?为什么栈会溢出呢?
我们先来看看百度百科的描述:

 

栈溢出就是缓冲区溢出的一种。由于缓冲区溢出而使得有用的存储单元被改写,往往会引发不可预料的后果。程序在运行过程中,为了临时存取数据的需要,一般都要分配一些内存空间,通常称这些空间为缓冲区。如果向缓冲区中写入超过其本身长度的数据,以致于缓冲区无法容纳,就会造成缓冲区以外的存储单元被改写,这种现象就称为缓冲区溢出。缓冲区长度一般与用户自己定义的缓冲变量的类型有关。

来自 百度百科

 

简单的说,内存在储存数据时,程序会分配到一部分内存空间,但是这一部分的空间并不会加上一个围墙当着拦着,也就是说,如果程序操作不当,则会跨出这块空间,对属于其他正常运行着的程序的空间进行了操作(修改),这就会造成其他程序无法正常运行。(再简单一点,就是碰了不该碰的东西)

 

python栈溢出利用 栈溢出后果_栈溢出

 

栈和堆里面的内容都时时刻刻都在变化着,随着我们程序的运行,这一块内存区域会不断的被释放、申请。而里面储存着的大都是程序运行时所需要的临时变量(数据),若正常程序的内容被别的程序修改了,则可能会带来一些较为严重后果。
运行一段C语言源代码,给大家演示一下,栈溢出是如何在我们程序运行的过程之中发生的。

 

#include <stdio.h>
#include <stdlib.h>

typedef struct {
    int a[2];
    double d;
} struct_t;

double fun(int i) {
    volatile struct_t s;
    s.d = 3.14;
    s.a[i] = 1073741824; /* Possibly out of bounds */
    return s.d; /* Should be 3.14 */
}

int main(int argc, char *argv[]) {
    int i = 0;
    if (argc >= 2)
	i = atoi(argv[1]);
    double d = fun(i);
    printf("fun(%d) --> %.10f\n", i, d);
    return 0;
}
/*
程序接收参数i,传入函数fun中,使得内存地址为(&s.a[0] + 4*i)的内容改为0x40000000
*/

 

在Ubuntu 18.04 LTS下用gcc 7.4.0进行编译后运行

 

hpj@asus:/mnt/d/code/csapp$ ./struct 1
fun(1) --> 3.1400000000
hpj@asus:/mnt/d/code/csapp$ ./struct 2
fun(2) --> 3.1399998665
hpj@asus:/mnt/d/code/csapp$ ./struct 3
fun(3) --> 2.0000006104
hpj@asus:/mnt/d/code/csapp$ ./struct 4
fun(4) --> 3.1400000000
hpj@asus:/mnt/d/code/csapp$ ./struct 5
fun(5) --> 3.1400000000
hpj@asus:/mnt/d/code/csapp$ ./struct 6
*** stack smashing detected ***: <unknown> terminated
已放弃 (核心已转储)

 

当参数为2、3的时候,程序能够正常的运行,但所输出的内容却不对;而当参数为6的时候,没有内容输出,程序无法正常运行。那为什么会这样呢?
我们一步步地来看看它是如何运行
首先我们观察一下汇编代码:

 

00000000000006fa <fun>:
 6fa:	55                   	push   %rbp
 6fb:	48 89 e5             	mov    %rsp,%rbp
 6fe:	48 83 ec 30          	sub    $0x30,%rsp
 702:	89 7d dc             	mov    %edi,-0x24(%rbp)
 705:	64 48 8b 04 25 28 00 	mov    %fs:0x28,%rax
 70c:	00 00 
 70e:	48 89 45 f8          	mov    %rax,-0x8(%rbp)
 712:	31 c0                	xor    %eax,%eax
 714:	f2 0f 10 05 44 01 00 	movsd  0x144(%rip),%xmm0        # 860 <_IO_stdin_used+0x20>
 71b:	00 
 71c:	f2 0f 11 45 e8       	movsd  %xmm0,-0x18(%rbp)
 721:	8b 45 dc             	mov    -0x24(%rbp),%eax
 724:	48 98                	cltq   
 726:	c7 44 85 e0 00 00 00 	movl   $0x40000000,-0x20(%rbp,%rax,4)
 72d:	40 
 72e:	f2 0f 10 45 e8       	movsd  -0x18(%rbp),%xmm0
 733:	48 8b 45 f8          	mov    -0x8(%rbp),%rax
 737:	64 48 33 04 25 28 00 	xor    %fs:0x28,%rax
 73e:	00 00 
 740:	74 05                	je     747 <fun+0x4d>
 742:	e8 69 fe ff ff       	callq  5b0 <__stack_chk_fail@plt>
 747:	c9                   	leaveq 
 748:	c3                   	retq   

0000000000000749 <main>:
 749:	55                   	push   %rbp
 74a:	48 89 e5             	mov    %rsp,%rbp
 74d:	48 83 ec 30          	sub    $0x30,%rsp
 751:	89 7d ec             	mov    %edi,-0x14(%rbp)
 754:	48 89 75 e0          	mov    %rsi,-0x20(%rbp)
 758:	c7 45 f4 00 00 00 00 	movl   $0x0,-0xc(%rbp)
 75f:	83 7d ec 01          	cmpl   $0x1,-0x14(%rbp)
 763:	7e 16                	jle    77b <main+0x32>
 765:	48 8b 45 e0          	mov    -0x20(%rbp),%rax
 769:	48 83 c0 08          	add    $0x8,%rax
 76d:	48 8b 00             	mov    (%rax),%rax
 770:	48 89 c7             	mov    %rax,%rdi
 773:	e8 58 fe ff ff       	callq  5d0 <atoi@plt>
 778:	89 45 f4             	mov    %eax,-0xc(%rbp)
 77b:	8b 45 f4             	mov    -0xc(%rbp),%eax
 77e:	89 c7                	mov    %eax,%edi
 780:	e8 75 ff ff ff       	callq  6fa <fun>
 785:	66 48 0f 7e c0       	movq   %xmm0,%rax
 78a:	48 89 45 f8          	mov    %rax,-0x8(%rbp)
 78e:	48 8b 55 f8          	mov    -0x8(%rbp),%rdx
 792:	8b 45 f4             	mov    -0xc(%rbp),%eax
 795:	48 89 55 d8          	mov    %rdx,-0x28(%rbp)
 799:	f2 0f 10 45 d8       	movsd  -0x28(%rbp),%xmm0
 79e:	89 c6                	mov    %eax,%esi
 7a0:	48 8d 3d a1 00 00 00 	lea    0xa1(%rip),%rdi        # 848 <_IO_stdin_used+0x8>
 7a7:	b8 01 00 00 00       	mov    $0x1,%eax
 7ac:	e8 0f fe ff ff       	callq  5c0 <printf@plt>
 7b1:	b8 00 00 00 00       	mov    $0x0,%eax
 7b6:	c9                   	leaveq 
 7b7:	c3                   	retq   
 7b8:	0f 1f 84 00 00 00 00 	nopl   0x0(%rax,%rax,1)
 7bf:	00

 

可以看到程序在第0x780行的时候调用fun函数

在第0x726行将指定内存区域修改为0x40000000

我们先来看看,如果我们带参数3,程序是如何运行的。

 


python栈溢出利用 栈溢出后果_python栈溢出利用_02

准备进入fun函数,参数3储存在寄存器edi里

 


python栈溢出利用 栈溢出后果_python栈溢出利用_03

把3.14存入0x00007fffe71a0618之中

 


python栈溢出利用 栈溢出后果_python栈溢出利用_04

储存3.14的被破坏,返回被破坏的的数据


 

我们可以看到在 0x00007fffe71a0618原本为0x40091eb8被改为了0x4000000

3.14就被改成了2.0000006104,且被储存进xmm0用于返回上一层函数(main)

Ps:对应上面的汇编代码第0x726行到第0x72e行的内容

这就是为什么我们参数为3的时候,输出的结果却不是3.14的原因

我们的程序在执行“s.a[i] = 1073741824;”的时候并不会检查你的i值合不合适,导致了我们正常的数据(3.14)被修改为了一个我们无法控制的结果


那如果我们的参数为6的时候又会发生什么呢?

前面的基本一样(我就不把图片贴出来了),但是到了第0x726行的时候就有点不一样了。通常来说,编译器会为我们的代码增加普通的栈溢出检查步骤:检测canary值是否被修改!

 

python栈溢出利用 栈溢出后果_数据_05

 

我们的程序在进入函数之前,都会把我们下一步要执行的命令所在的位置存入到栈里面,我们称之为返回地址(返回上一层函数的道路),如果发生了栈溢出,就很有可能会把返回地址给修改了,当我们要执行返回操作时,返回的位置就不一定是正常的位置了,这将导致我们的程序无法正常的执行。所以在返回地址前面增添一个“哨兵”,如果“哨兵”的值被修改了,则意味着返回地址很有可能被修改了。

 

python栈溢出利用 栈溢出后果_python栈溢出利用_06

在第0x70e行置入金丝雀值


 

python栈溢出利用 栈溢出后果_栈溢出_07

同样的,在第0x726行时修改了金丝雀的值


 

python栈溢出利用 栈溢出后果_数据_08

在第0x737行对金丝雀进行检测


 

上图是参数为6时,金丝雀被修改的大致过程。我们可以看到,在第0x737行进行异或运算,因为金丝雀值已经被修改了,异或的结果就不会是0,则无法执行第0x740行的转跳命令(转跳到第0x747行执行leave指令),在第0x742行的函数"__stack_chk_fail"就会被调用,以结束程序。


 

那到了这里,就可能有小伙伴觉得,既然都有金丝雀值来保护了,那我们岂不是就不用去担心这个栈溢出会对我们的系统造成破坏了?

 

不不不!

 

刚刚只是说“如果‘哨兵’的值被修改了,则意味着返回地址很有可能被修改了”
意味着返回地址被修改, ‘哨兵’的值会被修改 !
而且,返回地址被修改了,是一件非常可怕的事情
我们尝试运行一下下面的代码:

 

#include <stdio.h>
void fun()
{
    printf("blog.hpjpw.com\n");
}
void jump(int i)
{
    long a[2];
    a[0] = 0x11111111;
    a[1] = 0x22222222;
    a[i] = a[i]-122;
}
int main()
{
    jump(5);
    return 0;
}
/*
运行结果如下:
hpj@asus:/mnt/d/code/C/stack_overflow_hack_test$ ./a
blog.hpjpw.com
注意:我在不同的终端下运行这个程序时可能会出现段错误的情况,目前还未搞清楚原因,我并没有深入的了解系统对栈溢出的防护,还希望能够有大佬指点一二。
*/

 


明明fun函数没有被调用,那为什么还会输出一串字符串呢?

先看一下反汇编能得到些什么东西

 

00000000000006bd <jump>:
 6bd:	55                   	push   %rbp
 6be:	48 89 e5             	mov    %rsp,%rbp
 6c1:	48 83 ec 30          	sub    $0x30,%rsp
 6c5:	89 7d dc             	mov    %edi,-0x24(%rbp)
 6c8:	64 48 8b 04 25 28 00 	mov    %fs:0x28,%rax
 6cf:	00 00 
 6d1:	48 89 45 f8          	mov    %rax,-0x8(%rbp)
 6d5:	31 c0                	xor    %eax,%eax
 6d7:	48 c7 45 e0 11 11 11 	movq   $0x11111111,-0x20(%rbp)
 6de:	11 
 6df:	48 c7 45 e8 22 22 22 	movq   $0x22222222,-0x18(%rbp)
 6e6:	22 
 6e7:	8b 45 dc             	mov    -0x24(%rbp),%eax
 6ea:	48 98                	cltq   
 6ec:	48 8b 44 c5 e0       	mov    -0x20(%rbp,%rax,8),%rax
 6f1:	48 8d 50 86          	lea    -0x7a(%rax),%rdx
 6f5:	8b 45 dc             	mov    -0x24(%rbp),%eax
 6f8:	48 98                	cltq   
 6fa:	48 89 54 c5 e0       	mov    %rdx,-0x20(%rbp,%rax,8)
 6ff:	90                   	nop
 700:	48 8b 45 f8          	mov    -0x8(%rbp),%rax
 704:	64 48 33 04 25 28 00 	xor    %fs:0x28,%rax
 70b:	00 00 
 70d:	74 05                	je     714 <jump+0x57>
 70f:	e8 6c fe ff ff       	callq  580 <__stack_chk_fail@plt>
 714:	c9                   	leaveq 
 715:	c3                   	retq

 

可以看到在第0x6ec行把返回地址(看实际如何运行)放入寄存器rax,处理过后,在第0x6fa行把返回地址替换成处理过后的数据。

下面再来看看实际运行的结果是怎么样的

 

python栈溢出利用 栈溢出后果_栈溢出_09

准备调用函数jump,参数为5,存放在寄存器rdi中


 

python栈溢出利用 栈溢出后果_python栈溢出利用_10

进入函数jump,把下一条指令的地址(返回地址)压入栈


 

返回地址为0x00007f281d400724,被放在0x00007fffdb781ee8里面

 

python栈溢出利用 栈溢出后果_数据_11

把返回地址放入寄存器rax


 

再对返回地址进行处理“a[i] = a[i]-122;”

Ps:这里减122是因为fun函数入口在第0x6aa行,与返回地址第0x724行相差了122(十进制)

处理后再把它放回到0x00007fffdb781ee8

 

python栈溢出利用 栈溢出后果_栈溢出_12

修改后的返回地址入内存


 

python栈溢出利用 栈溢出后果_python栈溢出利用_13

a[5]指向的是返回地址


 

程序只是修改了返回地址,并没有碰金丝雀的值,所以在第0x70f行的__stack_chk_fail不会被调用,程序能够继续往下执行,当执行到第0x715行的ret时,就会进入到fun函数的入口位置继续执行,所以我们能够看到那串字符串被打印出来。

Ps:fun函数里面调用的printf函数被编译器优化为了puts函数,不得不说现在的编译器越来越聪明了。

最后来个总结。尽管我们的系统有机制以防护栈溢出带来的不良影响,但是对我们程序的稳定性来说却是一个致命的打击,在平时写代码的时候还是要多多注意栈溢出带来的不良影响。就比如说,对于strcpy、puts、gets等一些没有对边界进行检查的函数,我们要尽可能的避免使用,以免造成不好的后果。