20155306 白皎 0day漏洞——漏洞利用原理之栈溢出利用
一、系统栈的工作原理
1.1内存的用途
根据不同的操作系统,一个进程可能被分配到不同的内存区域去执行。但是不管什么样的操作系统、什么样的计算机架构,进程使用的内存都可以按照功能大致分为以下4个部分:
- 代码区:这个区域存储着被装入执行的二进制机器代码,处理器会到这个区域取指并执行。
- 数据区:用于存储全局变量等。
- 堆区:进程可以在堆区动态地请求一定大小的内存,并在用完之后归还给堆区。动态分配和回收是堆区的特点。
- 栈区:用于动态地存储函数之间的关系,以保证被调用函数在返回时恢复到母函数中继续执行。
在Windows平台下,高级语言写出的程序经过编译链接,最终会变成PE文件。当PE文件被装载运行后,就成了所谓的进程。四个区有着各自的功能,在进程运行中缺一不可,大致过程如下:
PE文件代码段中包含的二进制级别的机器代码会被装入内存的**代码区**(.text),处理器将到内存的这个区域一条一条地取出指令和在**数据区**存放的全局变量等操作数,并送入运算逻辑单元进行运算;如果代码中请求开辟动态内存,则会在内存的**堆区**分配一块大小合适的区域返回给代码区的代码使用;当函数调用发生时,函数的调用关系等信息会动态地保存在内存的**栈区**,以供处理器在执行完被调用函数的代码时,返回母函数。
1.2系统栈
栈指的是一种数据结构,是一种先进后出的数据表。内存中的战区实际上指的就是系统栈。
栈的最常见操作有两种:
压栈(PUSH)、弹栈(POP)。
用于标识栈的属性也有两个:
栈顶(TOP):push操作时,top增1;pop操作时,top减一。
栈底(BASE):与top正好相反,标识最下面的位置,一般不会变动的。
1.3寄存器与函数栈帧
每一个函数独占自己的栈帧空间。当前正在运行的函数的栈帧总是在栈顶。Win32系统提供两个特殊的寄存器用于标识位于系统栈顶端的栈帧。
- ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。
- EBP:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部,并非系统栈的底部。
除此之外,还有一个很重要的寄存器。
- EIP:指令寄存器(extended instruction pointer),其内存放着一个指针,该指针永远指向下一条等待执行的指令地址。 可以说如果控制了EIP寄存器的内容,就控制了进程——我们让EIP指向哪里,CPU就会去执行哪里的指令。这里不多说EIP的作用,我个人认为王爽老是的汇编里面讲EIP讲的已经是挺好的了~这里不想多写关于EIP的事情。
1.4函数调用约定与相关指令
函数调用大概包括以下几个步骤:
(1)参数入栈:将参数从右向左依次压入系统栈中。
(2)返回地址入栈:将当前代码区调用指令的下一条指令地址压入栈中,供函数返回时继续执行。
(3)代码区跳转:处理器从当前代码区跳转到被调用函数的入口处。
(4)栈帧调整:具体包括:
<1>保存当前栈帧状态值,已备后面恢复本栈帧时使用(EBP入栈)。
<2>将当前栈帧切换到新栈帧(将ESP值装入EBP,更新栈帧底部)。
<3>给新栈帧分配空间(把ESP减去所需空间的大小,抬高栈顶)。
<4>对于_stdcall调用约定,函数调用时用到的指令序列大致如下:
push 参数3 ;假设该函数有3个参数,将从右向做依次入栈
push 参数2
push 参数1
call 函数地址 ;call指令将同时完成两项工作:a)向栈中压入当前指令地址的下一个指令地址,即保存返回地址。 b)跳转到所调用函数的入口处。
push ebp ;保存旧栈帧的底部
mov ebp,esp ;设置新栈帧的底部 (栈帧切换)
sub esp,xxx ;设置新栈帧的顶部 (抬高栈顶,为新栈帧开辟空间)
函数返回的步骤如下:
<1>保存返回值,通常将函数的返回值保存在寄存器EAX中。
<2>弹出当前帧,恢复上一个栈帧。具体包括:
(1)在堆栈平衡的基础上,给ESP加上栈帧的大小,降低栈顶,回收当前栈帧的空间。
(2)将当前栈帧底部保存的前栈帧EBP值弹入EBP寄存器,恢复出上一个栈帧。
(3)将函数返回地址弹给EIP寄存器。
<3>跳转:按照函数返回地址跳回母函数中继续执行。
add esp,xxx ;降低栈顶,回收当前的栈帧
pop ebp ;将上一个栈帧底部位置恢复到ebp
retn ;该指令有两个功能:a)弹出当前栈顶元素,即弹出栈帧中的返回地址,至此,栈帧恢复到上一个栈帧工作完成。b)让处理器跳转到弹出的返回地址,恢复调用前代码区
二、栈溢出利用之修改邻接变量
-原理分析
本实验目的:是研究如何通过非法的超长密码去修改buffer的邻接变量authenticated来绕过密码验证。
原理:一般情况下函数的局部变量在栈中一个挨着一个排列,如果这些局部变量中有数组之类的缓冲区,并且程序中存在数组越界的缺陷,那么越界的数组元素就有可能破坏栈中相邻变量的值,甚至破坏栈帧中所保存的EBP值、返回地址等重要数据。
实验代码:
#include <stdio.h>
#include <string.h>
#define PASSWORD "1234567"
int verify_password(char *password){
int authenticated;
char buffer[8];
authenticated= strcmp(password,PASSWORD);
strcpy(buffer,password);
return flag;
}
void main(){
int valid_flag;
char password[1024];
while(1){
printf("Please input password: ");
scanf("%s",password);
valid_flag = verify_password(password);
if(valid_flag){
printf("Incorrect password!\n");
}
else{
printf("Congratulations!\n");
break;
}
}
}
通过代码,我们可以想象出代码执行到verify_password时候的栈帧状态,如图所示:
我们分析一下:局部变量authenticated正好位于缓冲区buffer的下方,为int型,占用4字节。因此,如果buffer越界,则buffer[8]——buffer[11]正好写入相邻的authenticated中。同时,通过源码,我们可以发现当authenticated为0时,验证成功;反之则不成功。所以,我们只要做到让越界的ASCII码修改authenticated的值为0,则绕过了密码认证。
-实验步骤
2.1 首先验证程序运行结果,只有正确输入“1234567”才可以通过验证:
2.2假设我们输入的密码为7个“qqqqqqq”,按照字符串的关系大于1234567,strcmp返回1,因此authenticated值为1,通过ollydbg调试的实际内存如图:【0x71是"q"的ASCII码表示】
2.3下面我们试试输入超过7个字符,输入“qqqqqqqqrst”,如图所示,正好从第9个字符开始,开始写入authenticated中,因此authenticated的值为0x00747372:
2.4 我们知道,字符串数据最后都有座位结束标志的NULL(0),当我们尝试输入8个“q”,正好第九个字符0被写入authenticated中,我们看一下:
果然密码验证成功了:
最后,我们可以明白只要输入一个大于1234567的8个字符的字符串,那么隐藏的第九个截断符就能将authenticated覆盖为0,从而绕过验证。
三、栈溢出利用之修改函数返回地址
-原理分析
上一个实验介绍的改写邻接变量的方法似然很管用,但是并不太通用,本节介绍一个相对更通用的办法,修改栈帧最下方的EBP和函数返回地址等栈帧状态值。
下面,我们分析一下本实验的原理:如果继续增加输入的字符,那么超出buffer[8]边界的字符将依次淹没authenticated、前栈帧EBP、返回地址。也就是说,控制好字符串的长度就可以让字符串中相应位置字符的ASCII码覆盖掉这些栈帧状态值。
因此,本实验的目的是:我们通过溢出来覆盖返回地址从而控制程序的执行流程。
我们大致可以得出以下结论:
可以得出以下的结论:
- 输入11个‘q‘,第9-11个字符连同NULL结束符将authenticated冲刷为0x00717171。
- 输入15个‘q‘,第9-12个字符将authenticated冲刷为0x71717171;第13-15个字符连同NULL结束符将前栈帧EBP冲刷为0x00717171。
- 输入19个‘q‘,第9-12字符将authenticated冲刷为0x71717171;第13-16个字符连同NULL结束符将前栈帧EBP冲刷为0x71717171;第17-19个字符连同NULL结束符将返回地址冲刷为0x00717171。
这里用19个字符作为输入,看看淹没返回地址会对程序产生什么影响。出于双字对齐的目的,我们输入的字符串按照"4321"为一个单元进行组织,最后输入的字符串为"4321432143214321432"进行测试,用OD分析如下图所示:
实际的内存状况和我们分析的结论一致,此时的栈状态见下表的内容:
由于键盘输入ASCII码范围有限,所以将代码稍作改动改为从文件读取字符串。源码如下:
#include <stdio.h>
#define PASSWORD "1234567"
int verify_password (char *password)
{
int authenticated;
char buffer[8];
authenticated=strcmp(password,PASSWORD);
strcpy(buffer,password);//over flowed here!
return authenticated;
}
main()
{
int valid_flag=0;
char password[1024];
FILE * fp;
if(!(fp=fopen("password.txt","rw+")))
{
exit(0);
}
fscanf(fp,"%s",password);
valid_flag = verify_password(password);
if(valid_flag)
{
printf("incorrect password!\n");
}
else
{
printf("Congratulation! You have passed the verification!\n");
}
fclose(fp);
}
-实验步骤
3.1 用OD加载可执行文件,通过阅读反汇编代码,可以知道通过验证的程序分支的指令地址为:0x00401028
3.2正常的执行是调用verify_password函数,然后进行比较来决定跳转到错误或正确的分支。如果我们直接把返回地址覆盖为验证通过的地址,而不进入需要比较判断的分支,岂不是可以绕过密码验证了。首先创建一个password.txt的文件,写入5个“4321”后保存到与实验程序同名的目录下,如图:
【buffer[8]需要2个“4321”,authenticated需要一个,EBP需要一个,因此要覆盖返回地址,需要5个“4321”】
3.3保存后,用Ultra_32打开,切换到十六进制编辑模式:
3.4将最后4个字节改为新的返回地址【由于“大端机”的缘故,为了使最终数据为0x00401128,我们需要逆序输入】
3.5切换为文本格式,这时也就验证了为什么我们不再用键盘输入字符串。
3.6将psaaword.txt保存后,用OD重新加载程序并调试,首先可以看到成功绕过密码验证:
3.7我们再回头看一下最终的栈状态:authenticated和EBP被覆盖后均为0x31323334,返回地址被覆盖后为0x00401128(正好为验证成功的地址)
四、栈溢出利用之代码植入
-原理分析
本实验目的:在buffer中植入我们想让他做的代码,然后通过返回地址让程序跳转到系统栈中执行。这样我们就可以让进程去干本来干不了的事情啦!
为了在buffer中植入代码,我们扩充了buffer的容量,来承载我们即将要植入的代码!简单的对代码进行的修改,源码如下:
#include<stdio.h>
#include<windows.h>
#define PASSWORD "1234567"
int verify_password(char * password)
{
int authenticated;
char buffer[44];
authenticated = strcmp(password,PASSWORD);
strcpy(buffer,password);
return authenticated;
}
int main()
{
int valid_flag = 0;
char password[1024];
FILE * fp;
LoadLibrary("user32.dll");//prepare for messagebox
if(!(fp = fopen("password.txt", "rw+")))
{
exit(0);
}
fscanf(fp,"%s",password);
valid_flag = verify_password(password);
if(valid_flag)
{
printf("incorrect password!\n");
}
else
{
printf("Congratulation! You have passed the verification!\n");
}
fclose(fp);
}
同样的,我们简单分析一下栈的布局:如果buffer中有44个字符,那么第45个字符null正好覆盖掉authenticated低字节中的1,从而可以突破密码的限制。
-实验步骤
4.1我们仍然以“4321”为一个单元,在password.txt中写入44个字符,如图:
4.2果然通过了验证。
4.3通过OD可以看到,authenticated低字节被覆盖。同时,我们可以知道buffer的起始地址为0x0012FB7C。因此password.txt中的第53-56个字符的ASCII码值将写入栈帧的返回地址中,成为函数返回后执行的指令。
4.4接下来,我们给password.txt植入机器代码。
用汇编语言调用MessageboxA需要3个步骤:
(1)装载动态链接库user32.dll。MessageBoxA是动态链接库user32.dll的导出函数。
(2)在汇编语言中调用这个函数需要获得这个函数的入口地址。【 MessageBoxA的入口参数可以通过user32.dll在系统中加载的基址和MessageBoxA在库中的偏移相加得到。(具体可以使用vc自带工具“Dependency Walker“获得这些信息) 】
(3)在调用前需要向栈中按从右向左的顺序压入MessageBoxA。
- 通过下图,我们可以得知user32.dll的基地址为0x77D10000,MessageBoxA的偏移地址为0x000407EA,基地址加上偏移地址得到入口地址为0x77D507EA。
- 开始编写函数调用的汇编代码,这里我们可以先把字符串“failwest”压入栈区,写出的汇编代码和指令对应的机器代码如图:
- 将上述汇编代码一十六进制形式抄入password.txt,,但是要注意!第53~56字节为自己的buffer的起始地址。
4.5程序运行情况如下:
4.6我们可以对压入的字符串进行修改,哈哈,改成自己的学号。
4.7在单击弹框“ok”之后,程序会报错崩溃,因为MessageA调用的代码执行完成后,我们没有写安全退出的代码。
原文地址:https://www.cnblogs.com/0831j/p/9219081.html