zoukankan      html  css  js  c++  java
  • 栈溢出原理与实现

    缓冲区溢出

    • 在大缓冲区的数据向小缓冲区复制的过程中,由于没注意小缓冲区的边界,“撑爆”了较小的缓冲区,从而冲掉了和小缓冲区相邻内存区域的其他数据而引起的内存问题。

    无论什么计算机架构,进程使用的内存都可以按照功能大致分为4个部分:

      (1)代码区:这个区域存储着被装入的执行的二进制代码,处理器会到这个区域取指并执行。

      (2)数据区:用于存储局部变量。

      (3)堆区:进程可以在堆区中动态的请求一定大小的内存,并在用完之后归还个堆区。动态分配和回收是堆区的特点。

      (4)栈区:用于动态的存储函数之间的调用关系。以保证被调用函数在返回时恢复到母函数中继续执行。

    栈与系统栈

    • 栈:指的是一种数据结构,是一种先入后出的数据表。系统栈:指的是内存中的栈,由系统自动维护,他用于实现高级语言中的函数调用。

    • 系统栈:指的是内存中的栈,由系统自动维护,他用于实现高级语言中的函数调用。

    函数调用过程

      当函数被调用时,系统栈会为这个函数新开辟一个栈帧,并把它压入栈中,这个栈帧的内存空间被它所属的函数独占,正常情况下是不会和别的函数共享的。

      函数调用大致包括以下几个步骤:

    • (1)参数入栈:将参数从右向左依次压入系统栈。

    • (2)返回地址入栈:将当前代码区调用的下一条指令地址压入栈中,供函数返回时继续执行。

    • (3)代码区跳转:处理器从当前代码区跳到被执行函数入口。

    • (3)栈帧调整:1.保存当前栈帧状态,已被后面恢复本栈帧使用(push ebp

                           2.将当前栈帧切换到新的栈帧(mov ebp,esp

                           3.给新栈帧分配空间(把ESP减去所需空间大小,抬高栈顶)

    例如:

      对于_stdcall 调用约定,函数调用时用到的指令序列如下:

              ;调用前
    
    
     push 参数3;         ;假设该函数有3个参数,将从右向左依次入栈
     push 参数2;
     push 参数1;
     call  函数地址;      ; 该指令同时完成两件事:(a)向栈中压入当前指令在内存中                             
    
                                              ;的位置,及保存返回地址
    
                                              ;(b)跳转到函数地址
    
     push  ebp
     mov  ebp,esp
     sub   esp,xxx
    

     

      类似的函数返回步骤:

    • (1)保存返回值,通常将函数返回值保存在寄存器eax中。

    • (2)弹出当前栈帧,恢复上一个栈帧

     add  esp,xxx ; 降低栈顶,回收当前栈帧
     pop  ebp      ; 将上一个栈帧底部位置ebp恢复
     retn         ; 这个指令有两个作用 (a)弹出当前栈顶元素,即弹出栈帧中的返回地址,
                                      ;至此,栈帧恢复。
     		                 ;(b)让处理器跳转到弹出的返回地址,恢复调用前的代码
    

      

      寄存器与函数栈帧

      每一个函数独占自己的栈帧空间。当前运行的函数的栈帧总在栈顶。Win32系统提供两个特殊的寄存器用于标识位于系统的栈顶端的栈帧。

    • (1)ESP:栈指针寄存器,其内存中是一个指针,该指针永远指系统栈最上面的一个栈帧的栈顶。

    • (2)EBP:基址指针寄存器,其内存中是一个指针,该指针永远指向系统栈最上面的要给栈帧的底部。

    • (3)EIP:指令寄存器,其内存中是一个指针,该指针永远指向下一条等待执行的指令地址。

      函数调用约定

    调用约定

    _cdecl

    _fastcall

    _stdcall

    参数入栈顺序

    右→左

    右→左

    右→左

    恢复平衡的位置

    调用者

    函数本身

    函数本身

      修改邻接变量

      通过上面的知识我们知道,函数的调用细节和栈中的数据分布情况,函数的局部变量在栈中一个挨着一个排着,如果这些局部变量中有数组之类的缓冲区,并且程序中存在数

     组越界的缺陷,那么越界的数组元素就有可能破坏栈中相邻变量的值,甚至破坏栈帧中保存的ebp的值、返回地址等重要数据。

      下面举个例子,来说明一下破坏栈内局部变量对程序安全性的影响:

      

     1 #include <IOSTREAM>
     2 using namespace std;
     3 #define  PASS_WORD "1234567"
     4 
     5 int verify_password(char* password)
     6 {
     7     int  authentitated;
     8     char buffer[8];
     9     authentitated = strcmp(password,PASS_WORD);
    10     strcpy(buffer,password);
    11     return authentitated;
    12 }
    13 
    14 int main()
    15 {
    16     int valid_flag = 0;
    17     char password[1024] = {0};
    18     while (1)
    19     {
    20         printf("please input password:");
    21         scanf("%s",password);
    22         valid_flag = verify_password(password);
    23         if(valid_flag)
    24         {
    25             printf("incorrect password!
    ");
    26         }
    27         else
    28         {
    29             printf("Congratulation! you have passed the verification!
    ");
    30         }
    31     }
    32     return 0;
    33 }

      当我们输入的是qqqqqqq,上述代码verify_password栈帧布局:

                            

      由此可知,在verify_password栈帧中,局部变量authenticated,位于缓冲区buffer[8]的下方,authenticatedint型,在内存中占4个字节,所以,如果能让buffer数组越界,就能够影

     响到authenticated在程序中,authenticated0表示验证成功,为1表示验证失败,我们通过让buffer数组越界,达到修改authenticated值得目的。

      通过我们输入可以造成缓冲区溢出,导致authenticated的值被修改。所以当我们输入8个字符,第9个字符,作为结尾的NULL字符,将刚好写到authenticated内存的低位上去,导致

     authenticated0x00000001 变为 0x00000000,验证通过。

      修改函数返回地址

      上述的的修改邻接变量的方法是很有用的,但是这种漏洞利用对代码的环境要求相对苛刻,更强大、更通用的攻击通过缓冲区溢出改写的目标往往不是一个变量,而是瞄准栈帧的

     最下方的EBP和函数返回地址等栈帧状态。

      也就是说,我们继续增加输入的字符串长度,超出buffer[8]边界,一次淹没authenticated、前栈帧EBP、返回地址。也就是说,控制好字符串长度就可以让字符串中相应的位置字符

     的ASCII覆盖这些栈帧的状态。

      当我们输入一个足够长的字符串是,程序崩溃,这是由于字符串足够的长,淹没了程序的返回地址,我们知道,当我们程序执行完毕之后,在执行retn指令时,栈顶恰好就是源程

     序的返回地址,”retn”指令会把这个地址pop,弹入eip寄存器中,之后跳转到这个地址去执行。

      

      程序崩溃的原因是因为,函数返回地址装入eip ,但是eip由于缓冲区溢出,淹没了,将值改变,程序执行找不到对应地址的指令,导致程序崩溃。但是,如果我们给出一个有效

     的地址,就可以让处理器跳转到任意的代码去执行,也就是说,我们可以通过淹没返回地址从而控制程序的执行。

      下面举个例子,通过缓冲区溢出,淹没eip,修改eip寄存器,从而控制程序执行:

     1 #include <IOSTREAM>
     2 using namespace std;
     3 #define  PASS_WORD "1234567"
     4 int verify_password(char* password)
     5 {
     6     int  authentitated;
     7     char szBuffer[8];
     8     authentitated = strcmp(password,PASS_WORD);
     9     strcpy(szBuffer,password);
    10     return authentitated;
    11 }
    12 int main()
    13 {
    14     int valid_flag = 0;
    15     char password[1024] = {0};
    16     FILE* fp ;
    17     fp=fopen("password.txt","rw+");
    18 
    19     if(fp==NULL)
    20     {
    21         exit(0);
    22     }
    23 
    24     fscanf(fp,"%s",password);
    25     valid_flag = verify_password(password);
    26 
    27     if(valid_flag)
    28     {
    29         printf("incorrect password!
    ");
    30     }
    31     else
    32     {
    33         printf("Congratulation! you have passed the verification!
    ");
    34     }
    35     fclose(fp);
    36     
    37     getchar();
    38     return 0;
    39 }

      通过OD分析可得:

      没有淹没时,verify_password栈帧如下:

          

      当淹没之后,verify_password栈帧如下:

          

      eip已经被修改,成功。很开心! 

      当我们可以利用栈溢出这一漏洞,修改eip,我们就可以干一些更牛的事情,让进程执行输入的数据的代码。

        下面举个例子,通过我们向password里添加一些机器指令,实现弹MessageBox。

     1 #include <IOSTREAM>
     2 #include <Windows.h>
     3 using namespace std;
     4 #define  PASS_WORD "1234567"
     5 int verify_password(char* password)
     6 {
     7     int  authentitated;
     8     char szBuffer[44];
     9     authentitated = strcmp(password,PASS_WORD);
    10     strcpy(szBuffer,password);
    11     return authentitated;
    12 }
    13 
    14 int main()
    15 {
    16     int valid_flag = 0;
    17     char password[1024] = {0};
    18     FILE* fp ;
    19     fp=fopen("password.txt","rw+");
    20     
    21     HMODULE h = LoadLibrary("user32.dll");  
    22     printf("%x
    ",h);
    23     //0x77760000
    24     //0x000774C0
    25     //0x777D74C0  //MessageBox地址
    26     //0x0018FA88  //buffer 的地址
    27     
    28     if(fp==NULL)
    29     {
    30         exit(0);
    31     }
    32     fscanf(fp,"%s",password);
    33     valid_flag = verify_password(password);
    34     
    35     if(valid_flag)
    36     {
    37         printf("incorrect password!
    ");
    38     }
    39     else
    40     {
    41         printf("Congratulation! you have passed the verification!
    ");
    42     }
    43     fclose(fp);
    44     return 0;
    45 }

       直接同过buffer中写入代码,这次的例子中,buffer定义的足够大,就是为了能将我们自己弹窗的代码完整的存放在里面,当我们执行拷贝,栈溢出,淹没了栈帧,将返回地址设

     置为buffer的首地址,此时,当函数栈retn之后,到返回地址继续执行,这就实现了我们的目的。

       通过OD分析可得,在没发生拷贝前,没有栈溢出时,verify_password栈帧如下:

       

       在发生拷贝后,产生了栈溢出,verify_password栈帧如下:

       

       在retn之后,eip指向buffer的基地址,进行程序的向下执行:

      

        最终弹出对话框:

     

       本文的实现,主要是通过参考《0day安全_软件漏洞分析技术(第二版)》进行学习,本文中的代码实现如下,点击下载:

         https://files.cnblogs.com/files/Donoy/%E6%A0%88%E6%BA%A2%E5%87%BA%E5%8E%9F%E7%90%86%E4%B8%8E%E5%AE%9E%E7%8E%B0.zip

       

  • 相关阅读:
    pl sql 编程
    maven(一) maven到底是个啥玩意~
    Oracle 数据库管理员
    Oracle 常用函数
    Oracle 事务
    Oracle 分页
    java String类
    java 方法
    java 数组
    java 跨平台 数据类型 修饰符 程序结构
  • 原文地址:https://www.cnblogs.com/Donoy/p/5690402.html
Copyright © 2011-2022 走看看