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

参考文献资料

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

原创作品,打字不易,转载请备注

  栈缓冲溢出一直是最流行, 我们理解最透彻的安全问题之一,尽管栈缓冲溢出是我们最了解,最公开的漏洞之一,但是现在在我们的软件中仍然存在栈溢出的问题,在这里首先介绍栈缓冲溢出的问题

  希望通过栈溢出这一系列的文章,可以对栈有更深刻的理解,避免在编码的时候出现这种问题,另外我们可以利用栈缓冲存在的漏洞对系统进行攻击动作,这是这次学习的目标

  一.  缓冲区

  “缓冲区”: 一片有限,连续的内存区域, 在C语言中,最常见的缓冲区就是数组, 这节主要介绍与数组有关的内容

  在C语言中,没有专门的机制去考虑检查缓冲区的内在边界, 所以栈溢出就容易出现,一旦输入的数据超出缓冲区的范围,从而改写其他缓冲区域, 什么事情都会发生

  先看一个例子:

1 #include <stdio.h>
2 #include <stdlib.h>
3 
4 int main()
5 {
6     int array[5] = {1, 2, 3, 4, 5};
7     printf("%d \n", array[5]);              
8 }

因为是作为演示,这里我们故意打印下标5, 编译运行结果会得到一个非常奇怪的数字:

32766

这个例子显示,当C没有提供内存保护机制的时候,越过缓冲区读取其他数据是非常容易的,如果我们输入的数据超出缓冲区会发生什么呢?

举个栗子:

int main()
{
    int array[5];
    int i;

    for (i = 0; i < 255; i++)
        array[i] = 10;
}

为什么要设置255这么大呢? 为了达到效果, 增大踩内存的概率, 因为并不是所有超出内存的读写都会导致问题, 只有超出的内存已经有有效数据的时候出错

运行之后结果:

Segmentation fault (core dumped)

下面我们从内存管理的角度来理解怎样溢出栈上存储的缓冲区

二. 栈

  为什么我们要介绍栈呢? 和上面内容有什么关系呢? 我们看一下下面的C 程序的内存布局,然后分析一下

 

 

 

一共有五个区:

文字段:

  • 文本段包含进程运行的程序的  机器语言指令。
  • 此段是只读的,因此进程不会通过错误指针意外更改任何值。
  • 文本段是可共享的,因此程序代码的单个副本一次驻留在虚拟地址空间中。

初始化数据段:

  • 包含显式初始化的全局变量和静态变量。
  • 当程序加载到内存中时,将从可执行文件中读取这些变量的值。

未初始化的数据段:

  • 包含未显式初始化的全局变量和静态变量。
  • 系统将此段中的所有内存初始化为ZERO(0)。
  • 该段也称为BSS(由符号开始的块)。
  • 没有必要为未初始化的数据分配空间,这就是为什么它们被放置在单独的段中的原因。
  • 该段由Loader在运行时分配,因此仅需要记录其位置和大小。

堆栈段:

  • 堆栈段是包含堆栈帧的动态增长和收缩段。
  • 它包含程序堆栈,LIFO结构。注册“堆栈指针”跟踪堆栈的顶部。
  • 每个函数都有一个堆栈框架。
  • 一个帧存储函数的局部变量参数和返回值。
  •  它们存储自动变量和局部变量。

堆段:

  • 它在运行时存储动态分配的内存。
  • malloc,realloc和free在堆区域动态分配,可以使用brk和sbrk系统调用来调整其大小
  • 它由进程中的所有共享库和动态加载的模块共享。

 那么我们看一下数组应该是在栈里面分配的, 为什么我们不放在堆里面呢?下面我们对比一下堆和栈的本质区别或许就知道原因了

1)申请后系统的响应
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,
会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块 内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的 大小,系统会自动的将多余的那部分重新放入空闲链表中。

2)申请大小的限制
栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因 此,能从栈获得的空间较小。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

3)申请效率的比较:
栈由系统自动分配,速度较快。但程序员是无法控制的。
堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活。

4)堆和栈中的存储内容
栈: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排

 

好了, 言归正传, 继续介绍栈:

  这里我们从底层的角度来看为什么函数使用栈呢?  主要目的是为了更有效的使用函数, 函数可以改变程序执行的流程, 因此,一条指令可以单独执行, 更重要的是,

函数执行结束之后将把控制权交给调用者, 通过使用栈, 函数的整个调用过程更有效率

下面举个栗子:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 void function(int a, int b)
 4 {
 5     int array[5];
 6 }
 7 void main()
 8 {
 9     function(1, 2);
10     printf("This is where the return address points");
11 }

这个例子中, 系统首先会先执行main里面的指令, 碰到函数调用的时候, 系统中断正常流程, 进行函数调用的前的处理,然后执行function里面的指令,整个过程是:

function 的参数a, b 参数圧入栈 ==> 系统调用函数,把函数的返回地址(RET,  RET保存的是调用函数是的指令指针EIP的地址)压入栈 ==>  调用函数

 

接下来我们从汇编角度来学习这个例子:

gcc -mpreferred-stack-boundary=2 -ggdb func.c -o func

但是发现报错了:

func.c:1:0: error: -mpreferred-stack-boundary=2 is not between 4 and 12
 #include <stdio.h>

 为什么智能在4 - 12之间取值呢? 现在我们看一下 -mpreferred-stack-boundary 做什么用的呢?

我找到了一个外文资料在这里贴一下,怕翻译不够精准

I'm trying to visualize and understand how to utilize mpreferred-stack-boundary(more like build
code to exploit it for school). From reading the gcc manual, it states that it aligns the stack
 according to mpreferred-stack-boundary=number, where number is the exponent to base 2. By
 default, number=4 so the alignment of the stack is 2^4= 16 bytes. I don't know if it's the caffeine 
messing with my brain, but all the shell code injections I've seen in class demand that we use
 mpreferred-stack-boundary=2 when compiling, which would align the stack by 4 bytes. So does
 that mean I have that the variables placed on the stack try to fill the stack 16 bytes at a time by
 default? Also, why does shellcode that I place in the buffer work when the boundary when it is set
 to 2 yet does not work when run it in default mode?

 下面是解决:

The size of the whole stack frame will be rounded up to 16 bytes, not each individual local variable.
 Shellcode would work either way, but code is written for one particular layout so you need to use
 different shellcode for different layout. – Jester

 这里我们修改对齐方式:

gcc -mpreferred-stack-boundary=4 -ggdb func.c -o func

 就可以编译通过了

说下为什么这么编译: 因为我们要进行反汇编,进行gdb调试所以需要加上-ggdb选项, 另外我们还应该使用优化栈边界选项,因为它将使栈以双字节单位递增(或递减),

否则gcc将对栈进行优化,这样分析起来更加复杂了

G480:huibian$ gdb func 
GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1
Copyright (C) 2014 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from func...done.
(gdb) 

 先看下程序是如何调用函数function的, 反汇编main:

(gdb) disassemble main
Dump of assembler code for function main:
   0x0000000000400539 <+0>:	push   %rbp
   0x000000000040053a <+1>:	mov    %rsp,%rbp
   0x000000000040053d <+4>:	mov    $0x2,%esi
   0x0000000000400542 <+9>:	mov    $0x1,%edi
   0x0000000000400547 <+14>:	callq  0x40052d <function>
   0x000000000040054c <+19>:	mov    $0x4005e8,%edi
   0x0000000000400551 <+24>:	mov    $0x0,%eax
   0x0000000000400556 <+29>:	callq  0x400410 <printf@plt>
   0x000000000040055b <+34>:	pop    %rbp
   0x000000000040055c <+35>:	retq   
End of assembler dump.

 在<main + 4> 和 <main + 9> 参数0x1 0x2 被先后压入栈中

在<main + 14> callq call指令将RET(EIP)压入栈中

后面将控制权交给位于 0x40052d

现在我们观察控制权交接之后发生了什么?

(gdb) disassemble
Dump of assembler code for function function:
   0x000000000040052d <+0>:	push   %rbp
   0x000000000040052e <+1>:	mov    %rsp,%rbp
   0x0000000000400531 <+4>:	mov    %edi,-0x24(%rbp)
   0x0000000000400534 <+7>:	mov    %esi,-0x28(%rbp)
=> 0x0000000000400537 <+10>:	pop    %rbp
   0x0000000000400538 <+11>:	retq   
End of assembler dump.

 可以看到这里面只是初始化了array, 没有做别的动作

关于栈上缓冲区的溢出下节再分析学习, 不知不觉很晚,起来还要搬砖

posted @ 2019-06-21 02:04  坚持,每天进步一点点  阅读(721)  评论(0编辑  收藏  举报