缓冲区溢出一:函数调用过程中的堆栈变化及缓冲区溢出利用原理
一、说明
本来是想直接写一个缓冲区溢出的例子,但是一是当前编译器和操作系统有溢出的保护措施没有完全弄清怎么取消,二是strcpy等遇到00会截断需要进行编码这比较难搞,所以最终没有实现。
但已经双看了一阵函数的调用过程,如果全然就此放弃那以后再研究缓冲区溢出又得从0开始研究函数的调用,所以就记些东西下来,免得以后双得从0开始。
在缓冲区溢出中堆栈变化是最为关键的,本文从堆栈入手。另见:“缓冲区溢出二:从缓冲区溢出到获取反弹shell实例”。
二、函数调用过程中的堆栈变化
2.1 使用程序
本文使用程序源代码如下(编写时我创建了一个StackChange工程,将源代码保存为StackChange.c),使用vc++ 6.0编译,使用olldbg逆向。
#include <stdio.h> int get_sum(int a, int b) { int sum; sum = a + b; printf("get_sum: calc sum success, now will return\n"); return sum; } int main(int argc, char **argv) { int a = 1; int b = 2; int sum; sum = get_sum(a,b); printf("main: the sum is %d\n", sum); return 1; }
2.2 堆栈在内存地址空间中的位置
今早等公交的时候看到一个问题大意是,如果堆栈不可执行那么程序为什么还可以执行,从概念上说堆栈段和代码段没关系所以堆栈段不可执行代码段还可以执行。
但我想这位小哥根本上是想知道,堆栈段和代码段分别在内存的什么位置,为什么堆栈段不可执行代码段还可执行。
将2.1中的程序编译后,使用olldbg载入exe,然后打开内存窗口(M),如下图:
图中各种信息都很明了了,具体到堆栈段和代码段的位置,堆栈段为0x0018e000-0x00190000(因为size为0x2000),代码段为0x00401000-0x00422000(因为size为0x2100)。所以堆栈段不可执行,不影向代码段可不可执行。
2.3 函数调用在汇编上的过程
main函数汇编解析,说明看其中的注释:
get_sum函数汇编解析如下,流程和main函数是类似的(应该说所有函数的流程框架都是这样的)不重复注释,需要注意的就是获取参数是使用ebp+8的形式回头获取的
2.4 函数调用过程中的堆栈变化
应该很多人和我一样2.3中的函数调用过程听了一万遍了,只是下次还是记不住;想以此去理解缓冲区溢出更是浮沙筑台,一知半解,过后又忘。
我想了想其症结在于我们总尝试去死记硬背这个过程,重点放在代码段上。
而如果我们从整个程序内存空间在程序执行过程中的变化情况,就会发现代码段区域和数据段区域内容都是不变的,只有堆栈段内容才会变(当然寄存器也是变化的但寄存器不在内存中)。
或者换言之,整个0x00000000-0xffffffff内存地址空间,从程序运行到进程结束,只有堆栈部分的内存即0x0018e000-0x00190000是会变的,其他部分在初始化后都是不变的。
所以也许重点放在堆栈段上,也许我们能更好更解函数调用过程。
(此时想起两年前去面试,面试官问输入的用户名密码存到哪了,回答存内存了,得到基础薄弱的评价,当时是有些不忿的;现在想来内存是0x00000000-0xffffffff,而别人想要的是0x0018e000-0x00190000这么一小段,差距确实是有点大)
2.4.1 堆栈在函数层次的变化
以下是olldbg进入get_sum函数的printf函数内部时的各函数堆栈情况(0x0018fe6c-0x0018ff4c):
以下是olldbg进入main函数的printf函数内部时各函数的堆栈情况(0x0018fe6c-0x0018ff4c):
从以上两图中的变化我们可以部结出以下几点:
1. 栈从高地址向低地址生长。我们前边说过栈地址空间为0x0018e000-0x00190000,这里main并没有从0x00190000开始是因为main函数其实是被系统函数调用启动的,并不能一开始就是main函数。见下图。
2. 层次为父子关系的函数(比如这里的main和get_sum、get_sum和其中的printf),父函数的堆栈在高地址子函数的堆栈在紧接父函数堆栈的低地址。
3. 层次为兄弟关系的函数(比如这里的get_sum和main函数的中的printf),前一函数调用完后其堆栈被释放归回未使用,后一函数执行时使用的堆栈和前一函数一样(开始地址一样,结束地址一般不一样,毕竟各自局部变量需要的空间不一样)
4.堆栈在使用时初始化在释放时不初始化。比如上边我们已进入到main函数的printf,但get_sum未被占用的那部份和get_sum中的printf的堆栈共保留的内容还和原来一样。
2.4.2 单个函数内部的堆栈空间分析
我们使用前边“olldbg进入get_sum函数的printf函数内部时的各函数堆栈情况”时的图来看get_sum函数堆栈的内容
我们将get_sum堆栈空间转化为以下表格:
对应图中地址 | 内容 | 说明 |
0x0018FE84 | ebp(get_sum函数的) | 这其实是printf函数中的push ebp |
0x0018FE88 | eip(printf返回get_sum的) | 这是get_sum函数中call printf压入栈的 |
0x0018FE8C | 形参(printf的) | |
0x0018FE90 | edi(main函数的) | 如果不调用参数,或调用参数返回后,esp指向此处 |
0x0018FE94 | esi(main函数的) | |
0x0018FE98 | ebx(main函数的) | |
0x0018FE9C-0x0018FED8 | 预留空间 | 编译器总会给比参数需要的空间多一些的空间,malloc的话会从0x0018FE9C往0x0018FED8方向使用 |
0x0018FEDC | 局部变量已使用空间 | 所谓缓冲区溢出,就是此处的参数长度溢出,向后覆盖0x0018FEE4处的eip |
0x0018FEE0 | ebp(main函数的) | 这是get_sum函数形头push ebp压入栈的 |
0x0018FEE4 | eip(get_sum返回main的) | 这是main函数中的call get_sum压入栈的,不属于get_sum的堆栈空间 |
三、缓冲区溢出
3.1 缓冲区溢出利用的原理
缓冲区溢出:给本函数局部变量赋的值其长度超过了变量定义的长度
结合2.4.2最后的表格中我们可以得到缓冲区溢出利用的原理就是:给本函数局部变量赋长度超过其定义长度的值,使本函数返回上层函数的eip值被覆盖成自己想去执行的语句的地址。
如果这个定义还感觉不是很明了,那我们举个例子:main函数调用了vuln_fun函数,vuln_fun函数调用了strcpy,strcpy没注意长度会引发缓冲区溢出。此时溢出发生在vuln_fun函数的堆栈中,而被覆盖的eip是vuln_fun执行完返回main的eip。
所以可以给缓冲区溢出漏洞下一个更简单的定义:缓冲区溢出发生在调用strcpy/strcat/sprintf/vsprintf/gets/scanf的函数中,被覆盖的eip是该函数返回上层函数的eip。
3.2 缓冲区溢出利用的难点
3.2.1 不考虑系统与编译器无保护机制时的难点
在给变量赋的超长字符串中包含以下三部份内容:填充数据、注入的要执行的汇编语句、注入的要执行的汇编语句的地址。
填充数据,随便点就行了不是难点。
注入的要执行的汇编语句,在不考虑编译器和系统保护机制的情况下,大概是把自己要执行的语句写成c程序,然后编译成exe,然后再把语句对应的十六进制dump出来就行了;不过strcpy等函数遇到00会认为字符串结束,而期望dump出的十六进制刚好没有00是不太现实的,需要对其进行编码处理。
注入的要执行的汇编语句的地址,这又有两个难点:一是要确定变量到eip的距离,以便刚好能在eip的位置写入想要执行的代码的地址;二是要确定注入的、想要执行的代码的地址是多少。
3.2.2 编译器与操作系统的保护措施
编译器有栈不可执行、栈保护两种措施,编译器与操作系统联动则还有内存布局随机化。更具体内容可参考:https://www.jianshu.com/p/47d484b9227e
3.2.3 为什么很多overflow的cve没有exp
长期以来,我都将存在缓冲区溢出漏洞等同于系统命令执行、等同于系统沦陷。但很多overflow类型的cve都只是评分“很低”的dos而不是execute code,而且只有极少数才有exp,这很令人不解。
而基于以上难点的讨论,这种现像就好理解了,缓冲区溢出到导致程序运行出错所以基本都能dos,但由于编写shellcode本身就比较困难再加上各种保护机制,有溢出不是必然就有exp的。
参考:
https://www.jianshu.com/p/47d484b9227e
https://www.shiyanlou.com/courses/231
https://www.cnblogs.com/yejianyong/p/7506465.html
https://blog.csdn.net/nicholas199109/article/details/8560988