必威体育Betway必威体育官网
当前位置:首页 > IT技术

栈溢出例子理解

时间:2019-10-04 04:45:37来源:IT技术作者:seo实验室小编阅读:71次「手机版」
 

栈溢出

背景:最近在看一些教学视频,然后主讲人敲了一段栈溢出的代码。我当场蒙蔽了-。- ! 而且他也没有细讲原理,最为一名爱探险星人,我决定Get it。

栈溢出示例代码:

#include<windows.h>
#include<stdio.h>
#include<stdlib.h>


void  Msg() {
	messageBoxA(NULL, "嘿嘿!", "堆栈溢出测试", 0);
}

int  Add(int a, int b) {
	int* p = &a;
	*(p-1) = (int)Msg;
	return a + b;
}

void main() {
	printf("%d", Add(1, 2));
	system("pause");
	return;
}

运行结果:

按下确定以后出现异常:

 

首先在讲解原理之前首先介绍一些基本知识便于理解原理:

汇编层面的函数调用过程

每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)。

下图表示当正在执行FunctonA函数时的栈情况:系统栈可以认为是全部栈空间。栈帧对应每一个函数调用,EBP寄存器存放当前活动栈帧的栈底,ESP寄存器存放当前活动栈帧的栈顶。当前函数可以在当前的栈帧区域内存放局部变量和信息,全局的变量不存放在栈,有专门的区域存放。

下图表示call FunctionB之前做的工作:首先PUSH 函数的参数,从右向左压入,然后保存call FunctionB下一条指令的地址,便于函数返回。这个保存下一条指令地址和跳转到FunctionB处由 call 指令完成。

下图表示创建新的FunctionB的栈帧:首先PUSH EBP 保存旧栈帧的栈底,用于函数返回。然后MOV EBP,ESP,设置当前EBP为旧栈帧栈底的地址处(如下图),最后SUB ESP, 0X0C0H ,ESP向上开辟空间,具体开辟多少根据编译器。到此新的栈帧开辟完了。(题外话:FunctionB可以通过EBP+8 获取到arg0,EBP+12获取到arg1,这就是为什么倒着压入参数的原因。如果FunctionB里面有局部变量,则可以放在EBP和ESP这段栈空间里面。)

下图表示FunctionB函数返回后栈的变化:首先 MOV ESP,EBP POP EBP来还原EBP为旧的栈帧栈底。然后RET 到call FunctionB的下一条指令处(RET 包含POP JMP,所以下一条指令地址恰好被提出),最后ADD ESP,8 ,去掉压入的参数,8是因为压入了2个参数。到此已经还原了原来的环境了。整个调用过程结束了。

 

现在进入主题,介绍原理:上面的代码核心思想就是改变调用Add(1,2)时,改变返回的地址(就是下一条指令的地址):

修改这个地址内容为Msg()入口地址,这样就会执行Msg()代码。关键时怎么确定这个地址,然后写入Msg()入口地址搞定他。其实我们可以通过Add(int a,int b)的参数a确定下一条地址的地址,如图:获取a变量的地址,然后向上退就可以到下一条指令的地址,如后覆盖为Msg(),入口地址即可。

关键代码解释:

int* p = &a;//获取a变量的地址
*(p-1) = (int)Msg;//上退覆盖地址为Msg入口地址,这里(p-1)而不是-4是因为p为地址,减一就是减一个字

下面上几张运行关键图:

 

 

友情提示:内存数据倒着读。可见 77 18 31 01 -> 01 31 18 77 确实是 add esp,8 的地址。

可以看到01 31 11 13并不是Msg地址,其实这个还要跳转一下,如下图:

下面讲一下崩溃的原因。由于正常调用Msg需要call 指令,而call 指令通常会有一个PUSH 下一条指令操作,而这里是直接通过Add 的RET POP 地址到EIP 执行,没有PUSH 操作,所以当执行玩Msg以后函数返回时RET 到EIP 的其实是参数a=1,然后EIP去00 00 00 01取指令当然GG了-。-!。可以贴一张内存证明下:

-------------------------------------------------------------------------------------------------------------------------------------------------------

例子原理算是讲完了,这个报错只是说明这个溢出姿势不够高雅。但是作为一名爱探险星人,有必要稍微高雅点,所以下面我就改进了下代码,使其正常运行结束。 这里我改的层面是C语言不是汇编层,所以想要即跳转执行Msg又完全还原寄存器状态当前没成功(有精力的同志去尝试下吧),当然汇编可以的。最后Add显示的结果就不是3了,因为状态没有完全还原。

int  Add(int a, int b) {
	int* p = &a;
	*p = *(p - 1) + 4;//添加的代码,p-1指向原来跳转地址,然后加4
	*(p - 1) = (int)Msg;
	return a + b;
}

由于报错的原因是因为参数a=1,出问题于是我们可以首先保存原来的跳转地址到a处,但是不能为原来的地址,需要加4跳到如图指令:

首先不能跳到add esp,8 是因为正常返回以后ESP 在参数a=1处,所以add esp,8可以去掉压入的两个参数,但是现在返回时已经在b=2处了,所以这句不要执行,否则会破坏以前的数据。push eax也不跳,因为按照正常的流程返回以后栈如下图:

然后push eax 压入prinf第二个参数(注意位置位于b=2处),即显示的Add结果,在push 第一个参数(注意位置位于a=1处)。但是当中途执行Msg返回后如图:

待定原跳转地址就是现在讨论的地址,可以看见ESP指向了b=2,这里本来应该是push eax存放Add返回结果的(这就是为什么显示的结果为2的原因,运行结果显示在下面),而上面的待命原跳转地址应该放printf的第一个参数,即"%d"。现在明白了吧,我们跳到压入第二个参数的地方,不跳第一个push eax。因为现在ESP已经指向了b=2,可以认为已经压完了第一个参数,如果你一定要跳第一个参数处,没问题!报错在等你!所以待定的跳转地址为原跳转地址加4。

代码展示:

int  Add(int a, int b) {
	int* p = &a;
	*p = *(p - 1) + 4;//添加的代码,*(p-1)提取旧的跳转地址,然后加4
	*(p - 1) = (int)Msg;
	return a + b;
}

运行效果图:

 

   -------------------------------------------------------------------------------------------------------------------------------------------

                看了这篇文字,不说别的,相信读者调试BUG的境界已经上升了一个境界-。- 可以在内存里面找BUG了。

相关阅读

文本溢出隐藏

1、单行文本溢出隐藏{overflow : hidden;text-overflow : ellipsis;white-space : nowrap;}2、多行文本溢出{overflow : hidden;t

JAVA堆栈

转自:https://blog.csdn.net/lk274857347/article/details/77512555基本概念1.寄存器:最快的存储区, 由编译器根据需求进行分配,我

【干货】堆栈溢出一般是什么原因?

堆栈是一个在计算机科学中经常使用的抽象数据类型。堆栈中的物体具有一个特性: 最后一个放入堆栈中的物体总是被最先拿出来, 这个特

Java堆栈简介

JAVA在程序运行时,在内存中划分5片空间进行数据的存储。分别是:1:寄存器。2:本地方法区。3:方法区。4:栈。5:堆。基本,栈stack和堆heap这

彻底理解Java中堆和栈的区别

1、概述 在Java中,内存分为两部分,一种是堆内存,另一种就是栈内存。 2、Java中变量在内存中的分配 1). 类变量(static修饰的变

分享到:

栏目导航

推荐阅读

热门阅读