20199121《网络攻防实践》第十周作业
前言
问题 | 回答 |
---|---|
这个作业属于那个课程 | 网络攻防实践 |
这个作业的要求在哪里 | 《网络攻防实践》第十周作业 |
学习内容 | 第十章 软件安全攻防——缓冲区溢出和shellcode |
1.知识点梳理与总结
1.1 软件安全概述
-
软件安全漏洞‘
软件、硬件、个人和组织管理中能被供给制利用来破坏安全策略的弱点称为安全漏洞,软件安全漏洞是其中最常见、影响最大的 。
-
软件安全的困境:
- 复杂性(Complexity) :代码复杂性意味着更多的bug,即更多的缺陷。
- 可扩展性(Extensibility) : 攻击者可能会用难以预测的扩展方式进行攻击,且分析可扩展性软件的安全性要比分析一个完全不能被更改的软件要困难得多。
- 连通性(Connectivity):高强度的连通性使一个小小的软件缺陷造成极大影响。
-
软件安全漏洞分类
- 内存安全违规类:
缓冲区溢出
;不安全指针
(堆内存释放例程缺陷、两次释放、释放内存被重新引用、指针的不安全类型转换) - 输入验证类:
格式化字符串
(C语言的一些特定字符串处理函数,被恶意利用后可打印内存某些地址的数据内容);XSS
、代码注入
- 竞争条件类:
TOCTTOU
(程序检查某谓词是否符合要求时,另一程序对谓词条件进行修改,使得检查与使用时状态不一致);符号链接竞争
- 权限混淆与提升类:
XSRF
;FTP反弹攻击
(攻击者绕过FTP服务器的权限限制,让FTP服务器作为中间代理,使用PORT命令向其他主机端口请求访问);权限提升
;“越狱”(jailbreak)
(在类UNIX系统中破解chroot
和jail
机制,从而访问系统管理员通过chroot
设置限制目录之外的文件系统内容)
- 内存安全违规类:
1.2 缓冲区溢出基础概念
-
缓冲区溢出攻击
- 概念:向特定缓冲区中填入过多的数据,超出边界,导致外溢数据覆盖了相邻内存空间的合法数据,改变程序执行流程破坏系统运行完整性。
- 攻击原理:缺乏缓冲区边界保护(C/C++语言程序中效率优先 ;
memcpy()
、strcpy()
等内存与字符串拷贝 函数并不检查内存越界问题;程序员缺乏安全编程意识、经验与技巧)。本质原因还是冯·诺依曼体系存在本质安全缺陷,计算机程序的数据和指令 都在同一内存中进行存储,而没有严格的分离。 - 分类: 栈溢出、堆溢出和内核溢出
- 挑战:找到覆盖的敏感位置(一般为返回地址);将敏感位置的值改为什么?通常是恶意指令的起始位置,如何确定注入指令在目标程序的位置并完成控制权移交;执行什么代码达到攻击目的(
shellcode
、payload
)
-
背景知识
-
编译器与调试器的使用
- 源码需要编译器和连接器才能生成可执行程序代码。
- 运行时使用调试器进行调试及分析。
GCC
的用法:执行“gcc –c test.c”
命令进行源码编译,生成test.o
,然后执行“gcc –o test test.o”
进行连接,生成test可执行程序,可以使用“gcc test.c –o test”
同时完成编译和连接过程。对于处理多个源码文件、包含头文件、引用库文件等多种情况,程序开发人员通常编写或自动生成Makefile
,来控制GCC
的编译和连接过程。- 类UNIX平台上进行程序的调试经常使用
GDB
调试器,GDB
调试器提供程序断点管理、执行控制、信息查看等多种类型的功能指令。
-
汇编语言基础知识
-
我们平时编写的代码是高级语言,需要通过编译,转换成计算机能够理解的低级语言,这里的低级语言即汇编语言,可以直接描述、控制CPU的运行。
-
寄存器分类:通用寄存器(用于普通算术运算,保存数据、地址、偏移量等,其中栈指针寄存器esp是栈溢出攻击的关键操纵对象);段寄存器;控制寄存器;其他寄存器
-
IA-32寄存器如下图
-
-
进程内存管理
-
Linux操作系统中的进程内存空间布局和管理机制:
程序在执行时,系统在内存中会为程序创建一个虚拟的内存地址空间,在32位机上即4GB
的空间大小,用于映射物理内存,并保存程序的指令和数据。内存空间3GB以下
为用户态空间,3GB-4GB
为内核态空间。操作系统将可执行程序加载到新创建的内存空间中,程序一般包含
.text
(包含程序指令)、.bss
(包含未经初始化的数据)和.data(包含静态初始化的数据)三种类型的段。则主要包含未经初始化的数据,两者都被映射至可写的内存空间中。加载完成后,系统开始为程序初始化“栈”和“堆”。程序执行时,就会按照程序逻辑执行.text中的指令,并在“堆”和“栈”中保存和读取数据,然而程序并不能正确地区分指令和数据,所以当我们修改内存空间中影响程序执行逻辑的敏感位置,并将恶意数据作为指令提交给处理器时,它仍会执行。
-
Windows操作系统中的进程内存空间布局和管理机制:
Windows操作系统的进程内存空间2GB-4GB
为内核态地址空间,用于映射Windows内核代码和一些核心态DLL,并用于存储一些内核态对象,0GB-2GB
为用户态地址空间。高地址段映射大量程序共用系统DLL,1GB
位置装载进程本身引用的DLL,可执行代码段从0x400000
开始,同样有栈和堆存储各进程的执行数据。
-
-
函数调用过程
-
栈结构与函数调用过程的底层细节是理解栈溢出攻击的重要基础,因为栈溢出攻击就是针对函数调用过程中返回地址在栈中的存储位置,进行缓冲区溢出,从而改写返回地址,达到让处理器指令寄存器跳转至攻击者指定位置执行恶意代码的目的。
-
“栈”是一种后进先出的数据结构,其地址空间从高地址向低地址增长,程序运行的环境变量
env
、运行参数argv
、运行参数数量argc
都被放置在“栈”底,然后是主函数及调用“栈”中各个函数的临时保存信息。“堆”则是一种先进先出的数据结构,用于保存程序动态分配的数据和变量,其地址空间从低地址往高地址增长,与“栈”正好相反。
-
程序进行函数调用的过程有如下三个步骤:调用:将参数和下一条指令地址入栈并跳转到函数入口地址;序言:对调用函数的栈基址入栈保存,创建函数自身栈结构等;返回:恢复调用者栈顶栈底指针,执行下一条指令。
-
-
-
取消Linux系统对缓冲区溢出的防范
- 取消“栈上数据不可执行”保护:
echo 0 > /proc/sys/kerne/exec-shield
- 取消“地址空间随机化”保护:
echo 0 > /proc/sys/kernel/randomize_va_space
- 编译时取消“/GS”保护:
加上gcc编译选项 –fno-stack-protecto
- 取消“栈上数据不可执行”保护:
1.3 Linux栈溢出与Shellcode
-
Linux平台中的栈溢出攻击按照攻击数据的构造方式不同,主要有NSR、RNS和RS三种模式。在Linux平台中,本地栈溢出攻击,即渗透攻击代码的攻击目标对象是本地的漏洞程序,可以用于特权提升。
- NSR模式
NSR模式主要适用于被溢出的缓冲区变量比较大,足以容纳Shellcode的情况,其攻击数据从低地址到高地址的构造方式是一堆Nop指令之后填充Shellcode,再加上一些期望覆盖RET返回地址的跳转地址,从而构成了NSR攻击数据缓冲区 - RNS模式
一般用于被溢出的变量比较小,不足于容纳Shellcode的情况。攻击数据从低地址到高地址的构造方式是首先填充一些期望覆盖RET返回地址的跳转地址,然后是一堆Nop指令填充出“着陆区”,最后再是Shellcode - RS模式
在这种模式下能够精确地定位出Shellcode在目标漏洞程序进程空间中的起始地址,因此也就无需引入Nop空指令构建“着陆区”。这种模式是将Shellcode放置在目标漏洞程序执行时的环境变量中,由于环境变量是位于Linux进程空间的栈底位置,因而不会受到各种变量内存分配与对齐因素的影响,其位置是固定的。
可以通过如下公式进行计算:ret=0xc0000000-sizeof(void*)-sizeof(FILENAME)-sizeof(Shellcode)
- NSR模式
-
Linux平台的shellcode实现技术
Linux平台上的远程栈溢出攻击的原理与本地栈溢出是一样的,区别在于用户输入传递的途径不同,以及Shellcode的编写方式不同。本地栈溢出的用户输入途径主要为argv命令行输入、文件输入等,而远程栈溢出攻击的用户输入传递途径则是通过网络,存在远程栈溢出漏洞往往是一些网络服务进程或网络应用程序。NSR和RNS模式也都适用于远程栈溢出,使用场景也主要取决于被溢出的目标缓冲区大小是否足够容纳Shellcode。
-
shellcode产生的5个通用步骤:
- 用高级语言(通常用C)编写shellcode程序(通过execve()启动/bin/sh)
- 编译并反汇编shellcode程序
- 从汇编代码分析执行流程
- 整理生成的汇编代码,尽量减小它的体积并使它可注入,并通过嵌入C语言进行运行测试和调试
- 提取汇编代码所对应的opcode二进制指令,创建Shellcode指令数组
-
远程缓冲区溢出与本地缓冲区溢出比较:原理一致。用户输入传递途径区别:远程缓冲区溢出采用网络而本地缓冲区溢出命令行/文件的方式。Shellcode编写区别:远程缓冲区溢出采用远程shell访问而本地缓冲区溢出是本地特权提升。
-
C语言实现Linux本地shellcode( 使用
execve()函数
启动/bin/sh命令
)#include <stdio.h> int main () { char * name[2]; name[0] = "/bin/sh"; name[1] = NULL; execve( name[0], name, NULL ); return 0; }
将这段代码进行编译并查看其汇编代码 ,对应的汇编代码如下(注意消除指令空字节):
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 ") }
通过查表将shellcode汇编代码转换为opcode二进制指令代码:
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
-
1.4 Windows栈溢出与Shellcode
-
原理上与Linux的大致相同,差异主要有:
- 对程序运行过程中废弃栈的处理方式差异:Windows会向废弃栈中写入一些随机的数据,而Linux则不进行任何的处理。
- 进程内存空间的布局差异:Linux进程内存空间中栈底指针在
0xc0000000
之下,栈中变量一般在0xbfff
附近,在这些地址中没有空字节。Windows平台的栈位置处于0x00FFFFFF
以下的用户内存空间,一般在0x0012
附近,这些内存地址的首字节均为0x00
空字节 - 系统功能调用实现方式差异:Linux系统中通过“int 80”中断处理来调用系统功能,而Windows系统则是通过操作系统中更为复杂的API及内核处理例程调用链来完成系统功能调用。
-
Windows平台的Shellcode实现
- 所需的Win32 API函数,生成函数调用表;
- 加载所需API函数库,定位函数加载地址;
- 消除空字节,编码对抗过滤;
- 确保自己可以正常退出,使目标程序进程继续运行 或终止;
- 在目标系统环境存在异常处理和安全防护机制时, 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 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),通过网络将运行结果反馈给客户端
1.5 堆溢出攻击
堆溢出是缓冲区溢出中第二种类型的攻击方式,由于堆中的内存分配与管理机制较栈更复杂,不同操作系统平台的实现机制具有显著的差异。堆中没有可以直接覆盖的返回地址,因此堆溢出攻击比栈溢出更难。
- 函数指针改写
- 要求被溢出的缓冲区临近全局函数指针存储地址,且在其低地址方向上。此时向缓冲区填充数据,如果没有边界控制和判断,就可以覆盖函数指针所在的内存区,改写函数指针的指向地址,则程序在使用这个函数指针的时候就会执行shellcode。
- C++类对象虚函数表改写
- 使用了虚函数机制的C++类,如果它的类成员变量中存在可被溢出的缓冲区,那么就可以进行堆溢出攻击,通过覆盖类对象的虚函数指针,使其指向一个特殊构造的虚函数表,从而转向执行攻击者恶意注入的指令。
- Linux下堆管理glibc库free()函数本身漏洞
- Linux操作系统的堆管理是通过glibc库来实现的,通过称为Bin的双向循环链表来保存内存空闲块的信息。glibc库中的free()函数在处理内存块回收时,会将被释放的空闲块和与之相邻的块合并,利用精心构造的块可以在合并时覆盖Bin前指针的内容。
1.6 缓冲区溢出攻击的防御技术
- 尝试杜绝溢出的防御技术
解决缓冲区溢出攻击最根本的办法是编写正确的、不存在缓冲区溢出安全漏洞的软件代码。 - 允许溢出但不让程序改变执行流程的防御技术
- 无法让攻击代码执行的防御技术
尝试解决冯·诺伊曼体系的本质缺陷,通过堆栈不可执行限制来防御攻击。
2.实践
有机会再来补这块儿的实践~
3.学习中遇到的问题及解决
- 问题:理论很抽象
- 解决:在实践中进步
4.学习感想和体会
放了几天假之后感觉自己不会学习了 ≖ˇェˇ≖
之前只做过Windows的溢出,在之后的学习中,要把Linux的内容也补齐~
参考资料
- 《网络攻防技术与实践》(诸葛建伟著)