栈溢出介绍

 

栈溢出介绍

 

零、前言:

在打pwnable.kr的passcode题目的时候,发现了自己存在一些基础薄弱,需要补充回来,这是栈溢出的笔记。

 

一、进程内存:

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

1、代码区:

存储着被转入的执行的二进制代码,处理器会到这个区域获取指令并执行。

 

2、数据区:

用来存储局部变量

 

3、堆区:

进程可以在堆区中动态请求一定大小的内存,并在用完之后归还给堆区。

动态分配和回收是堆区的特点。

 

4、栈区:

用于动态的存储函数之间的调用关系。以保证被调用函数在返回时恢复到主函数中继续执行。

 

二、栈:栈和系统栈

栈:栈是一种数据结构,是一种先入后出的数据表,按照一定的规则进行添加和删除数据。

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

 

三、溢出:

在达缓冲区的数据向小缓冲区复制的过程中,由于没有注意小缓冲区的边界,导致小缓冲区满了,从而覆盖了和小缓冲区相邻内存区域的其他数据而引起的内存问题。

 

四、X86通用寄存器:

1、

 

 

PUSH:压入

POP:弹出,删除

MOV:复制

SUB: sub,esp xxx  即预留xxx字节

 

2、

 

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

 

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

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

 

3、

EAX:累加器,多种加法乘法指令的默认寄存器

EBX:基地址寄存器,在内存寻址是存放基地址

ECX:计数器,重复(REP)前缀指令和LOOP指令的内定计数器

EDX:被用来放整数除法产生的余数

ESI/EDI:源/目标索引寄存器,在很多字符串操作指令中,DS:ESI指向源串,ES:EDI指向目标串。

 

五、函数调用过程:

1、步骤:

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

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

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

(4)栈帧调整:

1)保存当前栈帧状态,以被后面恢复本栈帧使用(push ebp)

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

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

 

2、图例:

1)普通C程序的内存布局:

Text:

包含要执行的程序代码

Data:

包含程序需要的全局数据、资源等

Stack:

包含函数的输入参数,返回地址以及保存函数的局部变量等。

Stack是后进先出的结构。随着函数的调用,它在内存中(从高地址到低地址)向下寻址。

Heap:保存所有动态分配的内存。每当用malloc(动态分配内存)分配获取内存指针时,这个地址就是从堆中分配的。

 

2)重点关注三大寄存器:EBP(栈底)、ESP(栈顶)、EIP(指向)

当执行一个函数的时候,相关参数以及局部变量等都会被记录在ESP、EBP中间的区域。

一旦函数执行完毕,相关栈帧就会从堆栈中弹出,然后从预先保存好的上下文中进行恢复,以便保持堆栈平衡。

CPU必须要知道函数调用完了后的EIP指向(要去哪里执行),这需要堆栈弹出的过程中进行EIP赋指。

 

3)main函数调用func()函数的程序:

main函数调用func函数,程序运行后,依次将所有的参数压入栈中,

func函数执行完成后,相应的栈帧依次弹出,此时存储返回值的地址被加载到EIP寄存器中,以继续执行main函数中剩余的部分。

目的就是要控制这个返回值,劫持func函数返回到指定的恶意代码中区。

3、例子A:

(1)代码:

 

 1 #include <stdio.h>
 2 #include <string.h>
 3 
 4 void function2(){
 5     printf("Execution flow changed\n");
 6 }
 7 void function1(char *str){
 8     char buffer[5];
 9     strcpy(buffer,str);
10 }
11 void main(int argc,char *argv[]){
12     function1(argv[1]);
13     printf("%s\n","Executed normally");
14 }

 

 

编译:

1 root@kali:~/test# gcc -g -fno-stack-protector -z execstack -o test1 test1.c

 

1)-g:将调试信息记忆符号等编译到程序中

2)-fno -stack-protestor:关闭堆栈保护机制

3)-z execstack:打开堆栈可执行机制,即关闭堆栈执行保护

 

运行与崩溃:

 

(2)审计调试:

PS:strcpy函数没有对参数进行检查边界大小。

 

gdb调试:

1)使用list显示源代码,然后在strcpy和函数返回的地方下断点

 

2)反复运行测试,变换参数内容,找到崩溃的地址

EBP和ESP地址

 

strcpy执行中的EBP和ESP地址

 

strcpy执行过后的EBP和ESP

 

3)找到EIP地址

 注意:EIP在EBP的下方,即

 

4)找到function2的开始地址:

 

(3)利用:

1)利用思路:

 

 

2)利用payload:

A: 相对于EBP(找到头晕)

B:相对于ESP(找到头晕+1)

C:直接地址相加:

 

 

1 (gdb) run $(python -c 'print "A" *17 + "\xb9\x11\x40\x00"')

 

 

 

4、例子B:

 

(1)代码:

 

 

(2)函数调用过程:

1、压入寄存器eax,ecx,edx等

2、压入参数arg2,arg1

3、压人返回地址eip

4、压入ebp

5、压入temp

6、压入buffer

7、压入ebx,esi和edi等寄存器

 

(3)利用:

1)思路:

由于局部变量buffer的长度是4个字节,arg1参数可控,

而strcpy函数并没有判断参数的长度,

所以传入的arg1的长度大于4个字节时,多出来的字节将会依次覆盖掉局部变量,ebp,返回地址等

通过精心构造arg1的值,就可以将返回地址覆盖为任意想要的值,挑到shellcode。

2)利用:

 

 

5、例子C:修改邻接变量

函数的局部变量在栈中一个挨着一个,如果这些局部比办理中有数组之类的缓冲区,

并且程序中存在数组越界的缺陷,那么越界的数组元素就有可能破坏栈中相邻变量的值,

甚至破坏栈中保存的ebp值、eip值等重要数据。 

 

(1)代码:

 

 1 #include <stdio.h>
 2 #include <string.h>
 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 }

 

(2)审计:

1)main函数输入password,调用verify_password函数验证输入的password是否等于1234567,相等返回0,否则返回1。

 

2)strcpy函数存在漏洞和buffer[8],构造利用条件

 

3)利用缓冲区溢出,修改值,返回0,完成验证。

思路:在verify_password栈帧中

authenticated位于buffer[8]的下方

authenticated是int型,在内存中占4个字节

buffer[8]占8个字节

控制buffer[]填满8个字节,然后越界1个字节,缓冲区溢出,使得原authenticated的1覆盖为0,返回通过。 

 

6、例子C:修改函数返回地址

 控制buffer[8]越界,覆盖其他值,修改eip

 

六、溢出关键函数:

1、输入:

gets

scanf

vscanf

2、输出:

sprintf

3、字符串:

strcpy

strcat

bcopy

 

4、等

 

七、栈溢出步骤:

1、寻找危险函数:

2、确定填充长度:计算所能操作的地址和所要覆盖的地址的距离长度

(1)方法:

1)相对于栈底地址

2)相对于栈顶地址

3)直接地址

 

(2)覆盖:

1)覆盖函数返回地址

2)覆盖变量内容

3)覆盖bss段某变量内容:bss段(指用来存放程序中未初始化的全局变量的一块内存区域,bss段属于静态内存分配。)

4)等

 

3、利用:

当完全控制这个程序后,使用一个直接存在指令地址来覆盖这个返回地址后,CPU将会转而执行我们的指令。

在uinx/Linux系统中,指令可以执行一个shell,这个shell将会获得和被溢出程序相同的权限。

如该程序是root权限,那么就会获得root shell

 

八、参考链接:

https://www.cnblogs.com/Donoy/p/5690402.html

https://www.sohu.com/a/226035403_268160

https://www.jianshu.com/p/58d03dd3680a

https://blog.csdn.net/aemperor/article/details/47310593

https://blog.csdn.net/guiguzi1110/article/details/77663046

 

 

posted @ 2019-01-14 00:37  beiweisanshidu  阅读(3702)  评论(0编辑  收藏  举报