缓冲区溢出
在大缓冲区的数据向小缓冲区复制的过程中,由于没注意小缓冲区的边界,“撑爆”了较小的缓冲区,从而冲掉了和小缓冲区相邻内存区域的其他数据而引起的内存问题。
无论什么计算机架构,进程使用的内存都可以按照功能大致分为4个部分:
(1)代码区:这个区域存储着被装入的执行的二进制代码,处理器会到这个区域取指并执行。
(2)数据区:用于存储局部变量。
(3)堆区:进程可以在堆区中动态的请求一定大小的内存,并在用完之后归还个堆区。动态分配和回收是堆区的特点。
(4)栈区:用于动态的存储函数之间的调用关系。以保证被调用函数在返回时恢复到母函数中继续执行。
栈与系统栈
栈:指的是一种数据结构,是一种先入后出的数据表。系统栈:指的是内存中的栈,由系统自动维护,他用于实现高级语言中的函数调用。
- 系统栈:指的是内存中的栈,由系统自动维护,他用于实现高级语言中的函数调用。
函数调用过程
当函数被调用时,系统栈会为这个函数新开辟一个栈帧,并把它压入栈中,这个栈帧的内存空间被它所属的函数独占,正常情况下是不会和别的函数共享的。
函数调用大致包括以下几个步骤:
(1)参数入栈:将参数从右向左依次压入系统栈。
(2)返回地址入栈:将当前代码区调用的下一条指令地址压入栈中,供函数返回时继续执行。
(3)代码区跳转:处理器从当前代码区跳到被执行函数入口。
(3)栈帧调整:1.保存当前栈帧状态,已被后面恢复本栈帧使用(push ebp)
2.将当前栈帧切换到新的栈帧(mov ebp,esp)
3.给新栈帧分配空间(把ESP减去所需空间大小,抬高栈顶)
例如:
对于_stdcall 调用约定,函数调用时用到的指令序列如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
;调用前 push 参数 3 ; ;假设该函数有 3 个参数,将从右向左依次入栈 push 参数 2 ; push 参数 1 ; call 函数地址; ; 该指令同时完成两件事:(a)向栈中压入当前指令在内存中 ;的位置,及保存返回地址 ;(b)跳转到函数地址 push ebp mov ebp,esp sub esp,xxx |
类似的函数返回步骤:
(1)保存返回值,通常将函数返回值保存在寄存器eax中。
(2)弹出当前栈帧,恢复上一个栈帧
1
2
3
4
5
|
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]的下方,authenticated是int型,在内存中占4个字节,所以,如果能让buffer数组越界,就能够影
响到authenticated。在程序中,authenticated为0表示验证成功,为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! "); 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安全_软件漏洞分析技术(第二版)》进行学习,本文中的代码实现如下,点击下载: