输入的大小超出了分配的缓冲区内存大小,导致溢出。
char string[5];
gets(string); // 输入helloworld
程序相关的数据一般保存在内存的以下区域:
- 数据段:静态存储区(比如字符串字面量)和全局变量
- 代码段:程序机器码
- 栈:函数栈帧,一个函数栈帧包括参数,返回地址(函数返回后cpu执行的下一条指令的地址),局部变量等
- 堆:程序运行时动态申请的内存数据
当调用函数时,逻辑堆栈帧被压入栈中,堆栈帧包括函数的参数、返回地址、EBP(EBP是当前函数的存取指针,即存储或者读取数时的指针基地址,可以看成一个标准的函数起始代码)和局部变量(如果函数有局部变量)。程序执行结束后,局部变量的内容将会消失,但是不会被清除。
当函数返回时,逻辑堆栈帧从栈中被弹出,然后弹出EBP,恢复堆栈到调用函数时的地址,最后弹出返回地址到EIP(寄存器存放下一个CPU指令存放的内存地址,当CPU执行完当前的指令后,从EIP寄存器中读取下一条指令的内存地址,然后继续执行。),从而继续运行程序。所以假如有下图的情况:
由于栈是低地址方向增长的,因此局部数组buffer的指针在缓冲区的下方。当把data的数据拷贝到buffer内时,超过缓冲区区域的高地址部分数据会“淹没”原本的其他栈帧数据,根据淹没数据的内容不同,可能会有产生以下情况:
- 淹没了其他的局部变量。如果被淹没的局部变量是条件变量,那么可能会改变函数原本的执行流程。这种方式可以用于破解简单的软件验证。
2. 淹没了ebp的值。修改了函数执行结束后要恢复的栈指针,将会导致栈帧失去平衡。
3. 淹没了返回地址。这是栈溢出原理的核心所在,通过淹没的方式修改函数的返回地址,使程序代码执行“意外”的流程!
4. 淹没参数变量。修改函数的参数变量也可能改变当前函数的执行结果和流程。
5. 淹没上级函数的栈帧,情况与上述4点类似,只不过影响的是上级函数的执行。当然这里的前提是保证函数能正常返回,即函数地址不能被随意修改(这可能很麻烦!)。
而缓冲区溢出攻击就是利用了第三点,改变了cpu要执行的下一条指令的地址。
但是从实现上来说很难,因为没法预估返回地址在栈区的什么位置,也就无法确定溢出多少字节才能覆盖到返回地址。