缓冲区溢出与数据执行保护DEP实验

1 实验目的

构造shellcode代码,Linux系统平台上在关闭数据执行保护机制情况下实现缓冲区溢出攻击。

2 缓冲区溢出原理

缓冲区溢出是指当计算机向缓冲区内填充数据位数时超过了缓冲区本身的容量溢出的数据覆盖在合法数据上。程序员通过往程序的缓冲区写超出其长度的内容,造成缓冲区的溢出,从而破坏程序的堆栈,使程序转而执行其它指令,以达到攻击的目的。造成缓冲区溢出的原因是程序中没有仔细检查用户输入的参数。例如下面程序:

  void function(char *str) {

      char buffer[16];

      strcpy(buffer,str);

  }

上面的strcpy()将直接把str中的内容copy到buffer中。这样只要str的长度大于16,就会造成buffer的溢出,使程序运行出错。存在像strcpy这样的问题的标准函数还有strcat(),sprintf(),vsprintf(),gets(),scanf()等。随便往缓冲区中填东西造成它溢出一般会出现“分段错误”(Segmentation fault),而不能达到攻击的目的。最常见的手段是通过制造缓冲区溢出使程序运行一个用户shell,再通过shell执行其它命令。如果该程序属于root且有suid(Set User ID,)权限的话,攻击者就获得了一个有root权限的shell,可以对系统进行任意操作了。

1)在程序的地址空间里安排适当的代码

 当向要攻击的程序里输入一个字符串时,程序就会把这个字符串放到缓冲区里,这个字符串包含的数据是可以在这个所攻击的目标的硬件平台上运行的指令序列。缓冲区可以设在:堆栈(自动变量)、堆(动态分配的)和静态数据区(初始化或者未初始化的数据)等的任何地方。

2)控制程序转移到攻击代码的形式

缓冲区溢出漏洞攻击都是在寻求改变程序的执行流程,使它跳转到攻击代码,最为基本的就是溢出一个没有检查或者其他漏洞的缓冲区,这样做就会扰乱程序的正常执行次序。通过溢出某缓冲区,可以改写相近程序的空间而直接跳转过系统对身份的验证。

(1)Function Pointers(函数指针)

  在程序中,“void (* foo) ( )”声明了个返回值为“void” Function Pointers的变量“foo”。Function Pointers可以用来定位任意地址空间,攻击时只需要在任意空间里的Function Pointers邻近处找到一个能够溢出的缓冲区,然后用溢出来改变Function Pointers。当程序通过Function Pointers调用函数,程序的流程就会实现。

   (2)Activation Records(激活记录)

当一个函数调用发生时,堆栈中会留驻一个Activation Records,它包含了函数结束时返回的地址。执行溢出这些自动变量,使这个返回的地址指向攻击代码,再通过改变程序的返回地址。当函数调用结束时,程序就会跳转到事先所设定的地址,而不是原来的地址。

程序的堆栈是先进后出的一种数据结构,堆栈的生长方向是和内存相反的如图1。当调用一个函数时,首先是函数的参数逆序进栈,然后将eip里面的内容进栈作为函数的返回地址(ret),即函数调用结束后程序跳转的地址,接着保存现在程序的栈基指针(ebp),并将当前栈顶指针esp拷入ebp作为新的基地址.最后将esp减去一定数值用来为本地变量留出一定空间。缓存区往往就分配在这段空间中。

uploading.4e448015.gif转存失败重新上传取消

图1

由于堆栈是由内存高地址向内存低地址方向增长,而数组的变量是从内存低地址向高地址方向增长,这时如果没有对数组的越界进行检查和限制,通过向程序的数组缓冲区写入超出其长度的内容,覆盖堆栈原来的返回地址(ret),就会造成缓冲区溢出,从而破坏程序的堆栈。如果构造特殊的注入向量覆盖ret值使程序转而执行恶意代码(shellcode),就达到攻击的目的。

3 实验环境

虚拟机:VMware Workstation , Version: 5.5.3 build-34685

虚拟机OS:Ubuntu 10.04.4 LTS i686

主机OS:Microsoft Windows7

gcc:4.4.3

gdb:5.3post-0.20021129.18rh

4 漏洞代码

4.1 系统调用

系统调用是用户态和内核态之间的一座桥梁。大多数操作系统都提供了很多应用程序可以访问到的核心函数,shellcode也需要调用这些核心函数。Linux系统提供的核心函数可以方便的实现用来访问文件,执行命令,网络通信等等功能。启动一个系统调用需要使用interrupt指令,linux系统调用位于中断0x80。当执行一个int 0x80指令后,发出一个软中断,强制内核停止当前工作来处理中断。内核首先检查传入参数的正确性,然后将下面寄存器的值复制到内核的内存空间,接下来参照中断描述符表(IDT)来处理中断。系统调用完成以后,继续执行interrupt指令后的下一条指令。

 系统调用号是确定一个系统调用的关键数字,在执行interrupt指令之前,它应当被传入EAX寄存器中,确定了一个系统调用号之后就要考虑给该系统调用传递什么参数来完成什么样的功能。存放参数的寄存器有5个,他们是EBX,ECX,EDX,ESI和EDI,这五个寄存器顺序的存放传入的系统调用参数。需要超过6个输入参数的系统调用使用不同的方法把参数传递给系统调用。EBX寄存器用于保护指向输入参数的内存位置的指针,输入参数按照连续的顺序存储。系统调用使用这个指针访问内存位置以便读取参数。

4.2 shellcode代码

一般的缓冲区溢出攻击都要得到一个用户shell,通过这个shell来控制目标系统。我们使用shell填充要溢出的缓冲区,然后覆盖函数的return地址,让其指向buffer,而我们的shellcode位于其中,于是程序就执行了我们的攻击代码,从而生成一个用户shell。shellcode是用作利用软件漏洞的有效载荷的一小段代码,因为它通常启动一个命令shell,攻击者可以从中控制受攻击的机器,所以称他为shellcode。任何执行类似任务的代码都可以称为shellcode。

shellcode基本的编写方式有以下三种

直接编写十六进制操作码。

使用c语言编写程序,然后进行编译,最后进行反汇编来获取汇编指令和十六进制操作码。

编写汇编程序,将该程序汇编,然后从二进制中提取十六进制操作码。

写shellcode的需要注意的两个重要问题:

系统调用的问题。

一般来说,shellcode都是由十几或是几十个字节组成,这样的小程序如果要像linux服务程序一样,引入头文件,导入符号表,调用系统函数,这样的步骤的话;那么短短的几十个字节根本就不能满足需求,这就需要利用系统最核心的调用机制,即通过软中断的方式获取需要的资源,以此来绕开系统调用。

坏字符问题

Shellcode如果存储在堆或是栈的内存中,这样在shellcode执行时就不能出现\x00这样的字符,这就需要我们在构造shellcode时防止此类坏字符的出现。

打开一个新的shell我们需要用到execve系统调用,查看man手册里关于execve函数的定义:

uploading.4e448015.gif转存失败重新上传取消可以看到execve系统调用需要3个参数,3个参数必须包含以下内容:

•filename必须指向包含要执行的二进制文件的路径的字符串。在这个栗子中,就是字符串[/ bin / sh]。

•argv []是程序的参数列表。大多数程序将使用强制性/选项参数运行。而我们只想执行“/ bin / sh”,而没有任何更多的参数,所以参数列表只是一个NULL指针。但是,按照惯例,第一个参数是我们要执行的文件名。所以,argv []就是['/ bin / sh',00000000]

•envp []是要以key:value格式传递给程序的任何其他环境选项的列表。为了我们的目的,这将是NULL指针\0x00000000。

一个简单的C程序来调用execve函数:

#include <stdio.h> 

int main() 

  char *sc[2]; 

  sc[0]="/bin/sh"; 

  sc[1]= NULL; 

  execve(sc[0],sc,NULL); 

通过execve执行一个/bin/sh从而获得一个新的shell,编译来看下结果:

uploading.4e448015.gif转存失败重新上传取消

查看execve反汇编后的结果

uploading.4e448015.gif转存失败重新上传取消

从反汇编结果来看,execve函数执行的前一部分首先将向寄存器ebx,ecx,edx中赋值。之后调用了(*0x0804f752)处的代码,通过中断指令int 0x80进入ring0。

4.3 提取shellcode

Shellcode的提取就是要获取exceve函数调用时的参数及软中断调用。通过软终端加载相应的系统调用号及参数来执行相应的任务。

第一步,就是需要将系统调用号加入到eax中。

第二步,ebx用于保存函数调用的第一个参数(ecx存放第二个参数,edx存放第三个参数,esi存放第四个参数,edi存放第五个参数)

如果参数个数超过5个,那么就必须将参数数组存储在内存中,而且必须将该数组的地址存储在ebx中。一旦加载寄存器之后,就会调用int 0x80 汇编指令来发出软中断,强迫内核暂停手头上的工作并处理该中断。

由上面的反汇编我们在调用execve地址0x0804f752打断点,执行到该地址处,此时寄存器eax,ebx,ecx,edx中都已经通过esp的偏移指针得到了赋值,接下来就是要调用int 0x80软中断指令。

uploading.4e448015.gif转存失败重新上传取消

查看四个寄存器赋值,第1个参数ebx,刚好是“/bin/sh”;第2个参数ecx是一个指针数组,第一个元素是第一个参数地址,第二个元素为空;第3个参数是edx为空。最后execve的系统调用号就放在了寄存器eax中=0xb。execve系统函数调用号是eax中的0xb。由此可以看出如果想要得到shellcode需将部分指令代码拼接。

根据int 0x80中断指令调用形式,要求eax存放系统调用号;ebx、ecx、edx分别存放参数部分。汇编源码中,首先是第4行eax清零;之后第5行压栈;然后第6行,第7行字符串压栈,这样在栈中就构造了以”\x00”结尾的字符串”/bin//sh”。注意这里的“/bin//sh”与“/bin/sh”同样效果。0x80中断指令的第一个参数ebx;第9行中eax入栈,此时eax值还是0;第10行ebx入栈也就是把字符串”/bin//sh”地址入栈,两次压栈,此时栈中就有了字符串地址和一个0,刚好构成了一个指针数组;第11行将该指针数组的地址也就是esp赋给ecx,系统调用的第2个参数ecx中就保持了指针数组的地址;第12行edx清零,刚好是系统调用的第3个参数为零。第13行将系统调用号0xB赋给al,这样可以避免出现坏字符。最后调用软中断指令执行。整个栈结果如下图所示

uploading.4e448015.gif转存失败重新上传取消

4.4 编写shellcode

为了编写execve的shellcode我们用汇编实现一下以上C程序的功能,一般来说shellcode的总长度都非常短,所以可以直接采用汇编形式编写,这样不但可以直接通过软中断形式执行系统调用,而且可以控制坏字符的出现。如下图所示,为一个返回汇编行的shellcode代码。

.section .text 

.globl _start 

_start: 

  xorl %eax,%eax  //首先为了避免mov赋值带来的00,用一个异或操作来把EAX寄存器清空

  pushl %eax  //将4字节的NULL压栈

  pushl $0x68732f6e   //将//sh压栈,保持对齐,第一个参数

  pushl $0x69622f2f    //将/bin压栈,保持对齐,第一个参数

  movl %esp,%ebx   //将/bin//sh地址存放到EBX寄存器,第2个参数

  pushl %eax   //压4字节的NULL,第3个参数,环境变量为 NULL

  pushl %ebx   //将/bin//sh地址EBX压栈

  movl %esp,%ecx   //把EBX地址存入ECX寄存器

  movb $0xb,%al   //将execve系统调用号11(0xb)压入AL寄存器,消00

  int $0x80  //调用int指令进入中断

Int 0x80软中断调用

第一步,就是需要将系统调用号加入到eax中。

第二步,ebx用于保存函数调用的第一个参数(ecx存放第二个参数,edx存放第三个参数,esi存放第四个参数,edi存放第五个参数)

如果参数个数超过5个,那么就必须将参数数组存储在内存中,而且必须将该数组的地址存储在ebx中。一旦加载寄存器之后,就会调用int 0x80 汇编指令来发出软中断,强迫内核暂停手头上的工作并处理该中断。

测试结果uploading.4e448015.gif转存失败重新上传取消

提取16进制机器码

uploading.4e448015.gif转存失败重新上传取消

5 漏洞代码

将shellcode放到一个C程序中来完成整个shellcode的编写

#include <stdio.h>

char shellcode[] =

  "/x31/xc0"

  "/x50"

  "/x68/x6e/x2f/x73/x68"

  "/x68/x2f/x2f/x62/x69"

  "/x89/xe3"

  "/x50"

  "/x53"

  "/x89/xe1"

"\x31\xd2"

  "/xb0/x0b"

  "/xcd/x80";

int main()

{

     int * ret;//声明了一个int型的指针变量

     ret = (int *)&ret +2;//ret是一般是位于堆栈顶部的,在往下就是EBP,EBP的下面就是主函数即main的返回地址将ret的地址向后移了两个int单位的内存地址,从而到达了程序的返回地址ret

     (*ret) = (int)shellcode;//将ret修改为shellcode的入口地址

}

不开启数据执行保护机制运行结果

uploading.4e448015.gif正在上传…重新上传取消

从图中可以看出,在主函数调用eax所执行地址时,此时eax=0x80c6008,在该地址执行异或指令时,此时eax还是0x80c6008,但是在执行后就报段错误。也就是说在地址0x80c6008执行写操作时(异或操作)发生了错误。由此我们可以想到要么该地址不让执行,要么该地址不让写。

查看内存地址0x80c6008的权限,如下图所示。

uploading.4e448015.gif正在上传…重新上传取消

图中显示堆栈空间只有读写权限,没有可执行权限,所以在该地址执行代码导致错误。

开启数据执行保护机制运行结果

uploading.4e448015.gif转存失败重新上传取消

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值