手把手教你栈溢出入门
文章目录
1.初识漏洞函数
观察下面C语言代码:
#include<stdio.h>
void vulnerable(){
char buf[10];
gets(buf);
}
int main(){
vulnerable();
printf("运行完毕");
}
你会发现这个代码很简单,但是它会产生巨大的安全隐患。在C语言中,gets()函数是典型的危险函数。那么gets()函数为什么会产生巨大的安全隐患呢?
2.栈及其栈帧结构与函数调用
这要从内存的运行原理说起。这里需要涉及一点点的汇编知识。
在内存中有一种叫做栈的结构,当C语言调用函数时,会在栈中产生一个栈帧。内存中的栈的工作原理与数据结构栈无异,同样是后进先出。如图所示:
C语言调用函数时,内存中的栈结构里面会产生栈帧,一个函数对应一个栈帧。
如图可知,这是调用函数时栈的变化情况。
我们以最初给出的C语言代码为例,我们假设调用main函数之前内存中的栈为空。进入main函数时,也就是main栈帧开始形成,在这里把main函数叫做caller_function。
在介绍栈帧形成之前,我们还得补充相关的寄存器知识。
在内存的栈结构中,EBP、ESP、EIP这三个寄存器起着主要作用。同时要知道的是,栈结构在变化过程中是从高地址向低地址生长的。EBP指向栈帧的基地址,ESP指向栈帧的顶部,同时也是栈结构的顶部。
3.栈帧的形成释放和栈溢出的产生过程
这里介绍vulnerable函数栈帧的形成过程及其释放过程。
为了更好的学习栈溢出,我们必须理解函数调用过程中栈帧的形成以及释放过程,栈帧的形成和释放过程是函数调用的底层实现,只有掌握了栈帧的形成和释放原理,才能更好地理解栈溢出漏洞原理。
在介绍栈帧的运行机理之前,必须先理解EBP、ESP、EIP三个寄存器的功能。EBP和ESP在之前已经讲过,栈帧的边界就由这两个寄存器划定,它们分别存储栈帧的基地址和顶部地址。而EIP寄存器的主要功能是控制代码段指令的执行,EIP存储的是代码段相关指令所在的地址,通过EIP寄存器的地址指向相关的机器指令,从而控制程序的执行。
(1)栈帧的形成过程
当函数进入主函数(main函数)并即将执行到vulnerable函数时。返回地址(return address)先行入栈,即将EIP寄存器中存储的地址压入栈中。然后将EBP入栈,即将EBP寄存器中存储的地址压入栈中,紧接返回地址之后。
调用函数时,内存为什么要将EIP寄存器和EBP寄存器的值压入栈中呢?其实这两个步骤在栈帧释放的时候起着重要的作用,这两步就是为了栈帧释放而准备的。
这里请读者先自行尝试理解,稍后再进行解释。
通常,人们在栈中将返回地址(return address)与EBP的之间的分界线作为一个新栈帧的分界线。在这里,我们把这个分界线叫做vulnerable栈帧的起点。所以,虽然我们同时压入了EIP寄存器和EBP寄存器的值,但是从EBP寄存器的值开始才是才是新栈帧的起点。
然后局部变量入栈,在这里是buf数组入栈,buf数组有是个字节的长度,buf数组入栈,意味着我们要在接下来的栈中开辟10个字节的空间。10个字节的空间开辟完毕,vulnerable函数的栈帧就此形成。
(2)gets()函数漏洞的形成机理
观察上述代码,我们发现随着栈空间数据的变化,在buf数组入栈后其实就是C语言代码执行到char buf[10],接下来执行gets()函数需要我们手动输入一串字符串。在我们输入这条字符串之前,vulnerable栈帧中buf数组开辟的10字节空间还处于无数据状态。当我们手工输入一串字符串时,计算机将会自动将数据保存到栈中开辟的10个字节空间中,而且是从低地址向高地址存储。如果我们正好输入10个字符的字符串,那么栈中buf数组的10个字节空间恰好被全部填满,此时不会产生任何安全隐患。
但是gets()函数的危险之处在于:gets()函数无法检查和限制人工输入字符串的长度,所以在实际情况下,我们输入的字符串长度是不受到任何限制的。假如我们输入了超过了10个字符的字符串,那么栈中的某些数据就会发生改变。
我们知道,从高地址往低地址看,我们先后将EIP值(返回地址)、EBP值、和buf数组入栈。由于EIP寄存器和EBP寄存器均是32位寄存器,里面存储的值均占用4个字节。所以EIP值入栈是会在栈中开辟4个字节的空间然后存储在EIP的值,EBP值入栈同样会在栈中开辟4个字节的空间将EBP的值存储进去。
在这里我们知道,在栈中,返回地址、EBP、buf数组所占空间分别为4个字节、4个字节、10个字节(从高地址往低地址)。
但是由于我们输入了超过10个字节的字符串,而且在内存中数据的读取又是从低地址往高地址开始读取。在这里我们假设输入了长度为18个字节的字符串,这个时候栈中返回地址和EBP恰好会被多出的8个字节的字符串所覆盖。这就是该C语言程序的不安全之处。
但是我们只是用多余的字符串将返回地址和EBP给覆盖了而已,如果多出的8个字节的字符串是随意的,那么它仅仅会使程序崩溃。
再换个角度想想,我们可以用这个栈中的数据溢出来做什么呢?
(3)栈帧的释放过程
为了让读者更好的理解如何利用栈溢出漏洞,读者必须知道栈帧是如何释放的。
在本例中,vulnerable函数执行完成后的第一件事就是将原来形成的vulnerable栈帧释放。
在这里我们假设我们输入的是正常的10个字节的字符串,不会产生任何安全隐患,使程序安全地运行。那么栈帧自然就会正常的释放。
从以上提到的知识我们知道,栈结构中存在返回地址、EBP、buf数组空间,并且我们已经往buf数组填入了10个字节的字符串。
栈是后进先出的,函数调用完毕时,自然是buf数组先行释放。在这里我们不讨论buf数组的数据会被释放到内存的哪个位置,我们只要知道buf数组空间被释放了即可,即出栈。
然后就是EBP出栈,EBP出栈并不会像buf数组那样的方式出栈。既然栈中的EBP是来自EBP寄存器,那么我们在弹栈时也是把EBP弹回EBP寄存器。该给的总是要还的,本来EBP寄存器是指向vulnerable函数的基址,但这时EBP寄存器就又指向了上一个栈帧(main栈帧)的基址。然后ESP寄存器进行自加运算,本来是指向vulnerable栈帧的顶部,运算之后就指向了上一个栈帧(main栈帧)的顶部。
然后是返回地址弹栈,返回地址也是如此,它被弹回到EIP寄存器中。被弹回后,EIP指向vulnerable函数的下一个指令printf(“运行完毕”)的机器码形式。其实在形成栈帧的过程中,EIP压入栈的就是vulnerable的下一个指令。这样才可以使得程序在调用完vulnerable函数后继续往下执行。
4.我们可以用栈溢出来做什么?
在黑客的世界里,最让人欣喜若狂的莫过于获取目标主机的root权限了。栈溢出就可以用来获取目标的主机权限。
如果读者已经完全理解并熟悉了前面的知识,就可以学习如何通过最简单的栈溢出来获取权限了。
从前面我们知道我们可以输入超过10个字节的字符串来覆盖返回地址,通过覆盖返回地址来操控EIP寄存器执行我们想要执行的指令。
在最简单的栈溢出利用中,如果我们能够找到system("/bin/sh")这条C语言指令并且执行它,我们就能获取目标主机权限。
假设system("/bin/sh")指令所在地址为0x111111。那么我们可以用这个地址来覆盖返回地址。只要能够获取主机权限,我们不在乎EBP被什么值覆盖。
我们可以向目标程序发送一条指令,我们把它叫做有效载荷(payload),它通过python的pwn模块实现。该指令为:
payload=0xA*'a'+0x4*'b'+p32(0x111111)
该有效载荷由三部分组成,0xA * ‘a’ 覆盖的是buf数组,0x4 * b 覆盖的是EBP,p32(0x111111)覆盖的是返回地址。
只要我们往目标主机发送了该有效载荷,就可以获取目标主机的root权限。
上一篇: CSS 单/多行文本溢出样式
下一篇: 手把手带你了解如何手动配置转账事务