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

Posted 0831j

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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!
");
        }
        else{
            printf("Congratulations!
");
            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!
");
    }
    else
    {
        printf("Congratulation! You have passed the verification!
");
    }
    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!
");  
    }  
    else  
    {  
        printf("Congratulation! You have passed the verification!
");  
    }  
    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调用的代码执行完成后,我们没有写安全退出的代码。
技术分享图片

以上是关于20155306 白皎 0day漏洞——漏洞利用原理之栈溢出利用的主要内容,如果未能解决你的问题,请参考以下文章

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

20155306 白皎 免考实践总结——0day漏洞

20155306 白皎 0day漏洞——漏洞的分析与复现

20155306 白皎 免考实践总结

利用Windows 0day漏洞部署DevilsTongue恶意软件

20155306 白皎 《网络攻防》 EXP8 Web基础