浅谈PWN基础-栈溢出

一、预备知识

缓冲区溢出简单介绍

缓冲区溢出:简单的说,缓冲区溢出就是超长的数据向缓冲区复制,导致数据超出了缓冲区,导致缓冲区其他的数据遭到破坏,这就是缓冲区溢出。而栈溢出是缓冲区溢出的一种,也是最常见的。只不过栈溢出发生在栈,堆溢出发生在堆,其实都是一样的。

如果想用栈溢出来执行攻击指令,就要在溢出数据内包含攻击指令的内容或地址,并且要将程序控制权交给该指令。攻击指令可以是自定义的指令片段,也可以利用系统内已有的函数及指令。

二、简介栈

栈:
栈是一种计算机系统中的数据结构,它按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来),是一种特殊的线性表。栈的操作常用的有进栈(PUSH),出栈(POP),还有常用的标识栈顶和栈底。

  • 可以把栈想象成一摞扑克牌一样,一张一张叠加起来。

  • 进栈(PUSH):将一个数据放入栈里叫进栈(PUSH),相当于在扑克牌的在最上面放了一张新的扑克牌。

  • 出栈(POP):将一个数据从栈里取出叫出栈(POP),相当于在扑克牌的在最上面拿走了一张扑克牌。

  • 栈顶:常用寄存器ESP,ESP是栈指针寄存器,其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。

  • 栈底:常用寄存器EBP,EBP是基址指针寄存器,其内存放着一个指针,该指针永远指向系统栈最下面一个栈帧的底部。

函数状态主要涉及这三个寄存器--esp,ebp,eip。esp 用来存储函数调用栈的栈顶地址,在压栈和退栈时发生变化。ebp 用来存储当前函数状态的基地址,在函数运行时不变,可以用来索引确定函数参数或局部变量的位置。eip 用来存储即将执行的程序指令的地址,cpu 依照 eip 的存储内容读取指令并执行,eip 随之指向相邻的下一条指令,如此反复,程序就得以连续执行指令。

三 函数调用栈的相关知识

浅谈PWN基础-栈溢出_第1张图片

发生函数调用时:

1,首先将被调用函数的参数按照逆序依次压入栈内
2, 将被调用函数的返回地址压入栈内。
3,将调用函数的基地址(ebp)压入栈内,并将当前栈顶地址传到 ebp 寄存器内。
4,将被调用函数的局部变量压入栈内。

函数调用结束时:

1, 首先被调用函数的局部变量会从栈内直接弹出,栈顶会指向被调用函数(callee)的基地址。
2,将调用函数(caller)的基地址(ebp)弹出栈外,并存到 ebp 寄存器内
3,将被调用函数的返回地址弹出栈外,并存到 eip 寄存器内

至此调用函数(caller)的函数状态就全部恢复了

三、 栈溢出原理及简单案例分析

那么,什么是栈溢出呢?栈溢出是指向向栈中写入了超出限定长度的数据,溢出的数据会覆盖栈中其它数据,从而影响程序的运行。

如果我们计算好溢出的长度,编写好溢出数据,让我们想要的地址数据正好覆盖到函数返回地址,那么被调函数调用完返回主函数时,就会跳转到我们覆盖的地址上。通过这样改变程序流程,接下来我们就可以干很多坏事了!
接下来举例说明

#include
 
int fun1()
{
	int a;
	gets((char *)&a);
	return 0;
}
 
int fun2()
{
	printf("stackflow success!\n");
	return 0;
}
 
int main(int argc, char *argv[])
{
	fun1();
 
return 0;
}

gets()是C中的危险函数之一,它不进行边界检查。在我们的例子中,a是int型只有4字节大小的空间,所以当输入的字符大于4字节时,就会发生溢出。而我们的目标就是,让我们的溢出数据覆盖fun1函数的返回地址,具体就是覆盖为fun2函数的地址,使程序的流程跳转到fun2函数去执行。

  • 首先,我们用gcc对这段代码进行编译:

  • gcc -z execstack -fno-stack-protector -o stackflow-example ./stackflow-example.c

(其中-z execstack开启堆栈可执行机制,-fno-stack-protector关闭堆栈保护机制)

  • 用gdb进行调试,可以直接在gets()函数下断点,也可以使用next、step指令快速调试到gets()函数这,在输入AAA后,查看堆栈数据。

  • 在执行完gets()函数并输入AAA后,程序的栈分布情况如下所示,0x00007fffffffe110即是上一函数(调用者main函数)的ebp,0x4005b4是fun1函数的返回地址。

  • 在输入AAAAA后呢,溢出的数据就会存在0x00007fffffffe0f0开始的栈上

所以,我们只需要输入AAAA+AAAAAAAA(覆盖上一函数ebp)+fun2地址(覆盖返回地址),就可以达到我们的目标。

浅谈PWN基础-栈溢出_第2张图片

可以使用examine命令(简写是x)来查看内存地址中的值。x命令的语法如下所示:

x/

n、f、u是可选的参数。

  • n是一个正整数,表示需要显示的内存单元的个数,也就是说从当前地址向后显示几个内存单元的内容,一个内存单元的大小由后面的u定义。

  • f 表示显示的格式,参见下面。如果地址所指的是字符串,那么格式可以是s,如果地十是指令地址,那么格式可以是i。

  • u 表示从当前地址往后请求的字节数,如果不指定的话,GDB默认是4个bytes。u参数可以用下面的字符来代替,b表示单字节,h表示双字节,w表示四字 节,g表示八字节。当我们指定了字节长度后,GDB会从指内存定的内存地址开始,读写指定字节,并把其当作一个值取出来。

  • 表示一个内存地址。

注意:严格区分n和u的关系,n表示单元个数,u表示每个单元的大小。

n/f/u三个参数可以一起使用。例如:

命令:x/3uh 0x54320 表示,从内存地址0x54320读取内容,h表示以双字节为一个单位,3表示输出三个单位,u表示按十六进制显示。

紧接着,我们需要找到fun2函数的起始地址,来完成我们对程序流程的劫持。这个程序比较简单,可以直接在调试的时候快速找到fun2函数的地址,正常我们可以使用如下命令查找。
例如:

   disass fun2,显示fun2函数对应的汇编代码 

浅谈PWN基础-栈溢出_第3张图片

fun2函数地址==0x400586

最后完成栈溢出,改变程序执行流程!
在这里插入图片描述
以上是我个人对于栈溢出的拙见,如有不足请大牛指正!

你可能感兴趣的:(浅谈PWN基础-栈溢出)