20155306 白皎 0day漏洞——漏洞利用原理之栈溢出利用

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调用的代码执行完成后,我们没有写安全退出的代码。

这两天学习0day漏洞原理利用之栈溢出,哈哈,整体非常开心。开始慢慢适应新的工具去帮助我解决一些问题,通过查看内存地址,寄存器的值以及栈中的数值变化来判断出程序运行目前的状态还有等等感觉自己之前数据结构学习栈或者l老师的课学习,还是没能够深入进去,只是知道程序运行和栈息息相关,但是具体什么关系,又不是很明确,但是这次学习就不一样,给了自己压力,需要一个人去完成一项任务,就会好好去看书,不懂查资料,会努力把一个东西弄明白,因为感觉自己连知识点都不会还怎么实践呀哈哈。结果是顺利的呀,学会了三种不同的方式来进行攻击。对了,如果看到有不一样的图,那时因为我用了不同版本的OD去调试哒

参考文献:
[1]王清.《0day安全:软件漏洞分析技术》[M].中国:电子工业出版社,2008.
[2]王清.《0day安全:软件漏洞分析技术(第2版)》[M].中国:电子工业出版社,2011.

posted on 2018-06-23 22:18  20155306  阅读(1080)  评论(1编辑  收藏  举报

导航