码迷,mamicode.com
首页 > 其他好文 > 详细

栈溢出原理与实现

时间:2016-07-21 08:41:32      阅读:432      评论:0      收藏:0      [点我收藏+]

标签:

缓冲区溢出

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

无论什么计算机架构,进程使用的内存都可以按照功能大致分为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!\r\n");
26         }
27         else
28         {
29             printf("Congratulation! you have passed the verification!\r\n");
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内存的低位上去,导致authenticated

 由 0x00000001 变为 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!\r\n");
30     }
31     else
32     {
33         printf("Congratulation! you have passed the verification!\r\n");
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\r\n",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!\r\n");
38     }
39     else
40     {
41         printf("Congratulation! you have passed the verification!\r\n");
42     }
43     fclose(fp);
44     return 0;
45 }

 

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

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

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

   技术分享

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

   技术分享

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

  技术分享

    最终弹出对话框:

  技术分享

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

   

 

 

栈溢出原理与实现

标签:

原文地址:http://www.cnblogs.com/Donoy/p/5690402.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!