栈溢出漏洞原理详解与利用
和我一样,有一些计算机专业的同学可能一直都在不停地码代码,却很少关注程序是怎么执行的,也不会考虑到自己写的代码是否会存在栈溢出漏洞,借此机会我们一起走进栈溢出。
本文首发于“合天智汇”公众号,作者:threepwn
0x01 前言
和我一样,有一些计算机专业的同学可能一直都在不停地码代码,却很少关注程序是怎么执行的,也不会考虑到自己写的代码是否会存在栈溢出漏洞,借此机会我们一起走进栈溢出。
0x02 程序是怎么运行的
在了解栈溢出之前我们先了解一下程序执行过程
程序的执行过程可看作连续的函数调用。当一个函数执行完毕时,程序要回到call指令的下一条指令继续执行,函数调用过程通常使用堆栈实现
#include <stdio.h> #include <stdlib.h> int main(int argc, char **argv) { test1(1); test2(2); test3(3); return 0; } int test1(int test1){ int a = 6; printf("1"); return 1; } int test2(int test2){ printf("2"); return 2; } int test3(int test3){ printf("3"); return 3; }
编译成32位可执行文件,放在ollydbg中就行调试,来详细看一下执行过程
因为程序的执行可以看做一个一个函数的执行(main函数也一样),因此我们挑选其中一个即可,在test1()函数设置断点
F7单步调试第一步mov dword ptr ss:[esp],0x1,进行传参,简洁明了。
第二步call mian.00401559,进入test(),这里我们关注一下esp和栈顶值,将该指令的下一条指令的地址进行压栈,既然有压栈那么就会有出栈,这就与函数中的retn指令形成呼应。
第三步push ebp,就是把ebp的值进行压栈,那么这个ebp是什么呢?有什么用呢?
EBP叫做扩展基址指针寄存器(extended base pointer) ,里面放一个指针,该指针指向系统栈最上面一个栈帧的底部,用于C运行库访问栈中的局部变量和参数。那么这一步的意义就是:保存旧栈帧中的帧基指针以便函数返回时恢复旧栈帧
第四步,mov ebp,esp,将esp的值放在ebp中,我们再来了解一下什么是esp?
ESP(Extended Stack Pointer)为扩展栈指针寄存器,是指针寄存器的一种,用于存放函数栈顶指针,指向栈的栈顶(下一个压入栈的活动记录的顶部),也就是它不停在变,刚才提到的ebp指向栈底,在函数内部执行过程中是不变。
那么我们再看一下这一步的作用:
从第三步可以知道esp存储的值是旧栈帧中的帧基指针,而esp值栈顶指针,随时都在变,因此为了函数结束后能恢复,把esp值(外层函数栈底地址)保存在本函数栈底ebp中。简而言之,将内部函数ebp的值作为地址,它存放外函数的ebp的值。这一步在末尾也存在逆向指令leave。
第五步是sub esp,0x28,开辟该函数的局部变量空间
紧接着第六步mov dword ptr ss:[ebp-0xC],0x6,给变量a一个大小是0xC的空间,并且赋值。
然后就是传参字符1的ascii码,调用printf函数,把返回值放到eax。
我们重点来看leave指令,可以发现ebp的值恢复了,esp的值也变了,相当于mov esp,ebp;pop ebp
最后执行retn指令,至此一个函数执行完毕,esp和eip的值都被改变,相当于pop eip,然后程序继续执行。EIP是指令寄存器,存放当前指令的下一条指令的地址。CPU该执行哪条指令就是通过EIP来指示的
0x03 栈溢出
分析完这一过程,相信大家对函数是怎么执行的应该明朗了,那么我们言归正传,继续聊一下栈溢出。首先我们先看一下什么是栈?
栈可以看作是一个漏斗,栈底地址大,栈顶地址小,然后在一个存储单元中,按照由小到大进行存储,它的目的是赋予程序一个方便的途径来访问特定函数的局部数据,并从函数调用者那边传递信息。
栈溢出属于缓冲区溢出,指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。
另外,我们也不难发现,发生栈溢出的基本前提是:程序必须向栈上写入数据、写入的数据大小没有被良好地控制。引用一个例子来了解一下栈溢出
#include <stdio.h> #include <string.h> void success() { puts("You Hava already controlled it."); } void vulnerable() { char s[12]; gets(s); puts(s); return; } int main(int argc, char **argv) { vulnerable(); return 0; }
很显然符合以上两个条件,gets()成为突破口我们在主函数处下断点,运行和调试
lea eax,dword ptr ss:[ebp-0x14] 这时开辟一个空间给变量,也即是s,如图所示
我们想执行sucess()函数,要怎么办呢?
执行完vulnerable()函数后,会还原ebp,改变esp的值(leave),然后retn,也就是pop eip,然后CPU根据eip指针指向的指令继续运行。
我们能抓到的点就是控制eip,怎么控制?通过控制栈顶的值,那么栈顶的值是什么?栈顶的值是进入该函数时储存的下一条指令的地址。这里提一点,进入函数,要保存两个值:下一条命令的地址、EBP旧栈帧的帧基指针,只有这样才能完全恢复。
此时我们可以构造payload,来控制我们要控制的地方,栈中存储EBP值的存储单元的上一个存储单元,也就是图中的存储address的存储单元
我们先试验一下输入0x14 *\’A\’+BBBB+0000,发生的变化
很好,按照我们的预想进行(python -c \’print “A”* 0x18+p32(0x00401520)\’) 就可以达到栈溢出的效果
0x04 尾记
还没有入门,只是个人的见解,如有错误,希望各位大佬指出。
参考:
https://en.wikipedia.org/wiki/Stack_buffer_overflow
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/stackoverflow/stackoverflow-basic-zh/
http://hetianlab.com/cour.do?w=1&c=CCID31b0-fe03-4277-8e2f-504c4960d33f