20199105 2019-2020-2 《网络攻防实践》 第十周作业

这个作业属于哪个课程 :网络攻防实践

这个作业的要求在哪里 : 第十次作业-软件安全攻防--缓冲区溢出和shellcode

我在这个课程的目标是 : 学习网络攻防相关技术并进行实践

这个作业在哪个具体方面帮助我实现目标:了解软件安全漏洞、学习linux和windows系统下的缓冲区溢出和shellcode

 

一、知识点总结


 

知识思维导图

 

1. 软件安全漏洞


 

安全漏洞: 在系统安全流程、设计、实现或内部控制中所存在的缺陷和弱点,能够被攻击者所利用并导致安全侵害或对系统安全策略的违反,包括三个基本元素:系统的脆弱性或缺点、攻击者对缺陷的可访问性以及攻击者对缺陷的可利用性。

书上对于安全漏洞的现状及数据分析过于陈旧,这里参考了CNCERT发布的《2019年我国互联网网络安全态势综述》中的数据报告

2019 年,国家信息安全漏洞共享平台(CNVD)收录安全漏洞数量共计16193个,其中,高危漏洞收录数量为4877个(占30.1%)

安全漏洞主要涵盖的厂商或平台为谷歌(Google)、WordPress、甲骨文(Oracle)等

按影响对象分类统计,排名前三的是应用程序漏洞(占 57.8%)、Web 应用漏洞(占 18.7%)、操作系统漏洞(占 10.6%)

软件安全“困境三要素”:

① 复杂性

  • 软件规模增大(以源代码行数作为为指标)、越来越复杂,bug随之增多

② 可扩展性

  • 操作系统通过动态装载设备驱动和模块支持
  • 客户端通过运行时编译或解释执行的虚拟机允许移动代码
  • 应用程序通过脚本语言、宏指令、控件、动态装载库

但扩展机制没有考虑安全问题

③连通性

  • 通过互联网建立起连通性

软件安全漏洞类型:

① 内存安全规范类

  • 定义:软件开发过程中,在处理RAM内存访问时引入的安全缺陷

  • 如缓冲区溢出漏洞;Double Free、User-after-Free等不安全指针问题

  • 主要出现在C/C++等编程语言所编写的软件程序中

② 输入验证类

  • 定义:软件程序在对用户输入进行数据验证时,没有保证数据的正确性、合法性和安全性,从而导致可能被恶意攻击和利用

  • 如格式化字符串、SQL注入、代码注入、远程文件包含、目录遍历、XSS等

  • 格式化字符串:恶意用户可以使用%s、%x等格式化选项,打印出内存中某些数据内容、将任意构造数据写入到任意栈中、修改库函数地址等关键数据,从而控制程序逻辑、运行攻击者恶意注入shellcode

③ 竞争条件类

  • 定义:是指处理进程的输出或结果无法预测,并依赖其他进程事件发生的次序或时间时导致错误,通常在设计多进程或多线程处理的程序中出现

④ 权限混淆与提升类

  • 定义:计算机程序由于自身编程疏忽或被第三方欺骗,从而滥用权限

  • 如Web应用程序中的跨站请求伪造(CSRF)、Clickjacking、FTP反弹攻击、权限提升、“越狱”等

  • FTP反弹攻击:利用FTP协议缺陷,攻击者可以绕过FTP服务器的权限限制,以FTP服务器为中间代理,使用POST命令向目标主机的端口请求访问,用隐蔽端口扫描

  • 越狱:是指在类UNIX系统中破解chroot和jail机制,从而访问系统管理员通过chroot设置限制目录之外的文件系统内容的一种技术

2. 缓冲区溢出

定义:计算机程序中存在的一类内存安全违规类漏洞,在计算机程序向特定缓冲区内填充数据时,超出了缓冲区本身的容量,导致外溢数据覆盖了相邻内存空间的合法数据,从而改变了程序执行流程破坏系统运行完整性。

常见于C/C++语言程序中的memcpy()、strcpy()等内存与字符串复制函数

缓冲区溢出攻击发生的根本原因,可以认为是现代计算机系统的基础构架——冯诺伊曼体系存在本质的安全缺陷,即采用了“存储程序”的原理,计算机程序的数据和指令都在同一内存中进行存储,而没有严格的分离。

2.1 汇编语言基础知识

这一部分如果没有基础,推荐学习清华大学出版社的《汇编语言》王爽著

汇编语言是理解软件安全漏洞机理,掌握软件渗透攻击代码技术的底层基础。对于软件安全漏洞分析,通常我们无法得到被分析软件的源代码,故只能使用反汇编技术,通过阅读和理解汇编代码,开展分析;在编写的渗透攻击代码中,也会包含以机器学习指令形式存在的shellcode,这在上一篇博客中有具体的分析实践。

汇编工作流程

从应用的角度一般将寄存器分为4类,即通用寄存器、段寄存器、控制寄存器和其他寄存器。

  • 通用寄存器 主要用于普通的算术运算,保存数据、地址、偏移量、计数值等

  • 段寄存器 一般用作段基址寄存器,在IA32构架中是16位的

  • 控制寄存器 用来控制处理器的执行流程

  • 其他寄存器 中值得关注的是“扩展标志”eflags寄存器,由不同的标志位组成,用于保存指令执行后的状态和控制指令执行流程的标志信息

IA32架构中的关键寄存器及功能

2.2 编译器与调试器

编译器连接器 是在使用C/C++等高级编程语言编写的源码,生成可直接在操作系统平台上运行的可执行程序代码。

GCC的编译流程包括四个过程:预处理 —— 编译 —— 汇编 —— 链接

gcc –c test.c命令进行源码编译,生成test.o

gcc –o test test.o进行连接,生成test可执行程序

可以使用gcc test.c –o test同时完成编译和连接过程

参数总结
-c : 只是编译不链接,生成目标文件“.o”
-S : 只是编译不汇编,生成汇编代码
-E : 只进行预编译,不做其他处理
-g : 在可执行程序中包含标准调试信息
-o file :把输出文件输出到file里

对于处理多个源码文件、包含头文件、引用库文件等多种情况,程序开发人员通常编写或自动生成Makefile,来控制GCC的编译和连接过程。

 


 

调试器 则是程序开发人员在运行时刻调试与分析程序行为的基本工具

类UNIX平台上进行程序的调试经常使用GDB调试器,GDB调试器提供程序断点管理、执行控制、信息查看等多种类型的功能指令

参数列表

命令 命令缩写 命令说明
list l 显示多行源代码
break b 设置断点,程序运行到断点的位置会停下来
info i 描述程序的状态
run r 开始运行程序
display disp 跟踪查看某个变量,每次停下来都显示它的值
step s 执行下一条语句,如果该语句为函数调用,则进入函数执行其中的第一条语句
next n 执行下一条语句,如果该语句为函数调用,不会进入函数内部执行(即不会一步步地调试函数内部语句)
print p 打印内部变量值
continue c 继续程序的运行,直到遇到下一个断点
start st 开始执行程序,在main函数的第一条语句前面停下来
file - 装入需要调试的程序
kill k 终止正在调试的程序
watch - 监视变量值的变化
backtrace bt 产看函数调用信息(堆栈)
frame f 查看栈帧
quit q 退出GDB环境

2.3 进程内存管理

程序在执行时,系统在内存中会为程序创建一个虚拟的内存地址空间,在32位机上即4GB的空间大小,用于映射物理内存,并保存程序的指令和数据。

Linux操作系统 的集成内存空间3GB以下为用户态空间,3GB-4GB为内核态空间,操作系统将可执行程序加载到新创建的内存空间中,程序一般包含.text、.bss和.data三种类型的段

  • .text 包含程序指令,在内存中被映射为只读

  • .bss 包含未经初始化的数据,在内存中被映射为可写

  • .data 包含静态初始化数据,在内存中被映射为可写

Windows操作系统 的进程内存空间2GB-4GB为内核态地址空间,用于映射Windows内核代码和一些核心态DLL,并用于存储一些内核态对象,0GB-2GB为用户态地址空间,高地址段映射一些大量应用程序共同使用的系统DLL

 

2.4 函数调用过程

  • 栈结构与函数调用过程的底层细节是理解栈溢出攻击的重要基础,因为栈溢出攻击就是针对函数调用过程中返回地址在栈中的存储位置,进行缓冲区溢出,从而改写返回地址,达到让处理器指令寄存器跳转至攻击者指定位置执行恶意代码的目的。

  • 栈是一种最基本的LIFO后进先出抽象数据结构,主要被用于实现程序中的函数或过程调用,在栈中会保存函数的调用参数、返回地址、调用者栈基址、函数本地局部变量等数据。
    (那些年,考研时候复习的数据结构…)

  • 在IA32构架寄存器中,两个与栈密切相关的寄存器为ebp和esp,分别保存当前运行函数的栈底地址和栈顶地址,而两个密切相关的指令为push和pop,分别是将数据压入栈,及将栈顶数据弹出至特定寄存器。

  • 程序进行函数调用的过程有如下三个步骤:
    ① 调用
    调用者将函数调用参数、函数调用下一条指令的返回地址压栈,并跳转至被调用函数入口地址
    ② 序言
    对调用函数的栈基址进行压栈保存,并创建自身函数的栈结构。对应图中,将EBP寄存器赋值为当前栈基址,为本地函数局部变量分配站地址空间,更新ESP寄存器为当前栈顶指针等
    ③ 返回
    执行leave和ret指令,恢复调用者的栈顶和栈底指针

2.5 缓冲区溢出攻击原理

  • 缓冲区溢出漏洞根据缓冲区在进程内存空间中的位置不同,又分为栈溢出、堆溢出和内核溢出这三种具体技术形态

    • 栈溢出:是指存储在栈上的一些缓冲区变量由于存在缺乏边界保护问题,能够被溢出并修改栈上的敏感信息(通常是返回地址),从而导致程序流程的改变

    • 堆溢出:是存储在堆上的缓冲区变量缺乏边界保护所遭受溢出攻击的安全问题

    • 内核溢出:是由于进程内存空间内核态中存储的缓冲区变量被溢出造成的

  • Linux系统可能会采取对抗缓冲区溢出的防范措施,取消措施才能重现基础的缓冲区溢出过程:

    ①取消“栈上数据不可执行”保护:echo 0 > /proc/sys/kerne/exec-shield

    ②取消“地址空间随机化”保护:echo 0 > /proc/sys/kernel/randomize_va_space

    ③编译时取消“/GS”保护:加上gcc编译选项 –fno-stack-protecto

栈溢出安全漏洞代码

include <stdio.h>

void return_input(void){
char array[30];   //字符串缓冲区,长度为30字节
gets(array);   //将输入存入array缓冲区
    printf("%s\n", array);
}

int main (void){
    return_input();
    return 0;
}

gets()函数没有进行换抽取边界检查与保护,若用户输入超过30字节,输入数据将覆盖array缓冲区上方的EBP和RET,RET返回地址可能无法访问,从而造成段错误

 

3. Linux平台上的栈溢出与Shellcode


 

3.1 Linux平台栈溢出攻击技术

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)

 

3.2 Linux平台上的远程栈溢出与本地栈溢出

Linux平台上的远程栈溢出攻击的原理与本地栈溢出是一样的,区别在于用户输入传递的途径不同,以及Shellcode的编写方式不同。本地栈溢出的用户输入途径主要为argv命令行输入、文件输入等,而远程栈溢出攻击的用户输入传递途径则是通过网络,存在远程栈溢出漏洞往往是一些网络服务进程或网络应用程序。NSR和RNS模式也都适用于远程栈溢出,使用场景也主要取决于被溢出的目标缓冲区大小是否足够容纳Shellcode。

 

3.3 Linux平台的Shellcode实现技术

Shellcode是一段机器指令,对于我们通常接触的IA32构架平台,Shellcode就是符合Intel 32位指令规范的一串CPU指令,被用于溢出之后改变系统正常流程,转而执行Shellcode以完成渗透测试者的攻击目的,通常是为他提供一个访问系统的本地或远程命令行访问。在Linux操作系统中,程序通过“int 0x80”软中断来执行系统调用,而在Windows操作系统中,则通过核心DLL中提供的API接口来完成系统调用。

 

3.4 Linux本地Shellcode实现机制

  • Linux系统本地Shellcode通常提供的功能就是为攻击者启动一个命令行Shell。在获得汇编语言实现的Shellcode之后,我们可以通过查找Intel opcode指令参考手册,即可获得opcode二进制指令形式的Shellcode

  • Linux系统中一个最简单的本地Shellcode的产生过程,即Shellcode的通用方法:

① 先用高级编程语言,通常用C,来编写Shellcode程序

② 编译并反汇编调试这个 Shellcode程序

③ 从编译语言代码级别分析程序执行流程

④ 整理生成的汇编代码,尽量减小它的体积并使它可注入,并通过嵌入C语言进行运行测试和调试

⑤ 提取汇编代码所对应的opcode二进制指令,创建Shellcode指令数组。

在Linux本地Shellcode中,往往还会在运行execve()启动shell之前,调用setreuid(0)将程序运行权限提升至Root用户,这样才能利用本地溢出攻击来提升权限,在执行execve()函数之后还需要执行exit()函数,从而在溢出攻击之后能够使程序正常退出

用C语言编写Linux本地shellcode(非注入攻击负载)

#include <stdio.h>
int main ()
{
    char * name[2];
    name[0] = "/bin/sh"; 
    name[1] = NULL;
    execve( name[0], name, NULL ); //execve函数启动/bin/sh提供命令行
    return 0;
}

int execve(const char *filename, char *const argv[ ], char *const envp[ ])

execve()第一个参数filename字符串所代表的文件路径,第二个参数是利用数组指针来传递给执行文件,并且需要以空指针(NULL)结束,最后一个参数则为传递给执行文件的新环境变量数组

 

3.5 Linux远程Shellcode实现机制

远程Shellcode实现机制实现方法与本地Shellcode实现机制完全一致。首先给出高级语言的功能代码实现,然后通过反汇编调试编译后的二进制程序,特权、优化和整理所获得的汇编代码,并最终产生opcode二进制指令代码

Linux远程Shellcode需要让攻击目标程序创建socket监听指定的端口等待客户端连接,启动一个命令行Shell,并将命令行的输入输出与socket绑定,这样攻击者就可以通过socket客户端连接目标程序所在初级的开放端口,与服务端socket建立起通信通道,并获得Shell。在Linux系统中,dup2()函数能够将标准输入输出与socket的网络通信通道进行绑定,使得socket的远程输入连接至命令行标准输入,将命令行标准输出连接至远程网络输出,因而完成远程Shell的功能

 

4. Windows平台上的栈溢出与shellcode


 

4.1 Windows VS Linux (栈溢出漏洞)

①对程序运行中废弃栈的处理方式差异
当一个函数调用完成返回至调用者,执行下一条指令之前,Windows平台会向废弃栈中写入一些随机的数据,而Linux则不进行任何的处理

②进程内存空间的布局差异
Linux进程内存空间中栈指针在0x0000000之下,即一般栈中变量的位置都在0xbfff····地址附近,在这些地址中没有空字节。
Windows平台的栈位置处于0x00FFFFFF以下的用户内存空间,一般为0x0012····地址附近,而这些内存地址的首字节均为0x00空字节

③系统功能调用的实现方式差异
Linux系统中通过“int80”中断处理来调用系统功能,而Windows系统则是通过操作系统中更为复杂的API及内核处理例程调用链来完成系统功能调用,对应用程序直接可见的是应用层中如Kernel32.dll、User32.dll等系统动态链接库中导出的一些系统API接口函数

 

4.2 Windows平台的Shellcode实现

  • Shellcode找到所需的Win32 API函数,生成函数调用表

  • 为加载所需API函数库,找出目标程序已加载函数地址

  • 消除空字节避免中断,编码以通过过滤

  • Shellcode确保自己可以正常退出,使目标程序进程继续运行或终止

  • 在目标系统环境存在异常处理和安全防护机制时,shellcode还需进一步考虑如何对抗这些机制

 

4.3 Windows本地Shellcode

在Windows平台上,典型的本地Shellcode同样也是启动一个命令行Shell,即“command.com”或Windows32的系统API中提供了system()函数调用,可以用于启动指定程序或运行特定命令,在调用system(“command.com”)之后即可启动命令行程序

编写Shellcode最简单的方式就是使用硬编码的函数地址,比如system()函数在Windows XP特定版本的目标程序内存空间中的加载地址为0x77bf93c7,那么我们就可以在Shellcode中使用Call 0x77bf93c7指令,让EIP指令寄存器跳转至硬编码的函数入口地址执行。system()函数是由msvcrt.dll动态链接库所导出的API函数,而攻击目标并不一定加载了该DLL,为了确保Shellcode能够正确地调用所需函数,一般需要将所需函数的动态链接库装载至目标程序内存中,然后再查询获得该函数的加载地址

用C语言编写Linux本地shellcode

#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";   //system()参数,启动命令行程序
        char sysbuf2[5] = "exit";   //退出进程
        LibHandle = LoadLibrary(dllbuf);   //加载动态链接库,便于shellcode调用函数
        ProcAdd = (MYPROC)GetProcAddress(   //获得system()加载入口地址,赋值给ProcAdd
			LibHandle, sysbuf);
        (ProcAdd) (cmdbuf);

        ProcAdd2 = (MYPROC2) GetProcAddress(
			LibHandle, sysbuf2);
		(ProcAdd2)(0);
}

 

4.4 Windows远程Shellcode

大致过程如下:

①创建一个服务器端socket,并在指定的端口上监听

②通过accept()接受客户端的网络连接

③创建子程序,运行cmd.exe,启动命令行

④创建两个管道,命令管道将服务器端socket接收(recv)到的客户端通过网络输入的执行命令,连接至cmd.exe的标准输入;然后输出管道将cmd.exe的标准输出连接至服务器端socket的发送(send),通过网络将运行结果反馈给客户端

 

5. 堆溢出攻击

一个典型的Linux程序在其进程内存空间中通常有如下一些数据区:包含已初始化全局函数的.data段、包含未经初始化数据的.bss段、运行时刻动态分配内存的数据区heap等。而这些内存数据区有着共同的特点,即数据分配与增长方向是从低地址向高地址,而非栈从高地址向低地址的增长方向。因此,在.data、.bss和heap中缓冲区溢出的情形,都被称为堆溢出。堆溢出之所以较栈溢出具有更高的难度,更重要的原因在于堆中没有可以直接覆盖并修改指针寄存器指针的返回地址,因此往往需要利用在堆中一些会影响程序执行流程的关键变量,如函数指针、C++类对象中的虚函数表。或者挖掘出堆中进行数据操作时可能存在的向指定内存地址改写内容的漏洞机会

 

6. 缓冲区溢出攻击的防御技术

①尝试杜绝溢出的防御技术
解决缓冲区溢出攻击最根本的办法是编写正确的、不存在缓冲区溢出安全漏洞的软件代码

②允许溢出但不让程序改变执行流程的防御技术

③无法让攻击代码执行的防御技术
尝试解决冯·诺伊曼体系的本质缺陷,通过堆栈不可执行限制来防御攻击

 

二、学习中遇到的问题及解决


本章重点是只是带你学习和代码学习,故没有像实践一样遇到很多问题,基本通过参考资料/博客都能弄懂。

 

三、学习感想和体会


本章知识点刚开始学习时觉得比较杂乱,为了能理解缓冲区溢出,延申出汇编语言、进程内存等很多方面的知识,但都是点到为止,好在之前都学过。整理起来有点不好把控,只能把最最关键的,书上提供了写一写。
书上给出了好多溢出攻击的代码段,分析起来比前面长篇大论的文字好清晰很多了。
刚好某门课作业学习了《2019年我国互联网网络安全态势综述》,在安全漏洞部分扩展了许多知识,推荐大家学习。

 

参考资料:

  1. Gcc编译器的工作流程

  2. 《2019年我国互联网网络安全态势综述》

posted @ 2020-05-05 20:12  FLwww  阅读(231)  评论(0编辑  收藏  举报