软件安全攻防——缓冲区溢出和shellcode
1.实践内容
1.1软件安全概述
攻击者能够轻易地对系统和网络实施攻击,很大程度上是因为安全漏洞在软件中的大规模存在,攻击者可以利用这些漏洞来违背系统和网络的安全属性。安全漏洞的类型多种多样,从最基本的缓冲区溢出,到格式化字符串漏洞,竞态条件漏洞,整数溢出,以及流行的XSS跨站脚本、SQL注入等。而针对安全漏洞的渗透利用技术也在与安全防护机制的博弈中日新月异,层出不穷,从最简单的栈溢出,到更难操控的堆溢出,内核溢出,对抗DEP(数据执行保护)机制的ret2libc攻击、对抗ASLR(地址空间布局随机化)机制的Heap Spraying攻击,最新的能够同时对抗DEP和ASLR的JIT Spraying攻击,在软件代码与内存空间上演着精彩的对弈。
1.1.1软件安全漏洞威胁
- 安全漏洞定义: 在系统安全流程、设计、实现或内部控制中所存在的缺陷或弱点,能够被攻击者所利用并导致安全侵害或对系统安全策略的违反
- 安全漏洞包括三个基本元素: 系统的脆弱性或缺陷、攻击者对缺陷的可访问性,以及攻击者对缺陷的可利用性
- 软件安全漏洞 则被定义为在软件的需求规范、开发阶段和配置过程中引入的缺陷实例,其执行会违反安全策略。软件安全漏洞同样符合安全漏洞的三个基本元素,同时被限制于在计算机软件中
- 软件缺陷和安全漏洞是计算机系统最致命的弱点,并在现在的大量软件中非常普遍地存在,软件缺陷和安全漏洞会导致巨大的经济损失,甚至于灾难性的后果
1.1.2软件安全困境
软件安全困境三要素:复杂性,可扩展性和连通性。
- 复杂性: 软件规模越来越大,越来越复杂,就意味着软件的bug会越来越多。
- 可扩展性:现代软件为了支持更优化的软件架构,更好的客户感受,往往都会提供一些扩展和交互渠道。可扩展性功能使得安全保证更加困难,很难阻止攻击者和恶意代码以不可预测的扩展方式来入侵软件和系统;分析可扩展性软件的安全性要比分析一个完全不能被更改的软件要困难得多。
- 连通性:高度的连通性使得一个小小的软件缺陷就有可能影响非常大的范围,从而引发巨大的损失。
1.1.3软件安全漏洞类型
软件安全漏洞类型技术上包括:
- 内存安全违规类:内存安全违规类漏洞是在软件开发过程中在处理RAM 内存访问时所引入的安全缺陷,如缓冲区溢出漏洞和Double Free、Use-after-Free 等不安全指针问题等。不安全指针是指在计算机程序中存在的并没有指向适当类型对象的非法指针,在对这些指针进行引用时,往往会发生一些不可预期的后果,导致程序内存访问错误,而一旦攻击者可以控制这些指针指向的内存内容,那他们就可以利用这些问题构造出恶意攻击,获得软件的控制权。
- 输入验证类类:输入验证类安全漏洞是指软件程序在对用户输入进行数据验证存在的错误,没有保证输入数据的正确性、合法性和安全性,从而导致可能被恶意攻击与利用。
- 竞争条件类:竞争条件类缺陷是系统或进程中一类比较特殊的错误,通常在涉及多进程或多线程处理的程序中出现,是指处理进程的输出或者结果无法预测,并依赖于其他进程事件发生的次序或时间时,所导致的错误。
— 权限混淆与提升类:权限混淆与提升类漏洞是指计算机程序由于自身编程疏忽或被第三方欺骗,从而滥用其权限,或赋予第三方不该给予的权限。
权限混淆与提升类漏洞的具体技术形式主要有Web应用程序中的跨站请求伪造(Cross Site Request Forgery, CSRF)、Clickjacking、 FTP反弹攻击、权限提升、“越狱”( jailbreak)等。
1.2缓冲区溢出基础概念
1.2.1 基本概念和发展过程
缓冲区溢出是计算机程序中存在的一类内存安全违规类漏洞,在计算机程序向特定缓冲区内填充数据时,超出了缓冲区本身的容量,导致外溢数据覆盖了相邻内存空间的合法数据,从而改变程序执行流程破坏系统运行完整性。理想情况下,程序应检查每个输入缓冲区的数据长度,并不允许输入超出缓冲区本身分配的空间容量,但是大量程序总是假设数据长度是与所分配的存储空间是相匹配的,因而很容易产生缓冲区溢出漏洞。
1.2.2缓冲区攻击背景知识
编译器调试的用法我们之前也了解过而且实验中很多都用过
- 汇编相关的知识:常用的寄存器和它们对应的功能,从应用的角度一般将寄存器分为4类,即 通用寄存器、段寄存器、控制寄存器和其他寄存器。段寄存器在IA32架构中是16位的,一般用作段基址寄存器。控制寄存器用来控制处理器的执行流程,其中最关键的是eip, 也被称为“指令指针”,它保存了下一条即将执行的机器指令的地址,因而也成为各种攻击控制程序执行流程的关键攻击目标对象,而如何修改与改变将要被装载至eip寄存器的内存数据,以及修改为何地址,是包括缓冲区溢出在内渗透攻击的关键所在。其他寄存器中值得关注的是“扩展标志”eflags寄存器,由不同的标志位组成,用于保存指令执行后的状态和控制指令执行流程的标志信息。
进程内存管理
- Linux操作系统中的进程内存空间布局和管理机制:程序在执行时,系统在内存中会为程序创建-一个虚拟的内存地址空间,在32位机上即4GB的空间大小,用于映射物理内存,并保存程序的指令和数据: 3GB (即0x0000000)以下为用户态空间,3GB .4GB为内核态空间;操作系统将可执行程序加载到新创建的内存空间中,程序-般包含.text、.bss 和.data三种类型的段,.text段 包含程序指令,在内存中被映射为只读,.data 段主要包含静态初始化的数据,而 .bss 段则主要包含未经初始化的数据,两者都被映射至可写的内存空间中;加载完成后,系统紧接着就开始为程序初始化“栈”(Stack)和“堆”(Heap),“栈”是一种后进先出的数据结
构,其地址空间从高地址向低地址增长,Linux程序运行的环境变量env、运行参数argv、运行参数数量argc都被放置在“栈”底,然后是主函数及调用“栈”中各个函数的临时保存信息,“堆”则是一种先进先出的数据结构,用于保存程序动态分配的数据和变量,其地址空间从低地址往高地址增长,与“栈”正好相反;程序执行时,就会按照程序逻辑执行.text中的指令,并在“堆”和"栈”中保存和读取数据。 - Windows操作系统中的进程内存空间布局和管理机制:Windows操作系统的进程内存空间2GB-4GB为内核态地址空间,用于映射Windows内核代码和一些核心态DLL,并用于存储一些内核态对象,0GB-2GB为用户态地址空间。高地址段映射大量程序共用系统DLL,1GB位置装载进程本身引用的DLL,可执行代码段从0x400000开始,同样有栈和堆存储各进程的执行数据。
函数调用过程
函数调用分为3个过程:调用、序言、返回
1.2.3 缓冲区溢出攻击原理
冲区溢出漏洞根据缓冲区在进程内存空间中的位置不同,又分为栈溢出、堆溢出和内核溢出三种具体技术形态。
- 栈溢出:存储在栈上的一些缓冲区变量由于存在缺乏边界保护问题,能够被溢出并修改栈上的敏感信息,从而导致程序流程的改变
- 堆溢出:存储在堆上的缓冲区变量缺乏边界保护所遭受溢出攻击的安全问题
- 内核溢出:由于进程内存空间内核态中存储的缓冲区变量被溢出造成的
1.3 Linux栈溢出与Shellcode
Linux平台中的栈溢出攻击按照攻击数据的构造方式不同,主要有NSR、RSN和RS三种模式 。
- NSR模式:NSR模式主要适用于被溢出的缓冲区变量比较大,足以容纳Shellcode的情况,其攻击数据从低地址到高地址的构造方式是一堆Nop指令(即空操作指令)之后填充Shellcode,再加上一些期望覆盖RET返回地址的跳转地址,从而构成了NSR攻击数据缓冲区。
- RNS模式:一般用于被溢出的变量比较小,不足于容纳Shellcode的情况,攻击数据从低地址到高地址的构造方式是首先填充一些期望覆盖RET返回地址的跳转地址,然后是一堆Nop指令填充出“着陆区”,最后再是Shellcode。在溢出攻击后,攻击数据将在RET区段即溢出了目标漏洞程序的小缓冲区,并覆盖了栈中的返回地址,然后跳转到Nop指令所构成的“着陆区”,并最终执行Shellcode。
- RS模式:在这种模式下能够精确定位出Shellcode在目标漏洞程序进程空间中的起始地址,因此也就无需引入Nop空指令构建“着陆区”。这种模式是将Shellcode放置在目标漏洞程序执行时的环境变量中,由于环境变量是位于Linux进程空间的栈底位置,因而不会受到各种变量内存分配与对齐因素的影响,其位置是固定的,可以通过如下公式进行计算:ret=0xc0000000 - sizeof(void*) - sizeof(FILENAME) - sizeof(Shellcode)
- 本地栈溢出攻击的shellcode主要包含提升权限,并给出本地shell访问;远程栈溢出攻击的shellcode需要shell访问与网络连接起来
Linux 中一个最简单的本地 Shellcode 的产生过程, 而这个过程事实上也体现了 Shellcode 的通用方法, 包括如下5个步骤:
- 先用高级编程语言, 通常用C, 来编写 Shellcode 程序;
- 编译并反汇编调试这个 Shellcode 程序;
- 从汇编语言代码级别分析程序执行流程;
- 整理生成的汇编代码, 尽量减小它的体积并使它可注入, 并可通过嵌入C语言进行运行测试和调试;
- 提取汇编代码所对应的 opcode 二进制指令, 创建 Shellcode 指令数组。
Linux远程Shellcode实现机制
Linux远程Shellcode需要让攻击目标程序创建socket监听指定的端口等待客户端连接,启动一个命令行Shell,并将命令行的输入输出与socket绑定,这样攻击者就可以通过socket客户端连接目标程序所在主机的开放端口,与服务端socket建立起通信通道,并获得远程访问Shell。
1.4 Windows平台上的栈溢出与Shellcode
Windows操作系统平台在很多方面与Linux操作系统具有显著不同的实现机制,而在这些差异中,与成功攻击应用程序中栈溢出漏洞密切相关的主要有如下三点。
-
对程序运行过程中废弃栈的处理方式差异:Windows会向废弃栈中写入一些随机的数据,而Linux则不进行任何的处理
-
进程内存空间的布局差异,Linux栈0xC0000000附近,这些地址中没有空字节,Windows栈在0x00FFFFFF以下的用户空间,这些地址首字节均为0x00空字节,也没有linux平台上通过suid/sgid程序来提升当前用户执行权限的限制,针对应用程序的本地溢出攻击对windows没有意义。
-
系统功能调用实现方式差异:Linux系统中通过“int 80”中断处理来调用系统功能,而Windows系统则是通过操作系统中更为复杂的API及内核处理例程调用链来完成系统功能调用
Windows远程Shellcode大致过程如下:
- 创建一个服务器端socket, 并在指定的端口上监听
- 通过accept()接受客户端的网络连接
- 创建子进程,运行“cmd.exe", 启动命令行
- 创建两个管道,命令管道将服务器端socket 接收(recv) 到的客户端通过网络输
入的执行命令,连接至cmd.exe的标准输入;然后输出管道将cmd.exe的标准输出连接至 - 服务器端socket的发送(send), 通过网络将运行结果反馈给客户端
1.5 堆溢出攻击
-
堆溢出之所以较栈溢出具有更高的难度,最重要的原因在于堆中并没有可以直接覆盖并修改指令寄存器指针的返回地址,因此往往需要利用在堆中一些会影响程序执行流程的关键变量,如函数指针、C++类对象中的虚函数表,或者挖掘出堆中进行数据操作时可能存在的向指定内存地址改写内容的漏洞机会。
-
函数指针改写 :需要被溢出的缓冲区临近全局函数指针存储地址,且在其低地址方向上。此时向缓冲区填充数据,如果没有边界控制和判断,就可以覆盖函数指针所在的内存区,改写函数指针的指向地址,则程序在使用这个函数指针的时候就会执行shellcode。
-
C++ 类对象虚函数表改写 :对于使用了虚函数机制的C++类,如果它的类成员变量中存在可被溢出的缓冲区,那么就可以进行堆溢出攻击,通过覆盖类对象的虚函数指针,使其指向一个特殊构造的虚函数表,从而转向执行攻击者恶意注入的指令。
-
Linux下堆管理glibc库free()函数本身漏洞 :Linux操作系统的堆管理是通过glibc库来实现的,通过称为Bin的双向循环链表来保存内存空闲块的信息。glibc库中的free()函数在处理内存块回收时,会将被释放的空闲块和与之相邻的块合并,利用精心构造的块可以在合并时覆盖Bin前指针的内容。
1.6 缓冲区溢出攻击的防御技术
- 尝试杜绝溢出的防御技术
- 允许溢出但不让程序改变执行流程的防御技术
- 无法让攻击代码执行的防御技术
2.实践过程
没有实践
4.实践总结
了解软件安全攻防的相关知识,对于缓冲区溢出有了更深的了解,本章有了很多代码的内容,并且需要了解汇编相关的知识,理解起来需要仔细的看。