栈溢出原理和利用学习

做PWN题经常遇到栈溢出,有时一些栈的基础知识总是记不清楚,脑子卡顿,所以整理一番,让自己彻底记住它!

1. 什么是栈?

栈,即堆栈,是一种具有一定规则的数据结构,它按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶。

堆栈数据结构的两种基本操作:

  • PUSH:将数据压入栈顶
  • POP  :将栈顶数据弹出

知道了什么是栈,我们看看栈在内存中是怎样一个分布情况。

以Linux 32位平台为例,进程有4GB大小的虚拟地址空间,其中1GB留给系统内核,3GB是进程自身拥有。一个进程大致的内存布局如下图所示。

简单说明一下

代码段:存放可执行程序的代码,可读不可写

数据段:存放程序中已经初始化的静态(全局)变量,可读写

bss段:存放程序中未初始化的静态(全局)变量,可读写

堆(heap):存放动态分配的内容,需要程序猿手动分配和释放

栈(stack):存放局部变量,如函数的参数、返回地址、局部变量等,有系统自动分配和释放

2. 函数调用栈

栈的基本概念和进程内存布局get,让我们继续看看函数调用栈,C语言函数调用过程中栈的PUSH和POP了解一下。

2.1 背景知识

栈增长方向:高地址->低地址

ESP:栈指针寄存器,指向栈顶的低地址

EBP:基址指针寄存器,指向栈底的高地址

EIP:指令指针,存储即将执行的程序指令的地址

函数调用约定:

调用方式cdeclstdcallfastcall
参数传递从右到左压栈从右到左压栈

左边两个参数

分别放在ECX

和EDX寄存器,

其余的参数从

右到左压栈

栈清理调用者函数自身函数自身

(C语言默认的函数调用方式为cdecl)

2.2 函数调用开始

在调用一个函数时,系统会为这个函数分配一个栈帧,栈帧空间为该函数所独有。

调用者调用一个函数的过程大致如下:

  • 函数参数从右到左入栈
  • 返回地址入栈
  • 上一函数ebp入栈
  • ...

在上一函数ebp入栈后,就开辟了被调函数的新栈帧,接下来便是被调函数临时变量入栈等操作,如果被调函数里有继续调用新函数的操作,将继续开始上述的一系列操作,不断循环嵌套下去。下图表示函数调用过程中栈的布局情况。

2.3 函数调用结束

函数调用结束时的变化,主要就是按相反的顺序将数据弹出栈:

  • 弹出临时变量
  • 弹出调用函数的ebp值,存到ebp寄存器中
  • 弹出返回地址,存到eip寄存器中

返回地址即是用call指令调用函数时下一条指令的地址,存到eip中,程序就知道在调用完后继续执行下一条指令。

我们会有一个疑惑,调用函数时将函数参数从右到左入栈,调用结束时怎么没有将它们弹出?

在这里,系统并不是用POP指令将它们弹出,而是通常通过ADD ESP让它们从栈中“消失”。

3. 栈溢出原理

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

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

我们以一个实例(64位程序)对上述栈溢出原理和利用做一个说明。

#include<stdio.h>

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地址(覆盖返回地址),就可以达到我们的目标。

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

fun2函数地址==0x400586

最后完成栈溢出,改变程序执行流程!(注意地址的小端字节序)

当然,在实际的栈溢出中,我们劫持程序的流程,一般会修改返回地址到shellcode或者内存中已经有的一段指令(ROP),一些具体的攻击利用技术,这里暂时先不做详细介绍了。

最后列一些C中会发生缓冲区溢出的危险函数:

  • gets()
  • strcpy
  • strcat
  • sprintf
  • ......
  • 52
    点赞
  • 269
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值