20199314 2019-2020-2 《网络攻防实践》第10周作业
20199314 2019-2020-2 《网络攻防实践》第10周作业
- 软件安全攻防--缓冲区溢出和Shellcode
问题 | 解答 |
---|---|
这个作业属于哪个课程 | 《网络攻防实践》(https://edu.cnblogs.com/campus/besti/19attackdefense) |
这个作业的要求在哪里 | https://edu.cnblogs.com/campus/besti/19attackdefense/homework/10723 |
作业正文 | 如下 |
软件安全攻防--缓冲区溢出和Shellcode
1. 软件安全概述
-
攻击者能够轻易地对系统和网络实施攻击,很大程度是因为安全漏洞在软件中的大规模存在,攻击者可以利用这些漏洞来违背系统和网络的安全属性。
-
安全漏洞在软件开发周期的各个环节(包括设计、编码、发布等)中都可能被引入,而只有软件设计与开发人员充分认识到安全漏洞的危害、掌握安全漏洞机理,以及如何避免漏洞的安全编程经验,并在软件厂商的软件开发生命周期中切实执行安全设计开发的流程,才有可能尽榄地减少发布软件中的安全漏洞数量,降低它们对网络与现实世界所带来的影响与危害。
1.1 软件安全漏洞威胁
-
软件安全漏洞定义: 为“在系统安全流程、设计、实现或内部控制中所存在的缺陷或弱点,能够被攻击者所利用并导致安全侵害或对系安全策略的违反“,bug中可以被攻击者利用并导致危害的安全漏洞。
-
安全漏洞(Vulnerability)的范畴则不限于软件安全漏洞(最常见),还包括硬件、个人与组织管理中存在的、能够被攻击者利用来破坏安全策略的弱点。
-
包括三个基本元素:系统的脆弱性或缺陷、攻击者对缺陷的可访问性,以及攻击者对缺陷的可利用性。
-
软件缺陷与安全漏洞除了造成经济损失外,还可能直接危及到人类的生命安全。
1.2 软件安全困境
软件安全困境三要素:复杂性(Complexity)、可扩展性(Extensibility)和连通性(Connectivity), 软件的这三个要素共同作用,使得软件的安全风险管理成为了一个巨大的挑战,从而很难根除安全漏洞。
-
复杂性
软件规模越来越大,越来越复杂,也就意味着软件的bug会越来越多。虽然这其中大多数 bug 并不会造成安全问题,或者无法被攻击者所利用,但只要攻击者能够从中发现出少数几个可利用的安全漏洞,他们就可以利用这些安全漏洞来危害软件的使用者。 -
可扩展性
现代可扩展软件本身的特性使得安全保证更加困难,首先,很难阻止攻击者和恶意代码以不可预测的扩展方式来入侵软件和系统;其次,分析可扩展性软件的安全性要比分析一个完全不能被更改的软件要因难得多。 -
连通性
互联网的普及使得全球更多的软件系统都连通在一起,不仅是接入互联网的计算机数量快速增加,一些控制关键基础设施的重要信息系统也与互联网建立起了连通性。高度的连通性使得—个小小的软件缺陷就有可能影呐非常大的范围,从而引发巨大的损失。
1.3 软件安全漏洞类型
MITRE 曾给出了在 CVE 中归档的安全漏洞类型统计情况及发展趋势, 从安全漏洞的技术机理方面一共列举出了37类, 并统计了2001-2006 年中最流行的 Top 10 安全漏洞类型,如下图所示:
软件安全漏洞类型从技术上主要分为以下几类:
-
内存安全违规类
内存安全违规类漏利是在软件开发过程中在处理RAM (random-access memory) 内存访问时所引入的安全缺陷,如缓冲区溢出漏洞和 Double Free、Use-after-Free 等不安全指针问题等。内存安全违规类漏洞主要出现在 C/C++ 等编程语言所编写的软件程序中,由于这类语言支待任意的内存分配与归还、任意的指针计算、转换,而这些操作通常没有进行保护确保内存安全,因而非常容易引入此类漏洞。 -
输入验证类(普遍利用的目标)
输入验证类安全漏洞是指软件程序在对用户输入进行数据验证存在的错误,没有保证输入数据的正确性、合法性和安全性,从而导致可能被恶意攻击与利用。包含格式化字符串、SQL 注入、代码注入、远程文件包含、目录遍历、XSS、HTTP Header 注入、HTTP 响应分割错误等多种安全漏洞技术形式。 -
竞争条件类
竞争条件类缺陷是系统或进程中一类比较特殊的错误,通常在涉及多进程或多线和处理的程序中出现,是指处理进程的输出或者结果无法预测,并依赖于其他进程事件发生的次序或时间时,所导致的错误。 -
权限混淆与提升类:
权限混淆与提升类漏洞是指计算机程序由千自身编程疏忽或被第三方欺骗,从而滥用其权限,或赋予第三方不该给予的权限。权限混淆与提升类漏洞的具体技术形式主要有 Web 应用程序中的跨站请求伪造(Cross-Site Request Forgery,CSRF)、Clickjacking、FTP反弹攻击、权限提升、"越狱" (jailbreak) 等。
2. 缓冲区溢出
缓冲区溢出 (Buffer Overflow) 是最早被发现也是最基础的软件安全漏洞技术类型之 一。
2.1 缓冲区溢出的基本概念与发展过程
定义: 缓冲区溢出是计算机程序中存在的一类内存安全违规类漏洞,在计算机程序向特定缓冲区内填充数据时,超出了缓冲区本身的容量,导致外溢数据覆盖了相邻内存空间的合法数据,从而改变程序执行流程破坏系统运行完整性。
根本原因: 冯·诺伊曼体系存在本质的安全缺陷,即采用了 “存储程序” 的原理,计算机程序的数据和指令都在同一内存中进行存储而没有严格的分离。
发展过程: Smashing the Stack for Fun and Profit --The Tao of Windows Bufer Oveflows--w00w00 on Heap Overflows.
2.2 缓冲区溢出攻击背景知识
- 编译器与调试器的使用:
C/C++ 等高级编程语言编写的源码,需要通过编译器和连接器才能生成可直接在操作系统平台上运行的可执行和序代码。而调试器则是程序开发人员在运行时刻调试与分析程序行为的基本工具。对于最常使用的 C/C++ 编程语言,最著名的编译与连接器是GCC,类UNIX 平台上进行程序的调试经常使用的调试器是 GDB 调试器。gdb的常用命令如下图:
- 汇编语言基础知识:
编语言,尤其是 IA32 (Intel 32位)架构下的汇编语言,是理解软件安全漏洞机理,掌握软件渗透攻击代码技术的底层基础。在IA32汇编语言中,首先我们需要熟悉常用的寄存器和它们对应的功能,我们从应用的角度一般将寄存器分为4类:通用寄存器、段寄存器、控制寄存器和其他寄存器。
-
Intel汇编格式与AT&T汇编格式:
-
进程内存管理
程序在执行时,系统在内存中会为程序创建一个虚拟的内存地址空间,操作系统将可执行程序加载到新创建的内存空间中,加载完成后,系统紧接着就开始为相序初始化 “栈” (后进先出)和“ 堆”(先进先出) .
- 函数调用过程:
1.调用(call):调用者将函数调用参数、函数调用下一条指令的返回地址压栈,并跳转至被调用函数入口地址。
2.序言(prologue): 被调用函数开始执行首先会进入序言阶段,将对调用函数的栈基址进行压栈保存,并创建自身函数的栈结构,具体包括将ebp寄存器赋值为当前栈基址,为本地函数局部变量分配栈地址空间,更新esp寄存器为当前栈顶指针等。
3.返回(return): 被调用函数执行完功能将指令控制权返回给调用者之前,会进行返回阶段的操作,通常执行leave和ret指令,即恢复调用者的栈顶与栈底指针,并将之前压栈的返回地址装载至指令寄存器eip中,继续执行调用者在函数调用之后的下一条指令。
- 函数调用过程:
2.3 缓冲区溢出攻击原理
缓冲区溢出漏洞根据缓冲区在进程内存空间中的位置不同,又分为栈溢出、堆溢出和内核溢出这三种具体技术形态。
-
栈溢出(最早被发现和利用)
是指存储在栈上的一些缓冲区变量由于存在缺乏边界保护问题,能够被溢出并修改栈上的敏感信息(通常是返回地址),从而导致程序流程的改变。 -
堆溢出
是存储在堆上的缓冲区变量缺乏边界保护所遭受溢出攻击的安全问题。 -
内核溢出
漏洞存在于一些内核模块或程序中,是由于进程内存空间内核态中存储的缓冲区变量被溢出造成的。
代码示例分析1(栈溢出)
在进行分析之前需要取消对抗缓冲区溢出的防范措施:
1.取消“栈上数据不可执行”保护:echo 0 > /proc/sys/kerne/exec-shield
2.取消“地址空间随机化”保护:echo 0 > /proc/sys/kernel/randomize_va_space
3.编译时取消“/GS”保护:加上gcc编译选项 –fno-stack-protecto。
以上代码的return_input()函数中定义了一个局部变量array,为30字节长度的字符串缓冲区,函数局部变量将被存储在栈上,并位于main函数调用时压栈的下一条指令返回地址之下,而在return_input()函数中执行gets函数将用户终端输入至array缓冲区时,没有进行缓冲区边界检查和保护,因此如果用户输入超出30字节的字符串时,输入数据将会溢出array缓冲区,从而覆盖array缓冲区上方的EBP(栈底指针寄存器)和RET(子程序的返回指令), 一旦覆盖了RET返回地址之后,在return_input()函数执行完毕返回main函数时,EIP寄存器将会装载栈中RET位置保存的值,此时该位置已经被溢出改写为"AAAA" (即0x41414141), 而该地址可能是进程无法读取的空间,如果错误则会造成程序的段错误("Segmentation fault")。
缓冲区溢出安全漏洞的根本问题: 在于用户输入可控制的缓冲区操作缺乏对目标缓冲区的边界安全保护。
-
首先是程序中存在着缺乏边界安全保护的缓冲区操作(通常称为漏洞利用点,gets函数)。
-
其次是这个缓冲区操作必须是用户输入可以控制的。
精心地构造缓冲区溢出攻击,解决如下三个问题:
1.如何找出缓冲区溢出要覆盖和修改的敏感位置?例如栈溢出中的RET返回地址在栈中的存储位置。
2.将敏感位置的值修改成什么?
3.执行什么代码指令来达到攻击目的?在程序控制权移父至攻击者注入的指令后,那么这段指令究成何种功能,如何编写?(这段代码被称为攻击的 payload ,通常会为攻出者给出一个远程的 Shell 访问, 因此也被称为 Shellcode)
代码示例分析2(栈溢出)
这段示例代码中,6字节长度的局部变量buffer在漏洞利用点strcpy()函数缺乏边界安全保护,攻击者通过精心构造large_string这一个128字节长度的数据, 仅在其在低地址包含一段 Shellcode代码(0-31),而其他均填充为指向buffer起始位置的地址(即被覆盖后large_string中的 Shellcode 起始地址),在漏洞利用点执行strcpy操作之后,buffer缓冲区会被溢出,main 函数的返回地址RET 将会被覆盖并改写为 Shellcode 的起始地址,因此在return时,EIP寄存器装载改写后RET值,并将程序执行流程跳转至 Shellcode 执行。
溢出攻击的第一个关键问题—定位需要修改的敏感位置,即栈中的返回地址,根据对栈结构与内存布局,我们可以定位返回地址位于要溢出的buffer变量的高地址位置。第二个关键问题——将敏感位置的值修改为什么,示例代码中将其改写为直接指向 Shellcode 的地址。第三个关键问题——执行什么代码,示例代码中保护了一段代码,用于系统调用,开启一个命令行 shell。
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。
-
RNS模式:
一般用于被溢出的变量比较小,不足于容纳Shellcode的情况,攻击数据从低地址到高地址的构造方式是首先填充一些期望覆盖RET返回地址的跳转地址,然后是一堆Nop指令填充出“着陆区”,最后再是Shellcode。在溢出攻击之后,攻击数据将在RET区段即溢出了目标漏洞程序的小缓冲区,并覆盖了栈中的返回地址,然后跳转至Nop指令所构成的“着陆区”,并最终执行Shellcode。
-
RS模式:
在这种模式下能够精确地定位出Shellcode在目标漏洞程序进程空间中的起始地址,因此也就无须引入Nop空指令构建“着陆区”。这种模式是将Shellcode放置在目标漏洞程序执行时的环境变量中,由于环境变量是位于Linux进程空间的栈底位置,因而不会受到各种变量内存分配与对齐因素的影响,其位置是固定的,可以通过如下公式进行计算:ret = 0xc0000000 - sizeof(void*) -sizeof(FILENAME) - sizeof(Shellcode) vulnerable3.c与RNS中的vulnerable2.c相同。
-
Linux 平台上的远程栈溢出攻击的原理与本地栈溢出是一样的,区别在于用户输入传递的途径不同,以及 Shellcode 的编写方式不同。
3.2 Linux平台的Shellcode实现技术
Shellcode 是一段机器指令,对于我们通常接触的 IA32 架构平台,Shellcode就是符合 Intel 32 位指令规范的一串 CPU 指令, 被用于溢出之后改变系统正常流程,转而执行 Shellcode 以完成渗透测试者的攻击目的,通常是为他提供一个访问系统的本地或远程命令行访问(即Shell)。按照在本地溢出攻击和远程溢出攻击使用场景的不同,又分为本地 Shellcode 和远程 Shellcode。
3.2.1 Linux本地Shellcode实现机制
Linux系统本地Shellcode通常提供的功能就是为攻击者启动一个命令行Shell。一般通过execve函数启动/bin/sh提供命令行(一般使用二进制指令的Shellcode)。
本地产生Shellcode的过程:
1.先用高级编程语言,通常用C,来编写Shellcode程序;
2.编译并反汇编调试这个Shellcode程序:
3.从汇编语言代码级别分析程序执行流程;
4.整理生成的汇编代码,尽量减小它的体积并使它可注入,并可通过嵌入C语言进行运行测试和调试;
5.提取汇编代码所对应的opcode二进制指令,创建Shellcode指令数组。
3.2.2 Linux 远程 Shellcode 实现机制
-
Linux 系统上的远程 Shellcode 的实现原理与本地 Shellcode 完全一致,也是通过执行一系列的系统调用来完成指定的功能。
-
实现方法步骤也是首先给出高级语言的功能代码实现,然后通过反汇编调试编译后的二进制程序,提取、优化和整理所获得的汇编代码,并最终产生 opcode 二进制指令代码。
-
Linux 远程 Shellcode 需要让攻击目标程序创建 socket 监听指定的端口等待客户端连接,启动一个命令行 Shell,并将命令行的输入输出与 socket 绑定,这样攻击者就可以通过 socket 客户端连接目标程序所在主机的开放端口,与服务端 socket 建立起通信通道,并获得远程访问 Shell。
4. Windows 平台上的栈溢出与 Shellcode
从技术上分析,由于Windows橾什系统与Linux操作系统在进程内存空间布局、系统对栈的处理方式、系统功能调用方式等方面的实现差异,虽然栈溢出的基础原理和大致流程是一致的,但在具体的攻击实施细节、Shellcode 编制等方面还是存在一些差别。
4.1 Windows平台栈溢出攻击技术
Windows平台栈溢出攻击技术机理
-
对程序运行过程中废弃栈的处理方式差异:
当一个函数调用完成返回至调用者,执行下一条指令之前,Windows平台会向废弃栈中写入一些随机的数据,而Linux则不进行任何的处理。 -
进程内存空间的布局差异:
Windows操作系统的进程内存空间布局与Linux存在着不同,Linux进程内存空间中栈底指针在0xc0000000之下,即一般栈中变量的位置都在0xbfff地址附近,在这些地址中没有空字节。Windows平台的栈位置处于0x00FFFFFF以下的用户内存空间,一般为0x0012地址附近,而这些内存地址的首字节均为0x00空字节。 -
系统功能调用的实现方式差异:
Windows平台上进行操作系统功能调用的实现方法较Linux更加复杂,Linux系统中通过int 80中断处理来调用系统功能,而Windows系统则是通过操作系统中更为复杂的API及内核处理例程调用链来完成系统功能调用,对应用程序直接可见的是应用层中如kernel32.dll、User32.dll等系统动态链接库中导出的一些系统API接口函数。
远程栈溢出攻击实例:
攻击过程:
-
首先创建一一个客户端socket,并连接目标漏洞服务程序所监听的IP地址与端口;
-
然后精心组装一个用于溢出目标程序缓冲区的攻击数据,攻击数据缓冲区Buff是一个1024字节长度的字符数组,填充了一段Nop指令;
-
最后在事先计算好的返回地址位置上放置了一个指向“JMP ESP"指令的地址,该指令地址在不同的目标程序运行系统上是不一样的,由攻击者通过在各个系统环境中调试获得。
野外Windows栈溢出
栈溢出攻击的野外代码一般是针对一个特定的安全漏洞实施攻击,并需要配置目标操作系统的类型与版本,程序执行的过程关键是组装出包含填充数据、指令跳转地址和Shellcode的攻击数据,然后通过网络协议交互将攻击数据注入至目标程序的漏洞利用点上,实施溢出攻击,控制目标程序流程,转而执行攻击者注入的Shellcode。
4.2 Windows平台shellcode实现技术
为了使得 Windows 中的 Shellcode 能够调用操作系统功能以完成攻击目标,并能够在期望注入的不同目标程序中正常运行,我们需要考虑如下问题:
-
Shellcode必须可以找到所需的 Windows 32 API 函数,并生成函数调用表。
-
为使用 API 函数,shellcode必须找出目标程序已加载的函数地址。
-
Shellcode 需考虑消除空字节,以避免在字符串操作函数中被截断。
-
Shellcode 需确保自己可以正常退出,并使原来的目标程序进程继续运行或终止。
-
在目标系统环境存在异常处理和安全防护机制时,Shellcode 需进一步考虑如何对抗这些机制。
前三个问题是实现Windows平台Shellcode的关键。
4.2.1 Windows本地Shellcode
-
在Windows平台上,典型的本地Shellcode同样也是启动一个命令行Shell, 即command.com或cmd.exe,Windows 32的系统 API 中提供了system()函数调用,可以用于启动指定程序或运行特定命令,在调用system(command.com)之后即可启动命令行程序。
-
编写shellcode最简单的方式是使用硬编码的函数地址,比如system()函数在Windows XP特定版本的目标程序内存空间加载地址为0x77bf93c7,我们在shellcode中可以使用Call 0x77bf93c7指令来让EIP指令寄存器跳转至硬编码的函数入口地址执行,这种方法可以有效压缩编码长度。
-
C语言版的Windows本地Shellcode程序,即使用LoadLibrary()函数加载msvert.dll动态链接库,通过GetProcAddress()函数获得system函数的加载入口地址,赋值给ProcAdd函数指针,然后通过函数指针调用system()函数,启动命令行Shell,最后还要调用exit()退出当前进程。
-
再将其转化为汇编语言。
4.2.2 Windows远程Shellcode
-
C语言实现示例代码:
-
大致过程:
1.创建一个服务器端socket,并在指定的端口上监听;
-
通过accept()接受客户端的网络连接;
-
- 3.创建子进程,运行“cmd.exe”,启动命令行;
-
创建两个管道,命令管道将服务器端socket接收(recv)到的客户端通过网络输入的执行命令,连接至cmd.exe的标准输入;然后输出管道将cmd.exe的标准输出连接至服务器端socket的发送(send),通过网络将运行结果反馈给客户端。
5. 栈溢出攻击
堆溢出(Heap Overflow)是缓冲区溢出中第二种类型的攻击方式,由于堆中的内存分配与管理机制较栈更为复杂,不同操作系统平台的实现机制都具有显著的差异,同时通过堆中的缓冲区溢出控制目标程序执行流程需要更精妙的构造,因此堆溢出攻击的难度较栈溢出要复杂很多,真正掌握、理解并运用堆溢出攻击也更为困难一些。函数指针改写、C++类对象虚函数表改写以及 Linux 下堆管埋漏洞。
-
函数指针改写
需要被溢出的缓冲区临近全局函数指针存储地址,且在其低地址方向。在符合这种变量布局的条件下,当向缓冲区填充数据时,如果没有边界判断和控制的话,那么缓冲区溢出之后就会自然地覆盖函数指针所在的内存区,从而改写函数指针的指向地址,攻击者只要能够将该函数指针指向恶意构造的Shellcode入口地址,在程序使用函数指针调用原先期望的函数时,就会转而执行Shellcode。
-
C++类对象虚函数表改写
C++类通过虚函数提供了一种Late binding运行过程绑定的机制,编译器为每个包含虚函数的类建立起虚函数表、 存放虚函数的地址,并在每个类对象的内存区中放入一个指向虚函数表的指针,通常称为虚函数指针vptr对于使用了虚函数机制的C++类,如果它的类成员变量中存在可被溢出的缓冲区,那么就可以进行堆溢出攻击,通过覆盖类对象的虚函数指针,使其指向一个特殊构造的虚丽数表,从而转向执行攻击者恶意注入的指令。
-
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宏定义如下:
6. 缓冲区溢出攻击的防御技术
-
尝试杜绝溢出的防御技术
采取如高级差错程序fault injection,通过Fuzz注入测试来寻找代码的安全漏洞,或者在编译器上引入针对缓冲区的便捷保护检查机制如Jone & Kelly针对gcc的数组边界检查、Compaq C对编译器进行改进杜绝溢出。 -
允许溢出但不让程序改变执行流程的防御技术
允许溢出发生,但对可能影响到程序流程的关键数据结构实施严密的安全保护,不让程序改变其执行流程,从而阻断溢出攻击。通过对编译器gcc加补丁,使得在函数入口处能够自动地在栈中返回地址的前面生成一个Canary检测标记,在函数调用结束检测该标记是否改变来阻止溢出改变返回地址,从而阻止缓冲区溢出攻击。 -
无法让攻击代码执行的防御技术
通过堆栈不可执行限制来防御缓冲区溢出攻击,通过CPU硬件和各种操作系统内核补丁来支持堆栈不可执行。
7. 学习中遇到的问题及解决
问题1:代码分析太难,汇编语言不易理解
问题1解决方案:百度多查资料
8. 学习总结
本章中内容还是很多,缓冲区溢出在Linux和Windows下实现过程不尽相同,和学习内容还是很多,不要做实践还是轻松很多的,头发掉的也少了。