《黑客攻防技术-系统实战》第二章--栈溢出4

参考文献:

https://en.wikipedia.org/wiki/Buffer_overflow_protection
https://www.zhihu.com/question/20871464/answer/18743160
http://www.ibm.com/developerworks/cn/linux/l-cn-gccstack/

《黑客攻防技术宝典-系统实战》

.....................................................................................

首先我们直接看一段栈空间溢出的代码:

1234567
void bob(){    int a[4]={0,1,2,3};    *(a + 4) = 4;}int main(){    bob();}

在gcc version 5.4.0 x86_64 Ubuntu 16.04.1 LTS环境下进行编译:
编译选项:g++ test.cpp -fno-stack-protector
下面是bob()函数的反汇编代码

1234567891011
00000000004004d6 <_Z3bobv>:  4004d6:       55                      push   %rbp  4004d7:       48 89 e5                mov    %rsp,%rbp  4004da:       c7 45 f0 00 00 00 00    movl   $0x0,-0x10(%rbp)  4004e1:       c7 45 f4 01 00 00 00    movl   $0x1,-0xc(%rbp)  4004e8:       c7 45 f8 02 00 00 00    movl   $0x2,-0x8(%rbp)  4004ef:       c7 45 fc 03 00 00 00    movl   $0x3,-0x4(%rbp)  4004f6:       c7 45 00 04 00 00 00    movl   $0x4,0x0(%rbp)  4004fd:       90                      nop  4004fe:       5d                      pop    %rbp  4004ff:       c3                      retq

编译选项:g++ test.cpp (默认含有-fstack-protector-strong选项)
下面是bob()函数的反汇编代码

123456789101112131415161718192021
0000000000400546 <_Z3bobv>:  400546:       55                      push   %rbp  400547:       48 89 e5                mov    %rsp,%rbp  40054a:       48 83 ec 20             sub    $0x20,%rsp  40054e:       64 48 8b 04 25 28 00    mov    %fs:0x28,%rax  400555:       00 00   400557:       48 89 45 f8             mov    %rax,-0x8(%rbp)  40055b:       31 c0                   xor    %eax,%eax  40055d:       c7 45 e0 00 00 00 00    movl   $0x0,-0x20(%rbp)  400564:       c7 45 e4 01 00 00 00    movl   $0x1,-0x1c(%rbp)  40056b:       c7 45 e8 02 00 00 00    movl   $0x2,-0x18(%rbp)  400572:       c7 45 ec 03 00 00 00    movl   $0x3,-0x14(%rbp)  400579:       c7 45 f0 04 00 00 00    movl   $0x4,-0x10(%rbp)  400580:       90                      nop  400581:       48 8b 45 f8             mov    -0x8(%rbp),%rax  400585:       64 48 33 04 25 28 00    xor    %fs:0x28,%rax  40058c:       00 00   40058e:       74 05                   je     400595 <_Z3bobv+0x4f>  400590:       e8 8b fe ff ff          callq  400420 <[email protected]>  400595:       c9                      leaveq   400596:       c3                      retq

在有stack-protector-strong选项的情况下,bob()函数多出来部分,是gcc为了进行栈空间溢出检测而插入的代码。在解析堆栈保护之前,先解析一下上述汇编代码中,关于堆栈对齐的部分。

1. GCC函数栈空间对齐

我们看看在含有-fstack-protector-strong编译选项下,bob()函数的反汇编代码的前三行:

1234567
0000000000400546 <_Z3bobv>://将上一个函数栈的栈底指针rbp压入栈  400546:       55                      push   %rbp//将上一个函数栈的栈顶指针rsp赋给rbp寄存器,当前bob函数的栈底指针  400547:       48 89 e5                mov    %rsp,%rbp//分配栈空间32bytes  40054a:       48 83 ec 20             sub    $0x20,%rsp

为什么这里申请了32bytes的栈空间呢,代码段中,只需要16bytes的数组空间呀。
因为增加了stack-protector,gcc为了进行栈空间溢出检测而插入了一个guard word字段,也就是canary word,该字段为8bytes。该字段在栈空间溢出后会被破坏(这里不是一定的,后面会详解),在函数结束时会校验该字段来判断是否发生栈溢出。
局部变量16bytes + canary word 8bytes = 24bytes < 32 bytes.
why? 为什么会多出来8个字节呢,这里就是GCC默认16bytes栈对齐的原因。

为什么默认是16bytes对齐呢,这和CPU相关,
Intel在Pentium III推出了SSE指令集,SSE 加入新的 8 个128Bit(16bytes)寄存器(XMM0~XMM7)。最初的时候,这些寄存器智能用来做单精度浮点数计算,自从SSE2开始,这些寄存器可以被用来计算任何基本数据类型的数据了。往XMM0~XMM7里存放数据,是以16字节为单位,所以呢 内存变量首地址必须要对齐16字节,否则会引起CPU异常,导致指令执行失败。所以这就是gcc默认采用16bytes进行栈对齐的原因。

感谢miloyip指出这里的问题:这里关于SSE指令, aligned/unaligned 的 load/store 指令,unaligned 是不会有问题的,具体16bytes对齐的原因有待商榷。。。

gcc中关于栈对齐的选项是-mpreferred-stack-boundary=num,栈空间的边界对齐为2的num次幂。该选项默认值是4,即16bytes
gcc 5.4 manual中关于该选项说明如下:

Warning: When generating code for the x86-64 architecture with SSE extensions disabled, ‘-mpreferred-stack-boundary=3’ can be used to keep the stack boundary aligned to 8 byte boundary. Since x86-64 ABI require 16 byte stack alignment, this is ABI incompatible and intended to be used in controlled environment where stack space is important limitation. This option leads to wrong code when functions compiled with 16 byte stack alignment (such as functions from a standard library) are called with misaligned stack. In this case, SSE instructions may lead to misaligned memory access traps. In addition, variable arguments are handled incorrectly for 16 byte aligned objects (including x87 long double and int128), leading to wrong results. You must build all modules with ‘-mpreferred-stack-boundary=3’, including any libraries. This includes the system libraries and startup modules.

简述就是:在SSE扩展被关闭时,-mpreferred-stack-boundary参数值是可以修改的。但是,但是,但是,当该选项值被修改后,编译链接16bytes栈对齐的库时,会导致错误。
下面修改栈对齐参数为8bytes后bob()函数的反汇编代码:

g++ test.cpp -mpreferred-stack-boundary=3 -mno-sse

123456789101112131415161718192021
0000000000400546 <_Z3bobv>:  400546:       55                      push   %rbp  400547:       48 89 e5                mov    %rsp,%rbp  40054a:       48 83 ec 18             sub    $0x18,%rsp  40054e:       64 48 8b 04 25 28 00    mov    %fs:0x28,%rax  400555:       00 00   400557:       48 89 45 f8             mov    %rax,-0x8(%rbp)  40055b:       31 c0                   xor    %eax,%eax  40055d:       c7 45 e8 00 00 00 00    movl   $0x0,-0x18(%rbp)  400564:       c7 45 ec 01 00 00 00    movl   $0x1,-0x14(%rbp)  40056b:       c7 45 f0 02 00 00 00    movl   $0x2,-0x10(%rbp)  400572:       c7 45 f4 03 00 00 00    movl   $0x3,-0xc(%rbp)  400579:       c7 45 f8 04 00 00 00    movl   $0x4,-0x8(%rbp)  400580:       90                      nop  400581:       48 8b 45 f8             mov    -0x8(%rbp),%rax  400585:       64 48 33 04 25 28 00    xor    %fs:0x28,%rax  40058c:       00 00   40058e:       74 05                   je     400595 <_Z3bobv+0x4f>  400590:       e8 8b fe ff ff          callq  400420 <[email protected]>  400595:       c9                      leaveq   400596:       c3                      retq

和默认16bytes栈对齐的反汇编代码区别:8bytes对齐的栈空间预留了0x18=24bytes的空间。
这里抛一个问题:8bytes栈对齐的这段代码运行的时候会core掉,而16bytes栈对齐的不会,为什么?(后面出讲到,也是本文的核心)
上面bob()函数的栈空间布局如下:

关于函数栈16bytes对齐还有一点想要阐述的:每当通过call指令进行函数调用时,都会发生两个操作

  • push %rip:将函数返回后下一条指令的地址入栈;这个是默认隐含执行的。
  • push %rbp,mov %rsp, %rbp: 将当前函数栈底指针入栈,然后将栈顶指针赋给栈底指针。这一步其实是在call执行后,在被调用函数最开始进行的。这一步就是要保存上一个函数的栈信息,用于返回执行。

上面两步操作是进程运行的关键,也是我认为最最基础的,这两步是理解程序运行过程的关键。写了这么多年代码,终于感觉入门了。。。
上面两步在x84_64下,会发现始终让栈空间保持16bytes的对齐,是不是很神奇。。。

2. GCC栈溢出检测

下面我们回归正题,gcc是如何进行栈空间溢出检测的。
<GCC 中的编译器堆栈保护技术>这篇文章介绍了gcc采用的堆栈保护技术,堆栈保护基本都是通过canaries探测来实现的,Canaries探测要检测对函数栈的破坏,需要修改函数栈的组织, 要在缓冲区和控制信息(压在栈中的函数返回后的RBP和RIP)中间插一个canary word,这样当缓冲区被破坏的时候,canary word会在函数栈控制信息被破坏之前被破坏,这样通过检测canary word的值是否被修改,就可以判断出是否发生溢出攻击。
这里对于一次接触Canaries这个词的人,可能会很困惑,维基中关于缓冲区溢出中有相关介绍:

“The terminology is a reference to the historic practice of using canaries in coal mines, since they would be affected by toxic gases earlier than the miners, thus providing a biological warning system”
本人译:Canary,原意是金丝雀,该术语来源于挖矿行业,用金丝雀来检查煤矿中的空气是否有毒。

GCC 4.1 堆栈保护才被加入,所采用的堆栈保护实现Stack-smashing Protection(SSP,又称 ProPolice)。到目前GCC6.2中与堆栈保护有关的编译选项,有如下几个:

  • -fstack-protector
    启用堆栈保护,不过只为局部变量中含有 char 数组的函数插入保护代码。
    gcc 5.4 manual中指明插入保护代码的条件是:函数内部有alloca()调用或者buffers空间大于8bytes。关于buffers大于8bytes,在测试的时候并没有插入canary word,为什么?求解
  • -fstack-protector-all
    启用堆栈保护,为所有函数插入保护代码。
  • -fstack-protector-strong(GCC4.9引入)
    和-fstack-protector一样,但包含额外的保护:函数内部有数组定义,以及有对局部栈上的地址引用。这两种条件也会进行栈保护代码的插入。最开始说了,在我的机器上:gcc version 5.4.0 20160609 x86_64 Ubuntu 16.04.1 LTS环境下,gcc编译时,默认是该选项生效。
  • -fstack-protector-explicit
    和-fstack-protector的区别是:只对有stack_protect属性的函数进行保护。
  • -fno-stack-protector
    禁用堆栈保护。

开启stack-protector后,会在函数栈的数据和控制信息直接预留多余的缓冲区,然后会在缓冲区的最后一个word(8bytes),其实gcc本意该缓冲区的大小就是8 bytes,即够写入一个canary word的,但实际你会发现缓冲区比8个字节要大。这里的原因就是上面一节讲述的栈空间对齐的结果:
实际缓冲区大小 = 栈空间对齐后的栈大小 – 局部变量占用的栈大小
而且gcc会把canary word写入到栈缓冲区最高的位置,即紧挨着上一个调用函数的rbp栈底指针存放的位置
所以回到最开始的栈溢出的代码,下面是bob()函数在-fstack-protector-strong选项或者-fstack-protector-all选项下的反汇编代码

12345678910111213141516171819202122232425
0000000000400546 <_Z3bobv>:  400546:       55                      push   %rbp  400547:       48 89 e5                mov    %rsp,%rbp#函数栈中分配了32bytes的空间  40054a:       48 83 ec 20             sub    $0x20,%rsp  40054e:       64 48 8b 04 25 28 00    mov    %fs:0x28,%rax  400555:       00 00   400557:       48 89 45 f8             mov    %rax,-0x8(%rbp)  40055b:       31 c0                   xor    %eax,%eax  40055d:       c7 45 e0 00 00 00 00    movl   $0x0,-0x20(%rbp)  400564:       c7 45 e4 01 00 00 00    movl   $0x1,-0x1c(%rbp)  40056b:       c7 45 e8 02 00 00 00    movl   $0x2,-0x18(%rbp)  400572:       c7 45 ec 03 00 00 00    movl   $0x3,-0x14(%rbp)#函数栈中上面数组使用了16bytes,#剩下的缓冲区大小也是16bytes,canary word放在最高的8bytes 

  400579:       c7 45 f0 04 00 00 00    movl   $0x4,-0x10(%rbp)  400580:       90                      nop  400581:       48 8b 45 f8             mov    -0x8(%rbp),%rax  400585:       64 48 33 04 25 28 00    xor    %fs:0x28,%rax  40058c:       00 00   40058e:       74 05                   je     400595 <_Z3bobv+0x4f>  400590:       e8 8b fe ff ff          callq  400420 <[email protected]>  400595:       c9                      leaveq   400596:       c3                      retq

bob()函数栈空间的大小对32字节,栈上局部数组使用了16bytes,多出来的16bytes缓冲区其中8bytes用于canaries溢出检测。另外多出来的8bytes,只是栈对齐的原因预留下来的。所以在这种情况下,溢出写入8bytes的数据都不会对代码有任何影响。下图是bob()函数执行完时,栈的情况:

如果溢出写入超过8bytes,覆盖到canary word,就是触发gcc的Stack-smashing Protection,出现如下异常:

1234567
void bob(){    int a[4]={0,1,2,3};    *(a + 6) = 4;}$./a.out *** stack smashing detected ***: ./a.out terminatedAborted (core dumped)

其实本文的核心就是这一点,gcc通过cannary word来进行栈溢出检测,这也是上一节提出的问题的答案,在栈空间8字节对齐情况下,代码coredump的原因也是cannary word被覆盖了。

3. GCC栈溢出检测说明

下面上面已经说到gcc关于栈溢出检测的几个参数,可能比较乱,这里汇总一下:

本文的实例代码bob()函数之所以被插入栈保护代码,就是因为在默认-fstack-protector-strong,bob()函数有数组定义。如果该代码在gcc4.9之前的版本编译,且不加-fstack-protector-all选项,是不会生成保护代码的。
如下是在gcc version 4.8.4 ubuntu1~14.04.3 x86_64下编译的bob()函数的反汇编代码,等同于 -fno-stack-protector

12345678910
00000000004004ed  <_Z3bob>:  4004ed:	55                   	push   %rbp  4004ee:	48 89 e5             	mov    %rsp,%rbp  4004f1:	c7 45 f0 00 00 00 00 	movl   $0x0,-0x10(%rbp)  4004f8:	c7 45 f4 01 00 00 00 	movl   $0x1,-0xc(%rbp)  4004ff:	c7 45 f8 02 00 00 00 	movl   $0x2,-0x8(%rbp)  400506:	c7 45 fc 03 00 00 00 	movl   $0x3,-0x4(%rbp)  40050d:	c7 45 00 04 00 00 00 	movl   $0x4,0x0(%rbp)  400514:	5d                   	pop    %rbp  400515:	c3                   	retq

这里由于没有插入溢出检测代码,所以bob()函数栈空间没有进行16bytes倍数的栈对齐预留。因为该函数调用完毕就会返回,因为没有进行预留,所以你就会发现数组溢出写入的字段,已经覆盖了返回函数栈的rbp栈底指针,如下图:

哎呦,完了,程序要跪了,我们执行一下,结果你会发现,并没有挂掉:我们看看上一个函数main函数的汇编代码:

123456789
0000000000400516 <main>:  400516:	55                   	push   %rbp  400517:	48 89 e5             	mov    %rsp,%rbp  40051a:	e8 ce ff ff ff       	callq  4004ed <_Z3bobv>  40051f:	b8 00 00 00 00       	mov    $0x0,%eax  400524:	5d                   	pop    %rbp  400525:	c3                   	retq     400526:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)  40052d:	00 00 00

你会发现,bob()结束时,pop出来被覆盖了的rbp寄存器的值,在main函数中并没有被使用,main中结尾直接pop出了上一层函数的rbp寄存器的值,所已这个溢出问题并没有导致代码coredump。当修改main()中的代码,填加一个变量后:

12345678
void bob(){    int a[4]={0,1,2,3};    *(a + 4) = 4;}int main(){    int a = 5;    bob();}

上面gcc4.8.4环境下同样编译,你就会发现,该代码会coredump
main()汇编代码如下:

12345678910111213
0000000000400516 <main>:  400516:	55                   	push   %rbp  400517:	48 89 e5             	mov    %rsp,%rbp  40051a:	48 83 ec 10          	sub    $0x10,%rsp  40051e:	c7 45 fc 05 00 00 00 	movl   $0x5,-0x4(%rbp)  400525:	e8 c3 ff ff ff       	callq  4004ed <_Z3bobv>  40052a:	b8 00 00 00 00       	mov    $0x0,%eax  //core原因在这一条语句  40052f:	c9                   	leaveq   400530:	c3                   	retq     400531:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)  400538:	00 00 00   40053b:	0f 1f 44 00 00       	nopl   0x0(%rax,%rax,1)

leaveq等同于:

12
mov %rbp %rsp                                            pop rbp

我们知道rbp寄存器的值已经是被覆盖了的错误地址。所以,把他赋值给rsp后,栈顶指针就是一个错误的指向,再去执行pop指令,就会导致异常。

4. 总结

讲了这么多,其实一开始就只是想研究一个GCC的栈溢出检测机制,结果发现自己知之甚少,就从编译选项,研究到栈空间对齐,最终对程序的运行过程从汇编的角度又是深入了几分。
所以本文彻底解除了我多年的困惑:数组越界,为什么有时候会coredump,有时候不会coredump,这个gcc编译器的环境,以及os都有着莫大的关系。

首先我们直接看一段栈空间溢出的代码:

1234567
void bob(){    int a[4]={0,1,2,3};    *(a + 4) = 4;}int main(){    bob();}

在gcc version 5.4.0 x86_64 Ubuntu 16.04.1 LTS环境下进行编译:
编译选项:g++ test.cpp -fno-stack-protector
下面是bob()函数的反汇编代码

1234567891011
00000000004004d6 <_Z3bobv>:  4004d6:       55                      push   %rbp  4004d7:       48 89 e5                mov    %rsp,%rbp  4004da:       c7 45 f0 00 00 00 00    movl   $0x0,-0x10(%rbp)  4004e1:       c7 45 f4 01 00 00 00    movl   $0x1,-0xc(%rbp)  4004e8:       c7 45 f8 02 00 00 00    movl   $0x2,-0x8(%rbp)  4004ef:       c7 45 fc 03 00 00 00    movl   $0x3,-0x4(%rbp)  4004f6:       c7 45 00 04 00 00 00    movl   $0x4,0x0(%rbp)  4004fd:       90                      nop  4004fe:       5d                      pop    %rbp  4004ff:       c3                      retq

编译选项:g++ test.cpp (默认含有-fstack-protector-strong选项)
下面是bob()函数的反汇编代码

123456789101112131415161718192021
0000000000400546 <_Z3bobv>:  400546:       55                      push   %rbp  400547:       48 89 e5                mov    %rsp,%rbp  40054a:       48 83 ec 20             sub    $0x20,%rsp  40054e:       64 48 8b 04 25 28 00    mov    %fs:0x28,%rax  400555:       00 00   400557:       48 89 45 f8             mov    %rax,-0x8(%rbp)  40055b:       31 c0                   xor    %eax,%eax  40055d:       c7 45 e0 00 00 00 00    movl   $0x0,-0x20(%rbp)  400564:       c7 45 e4 01 00 00 00    movl   $0x1,-0x1c(%rbp)  40056b:       c7 45 e8 02 00 00 00    movl   $0x2,-0x18(%rbp)  400572:       c7 45 ec 03 00 00 00    movl   $0x3,-0x14(%rbp)  400579:       c7 45 f0 04 00 00 00    movl   $0x4,-0x10(%rbp)  400580:       90                      nop  400581:       48 8b 45 f8             mov    -0x8(%rbp),%rax  400585:       64 48 33 04 25 28 00    xor    %fs:0x28,%rax  40058c:       00 00   40058e:       74 05                   je     400595 <_Z3bobv+0x4f>  400590:       e8 8b fe ff ff          callq  400420 <[email protected]>  400595:       c9                      leaveq   400596:       c3                      retq

在有stack-protector-strong选项的情况下,bob()函数多出来部分,是gcc为了进行栈空间溢出检测而插入的代码。在解析堆栈保护之前,先解析一下上述汇编代码中,关于堆栈对齐的部分。

1. GCC函数栈空间对齐

我们看看在含有-fstack-protector-strong编译选项下,bob()函数的反汇编代码的前三行:

1234567
0000000000400546 <_Z3bobv>://将上一个函数栈的栈底指针rbp压入栈  400546:       55                      push   %rbp//将上一个函数栈的栈顶指针rsp赋给rbp寄存器,当前bob函数的栈底指针  400547:       48 89 e5                mov    %rsp,%rbp//分配栈空间32bytes  40054a:       48 83 ec 20             sub    $0x20,%rsp

为什么这里申请了32bytes的栈空间呢,代码段中,只需要16bytes的数组空间呀。
因为增加了stack-protector,gcc为了进行栈空间溢出检测而插入了一个guard word字段,也就是canary word,该字段为8bytes。该字段在栈空间溢出后会被破坏(这里不是一定的,后面会详解),在函数结束时会校验该字段来判断是否发生栈溢出。
局部变量16bytes + canary word 8bytes = 24bytes < 32 bytes.
why? 为什么会多出来8个字节呢,这里就是GCC默认16bytes栈对齐的原因。

为什么默认是16bytes对齐呢,这和CPU相关,
Intel在Pentium III推出了SSE指令集,SSE 加入新的 8 个128Bit(16bytes)寄存器(XMM0~XMM7)。最初的时候,这些寄存器智能用来做单精度浮点数计算,自从SSE2开始,这些寄存器可以被用来计算任何基本数据类型的数据了。往XMM0~XMM7里存放数据,是以16字节为单位,所以呢 内存变量首地址必须要对齐16字节,否则会引起CPU异常,导致指令执行失败。所以这就是gcc默认采用16bytes进行栈对齐的原因。

感谢miloyip指出这里的问题:这里关于SSE指令, aligned/unaligned 的 load/store 指令,unaligned 是不会有问题的,具体16bytes对齐的原因有待商榷。。。

gcc中关于栈对齐的选项是-mpreferred-stack-boundary=num,栈空间的边界对齐为2的num次幂。该选项默认值是4,即16bytes
gcc 5.4 manual中关于该选项说明如下:

Warning: When generating code for the x86-64 architecture with SSE extensions disabled, ‘-mpreferred-stack-boundary=3’ can be used to keep the stack boundary aligned to 8 byte boundary. Since x86-64 ABI require 16 byte stack alignment, this is ABI incompatible and intended to be used in controlled environment where stack space is important limitation. This option leads to wrong code when functions compiled with 16 byte stack alignment (such as functions from a standard library) are called with misaligned stack. In this case, SSE instructions may lead to misaligned memory access traps. In addition, variable arguments are handled incorrectly for 16 byte aligned objects (including x87 long double and int128), leading to wrong results. You must build all modules with ‘-mpreferred-stack-boundary=3’, including any libraries. This includes the system libraries and startup modules.

简述就是:在SSE扩展被关闭时,-mpreferred-stack-boundary参数值是可以修改的。但是,但是,但是,当该选项值被修改后,编译链接16bytes栈对齐的库时,会导致错误。
下面修改栈对齐参数为8bytes后bob()函数的反汇编代码:

g++ test.cpp -mpreferred-stack-boundary=3 -mno-sse

123456789101112131415161718192021
0000000000400546 <_Z3bobv>:  400546:       55                      push   %rbp  400547:       48 89 e5                mov    %rsp,%rbp  40054a:       48 83 ec 18             sub    $0x18,%rsp  40054e:       64 48 8b 04 25 28 00    mov    %fs:0x28,%rax  400555:       00 00   400557:       48 89 45 f8             mov    %rax,-0x8(%rbp)  40055b:       31 c0                   xor    %eax,%eax  40055d:       c7 45 e8 00 00 00 00    movl   $0x0,-0x18(%rbp)  400564:       c7 45 ec 01 00 00 00    movl   $0x1,-0x14(%rbp)  40056b:       c7 45 f0 02 00 00 00    movl   $0x2,-0x10(%rbp)  400572:       c7 45 f4 03 00 00 00    movl   $0x3,-0xc(%rbp)  400579:       c7 45 f8 04 00 00 00    movl   $0x4,-0x8(%rbp)  400580:       90                      nop  400581:       48 8b 45 f8             mov    -0x8(%rbp),%rax  400585:       64 48 33 04 25 28 00    xor    %fs:0x28,%rax  40058c:       00 00   40058e:       74 05                   je     400595 <_Z3bobv+0x4f>  400590:       e8 8b fe ff ff          callq  400420 <[email protected]>  400595:       c9                      leaveq   400596:       c3                      retq

和默认16bytes栈对齐的反汇编代码区别:8bytes对齐的栈空间预留了0x18=24bytes的空间。
这里抛一个问题:8bytes栈对齐的这段代码运行的时候会core掉,而16bytes栈对齐的不会,为什么?(后面出讲到,也是本文的核心)
上面bob()函数的栈空间布局如下:

关于函数栈16bytes对齐还有一点想要阐述的:每当通过call指令进行函数调用时,都会发生两个操作

  • push %rip:将函数返回后下一条指令的地址入栈;这个是默认隐含执行的。
  • push %rbp,mov %rsp, %rbp: 将当前函数栈底指针入栈,然后将栈顶指针赋给栈底指针。这一步其实是在call执行后,在被调用函数最开始进行的。这一步就是要保存上一个函数的栈信息,用于返回执行。

上面两步操作是进程运行的关键,也是我认为最最基础的,这两步是理解程序运行过程的关键。写了这么多年代码,终于感觉入门了。。。
上面两步在x84_64下,会发现始终让栈空间保持16bytes的对齐,是不是很神奇。。。

2. GCC栈溢出检测

下面我们回归正题,gcc是如何进行栈空间溢出检测的。
<GCC 中的编译器堆栈保护技术>这篇文章介绍了gcc采用的堆栈保护技术,堆栈保护基本都是通过canaries探测来实现的,Canaries探测要检测对函数栈的破坏,需要修改函数栈的组织, 要在缓冲区和控制信息(压在栈中的函数返回后的RBP和RIP)中间插一个canary word,这样当缓冲区被破坏的时候,canary word会在函数栈控制信息被破坏之前被破坏,这样通过检测canary word的值是否被修改,就可以判断出是否发生溢出攻击。
这里对于一次接触Canaries这个词的人,可能会很困惑,维基中关于缓冲区溢出中有相关介绍:

“The terminology is a reference to the historic practice of using canaries in coal mines, since they would be affected by toxic gases earlier than the miners, thus providing a biological warning system”
本人译:Canary,原意是金丝雀,该术语来源于挖矿行业,用金丝雀来检查煤矿中的空气是否有毒。

GCC 4.1 堆栈保护才被加入,所采用的堆栈保护实现Stack-smashing Protection(SSP,又称 ProPolice)。到目前GCC6.2中与堆栈保护有关的编译选项,有如下几个:

  • -fstack-protector
    启用堆栈保护,不过只为局部变量中含有 char 数组的函数插入保护代码。
    gcc 5.4 manual中指明插入保护代码的条件是:函数内部有alloca()调用或者buffers空间大于8bytes。关于buffers大于8bytes,在测试的时候并没有插入canary word,为什么?求解
  • -fstack-protector-all
    启用堆栈保护,为所有函数插入保护代码。
  • -fstack-protector-strong(GCC4.9引入)
    和-fstack-protector一样,但包含额外的保护:函数内部有数组定义,以及有对局部栈上的地址引用。这两种条件也会进行栈保护代码的插入。最开始说了,在我的机器上:gcc version 5.4.0 20160609 x86_64 Ubuntu 16.04.1 LTS环境下,gcc编译时,默认是该选项生效。
  • -fstack-protector-explicit
    和-fstack-protector的区别是:只对有stack_protect属性的函数进行保护。
  • -fno-stack-protector
    禁用堆栈保护。

开启stack-protector后,会在函数栈的数据和控制信息直接预留多余的缓冲区,然后会在缓冲区的最后一个word(8bytes),其实gcc本意该缓冲区的大小就是8 bytes,即够写入一个canary word的,但实际你会发现缓冲区比8个字节要大。这里的原因就是上面一节讲述的栈空间对齐的结果:
实际缓冲区大小 = 栈空间对齐后的栈大小 – 局部变量占用的栈大小
而且gcc会把canary word写入到栈缓冲区最高的位置,即紧挨着上一个调用函数的rbp栈底指针存放的位置
所以回到最开始的栈溢出的代码,下面是bob()函数在-fstack-protector-strong选项或者-fstack-protector-all选项下的反汇编代码

12345678910111213141516171819202122232425
0000000000400546 <_Z3bobv>:  400546:       55                      push   %rbp  400547:       48 89 e5                mov    %rsp,%rbp#函数栈中分配了32bytes的空间  40054a:       48 83 ec 20             sub    $0x20,%rsp  40054e:       64 48 8b 04 25 28 00    mov    %fs:0x28,%rax  400555:       00 00   400557:       48 89 45 f8             mov    %rax,-0x8(%rbp)  40055b:       31 c0                   xor    %eax,%eax  40055d:       c7 45 e0 00 00 00 00    movl   $0x0,-0x20(%rbp)  400564:       c7 45 e4 01 00 00 00    movl   $0x1,-0x1c(%rbp)  40056b:       c7 45 e8 02 00 00 00    movl   $0x2,-0x18(%rbp)  400572:       c7 45 ec 03 00 00 00    movl   $0x3,-0x14(%rbp)#函数栈中上面数组使用了16bytes,#剩下的缓冲区大小也是16bytes,canary word放在最高的8bytes 

  400579:       c7 45 f0 04 00 00 00    movl   $0x4,-0x10(%rbp)  400580:       90                      nop  400581:       48 8b 45 f8             mov    -0x8(%rbp),%rax  400585:       64 48 33 04 25 28 00    xor    %fs:0x28,%rax  40058c:       00 00   40058e:       74 05                   je     400595 <_Z3bobv+0x4f>  400590:       e8 8b fe ff ff          callq  400420 <[email protected]>  400595:       c9                      leaveq   400596:       c3                      retq

bob()函数栈空间的大小对32字节,栈上局部数组使用了16bytes,多出来的16bytes缓冲区其中8bytes用于canaries溢出检测。另外多出来的8bytes,只是栈对齐的原因预留下来的。所以在这种情况下,溢出写入8bytes的数据都不会对代码有任何影响。下图是bob()函数执行完时,栈的情况:

如果溢出写入超过8bytes,覆盖到canary word,就是触发gcc的Stack-smashing Protection,出现如下异常:

1234567
void bob(){    int a[4]={0,1,2,3};    *(a + 6) = 4;}$./a.out *** stack smashing detected ***: ./a.out terminatedAborted (core dumped)

其实本文的核心就是这一点,gcc通过cannary word来进行栈溢出检测,这也是上一节提出的问题的答案,在栈空间8字节对齐情况下,代码coredump的原因也是cannary word被覆盖了。

3. GCC栈溢出检测说明

下面上面已经说到gcc关于栈溢出检测的几个参数,可能比较乱,这里汇总一下:

本文的实例代码bob()函数之所以被插入栈保护代码,就是因为在默认-fstack-protector-strong,bob()函数有数组定义。如果该代码在gcc4.9之前的版本编译,且不加-fstack-protector-all选项,是不会生成保护代码的。
如下是在gcc version 4.8.4 ubuntu1~14.04.3 x86_64下编译的bob()函数的反汇编代码,等同于 -fno-stack-protector

12345678910
00000000004004ed  <_Z3bob>:  4004ed:	55                   	push   %rbp  4004ee:	48 89 e5             	mov    %rsp,%rbp  4004f1:	c7 45 f0 00 00 00 00 	movl   $0x0,-0x10(%rbp)  4004f8:	c7 45 f4 01 00 00 00 	movl   $0x1,-0xc(%rbp)  4004ff:	c7 45 f8 02 00 00 00 	movl   $0x2,-0x8(%rbp)  400506:	c7 45 fc 03 00 00 00 	movl   $0x3,-0x4(%rbp)  40050d:	c7 45 00 04 00 00 00 	movl   $0x4,0x0(%rbp)  400514:	5d                   	pop    %rbp  400515:	c3                   	retq

这里由于没有插入溢出检测代码,所以bob()函数栈空间没有进行16bytes倍数的栈对齐预留。因为该函数调用完毕就会返回,因为没有进行预留,所以你就会发现数组溢出写入的字段,已经覆盖了返回函数栈的rbp栈底指针,如下图:

哎呦,完了,程序要跪了,我们执行一下,结果你会发现,并没有挂掉:我们看看上一个函数main函数的汇编代码:

123456789
0000000000400516 <main>:  400516:	55                   	push   %rbp  400517:	48 89 e5             	mov    %rsp,%rbp  40051a:	e8 ce ff ff ff       	callq  4004ed <_Z3bobv>  40051f:	b8 00 00 00 00       	mov    $0x0,%eax  400524:	5d                   	pop    %rbp  400525:	c3                   	retq     400526:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)  40052d:	00 00 00

你会发现,bob()结束时,pop出来被覆盖了的rbp寄存器的值,在main函数中并没有被使用,main中结尾直接pop出了上一层函数的rbp寄存器的值,所已这个溢出问题并没有导致代码coredump。当修改main()中的代码,填加一个变量后:

12345678
void bob(){    int a[4]={0,1,2,3};    *(a + 4) = 4;}int main(){    int a = 5;    bob();}

上面gcc4.8.4环境下同样编译,你就会发现,该代码会coredump
main()汇编代码如下:

12345678910111213
0000000000400516 <main>:  400516:	55                   	push   %rbp  400517:	48 89 e5             	mov    %rsp,%rbp  40051a:	48 83 ec 10          	sub    $0x10,%rsp  40051e:	c7 45 fc 05 00 00 00 	movl   $0x5,-0x4(%rbp)  400525:	e8 c3 ff ff ff       	callq  4004ed <_Z3bobv>  40052a:	b8 00 00 00 00       	mov    $0x0,%eax  //core原因在这一条语句  40052f:	c9                   	leaveq   400530:	c3                   	retq     400531:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)  400538:	00 00 00   40053b:	0f 1f 44 00 00       	nopl   0x0(%rax,%rax,1)

leaveq等同于:

12
mov %rbp %rsp                                            pop rbp

我们知道rbp寄存器的值已经是被覆盖了的错误地址。所以,把他赋值给rsp后,栈顶指针就是一个错误的指向,再去执行pop指令,就会导致异常。

4. 总结

讲了这么多,其实一开始就只是想研究一个GCC的栈溢出检测机制,结果发现自己知之甚少,就从编译选项,研究到栈空间对齐,最终对程序的运行过程从汇编的角度又是深入了几分。
所以本文彻底解除了我多年的困惑:数组越界,为什么有时候会coredump,有时候不会coredump,这个gcc编译器的环境,以及os都有着莫大的关系。

原文地址:https://www.cnblogs.com/mysky007/p/11105307.html

时间: 2024-10-08 12:03:00

《黑客攻防技术-系统实战》第二章--栈溢出4的相关文章

《黑客攻防技术-系统实战》第二章--栈溢出2

参考文献: <黑客攻防技术宝典-系统实战> <汇编语言> 上一节我们已经对栈有个一个清楚的认识,这节从以下几个点来讲解栈溢出: 1)栈缓冲溢出 2)控制EIP 3)利用漏洞获取特权 4)战胜不可执行栈 一. 栈缓冲溢出 这一节我们看下过多数据放进缓冲区之后,缓冲区将发生什么变化,在了解这些变化之后,我们就可以利用缓冲区溢出做一些骚操作了 先来看一个栗子: #include <stdio.h> #include <stdlib.h> void return_i

《黑客攻防技术宝典Web实战篇》.Dafydd.Stuttard.第2版中文高清版pdf

下载地址:网盘下载 内容简介 编辑 <黑客攻防技术宝典(Web实战篇第2版)>从介绍当前Web应用程序安全概况开始,重点讨论渗透测试时使用的详细步骤和技巧,最后总结书中涵盖的主题.每章后还附有习题,便于读者巩固所学内容. <黑客攻防技术宝典(Web实战篇第2版)>适合各层次计算机安全人士和Web开发与管理领域的技术人员阅读.本书由斯图塔德.平托著. 目录 编辑 第1章 Web应用程序安全与风险 1.1 Web应用程序的发展历程 [1] 1.1.1 Web应用程序的常见功能 1.1.

黑客攻防技术宝典web实战篇:核心防御机制习题

猫宁!!! 参考链接:http://www.ituring.com.cn/book/885 黑客攻防技术宝典web实战篇是一本非常不错的书,它的著作人之一是burpsuite的作者,课后的习题值得关注,而随书附带有答案. 1. 为什么说应用程序处理用户访问的机制是所有机制中最薄弱的机制? 典型的应用程序使用三重机制(身份验证.会话管理和访问控制)来处理访问.这些组件之间高度相互依赖,其中任何一个组件存在缺陷都会降低整个访问控制并访问他机制的效率.例如,攻击者可以利用身份验证机制中的漏洞以任何用户

Delphi知识点与技术概述【第二章 核心类库】

第三章 核心类库 Delhpi可视化编程依赖于庞大的巨型类库.Delphi 标准类库包含了数百个类以及数以千计的方法. 内容提要: *RTL包.CLX与VCL CLX用作linux中,VCL用作Windows中 VCL是一个独立的大型库(组件,控件,非可视组件,数据集合,数据感应控件,等等). 库的核心非可视化组件与类属于RTL包. Vcl结构: CLX结构: BaseCLX VisualCLX DateCLX NetCLX 库的VCL专用部分: VCL还提供了Windows专用的: Delph

《The Django Book》实战--第二章--动态网页基础

这章演示了一些最基本的Django开发动态网页的实例,由于版本不一样,我用的是Django 1.,6.3,有些地方按书上的做是不行的,所以又改了一些,写出来让大家参考. 这是一个用python写的一个显示当前时间的网页. 1.开始一个项目. 在命令行中(指定要保存项目代码的盘或文件夹下)输入 python ...\django-admin.py startproject djangobook  (虽然在环境变量Path中加入了django-admin.py的地址,但是在前面还是要加上路径名,不知

Delphi知识点与技术概述【第二章 运行时库(RTL)】

内容提要: *RTL概述 运行时库简称RTL,是一个非常庞大的函数集合. RTL的单元 SysUtils与SySConst单元 Sysconst单元定义了一些由其他RTL单元显示消息的常量字符串,这些字符串用resourcestring关键字来声明,并保存在程序资源中.它一些特性我们经常使用,如:IntToStr或Format,windows版本信息等. 时间日期操作,不会引起异常. TryStrToDate 将字符串转换为日期 TryEncodeDate 对日期进行编码 TryEncodeTi

.NET Core IdentityServer4实战 第二章-OpenID Connect添加用户认证

原文:.NET Core IdentityServer4实战 第二章-OpenID Connect添加用户认证 内容:本文带大家使用IdentityServer4进行使用OpenID Connect添加用户认证 作者:zara(张子浩) 欢迎分享,但需在文章鲜明处留下原文地址. 在这一篇文章中我们希望使用OpenID Connect这种方式来验证我们的MVC程序(需要有IdentityServer4),我们首先需要干什么呢?那就是搞一个UI,这样非常美观既可以看到我们的身份验证效果,那么Iden

谈谈黑客攻防技术的成长规律(aullik5)

黑莓末路 昨晚听FM里谈到了RIM这家公司,有分析师认为它需要很悲催的裁员90%,才能保证活下去.这是一个意料之中,但又有点兔死狐悲的消息.可能在不久的将来,RIM这家公司就会走到尽头,或被收购,或申请破产保护. RIM的黑莓手机以在911事件中仍然能够保持通信而名声大振,在此后美国政府与很多商务人士都采购了黑莓手机,由此黑莓把重点放在了安全性上.很遗憾的是,成也萧何败也萧何,黑莓从此以后错误的判断了智能手机的未来,一直死盯着安全与商务功能不放,最终走到了今天的地步. RIM的问题很多,比如在软

黑客攻防技术宝典——web实战篇

第7章 攻击会话管理 在绝大多数Web应用程序中,会话管理机制是一个基本的安全组件.它帮助应用程序从大量不同的请求中确认特定的用户,并处理它收集的关于用户与应用程序交互状态的数据.会话管理在应用程序执行登录功能时显得特别重要,因为它可在用户通过请求提交他们的证书后,持续向应用程序保证任何特定用户身份的真实性. 由于会话管理机制所发挥的关键作用,它们成为针对应用程序的恶意攻击的主要目标.如果攻击者能够破坏应用程序的会话管理,他就能轻易避开其实施的验证机制,不需要用户证书即可伪装成其他应用程序用户.